View Javadoc

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 }