1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: Daniel Gredler |
5 | */ |
6 | package org.h2.util; |
7 | |
8 | import static java.lang.Math.abs; |
9 | import java.math.BigDecimal; |
10 | import java.sql.Date; |
11 | import java.sql.Timestamp; |
12 | import java.text.DecimalFormat; |
13 | import java.text.DecimalFormatSymbols; |
14 | import java.text.SimpleDateFormat; |
15 | import java.util.Calendar; |
16 | import java.util.Currency; |
17 | import java.util.GregorianCalendar; |
18 | import java.util.Locale; |
19 | import java.util.TimeZone; |
20 | |
21 | import org.h2.api.ErrorCode; |
22 | import org.h2.message.DbException; |
23 | |
24 | /** |
25 | * Emulates Oracle's TO_CHAR function. |
26 | */ |
27 | public class ToChar { |
28 | |
29 | /** |
30 | * The beginning of the Julian calendar. |
31 | */ |
32 | private static final long JULIAN_EPOCH; |
33 | |
34 | static { |
35 | GregorianCalendar epoch = new GregorianCalendar(Locale.ENGLISH); |
36 | epoch.setGregorianChange(new Date(Long.MAX_VALUE)); |
37 | epoch.clear(); |
38 | epoch.set(4713, Calendar.JANUARY, 1, 0, 0, 0); |
39 | epoch.set(Calendar.ERA, GregorianCalendar.BC); |
40 | JULIAN_EPOCH = epoch.getTimeInMillis(); |
41 | } |
42 | |
43 | private ToChar() { |
44 | // utility class |
45 | } |
46 | |
47 | /** |
48 | * Emulates Oracle's TO_CHAR(number) function. |
49 | * |
50 | * <p><table border="1"> |
51 | * <th><td>Input</td> |
52 | * <td>Output</td> |
53 | * <td>Closest {@link DecimalFormat} Equivalent</td></th> |
54 | * <tr><td>,</td> |
55 | * <td>Grouping separator.</td> |
56 | * <td>,</td></tr> |
57 | * <tr><td>.</td> |
58 | * <td>Decimal separator.</td> |
59 | * <td>.</td></tr> |
60 | * <tr><td>$</td> |
61 | * <td>Leading dollar sign.</td> |
62 | * <td>$</td></tr> |
63 | * <tr><td>0</td> |
64 | * <td>Leading or trailing zeroes.</td> |
65 | * <td>0</td></tr> |
66 | * <tr><td>9</td> |
67 | * <td>Digit.</td> |
68 | * <td>#</td></tr> |
69 | * <tr><td>B</td> |
70 | * <td>Blanks integer part of a fixed point number less than 1.</td> |
71 | * <td>#</td></tr> |
72 | * <tr><td>C</td> |
73 | * <td>ISO currency symbol.</td> |
74 | * <td>\u00A4</td></tr> |
75 | * <tr><td>D</td> |
76 | * <td>Local decimal separator.</td> |
77 | * <td>.</td></tr> |
78 | * <tr><td>EEEE</td> |
79 | * <td>Returns a value in scientific notation.</td> |
80 | * <td>E</td></tr> |
81 | * <tr><td>FM</td> |
82 | * <td>Returns values with no leading or trailing spaces.</td> |
83 | * <td>None.</td></tr> |
84 | * <tr><td>G</td> |
85 | * <td>Local grouping separator.</td> |
86 | * <td>,</td></tr> |
87 | * <tr><td>L</td> |
88 | * <td>Local currency symbol.</td> |
89 | * <td>\u00A4</td></tr> |
90 | * <tr><td>MI</td> |
91 | * <td>Negative values get trailing minus sign, |
92 | * positive get trailing space.</td> |
93 | * <td>-</td></tr> |
94 | * <tr><td>PR</td> |
95 | * <td>Negative values get enclosing angle brackets, |
96 | * positive get spaces.</td> |
97 | * <td>None.</td></tr> |
98 | * <tr><td>RN</td> |
99 | * <td>Returns values in Roman numerals.</td> |
100 | * <td>None.</td></tr> |
101 | * <tr><td>S</td> |
102 | * <td>Returns values with leading/trailing +/- signs.</td> |
103 | * <td>None.</td></tr> |
104 | * <tr><td>TM</td> |
105 | * <td>Returns smallest number of characters possible.</td> |
106 | * <td>None.</td></tr> |
107 | * <tr><td>U</td> |
108 | * <td>Returns the dual currency symbol.</td> |
109 | * <td>None.</td></tr> |
110 | * <tr><td>V</td> |
111 | * <td>Returns a value multiplied by 10^n.</td> |
112 | * <td>None.</td></tr> |
113 | * <tr><td>X</td> |
114 | * <td>Hex value.</td> |
115 | * <td>None.</td></tr> |
116 | * </table> |
117 | * See also TO_CHAR(number) and number format models |
118 | * in the Oracle documentation. |
119 | * |
120 | * @param number the number to format |
121 | * @param format the format pattern to use (if any) |
122 | * @param nlsParam the NLS parameter (if any) |
123 | * @return the formatted number |
124 | */ |
125 | public static String toChar(BigDecimal number, String format, |
126 | String nlsParam) { |
127 | |
128 | // short-circuit logic for formats that don't follow common logic below |
129 | String formatUp = format != null ? format.toUpperCase() : null; |
130 | if (formatUp == null || formatUp.equals("TM") || formatUp.equals("TM9")) { |
131 | String s = number.toPlainString(); |
132 | return s.startsWith("0.") ? s.substring(1) : s; |
133 | } else if (formatUp.equals("TME")) { |
134 | int pow = number.precision() - number.scale() - 1; |
135 | number = number.movePointLeft(pow); |
136 | return number.toPlainString() + "E" + |
137 | (pow < 0 ? '-' : '+') + (abs(pow) < 10 ? "0" : "") + abs(pow); |
138 | } else if (formatUp.equals("RN")) { |
139 | boolean lowercase = format.startsWith("r"); |
140 | String rn = StringUtils.pad(toRomanNumeral(number.intValue()), 15, " ", false); |
141 | return lowercase ? rn.toLowerCase() : rn; |
142 | } else if (formatUp.equals("FMRN")) { |
143 | boolean lowercase = format.charAt(2) == 'r'; |
144 | String rn = toRomanNumeral(number.intValue()); |
145 | return lowercase ? rn.toLowerCase() : rn; |
146 | } else if (formatUp.endsWith("X")) { |
147 | return toHex(number, format); |
148 | } |
149 | |
150 | String originalFormat = format; |
151 | DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(); |
152 | char localGrouping = symbols.getGroupingSeparator(); |
153 | char localDecimal = symbols.getDecimalSeparator(); |
154 | |
155 | boolean leadingSign = formatUp.startsWith("S"); |
156 | if (leadingSign) { |
157 | format = format.substring(1); |
158 | } |
159 | |
160 | boolean trailingSign = formatUp.endsWith("S"); |
161 | if (trailingSign) { |
162 | format = format.substring(0, format.length() - 1); |
163 | } |
164 | |
165 | boolean trailingMinus = formatUp.endsWith("MI"); |
166 | if (trailingMinus) { |
167 | format = format.substring(0, format.length() - 2); |
168 | } |
169 | |
170 | boolean angleBrackets = formatUp.endsWith("PR"); |
171 | if (angleBrackets) { |
172 | format = format.substring(0, format.length() - 2); |
173 | } |
174 | |
175 | int v = formatUp.indexOf("V"); |
176 | if (v >= 0) { |
177 | int digits = 0; |
178 | for (int i = v + 1; i < format.length(); i++) { |
179 | char c = format.charAt(i); |
180 | if (c == '0' || c == '9') { |
181 | digits++; |
182 | } |
183 | } |
184 | number = number.movePointRight(digits); |
185 | format = format.substring(0, v) + format.substring(v + 1); |
186 | } |
187 | |
188 | Integer power; |
189 | if (format.endsWith("EEEE")) { |
190 | power = number.precision() - number.scale() - 1; |
191 | number = number.movePointLeft(power); |
192 | format = format.substring(0, format.length() - 4); |
193 | } else { |
194 | power = null; |
195 | } |
196 | |
197 | int maxLength = 1; |
198 | boolean fillMode = !formatUp.startsWith("FM"); |
199 | if (!fillMode) { |
200 | format = format.substring(2); |
201 | } |
202 | |
203 | // blanks flag doesn't seem to actually do anything |
204 | format = format.replaceAll("[Bb]", ""); |
205 | |
206 | // if we need to round the number to fit into the format specified, |
207 | // go ahead and do that first |
208 | int separator = findDecimalSeparator(format); |
209 | int formatScale = calculateScale(format, separator); |
210 | if (formatScale < number.scale()) { |
211 | number = number.setScale(formatScale, BigDecimal.ROUND_HALF_UP); |
212 | } |
213 | |
214 | // any 9s to the left of the decimal separator but to the right of a |
215 | // 0 behave the same as a 0, e.g. "09999.99" -> "00000.99" |
216 | for (int i = format.indexOf('0'); i >= 0 && i < separator; i++) { |
217 | if (format.charAt(i) == '9') { |
218 | format = format.substring(0, i) + "0" + format.substring(i + 1); |
219 | } |
220 | } |
221 | |
222 | StringBuilder output = new StringBuilder(); |
223 | String unscaled = number.unscaledValue().abs().toString(); |
224 | |
225 | // start at the decimal point and fill in the numbers to the left, |
226 | // working our way from right to left |
227 | int i = separator - 1; |
228 | int j = unscaled.length() - number.scale() - 1; |
229 | for (; i >= 0; i--) { |
230 | char c = format.charAt(i); |
231 | maxLength++; |
232 | if (c == '9' || c == '0') { |
233 | if (j >= 0) { |
234 | char digit = unscaled.charAt(j); |
235 | output.insert(0, digit); |
236 | j--; |
237 | } else if (c == '0' && power == null) { |
238 | output.insert(0, '0'); |
239 | } |
240 | } else if (c == ',') { |
241 | // only add the grouping separator if we have more numbers |
242 | if (j >= 0 || (i > 0 && format.charAt(i - 1) == '0')) { |
243 | output.insert(0, c); |
244 | } |
245 | } else if (c == 'G' || c == 'g') { |
246 | // only add the grouping separator if we have more numbers |
247 | if (j >= 0 || (i > 0 && format.charAt(i - 1) == '0')) { |
248 | output.insert(0, localGrouping); |
249 | } |
250 | } else if (c == 'C' || c == 'c') { |
251 | Currency currency = Currency.getInstance(Locale.getDefault()); |
252 | output.insert(0, currency.getCurrencyCode()); |
253 | maxLength += 6; |
254 | } else if (c == 'L' || c == 'l' || c == 'U' || c == 'u') { |
255 | Currency currency = Currency.getInstance(Locale.getDefault()); |
256 | output.insert(0, currency.getSymbol()); |
257 | maxLength += 9; |
258 | } else if (c == '$') { |
259 | Currency currency = Currency.getInstance(Locale.getDefault()); |
260 | String cs = currency.getSymbol(); |
261 | output.insert(0, cs); |
262 | } else { |
263 | throw DbException.get( |
264 | ErrorCode.INVALID_TO_CHAR_FORMAT, originalFormat); |
265 | } |
266 | } |
267 | |
268 | // if the format (to the left of the decimal point) was too small |
269 | // to hold the number, return a big "######" string |
270 | if (j >= 0) { |
271 | return StringUtils.pad("", format.length() + 1, "#", true); |
272 | } |
273 | |
274 | if (separator < format.length()) { |
275 | |
276 | // add the decimal point |
277 | maxLength++; |
278 | char pt = format.charAt(separator); |
279 | if (pt == 'd' || pt == 'D') { |
280 | output.append(localDecimal); |
281 | } else { |
282 | output.append(pt); |
283 | } |
284 | |
285 | // start at the decimal point and fill in the numbers to the right, |
286 | // working our way from left to right |
287 | i = separator + 1; |
288 | j = unscaled.length() - number.scale(); |
289 | for (; i < format.length(); i++) { |
290 | char c = format.charAt(i); |
291 | maxLength++; |
292 | if (c == '9' || c == '0') { |
293 | if (j < unscaled.length()) { |
294 | char digit = unscaled.charAt(j); |
295 | output.append(digit); |
296 | j++; |
297 | } else { |
298 | if (c == '0' || fillMode) { |
299 | output.append('0'); |
300 | } |
301 | } |
302 | } else { |
303 | throw DbException.get( |
304 | ErrorCode.INVALID_TO_CHAR_FORMAT, originalFormat); |
305 | } |
306 | } |
307 | } |
308 | |
309 | addSign(output, number.signum(), leadingSign, trailingSign, |
310 | trailingMinus, angleBrackets, fillMode); |
311 | |
312 | if (power != null) { |
313 | output.append('E'); |
314 | output.append(power < 0 ? '-' : '+'); |
315 | output.append(Math.abs(power) < 10 ? "0" : ""); |
316 | output.append(Math.abs(power)); |
317 | } |
318 | |
319 | if (fillMode) { |
320 | if (power != null) { |
321 | output.insert(0, ' '); |
322 | } else { |
323 | while (output.length() < maxLength) { |
324 | output.insert(0, ' '); |
325 | } |
326 | } |
327 | } |
328 | |
329 | return output.toString(); |
330 | } |
331 | |
332 | private static void addSign(StringBuilder output, int signum, |
333 | boolean leadingSign, boolean trailingSign, boolean trailingMinus, |
334 | boolean angleBrackets, boolean fillMode) { |
335 | if (angleBrackets) { |
336 | if (signum < 0) { |
337 | output.insert(0, '<'); |
338 | output.append('>'); |
339 | } else if (fillMode) { |
340 | output.insert(0, ' '); |
341 | output.append(' '); |
342 | } |
343 | } else { |
344 | String sign; |
345 | if (signum == 0) { |
346 | sign = ""; |
347 | } else if (signum < 0) { |
348 | sign = "-"; |
349 | } else { |
350 | if (leadingSign || trailingSign) { |
351 | sign = "+"; |
352 | } else if (fillMode) { |
353 | sign = " "; |
354 | } else { |
355 | sign = ""; |
356 | } |
357 | } |
358 | if (trailingMinus || trailingSign) { |
359 | output.append(sign); |
360 | } else { |
361 | output.insert(0, sign); |
362 | } |
363 | } |
364 | } |
365 | |
366 | private static int findDecimalSeparator(String format) { |
367 | int index = format.indexOf('.'); |
368 | if (index == -1) { |
369 | index = format.indexOf('D'); |
370 | if (index == -1) { |
371 | index = format.indexOf('d'); |
372 | if (index == -1) { |
373 | index = format.length(); |
374 | } |
375 | } |
376 | } |
377 | return index; |
378 | } |
379 | |
380 | private static int calculateScale(String format, int separator) { |
381 | int scale = 0; |
382 | for (int i = separator; i < format.length(); i++) { |
383 | char c = format.charAt(i); |
384 | if (c == '0' || c == '9') { |
385 | scale++; |
386 | } |
387 | } |
388 | return scale; |
389 | } |
390 | |
391 | private static String toRomanNumeral(int number) { |
392 | int[] values = new int[] { 1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, |
393 | 5, 4, 1 }; |
394 | String[] numerals = new String[] { "M", "CM", "D", "CD", "C", "XC", |
395 | "L", "XL", "X", "IX", "V", "IV", "I" }; |
396 | StringBuilder result = new StringBuilder(); |
397 | for (int i = 0; i < values.length; i++) { |
398 | int value = values[i]; |
399 | String numeral = numerals[i]; |
400 | while (number >= value) { |
401 | result.append(numeral); |
402 | number -= value; |
403 | } |
404 | } |
405 | return result.toString(); |
406 | } |
407 | |
408 | private static String toHex(BigDecimal number, String format) { |
409 | |
410 | boolean fillMode = !format.toUpperCase().startsWith("FM"); |
411 | boolean uppercase = !format.contains("x"); |
412 | boolean zeroPadded = format.startsWith("0"); |
413 | int digits = 0; |
414 | for (int i = 0; i < format.length(); i++) { |
415 | char c = format.charAt(i); |
416 | if (c == '0' || c == 'X' || c == 'x') { |
417 | digits++; |
418 | } |
419 | } |
420 | |
421 | int i = number.setScale(0, BigDecimal.ROUND_HALF_UP).intValue(); |
422 | String hex = Integer.toHexString(i); |
423 | if (digits < hex.length()) { |
424 | hex = StringUtils.pad("", digits + 1, "#", true); |
425 | } else { |
426 | if (uppercase) { |
427 | hex = hex.toUpperCase(); |
428 | } |
429 | if (zeroPadded) { |
430 | hex = StringUtils.pad(hex, digits, "0", false); |
431 | } |
432 | if (fillMode) { |
433 | hex = StringUtils.pad(hex, format.length() + 1, " ", false); |
434 | } |
435 | } |
436 | |
437 | return hex; |
438 | } |
439 | |
440 | /** |
441 | * Emulates Oracle's TO_CHAR(datetime) function. |
442 | * |
443 | * <p><table border="1"> |
444 | * <th><td>Input</td> |
445 | * <td>Output</td> |
446 | * <td>Closest {@link SimpleDateFormat} Equivalent</td></th> |
447 | * <tr><td>- / , . ; : "text"</td> |
448 | * <td>Reproduced verbatim.</td> |
449 | * <td>'text'</td></tr> |
450 | * <tr><td>A.D. AD B.C. BC</td> |
451 | * <td>Era designator, with or without periods.</td> |
452 | * <td>G</td></tr> |
453 | * <tr><td>A.M. AM P.M. PM</td> |
454 | * <td>AM/PM marker.</td> |
455 | * <td>a</td></tr> |
456 | * <tr><td>CC SCC</td> |
457 | * <td>Century.</td> |
458 | * <td>None.</td></tr> |
459 | * <tr><td>D</td> |
460 | * <td>Day of week.</td> |
461 | * <td>u</td></tr> |
462 | * <tr><td>DAY</td> |
463 | * <td>Name of day.</td> |
464 | * <td>EEEE</td></tr> |
465 | * <tr><td>DY</td> |
466 | * <td>Abbreviated day name.</td> |
467 | * <td>EEE</td></tr> |
468 | * <tr><td>DD</td> |
469 | * <td>Day of month.</td> |
470 | * <td>d</td></tr> |
471 | * <tr><td>DDD</td> |
472 | * <td>Day of year.</td> |
473 | * <td>D</td></tr> |
474 | * <tr><td>DL</td> |
475 | * <td>Long date format.</td> |
476 | * <td>EEEE, MMMM d, yyyy</td></tr> |
477 | * <tr><td>DS</td> |
478 | * <td>Short date format.</td> |
479 | * <td>MM/dd/yyyy</td></tr> |
480 | * <tr><td>E</td> |
481 | * <td>Abbreviated era name (Japanese, Chinese, Thai)</td> |
482 | * <td>None.</td></tr> |
483 | * <tr><td>EE</td> |
484 | * <td>Full era name (Japanese, Chinese, Thai)</td> |
485 | * <td>None.</td></tr> |
486 | * <tr><td>FF[1-9]</td> |
487 | * <td>Fractional seconds.</td> |
488 | * <td>S</td></tr> |
489 | * <tr><td>FM</td> |
490 | * <td>Returns values with no leading or trailing spaces.</td> |
491 | * <td>None.</td></tr> |
492 | * <tr><td>FX</td> |
493 | * <td>Requires exact matches between character data and format model.</td> |
494 | * <td>None.</td></tr> |
495 | * <tr><td>HH HH12</td> |
496 | * <td>Hour in AM/PM (1-12).</td> |
497 | * <td>hh</td></tr> |
498 | * <tr><td>HH24</td> |
499 | * <td>Hour in day (0-23).</td> |
500 | * <td>HH</td></tr> |
501 | * <tr><td>IW</td> |
502 | * <td>Week in year.</td> |
503 | * <td>w</td></tr> |
504 | * <tr><td>WW</td> |
505 | * <td>Week in year.</td> |
506 | * <td>w</td></tr> |
507 | * <tr><td>W</td> |
508 | * <td>Week in month.</td> |
509 | * <td>W</td></tr> |
510 | * <tr><td>IYYY IYY IY I</td> |
511 | * <td>Last 4/3/2/1 digit(s) of ISO year.</td> |
512 | * <td>yyyy yyy yy y</td></tr> |
513 | * <tr><td>RRRR RR</td> |
514 | * <td>Last 4/2 digits of year.</td> |
515 | * <td>yyyy yy</td></tr> |
516 | * <tr><td>Y,YYY</td> |
517 | * <td>Year with comma.</td> |
518 | * <td>None.</td></tr> |
519 | * <tr><td>YEAR SYEAR</td> |
520 | * <td>Year spelled out (S prefixes BC years with minus sign).</td> |
521 | * <td>None.</td></tr> |
522 | * <tr><td>YYYY SYYYY</td> |
523 | * <td>4-digit year (S prefixes BC years with minus sign).</td> |
524 | * <td>yyyy</td></tr> |
525 | * <tr><td>YYY YY Y</td> |
526 | * <td>Last 3/2/1 digit(s) of year.</td> |
527 | * <td>yyy yy y</td></tr> |
528 | * <tr><td>J</td> |
529 | * <td>Julian day (number of days since January 1, 4712 BC).</td> |
530 | * <td>None.</td></tr> |
531 | * <tr><td>MI</td> |
532 | * <td>Minute in hour.</td> |
533 | * <td>mm</td></tr> |
534 | * <tr><td>MM</td> |
535 | * <td>Month in year.</td> |
536 | * <td>MM</td></tr> |
537 | * <tr><td>MON</td> |
538 | * <td>Abbreviated name of month.</td> |
539 | * <td>MMM</td></tr> |
540 | * <tr><td>MONTH</td> |
541 | * <td>Name of month, padded with spaces.</td> |
542 | * <td>MMMM</td></tr> |
543 | * <tr><td>RM</td> |
544 | * <td>Roman numeral month.</td> |
545 | * <td>None.</td></tr> |
546 | * <tr><td>Q</td> |
547 | * <td>Quarter of year.</td> |
548 | * <td>None.</td></tr> |
549 | * <tr><td>SS</td> |
550 | * <td>Seconds in minute.</td> |
551 | * <td>ss</td></tr> |
552 | * <tr><td>SSSSS</td> |
553 | * <td>Seconds in day.</td> |
554 | * <td>None.</td></tr> |
555 | * <tr><td>TS</td> |
556 | * <td>Short time format.</td> |
557 | * <td>h:mm:ss aa</td></tr> |
558 | * <tr><td>TZD</td> |
559 | * <td>Daylight savings time zone abbreviation.</td> |
560 | * <td>z</td></tr> |
561 | * <tr><td>TZR</td> |
562 | * <td>Time zone region information.</td> |
563 | * <td>zzzz</td></tr> |
564 | * <tr><td>X</td> |
565 | * <td>Local radix character.</td> |
566 | * <td>None.</td></tr> |
567 | * </table> |
568 | * <p> |
569 | * See also TO_CHAR(datetime) and datetime format models |
570 | * in the Oracle documentation. |
571 | * |
572 | * @param ts the timestamp to format |
573 | * @param format the format pattern to use (if any) |
574 | * @param nlsParam the NLS parameter (if any) |
575 | * @return the formatted timestamp |
576 | */ |
577 | public static String toChar(Timestamp ts, String format, String nlsParam) { |
578 | |
579 | if (format == null) { |
580 | format = "DD-MON-YY HH.MI.SS.FF PM"; |
581 | } |
582 | |
583 | GregorianCalendar cal = new GregorianCalendar(Locale.ENGLISH); |
584 | cal.setTimeInMillis(ts.getTime()); |
585 | StringBuilder output = new StringBuilder(); |
586 | boolean fillMode = true; |
587 | |
588 | for (int i = 0; i < format.length();) { |
589 | |
590 | Capitalization cap; |
591 | |
592 | // AD / BC |
593 | |
594 | if ((cap = containsAt(format, i, "A.D.", "B.C.")) != null) { |
595 | String era = cal.get(Calendar.ERA) == GregorianCalendar.AD ? "A.D." : "B.C."; |
596 | output.append(cap.apply(era)); |
597 | i += 4; |
598 | } else if ((cap = containsAt(format, i, "AD", "BC")) != null) { |
599 | String era = cal.get(Calendar.ERA) == GregorianCalendar.AD ? "AD" : "BC"; |
600 | output.append(cap.apply(era)); |
601 | i += 2; |
602 | |
603 | // AM / PM |
604 | |
605 | } else if ((cap = containsAt(format, i, "A.M.", "P.M.")) != null) { |
606 | String am = cal.get(Calendar.AM_PM) == Calendar.AM ? "A.M." : "P.M."; |
607 | output.append(cap.apply(am)); |
608 | i += 4; |
609 | } else if ((cap = containsAt(format, i, "AM", "PM")) != null) { |
610 | String am = cal.get(Calendar.AM_PM) == Calendar.AM ? "AM" : "PM"; |
611 | output.append(cap.apply(am)); |
612 | i += 2; |
613 | |
614 | // Long/short date/time format |
615 | |
616 | } else if ((cap = containsAt(format, i, "DL")) != null) { |
617 | output.append(new SimpleDateFormat("EEEE, MMMM d, yyyy").format(ts)); |
618 | i += 2; |
619 | } else if ((cap = containsAt(format, i, "DS")) != null) { |
620 | output.append(new SimpleDateFormat("MM/dd/yyyy").format(ts)); |
621 | i += 2; |
622 | } else if ((cap = containsAt(format, i, "TS")) != null) { |
623 | output.append(new SimpleDateFormat("h:mm:ss aa").format(ts)); |
624 | i += 2; |
625 | |
626 | // Day |
627 | |
628 | } else if ((cap = containsAt(format, i, "DDD")) != null) { |
629 | output.append(cal.get(Calendar.DAY_OF_YEAR)); |
630 | i += 3; |
631 | } else if ((cap = containsAt(format, i, "DD")) != null) { |
632 | output.append(String.format("%02d", |
633 | cal.get(Calendar.DAY_OF_MONTH))); |
634 | i += 2; |
635 | } else if ((cap = containsAt(format, i, "DY")) != null) { |
636 | String day = new SimpleDateFormat("EEE").format(ts).toUpperCase(); |
637 | output.append(cap.apply(day)); |
638 | i += 2; |
639 | } else if ((cap = containsAt(format, i, "DAY")) != null) { |
640 | String day = new SimpleDateFormat("EEEE").format(ts); |
641 | if (fillMode) { |
642 | day = StringUtils.pad(day, "Wednesday".length(), " ", true); |
643 | } |
644 | output.append(cap.apply(day)); |
645 | i += 3; |
646 | } else if ((cap = containsAt(format, i, "D")) != null) { |
647 | output.append(cal.get(Calendar.DAY_OF_WEEK)); |
648 | i += 1; |
649 | } else if ((cap = containsAt(format, i, "J")) != null) { |
650 | long millis = ts.getTime() - JULIAN_EPOCH; |
651 | long days = (long) Math.floor(millis / (1000 * 60 * 60 * 24)); |
652 | output.append(days); |
653 | i += 1; |
654 | |
655 | // Hours |
656 | |
657 | } else if ((cap = containsAt(format, i, "HH24")) != null) { |
658 | output.append(new DecimalFormat("00").format(cal.get(Calendar.HOUR_OF_DAY))); |
659 | i += 4; |
660 | } else if ((cap = containsAt(format, i, "HH12")) != null) { |
661 | output.append(new DecimalFormat("00").format(cal.get(Calendar.HOUR))); |
662 | i += 4; |
663 | } else if ((cap = containsAt(format, i, "HH")) != null) { |
664 | output.append(new DecimalFormat("00").format(cal.get(Calendar.HOUR))); |
665 | i += 2; |
666 | |
667 | // Minutes |
668 | |
669 | } else if ((cap = containsAt(format, i, "MI")) != null) { |
670 | output.append(new DecimalFormat("00").format(cal.get(Calendar.MINUTE))); |
671 | i += 2; |
672 | |
673 | // Seconds |
674 | |
675 | } else if ((cap = containsAt(format, i, "SSSSS")) != null) { |
676 | int seconds = cal.get(Calendar.HOUR_OF_DAY) * 60 * 60; |
677 | seconds += cal.get(Calendar.MINUTE) * 60; |
678 | seconds += cal.get(Calendar.SECOND); |
679 | output.append(seconds); |
680 | i += 5; |
681 | } else if ((cap = containsAt(format, i, "SS")) != null) { |
682 | output.append(new DecimalFormat("00").format(cal.get(Calendar.SECOND))); |
683 | i += 2; |
684 | |
685 | // Fractional seconds |
686 | |
687 | } else if ((cap = containsAt(format, i, "FF1", "FF2", |
688 | "FF3", "FF4", "FF5", "FF6", "FF7", "FF8", "FF9")) != null) { |
689 | int x = Integer.parseInt(format.substring(i + 2, i + 3)); |
690 | int ff = (int) (cal.get(Calendar.MILLISECOND) * Math.pow(10, x - 3)); |
691 | output.append(ff); |
692 | i += 3; |
693 | } else if ((cap = containsAt(format, i, "FF")) != null) { |
694 | output.append(cal.get(Calendar.MILLISECOND) * 1000); |
695 | i += 2; |
696 | |
697 | // Time zone |
698 | |
699 | } else if ((cap = containsAt(format, i, "TZR")) != null) { |
700 | TimeZone tz = TimeZone.getDefault(); |
701 | output.append(tz.getID()); |
702 | i += 3; |
703 | } else if ((cap = containsAt(format, i, "TZD")) != null) { |
704 | TimeZone tz = TimeZone.getDefault(); |
705 | boolean daylight = tz.inDaylightTime(new java.util.Date()); |
706 | output.append(tz.getDisplayName(daylight, TimeZone.SHORT)); |
707 | i += 3; |
708 | |
709 | // Week |
710 | |
711 | } else if ((cap = containsAt(format, i, "IW", "WW")) != null) { |
712 | output.append(cal.get(Calendar.WEEK_OF_YEAR)); |
713 | i += 2; |
714 | } else if ((cap = containsAt(format, i, "W")) != null) { |
715 | int w = (int) (1 + Math.floor(cal.get(Calendar.DAY_OF_MONTH) / 7)); |
716 | output.append(w); |
717 | i += 1; |
718 | |
719 | // Year |
720 | |
721 | } else if ((cap = containsAt(format, i, "Y,YYY")) != null) { |
722 | output.append(new DecimalFormat("#,###").format(getYear(cal))); |
723 | i += 5; |
724 | } else if ((cap = containsAt(format, i, "SYYYY")) != null) { |
725 | if (cal.get(Calendar.ERA) == GregorianCalendar.BC) { |
726 | output.append('-'); |
727 | } |
728 | output.append(new DecimalFormat("0000").format(getYear(cal))); |
729 | i += 5; |
730 | } else if ((cap = containsAt(format, i, "YYYY", "IYYY", "RRRR")) != null) { |
731 | output.append(new DecimalFormat("0000").format(getYear(cal))); |
732 | i += 4; |
733 | } else if ((cap = containsAt(format, i, "YYY", "IYY")) != null) { |
734 | output.append(new DecimalFormat("000").format(getYear(cal) % 1000)); |
735 | i += 3; |
736 | } else if ((cap = containsAt(format, i, "YY", "IY", "RR")) != null) { |
737 | output.append(new DecimalFormat("00").format(getYear(cal) % 100)); |
738 | i += 2; |
739 | } else if ((cap = containsAt(format, i, "I", "Y")) != null) { |
740 | output.append(getYear(cal) % 10); |
741 | i += 1; |
742 | |
743 | // Month / quarter |
744 | |
745 | } else if ((cap = containsAt(format, i, "MONTH")) != null) { |
746 | String month = new SimpleDateFormat("MMMM").format(ts); |
747 | if (fillMode) { |
748 | month = StringUtils.pad(month, "September".length(), " ", true); |
749 | } |
750 | output.append(cap.apply(month)); |
751 | i += 5; |
752 | } else if ((cap = containsAt(format, i, "MON")) != null) { |
753 | String month = new SimpleDateFormat("MMM").format(ts); |
754 | output.append(cap.apply(month)); |
755 | i += 3; |
756 | } else if ((cap = containsAt(format, i, "MM")) != null) { |
757 | output.append(String.format("%02d", cal.get(Calendar.MONTH) + 1)); |
758 | i += 2; |
759 | } else if ((cap = containsAt(format, i, "RM")) != null) { |
760 | int month = cal.get(Calendar.MONTH) + 1; |
761 | output.append(cap.apply(toRomanNumeral(month))); |
762 | i += 2; |
763 | } else if ((cap = containsAt(format, i, "Q")) != null) { |
764 | int q = (int) (1 + Math.floor(cal.get(Calendar.MONTH) / 3)); |
765 | output.append(q); |
766 | i += 1; |
767 | |
768 | // Local radix character |
769 | |
770 | } else if ((cap = containsAt(format, i, "X")) != null) { |
771 | char c = DecimalFormatSymbols.getInstance().getDecimalSeparator(); |
772 | output.append(c); |
773 | i += 1; |
774 | |
775 | // Format modifiers |
776 | |
777 | } else if ((cap = containsAt(format, i, "FM")) != null) { |
778 | fillMode = !fillMode; |
779 | i += 2; |
780 | } else if ((cap = containsAt(format, i, "FX")) != null) { |
781 | i += 2; |
782 | |
783 | // Literal text |
784 | |
785 | } else if ((cap = containsAt(format, i, "\"")) != null) { |
786 | for (i = i + 1; i < format.length(); i++) { |
787 | char c = format.charAt(i); |
788 | if (c != '"') { |
789 | output.append(c); |
790 | } else { |
791 | i++; |
792 | break; |
793 | } |
794 | } |
795 | } else if (format.charAt(i) == '-' |
796 | || format.charAt(i) == '/' |
797 | || format.charAt(i) == ',' |
798 | || format.charAt(i) == '.' |
799 | || format.charAt(i) == ';' |
800 | || format.charAt(i) == ':' |
801 | || format.charAt(i) == ' ') { |
802 | output.append(format.charAt(i)); |
803 | i += 1; |
804 | |
805 | // Anything else |
806 | |
807 | } else { |
808 | throw DbException.get(ErrorCode.INVALID_TO_CHAR_FORMAT, format); |
809 | } |
810 | } |
811 | |
812 | return output.toString(); |
813 | } |
814 | |
815 | private static int getYear(Calendar cal) { |
816 | int year = cal.get(Calendar.YEAR); |
817 | if (cal.get(Calendar.ERA) == GregorianCalendar.BC) { |
818 | year--; |
819 | } |
820 | return year; |
821 | } |
822 | |
823 | /** |
824 | * Returns a capitalization strategy if the specified string contains any of |
825 | * the specified substrings at the specified index. The capitalization |
826 | * strategy indicates the casing of the substring that was found. If none of |
827 | * the specified substrings are found, this method returns <code>null</code> |
828 | * . |
829 | * |
830 | * @param s the string to check |
831 | * @param index the index to check at |
832 | * @param substrings the substrings to check for within the string |
833 | * @return a capitalization strategy if the specified string contains any of |
834 | * the specified substrings at the specified index, |
835 | * <code>null</code> otherwise |
836 | */ |
837 | private static Capitalization containsAt(String s, int index, |
838 | String... substrings) { |
839 | for (String substring : substrings) { |
840 | if (index + substring.length() <= s.length()) { |
841 | boolean found = true; |
842 | Boolean up1 = null; |
843 | Boolean up2 = null; |
844 | for (int i = 0; i < substring.length(); i++) { |
845 | char c1 = s.charAt(index + i); |
846 | char c2 = substring.charAt(i); |
847 | if (c1 != c2 && Character.toUpperCase(c1) != Character.toUpperCase(c2)) { |
848 | found = false; |
849 | break; |
850 | } else if (Character.isLetter(c1)) { |
851 | if (up1 == null) { |
852 | up1 = Character.isUpperCase(c1); |
853 | } else if (up2 == null) { |
854 | up2 = Character.isUpperCase(c1); |
855 | } |
856 | } |
857 | } |
858 | if (found) { |
859 | return Capitalization.toCapitalization(up1, up2); |
860 | } |
861 | } |
862 | } |
863 | return null; |
864 | } |
865 | |
866 | /** Represents a capitalization / casing strategy. */ |
867 | private enum Capitalization { |
868 | |
869 | /** |
870 | * All letters are uppercased. |
871 | */ |
872 | UPPERCASE, |
873 | |
874 | /** |
875 | * All letters are lowercased. |
876 | */ |
877 | LOWERCASE, |
878 | |
879 | /** |
880 | * The string is capitalized (first letter uppercased, subsequent |
881 | * letters lowercased). |
882 | */ |
883 | CAPITALIZE; |
884 | |
885 | /** |
886 | * Returns the capitalization / casing strategy which should be used |
887 | * when the first and second letters have the specified casing. |
888 | * |
889 | * @param up1 whether or not the first letter is uppercased |
890 | * @param up2 whether or not the second letter is uppercased |
891 | * @return the capitalization / casing strategy which should be used |
892 | * when the first and second letters have the specified casing |
893 | */ |
894 | public static Capitalization toCapitalization(Boolean up1, Boolean up2) { |
895 | if (up1 == null) { |
896 | return Capitalization.CAPITALIZE; |
897 | } else if (up2 == null) { |
898 | return up1 ? Capitalization.UPPERCASE : Capitalization.LOWERCASE; |
899 | } else if (up1) { |
900 | return up2 ? Capitalization.UPPERCASE : Capitalization.CAPITALIZE; |
901 | } else { |
902 | return Capitalization.LOWERCASE; |
903 | } |
904 | } |
905 | |
906 | /** |
907 | * Applies this capitalization strategy to the specified string. |
908 | * |
909 | * @param s the string to apply this strategy to |
910 | * @return the resultant string |
911 | */ |
912 | public String apply(String s) { |
913 | if (s == null || s.isEmpty()) { |
914 | return s; |
915 | } |
916 | switch (this) { |
917 | case UPPERCASE: |
918 | return s.toUpperCase(); |
919 | case LOWERCASE: |
920 | return s.toLowerCase(); |
921 | case CAPITALIZE: |
922 | return Character.toUpperCase(s.charAt(0)) + |
923 | (s.length() > 1 ? s.toLowerCase().substring(1) : ""); |
924 | default: |
925 | throw new IllegalArgumentException( |
926 | "Unknown capitalization strategy: " + this); |
927 | } |
928 | } |
929 | } |
930 | } |