1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.myfaces.dateformat;
20
21 import java.util.Date;
22 import java.util.LinkedList;
23 import java.util.List;
24
25 /**
26 * A reimplementation of the java.text.SimpleDateFormat class.
27 * <p>
28 * This class has been created for use with the tomahawk InputCalendar
29 * component. It exists for the following reasons:
30 * <ul>
31 * <li>The java.text.SimpleDateFormat class is simply broken with respect
32 * to "week of year" functionality.
33 * <li>The inputCalendar needs a javascript equivalent of SimpleDateFormat
34 * in order to process data in the popup calendar. But it is hard to
35 * unit-test javascript code. By maintaining a version in Java that is
36 * unit-tested, then making the javascript version a direct "port" of that
37 * code the javascript gets improved reliability.
38 * <li>Documentation is necessary for this code, but it is not desirable to
39 * add lots of docs to a javascript file that is downloaded. The javascript
40 * version can simply reference the documentation here.
41 * </ul>
42 * Note that the JODA project also provides a SimpleDateFormat implementation,
43 * but that does not support firstDayOfWeek functionality. In any case,
44 * it is not desirable to add a dependency from tomahawk on JODA just for
45 * the InputCalendar.
46 * <p>
47 * This implementation does extend the SimpleDateFormat class by adding the
48 * JODA "xxxx" yearOfWeekYear format option, as this is missing in the
49 * standard SimpleDateFormat class.
50 * <p>
51 * The parse methods also return null on error rather than throw an exception.
52 * <p>
53 * The code here was originally written in javascript (date.js), and has been
54 * ported to java.
55 * <p>
56 * At the current time, the following format options are NOT supported:
57 * <code>DFkKSzZ</code>.
58 * <p>
59 * <h2>Week Based Calendars</h2>
60 * <p>
61 * ISO standard ISO-8601 defines a calendaring system based not upon
62 * year/month/day_in_month but instead year/week/day_in_week. This is
63 * particularly popular in embedded systems as date arithmetic is
64 * much simpler; there are no irregular month lengths to handle.
65 * <p>
66 * The only tricky part is mapping to and from year/month/day formats.
67 * Unfortunately, while java.text.SimpleDateFormat does support a "ww"
68 * week format character, it has a number of flaws.
69 * <p>
70 * Weeks are always complete and discrete, ie week yyyy-ww always has
71 * 7 days in it, and never "shares" days with yyyy-(ww+1). However to
72 * achieve this, the last week of a year might include a few days of
73 * the next year, or the last few days of a year might be counted as
74 * part of the first week of the following year. The decision is made
75 * depending on which year the "majority" of days in that week belong to.
76 * <p>
77 * With ISO-8601, a week always starts on a monday. However many countries
78 * use a different convention, starting weeks on saturday, sunday or monday.
79 * This class supports setting the firstDayOfWeek.
80 *
81 * @since 1.1.7
82 * @author Simon Kitching (latest modification by $Author: grantsmith $)
83 * @version $Revision: 472638 $ $Date: 2006-11-08 15:54:13 -0500 (Wed, 08 Nov 2006) $
84 */
85 public class SimpleDateFormatter
86 {
87 private static final long MSECS_PER_SEC = 1000;
88 private static final long MSECS_PER_MIN = 60 * MSECS_PER_SEC;
89 private static final long MSECS_PER_HOUR = 60 * MSECS_PER_MIN;
90 private static final long MSECS_PER_DAY = 24 * MSECS_PER_HOUR;
91 private static final long MSECS_PER_WEEK = 7 * MSECS_PER_DAY;
92
93 // ======================================================================
94 // Static Week-handling Methods
95 // ======================================================================
96
97 /**
98 * Cumulative sum of the number of days in the year up to the first
99 * day of each month.
100 */
101 private static final int[] MONTH_LEN =
102 {
103 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334
104 };
105
106 /**
107 * Return the ISO week# represented by the specified date (1..53).
108 *
109 * This implements the ISO-8601 standard for week numbering, as documented in
110 * Klaus Tondering's Calendar document, version 2.8:
111 * http://www.tondering.dk/claus/calendar.html
112 *
113 * For dates in January and February, calculate:
114 *
115 * a = year-1
116 * b = a/4 - a/100 + a/400
117 * c = (a-1)/4 - (a-1)/100 + (a-1)/400
118 * s = b-c
119 * e = 0
120 * f = day - 1 + 31*(month-1)
121 *
122 * For dates in March through December, calculate:
123 *
124 * a = year
125 * b = a/4 - a/100 + a/400
126 * c = (a-1)/4 - (a-1)/100 + (a-1)/400
127 * s = b-c
128 * e = s+1
129 * f = day + (153*(month-3)+2)/5 + 58 + s
130 *
131 * Then, for any month continue thus:
132 *
133 * g = (a + b) mod 7
134 * d = (f + g - e) mod 7
135 * n = f + 3 - d
136 *
137 * We now have three situations:
138 *
139 * If n<0, the day lies in week 53-(g-s)/5 of the previous year.
140 * If n>364+s, the day lies in week 1 of the coming year.
141 * Otherwise, the day lies in week n/7 + 1 of the current year.
142 *
143 * This algorithm gives you a couple of additional useful values:
144 *
145 * d indicates the day of the week (0=Monday, 1=Tuesday, etc.)
146 * f+1 is the ordinal number of the date within the current year.
147 *
148 * Note that ISO-8601 specifies that week1 of a year is the first week in
149 * which the majority of days lie in that year. An equivalent description
150 * is that it is the first week including the 4th of january. This means
151 * that the 1st, 2nd and 3rd of January might lie in the last week of the
152 * previous year, and that the last week of a year may include the first
153 * few days of the following year.
154 *
155 * ISO-8601 also specifies that the first day of the week is always Monday.
156 *
157 * This function returns the week number regardless of which year it lies in.
158 * That means that asking for the week# of 01/01/yyyy might return 52 or 53,
159 * and asking for the week# of 31/12/yyyy might return 1.
160 */
161 public static WeekDate getIsoWeekDate(Date date)
162 {
163 int year = fullYearFromDate(date.getYear());
164 int month = date.getMonth() + 1;
165 int day = date.getDate();
166
167 int a,b,c,d,e,f,g,s,n;
168
169 if (month <= 2)
170 {
171 a = year - 1;
172 b = (int) Math.floor(a/4) - (int) Math.floor(a/100) + (int) Math.floor(a/400);
173 c = (int) Math.floor((a-1)/4) - (int) Math.floor((a-1)/100) + (int) Math.floor((a-1)/400);
174 s = b - c;
175 e = 0;
176 f = day - 1 + 31*(month-1);
177 }
178 else
179 {
180 a = year;
181 b = (int) Math.floor(a/4) - (int) Math.floor(a/100) + (int) Math.floor(a/400);
182 c = (int) Math.floor((a-1)/4) - (int) Math.floor((a-1)/100) + (int) Math.floor((a-1)/400);
183 s = b - c;
184 e = s + 1;
185 f = day + (int) Math.floor((153*(month-3) + 2)/5) + 58 + s;
186 }
187
188 g = (a + b) % 7;
189 d = (f + g - e) % 7;
190 n = f + 3 - d;
191
192 if (n<0)
193 {
194 // previous year
195 int resultWeek = 53 - (int) Math.floor((g-s)/5);
196 return new WeekDate(year-1, resultWeek);
197 }
198 else if (n > (364+s))
199 {
200 // next year
201 int resultWeek = 1;
202 return new WeekDate(year+1, resultWeek);
203 }
204 else
205 {
206 // current year
207 int resultWeek = (int) Math.floor(n/7) + 1;
208 return new WeekDate(year, resultWeek);
209 }
210 }
211
212 /** Return true if the specified year is a leapyear (has 29 days in feb). */
213 private static boolean isLeapYear(int year)
214 {
215 return ((year%4 == 0) && (year%100 != 0)) || (year%400 == 0);
216 }
217
218 /**
219 * Compute which day of the week (sun,mon, etc) a particular date
220 * falls on.
221 * <p>
222 * Returns 0 for sunday, 1 for monday, 6 for saturday (the java.util.Date
223 * and the javascript Date convention):
224 * <p>
225 * Note that java.util.Calendar uses 1=sun, 7=sat.
226 * <p>
227 * This algorithm is documented as part of the RFC3339 specification.
228 *
229 * @param year is full year value (eg 2007).
230 * @param month is 1..12
231 * @param day is 1..31
232 */
233 private static int dayOfWeek(int year, int month, int day)
234 {
235 /* adjust months so February is the last one */
236 month -= 2;
237 if (month < 1)
238 {
239 month += 12;
240 --year;
241 }
242
243 /* split by century */
244 int cent = year / 100;
245 year %= 100;
246
247 // dow (0=sunday)
248 int base =
249 (26 * month - 2) / 10
250 + day
251 + year
252 + (year / 4)
253 + (cent / 4)
254 + (5 * cent);
255
256 int dow = base % 7;
257
258 return dow;
259 }
260
261 /**
262 * Return the (year, week) representation of the given date.
263 * <p>
264 * This is exactly like getIsoWeekNumber, except that a firstDayOfWeek
265 * can be specified; ISO-8601 hard-wires "monday" as first day of week.
266 * <p>
267 * TODO: support minimumDaysInWeek property. Currently, assumes
268 * this is set to 4 (the ISO standard).
269 * <p>
270 * @param firstDayOfWeek is: 0=sunday, 1=monday, 6=sat. This is the
271 * convention used by java.util.Date. NOTE: java.util.Calendar uses
272 * 1=sunday, 2=monday, 7=saturday.
273 */
274 public static WeekDate getWeekDate(Date date, int firstDayOfWeek)
275 {
276 int year = fullYearFromDate(date.getYear());
277 int month = date.getMonth() + 1;
278 int day = date.getDate();
279
280 boolean thisIsLeapYear = isLeapYear(year);
281
282 int dayOfYear = day + MONTH_LEN[month-1];
283 if (thisIsLeapYear && (month>2))
284 {
285 ++dayOfYear;
286 }
287
288 int jan1Weekday = dayOfWeek(year, 1, 1);
289
290 // The first week of a year always starts on firstDayOfWeek. However that
291 // week starts up to 3 days before the 1st of the year, or 3 days after.
292 //
293 // Here, we find where the first week actually starts, measured as an
294 // offset from the first day of the year (-3..+3).
295 //
296 // Examples:
297 // * if firstDayOfWeek=mon, and 1st jan is wed, then pivotOffset=-2,
298 // ie 30 dec of previous year is where the first week starts.
299 // * if firstDayOfWeek=sun and 1st jan is fri, then pivotOffset=2,
300 // ie 3 jan is where the first week starts.
301 int pivotOffset = firstDayOfWeek - jan1Weekday;
302 if (pivotOffset > 3)
303 {
304 pivotOffset -= 7;
305 }
306 else if (pivotOffset < -3)
307 {
308 pivotOffset += 7;
309 }
310
311 // Compute the offset of date relative to the start of this year.
312 // This will be in range 0..364 (or 365 for leap year)
313 int dayOffset = dayOfYear-1;
314 if (dayOffset < pivotOffset)
315 {
316 // This date falls in either week52 or week53 of the previous year
317 //
318 // Because (365%7)=1, the pivotOffset moves forweards by one if the previous
319 // year is a normal one, or two if the previous year is a leapyear (wrapping
320 // around from +3 to -3). And a year has 53 weeks only when its pivotOffset
321 // is -3 (or -2 for leapyear).
322 //
323 // so:
324 // when prev is not leapyear, has53 when pivotOffset is 3 for this year.
325 // when prev is leapyear, has53 when pivotOffset is 2 or 3 for this year.
326 boolean prevIsLeapYear = isLeapYear(year-1);
327 if ((pivotOffset==3) || ((pivotOffset==2) && prevIsLeapYear))
328 {
329 return new WeekDate(year-1, 53);
330 }
331 return new WeekDate(year-1, 52);
332 }
333
334 // Compute the number of days relative to the start of the first
335 // week in this year, then divide by seven to get the week count.
336 int daysFromFirstWeekStart = (dayOfYear - 1 - pivotOffset);
337 int weekNumber = daysFromFirstWeekStart/7 + 1;
338
339 // In a normal year, there are 52 weeks with 1 day (365%7) left over.
340 //
341 // So, when weeks start on the first day of a year, there is one day left
342 // at the end, which will fall into the first week of the next year. When
343 // weeks start on the 2nd, then week 52 ends on 31 dec. When weeks start on
344 // the max pivotOffset of +3, then week52 includes 3jan of next year. It is
345 // still week52 because only 3 days are from the next year adn 4 are in the
346 // current year.
347 //
348 // But when pivotOffset is -3, then there are 4 days left over at the end of
349 // the year - making week 53. And in a leap year, pivotOffset=-2 is sufficient
350 // to create a week53.
351 if ((weekNumber < 53)
352 || (pivotOffset==-3)
353 || (pivotOffset==-2 && thisIsLeapYear))
354 {
355 return new WeekDate(year, weekNumber);
356 }
357 else
358 {
359 // weekNumber=53, but this year only has 52 weeks so this must be week
360 // one of the next year.
361 return new WeekDate(year+1, 1);
362 }
363 }
364
365 /**
366 * Return the point in time at which the first week of the specified year starts.
367 */
368 private static long getStartOfWeekYear(int year, int firstDayOfWeek)
369 {
370 // Create a new date on the 1st.
371 Date d1 = new Date(shortYearFromDate(year), 0, 1, 0, 0, 0);
372
373 // adjust forward or backwards to the nearest firstDayOfWeek
374 int firstDayOfYear = d1.getDay(); // 0 = sunday
375 int dayDiff = firstDayOfWeek - firstDayOfYear;
376 int dayShift;
377 if (dayDiff >= 4)
378 {
379 dayShift = 7-dayDiff;
380 }
381 else if (dayDiff >= 0)
382 {
383 dayShift = dayDiff;
384 }
385 else if (dayDiff >= -3)
386 {
387 dayShift = dayDiff;
388 } else
389 {
390 dayShift = 7 + dayDiff;
391 }
392
393 // now compute the number of weeks between start of weekYear and input date.
394 long weekYearStartMsecs = d1.getTime() + (dayShift* MSECS_PER_DAY);
395 return weekYearStartMsecs;
396 }
397
398 /**
399 * This is the inverse of method getJavaWeekNumber.
400 */
401 private static Date getDateForWeekDate
402 (
403 int year, int week, int day,
404 int hour, int min, int sec,
405 int firstDayOfWeek)
406 {
407 long msecsBase = getStartOfWeekYear(year, firstDayOfWeek);
408
409 long msecsOffset = (week - 1) * MSECS_PER_WEEK;
410 msecsOffset += (day-1) * MSECS_PER_DAY;
411 msecsOffset += hour * MSECS_PER_HOUR;
412 msecsOffset += min * MSECS_PER_MIN;
413 msecsOffset += sec * MSECS_PER_SEC;
414
415 Date finalDate = new Date();
416 finalDate.setTime(msecsBase + msecsOffset);
417 return finalDate;
418 }
419
420 // ======================================================================
421 // Static Generic Date Manipulation Methods
422 // ======================================================================
423
424 private static int fullYearFromDate(int year)
425 {
426 if (year < 1900)
427 {
428 return year + 1900;
429 }
430 else
431 {
432 return year;
433 }
434 }
435
436 private static int shortYearFromDate(int year)
437 {
438 if (year > 1900)
439 {
440 return year - 1900;
441 }
442 else
443 {
444 return year;
445 }
446 }
447
448 private static Date createDateFromContext(ParserContext context)
449 {
450 Date date;
451 if (context.weekOfWeekYear != 0)
452 {
453 date = getDateForWeekDate(
454 context.weekYear, context.weekOfWeekYear, context.day,
455 context.hour, context.min, context.sec,
456 context.firstDayOfWeek);
457 }
458 else
459 {
460 // Class java.util.Date expects year to be relative to 1900. Note that
461 // this is different for javascript Date class - that takes a year
462 // relative to 0AD.
463 date = new Date(
464 context.year - 1900, context.month, context.day,
465 context.hour, context.min, context.sec);
466 }
467 return date;
468 }
469
470 /**
471 * Return a substring starting from a specific location, and extending
472 * len characters.
473 * <p>
474 * It is an error if s is null.
475 * It is an error if s.length <= start.
476 * <p>
477 * It is NOT an error if s.length < start+len; in this case a string
478 * starting at "start" but less than len characters will be returned.
479 */
480 private static String substr(String s, int start, int len)
481 {
482 String s2 = s.substring(start);
483 if (s2.length() <= len)
484 return s2;
485 else
486 return s2.substring(0, len);
487 }
488
489 // ======================================================================
490 // Static Parsing Methods
491 // ======================================================================
492
493 /**
494 * Parse a string according to the provided sequence of parsing ops.
495 * <p>
496 * Returns a ParserContext object that has its year/month/day etc fields
497 * set according to data extracted from the string.
498 * <p>
499 * If an error has occured during parsing, context.invalid will be true.
500 */
501 private static ParserContext parseOps(
502 DateFormatSymbols symbols, boolean yearIsWeekYear,
503 int firstDayOfWeek,
504 String[] ops, String dateStr)
505 {
506
507 ParserContext context = new ParserContext(firstDayOfWeek);
508
509 int dateIndex = 0;
510 int dateStrLen = dateStr.length();
511 for(int i=0; (i<ops.length) && (dateIndex < dateStrLen); ++i)
512 {
513 String op = ops[i];
514 String optype = op.substring(0, 2);
515 String opval = op.substring(2);
516
517 if (optype.equals("f:"))
518 {
519 parsePattern(symbols, yearIsWeekYear, context, opval, dateStr, dateIndex);
520
521 if ((context.newIndex < 0) || context.invalid)
522 break;
523
524 dateIndex = context.newIndex;
525 }
526 else if (optype.equals("q:") || optype.equals("l:"))
527 {
528 // verify that opval matches the next chars in dateStr
529 int oplen = opval.length();
530 String s = substr(dateStr, dateIndex, oplen);
531 if (!opval.equals(s))
532 {
533 context.invalid = true;
534 break;
535 }
536 dateIndex += oplen;
537 }
538 }
539
540 if (dateIndex < dateStrLen)
541 {
542 // TOMAHAWK-1390
543 //Remaining chars are on the string. All chars should be processed, otherwise
544 //the dateStr is invalid
545 context.invalid = true;
546 }
547
548 return context;
549 }
550
551 /**
552 * Handle parsing of a single property, eg "yyyy" or "EEE".
553 */
554 private static void parsePattern(DateFormatSymbols symbols, boolean yearIsWeekYear,
555 ParserContext context, String patternSub,
556 String dateStr, int dateIndex)
557 {
558
559 char c = patternSub.charAt(0);
560 int patlen = patternSub.length();
561 context.newIndex = dateIndex;
562
563 if (c == 'y')
564 {
565 int year = parseNum(context, dateStr, 4, dateIndex);
566 if ((context.newIndex-dateIndex) < 4)
567 {
568 // see method adjustTwoDigitYear
569 context.year = year;
570 context.ambiguousYear = true;
571 }
572 else
573 {
574 context.year = year;
575 }
576
577 if (yearIsWeekYear)
578 {
579 // There is a "ww" pattern present, so set weekYear as well as year.
580 context.weekYear = context.year;
581 context.ambiguousWeekYear = context.ambiguousYear;
582 }
583 }
584 else if (c == 'x')
585 {
586 // extension to standard java.text.SimpleDateFormat class, to support the
587 // JODA "weekYear" formatter.
588 int year = parseNum(context, dateStr, 4, dateIndex);
589
590 if ((context.newIndex-dateIndex) < 4)
591 {
592 context.weekYear = year;
593 context.ambiguousWeekYear = true;
594 }
595 else
596 {
597 context.weekYear = year;
598 }
599 }
600 else if (c == 'M')
601 {
602 if (patlen == 3)
603 {
604 String fragment = substr(dateStr, dateIndex, 3);
605 int index = parseIndexOf(context, symbols.shortMonths, fragment);
606 if (index != -1)
607 {
608 context.month = index;
609 }
610 }
611 else if (patlen >= 4)
612 {
613 String fragment = dateStr.substring(dateIndex);
614 int index = parsePrefixOf(context, symbols.months, fragment);
615 if (index != -1)
616 {
617 context.month = index;
618 }
619 }
620 else
621 {
622 context.month = parseNum(context, dateStr, 2, dateIndex) - 1;
623 }
624 }
625 else if (c == 'd')
626 {
627 context.day = parseNum(context, dateStr, 2, dateIndex);
628 }
629 else if (c == 'E')
630 {
631 if (patlen <= 3)
632 {
633 String fragment = dateStr.substring(dateIndex, dateIndex+3);
634 int index = parseIndexOf(context, symbols.shortWeekdays, fragment);
635 if (index != -1)
636 {
637 context.dayOfWeek = index;
638 }
639 }
640 else
641 {
642 String fragment = dateStr.substring(dateIndex);
643 int index = parsePrefixOf(context, symbols.weekdays, fragment);
644 if (index != -1)
645 {
646 context.dayOfWeek = index;
647 }
648 }
649 }
650 else if (c == 'H')
651 {
652 // H is in range 0..23
653 context.hour = parseNum(context, dateStr, 2, dateIndex);
654 }
655 else if (c == 'h')
656 {
657 // h is in range 1am..12pm or 1pm-12am.
658 // Note that this field is later post-adjusted
659 context.hourAmpm = parseNum(context, dateStr, 2, dateIndex);
660 }
661 else if (c == 'm')
662 {
663 context.min = parseNum(context, dateStr, 2, dateIndex);
664 }
665 else if (c == 's')
666 {
667 context.sec = parseNum(context, dateStr, 2, dateIndex);
668 }
669 else if (c == 'a')
670 {
671 context.ampm = parseString(context, dateStr, dateIndex, symbols.ampms);
672 }
673 else if (c == 'w')
674 {
675 context.weekOfWeekYear = parseNum(context, dateStr, 2, dateIndex);
676 }
677 else
678 {
679 context.invalid = true;
680 }
681 }
682
683 /**
684 * Convert a string of digits (in base 10) to an integer.
685 * <p>
686 * Only positive values are accepted. Returns -1 on failure.
687 */
688 private static int parseInt(String value)
689 {
690 int sum = 0;
691 for(int i=0; i< value.length(); ++i)
692 {
693 char c = value.charAt(i);
694
695 if ((c<'0') || (c>'9'))
696 {
697 return -1;
698 }
699 sum = sum*10 + (c-'0');
700 }
701 return sum;
702 }
703
704 /**
705 * Convert at most the next nChars characters to numeric, starting from offset dateIndex
706 * within dateStr.
707 * <p>
708 * Updates context.newIndex to contain the offset of the next unparsed char.
709 */
710 private static int parseNum(ParserContext context, String dateStr, int nChars, int dateIndex)
711 {
712 // Try to convert the most possible characters (nChars). If that fails,
713 // then try again without the last character. Repeat until successful
714 // numeric conversion occurs.
715 int nToParse = Math.min(nChars, dateStr.length() - dateIndex);
716 for(int i=nToParse;i>0;i--)
717 {
718 String numStr = dateStr.substring(dateIndex,dateIndex+i);
719 int value = parseInt(numStr);
720
721 if(value == -1)
722 continue;
723
724 context.newIndex = dateIndex+i;
725 return value;
726 }
727
728 context.newIndex = -1;
729 context.invalid = true;
730 return -1;
731 }
732
733 /**
734 * Return the index of the array element which matches the provided string.
735 * <p>
736 * This is used when the next thing in value (string being parsed) is expected
737 * to be one of the values in the provided array, AND all the array entries
738 * are of the same length. The appropriate sequence of chars can then be
739 * extracted from the string to parse, and passed here as the exact value
740 * to be matched.
741 */
742 private static int parseIndexOf(ParserContext context, String[] array, String value)
743 {
744 for(int i=0; i<array.length; ++i)
745 {
746 String s = array[i];
747 if (value.equals(s))
748 {
749 context.newIndex += s.length();
750 return i;
751 }
752 }
753 context.invalid = true;
754 context.newIndex = -1;
755 return -1;
756 }
757
758 /**
759 * Return the index of the array element which is a prefix of the value string.
760 * <p>
761 * This is used when the next thing in value (string being parsed) is expected
762 * to be one of the values in the provided array.
763 * <p>
764 * This is like indexOf, except that an exact match is not expected.
765 */
766 private static int parsePrefixOf(ParserContext context, String[] array, String value)
767 {
768 for(int i=0; i<array.length; ++i)
769 {
770 String s = array[i];
771 if (value.startsWith(s))
772 {
773 context.newIndex += s.length();
774 return i;
775 }
776 }
777 context.invalid = true;
778 context.newIndex = -1;
779 return -1;
780 }
781
782 /**
783 * This is used when parsing is currently at location dateIndex within the date string,
784 * and one of the values in the strings array is now expected.
785 * <p>
786 * Returns an index into the strings array, or -1 if none match.
787 * <p>
788 * Also updates context.newIndex to be the location after the matched string (if any).
789 * On failure, the context.invalid flag is set before returning -1.
790 */
791 private static int parseString(ParserContext context, String dateStr, int dateIndex, String[] strings)
792 {
793 String fragment = dateStr.substring(dateIndex);
794 return parsePrefixOf(context, strings, fragment);
795 }
796
797 /**
798 * Handle fields that need to be processed after all information is available.
799 */
800 private static void parsePostProcess(DateFormatSymbols symbols, ParserContext context)
801 {
802 if (context.ambiguousYear)
803 {
804 // TODO: maybe this adjustment could be made while parsing?
805
806 context.year += 1900;
807 Date date = createDateFromContext(context);
808 Date threshold = symbols.twoDigitYearStart;
809 if (date.getTime() < threshold.getTime())
810 {
811 context.year += 100;
812 }
813 }
814
815 if (context.hourAmpm > 0)
816 {
817 // yes, the user has set the hour using 12-hour clock
818 // 01am->01, 11am->11, 12pm->12, 1pm->13, 11pm->23, 12pm->00
819 if (context.ampm == 1)
820 {
821 context.hour = context.hourAmpm + 12;
822 if (context.hour == 24)
823 context.hour = 0;
824 }
825 else
826 {
827 context.hour = context.hourAmpm;
828 }
829 }
830 }
831
832 // ======================================================================
833 // Static Formatting Methods
834 // ======================================================================
835
836 private static String formatOps(
837 DateFormatSymbols symbols, boolean yearIsWeekYear,
838 int firstDayOfWeek,
839 String[] ops, Date date)
840 {
841 ParserContext context = new ParserContext(firstDayOfWeek);
842
843 context.year = fullYearFromDate(date.getYear());
844 context.month = date.getMonth();
845 context.day = date.getDate();
846 context.dayOfWeek = date.getDay();
847 context.hour = date.getHours();
848 context.min = date.getMinutes();
849 context.sec = date.getSeconds();
850
851 // 00 --> 12am, 01->1am, 12 --> 12pm, 13 -> 1pm, 23->11pm
852 context.ampm = (context.hour < 12) ? 0 : 1;
853
854 WeekDate weekDate = getWeekDate(date, firstDayOfWeek);
855 context.weekYear = weekDate.getYear();
856 context.weekOfWeekYear = weekDate.getWeek();
857
858 StringBuffer str = new StringBuffer();
859 for(int i=0; i<ops.length; ++i)
860 {
861 String op = ops[i];
862 String optype = op.substring(0, 2);
863 String opval = op.substring(2);
864
865 if (optype.equals("f:"))
866 {
867 formatPattern(symbols, context, opval, yearIsWeekYear, str);
868 if (context.invalid)
869 break;
870 }
871 else if (optype.equals("l:"))
872 {
873 // Just copy the literal sequence
874 str.append(opval);
875 }
876 else if (optype.equals("q:"))
877 {
878 // Just copy the literal sequence
879 str.append(opval);
880 }
881 }
882
883 if (context.invalid)
884 return null;
885 else
886 return str.toString();
887 }
888
889 private static void formatPattern(DateFormatSymbols symbols, ParserContext context,
890 String patternSub, boolean yearIsWeekYear, StringBuffer out)
891 {
892 char c = patternSub.charAt(0);
893 int patlen = patternSub.length();
894
895 if (c == 'y')
896 {
897 if (!yearIsWeekYear)
898 formatNum(context.year, patlen <= 3 ? 2 : 4, true, out);
899 else
900 formatNum(context.weekYear, patlen <= 3 ? 2 : 4, true, out);
901 }
902 else if (c == 'x')
903 {
904 formatNum(context.weekYear, patlen <= 3 ? 2 : 4, true, out);
905 }
906 else if (c == 'M')
907 {
908 if (patlen == 3)
909 {
910 out.append(symbols.shortMonths[context.month]);
911 }
912 else if (patlen >= 4)
913 {
914 out.append(symbols.months[context.month]);
915 }
916 else
917 {
918 formatNum(context.month+1, patlen, false, out);
919 }
920 }
921 else if (c == 'd')
922 {
923 formatNum(context.day, patlen, false, out);
924 }
925 else if (c == 'E')
926 {
927 if (patlen <= 3)
928 {
929 out.append(symbols.shortWeekdays[context.dayOfWeek]);
930 }
931 else
932 {
933 out.append(symbols.weekdays[context.dayOfWeek]);
934 }
935 }
936 else if (c == 'H')
937 {
938 // output hour in range 0..23
939 formatNum(context.hour, patlen, false, out);
940 }
941 else if (c == 'h')
942 {
943 // output hour in range 1..12:
944 // 00 --> 12am, 01->1am, 12 --> 12pm, 13 -> 1pm, 23->11pm
945 int hour = context.hour;
946 if (hour == 0)
947 {
948 hour = 12; // 12am
949 }
950 else if (hour > 12)
951 {
952 hour = hour - 12;
953 }
954 formatNum(hour, patlen, false, out);
955 }
956 else if (c == 'm')
957 {
958 formatNum(context.min, patlen, false, out);
959 }
960 else if (c == 's')
961 {
962 formatNum(context.sec, patlen, false, out);
963 }
964 else if (c == 'a')
965 {
966 out.append(symbols.ampms[context.ampm]);
967 }
968 else if (c == 'w')
969 {
970 formatNum(context.weekOfWeekYear, patlen, false, out);
971 }
972 else
973 {
974 context.invalid = true;
975 }
976 }
977
978 /**
979 * Write out an integer padded with leading zeros to a specified width.
980 * <p>
981 * If ensureLength is set, and the number is longer than length, then display only the
982 * rightmost length digits.
983 */
984 private static void formatNum(int num, int length, boolean ensureLength, StringBuffer out)
985 {
986 String str = String.valueOf(num);
987 while (str.length() < length)
988 {
989 str = "0" + str;
990 }
991
992 // XXX do we have to distinguish left and right 'cutting'
993 //ensureLength - enable cutting only for parameters like the year, the other
994 if (ensureLength && str.length() > length)
995 {
996 str = str.substring(str.length() - length);
997 }
998
999 out.append(str);
1000 }
1001
1002 // ======================================================================
1003 // Pattern Processing Methods
1004 // ======================================================================
1005
1006 /**
1007 * Given a date parsing or formatting pattern, split it up into an
1008 * array of separate pieces to be processed.
1009 * <p>
1010 * Each piece is either:
1011 * <ul>
1012 * <li> a "format" section
1013 * <li> a "quote" section,
1014 * <li> a "literal" section, or
1015 * </ul>
1016 * <p>
1017 * A format section is a sequence of 1 or more identical alphabetical
1018 * characters, eg "yyyy", "MMM" or "dd". When parsing, this indicates what
1019 * data is expected next; if it is not a recognised sequence then it is
1020 * just ignored. When formatting, this indicates which part of the provided
1021 * date object should be output, and how to format it; if it is not a
1022 * recognised sequence then it is simply written literally to the output.
1023 * <p>
1024 * A quote section is something in the pattern that was enclosed in quote
1025 * marks. When parsing, quote sections are expected to be present in exactly
1026 * the same form in the input string; an error is reported if the data is
1027 * not present. When formatting, quote sections are output literally as
1028 * they occurred in the pattern.
1029 * <p>
1030 * A literal section is a sequence of 1 or more non-quoted non-alphabetical
1031 * characters, eg "-" or "+++". When parsing, literal sections just cause
1032 * the same number of characters in the input stream to be skipped. When
1033 * formatting, they are just output literally.
1034 * <p>
1035 * The elements of the string array returned are of form "f:xxxx" (format
1036 * section), "q:text" (quote section), or "l:-" (literal section).
1037 * <p>
1038 * TODO: when formatting, should literal chars really just cause skipping?
1039 */
1040 private static String[] analysePattern(String pattern)
1041 {
1042 int patternIndex = 0;
1043 int patternLen = pattern.length();
1044 char lastChar = 0;
1045 StringBuffer patternSub = null;
1046 boolean quoteMode = false;
1047
1048 List ops = new LinkedList();
1049
1050 while (patternIndex < patternLen)
1051 {
1052 char currentChar = pattern.charAt(patternIndex);
1053 char nextChar;
1054
1055 if (patternIndex < patternLen - 1)
1056 {
1057 nextChar = pattern.charAt(patternIndex + 1);
1058 }
1059 else
1060 {
1061 nextChar = 0;
1062 }
1063
1064 if (currentChar == '\'' && lastChar != '\\')
1065 {
1066 if (patternSub != null)
1067 {
1068 ops.add(patternSub.toString());
1069 patternSub = null;
1070 }
1071 quoteMode = !quoteMode;
1072 }
1073 else if (quoteMode)
1074 {
1075 if (patternSub == null)
1076 {
1077 patternSub = new StringBuffer("q:");
1078 }
1079 patternSub.append(currentChar);
1080 }
1081 else
1082 {
1083 if (currentChar == '\\' && lastChar != '\\')
1084 {
1085 // do nothing
1086 }
1087 else
1088 {
1089 if (patternSub == null)
1090 {
1091 if (Character.isLetter(currentChar))
1092 {
1093 patternSub = new StringBuffer("f:");
1094 }
1095 else
1096 {
1097 patternSub = new StringBuffer("l:");
1098 }
1099 }
1100
1101 patternSub.append(currentChar);
1102 if (currentChar != nextChar)
1103 {
1104 ops.add(patternSub.toString());
1105 patternSub = null;
1106 }
1107 }
1108 }
1109
1110 patternIndex++;
1111 lastChar = currentChar;
1112 }
1113
1114 if (patternSub != null)
1115 {
1116 ops.add(patternSub.toString());
1117 }
1118
1119 String[] data = new String[ops.size()];
1120 return (String[]) ops.toArray(data);
1121 }
1122
1123 /**
1124 * Determine whether to make the "yyyy" pattern behave in a non-standard manner.
1125 * <p>
1126 * The java.text.SimpleDateFormat class has no option to output the "weekyear"
1127 * property, ie the year in which the "ww" value occurs. This makes the "ww"
1128 * formatter basically useless.
1129 * <p>
1130 * This class therefore implements the JODA "xxxx" formatter that does exactly
1131 * that. However many people will use "ww/yyyy" patterns without realising that
1132 * this generates garbage (eg 01/2000 when it should output 01/2001 because the
1133 * week has rolled over from one year to the next). This therefore checks whether
1134 * ww is present in the pattern string, and if so makes yy work like xx. Of
1135 * course this does not allow patterns like "xxxx-ww yyyy-MM-dd", so we then
1136 * disable this hack if "xx" is also present.
1137 */
1138 private static boolean hasWeekPattern(String[] ops)
1139 {
1140 boolean wwPresent = false;
1141 boolean xxPresent = false;
1142 for(int i=0; i<ops.length; ++i)
1143 {
1144 String s = ops[i];
1145 wwPresent = wwPresent || s.startsWith("f:ww");
1146 xxPresent = xxPresent || s.startsWith("f:xx");
1147 }
1148
1149 return wwPresent && !xxPresent;
1150 }
1151
1152 // ======================================================================
1153 // Instance methods
1154 // ======================================================================
1155
1156 private DateFormatSymbols symbols;
1157
1158 private String[] ops;
1159 boolean yearIsWeekYear;
1160 int firstDayOfWeek;
1161
1162 /**
1163 * Constructor.
1164 *
1165 * @param firstDayOfWeek uses java.util.Date convention, ie 0=sun, 1=mon, 6=sat.
1166 */
1167 public SimpleDateFormatter(String pattern, DateFormatSymbols symbols, int firstDayOfWeek)
1168 {
1169 if (symbols == null)
1170 {
1171 this.symbols = new DateFormatSymbols();
1172 }
1173 else
1174 {
1175 this.symbols = symbols;
1176 }
1177
1178 this.ops = analysePattern(pattern);
1179 this.yearIsWeekYear = hasWeekPattern(ops);
1180
1181 this.firstDayOfWeek = firstDayOfWeek;
1182 }
1183
1184 /**
1185 * Constructor that sets the firstDayOfWeek to the ISO standard (1=monday).
1186 */
1187 public SimpleDateFormatter(String pattern, DateFormatSymbols symbols)
1188 {
1189 this(pattern, symbols, 1);
1190 }
1191
1192 public void setFirstDayOfWeek(int dow)
1193 {
1194 this.firstDayOfWeek = dow;
1195 }
1196
1197 public Date parse(String dateStr)
1198 {
1199 if ((dateStr==null) || (dateStr.length() == 0))
1200 return null;
1201
1202 ParserContext context = parseOps(symbols, yearIsWeekYear, firstDayOfWeek, ops, dateStr);
1203
1204 if (context.invalid)
1205 {
1206 return null;
1207 }
1208
1209 parsePostProcess(symbols, context);
1210 return createDateFromContext(context);
1211 }
1212
1213 public String format(Date date)
1214 {
1215 if (date instanceof java.sql.Date)
1216 {
1217 return formatOps(symbols, yearIsWeekYear, firstDayOfWeek, ops, new Date(date.getTime()));
1218 }
1219 else
1220 {
1221 return formatOps(symbols, yearIsWeekYear, firstDayOfWeek, ops, date);
1222 }
1223 }
1224
1225 public WeekDate getWeekDate(Date date)
1226 {
1227 if (date instanceof java.sql.Date)
1228 {
1229 return getWeekDate(new Date(date.getTime()), this.firstDayOfWeek);
1230 }
1231 else
1232 {
1233 return getWeekDate(date, this.firstDayOfWeek);
1234 }
1235 }
1236
1237 public Date getDateForWeekDate(WeekDate wdate)
1238 {
1239 return getDateForWeekDate(
1240 wdate.getYear(), wdate.getWeek(), 1,
1241 0, 0, 0,
1242 this.firstDayOfWeek);
1243 }
1244 }