• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.util;
2 
3 import java.text.ParsePosition;
4 import java.util.ArrayList;
5 import java.util.Collection;
6 import java.util.Collections;
7 import java.util.Comparator;
8 import java.util.Date;
9 import java.util.EnumSet;
10 import java.util.HashMap;
11 import java.util.HashSet;
12 import java.util.Iterator;
13 import java.util.LinkedHashSet;
14 import java.util.List;
15 import java.util.Map;
16 import java.util.Set;
17 import java.util.TreeMap;
18 import java.util.TreeSet;
19 import java.util.regex.Pattern;
20 
21 import org.unicode.cldr.util.Dictionary.DictionaryBuilder;
22 import org.unicode.cldr.util.Dictionary.Matcher;
23 import org.unicode.cldr.util.Dictionary.Matcher.Filter;
24 import org.unicode.cldr.util.Dictionary.Matcher.Status;
25 import org.unicode.cldr.util.LenientDateParser.Token.Type;
26 
27 import com.ibm.icu.dev.util.CollectionUtilities;
28 import com.ibm.icu.impl.OlsonTimeZone;
29 import com.ibm.icu.impl.Relation;
30 import com.ibm.icu.lang.UCharacter;
31 import com.ibm.icu.text.BreakIterator;
32 import com.ibm.icu.text.DateFormat;
33 import com.ibm.icu.text.DateFormatSymbols;
34 import com.ibm.icu.text.DateTimePatternGenerator.FormatParser;
35 import com.ibm.icu.text.DecimalFormat;
36 import com.ibm.icu.text.SimpleDateFormat;
37 import com.ibm.icu.text.UnicodeSet;
38 import com.ibm.icu.util.Calendar;
39 import com.ibm.icu.util.SimpleTimeZone;
40 import com.ibm.icu.util.TimeZone;
41 import com.ibm.icu.util.TimeZoneTransition;
42 import com.ibm.icu.util.ULocale;
43 
44 /**
45  * Immutable class that will parse dates and times for a particular ULocale.
46  *
47  * @author markdavis
48  */
49 public class LenientDateParser {
50     public static boolean DEBUG = false;
51 
52     private static final int SECOND = 1000;
53     private static final int MINUTE = 60 * SECOND;
54     private static final int HOUR = 60 * MINUTE;
55 
56     static final long startDate;
57 
58     static final long endDate;
59 
60     static final SimpleDateFormat neutralFormat = new SimpleDateFormat(
61         "yyyy-MM-dd HH:mm:ss", ULocale.ENGLISH);
62     static final DecimalFormat threeDigits = new DecimalFormat("000");
63     static final DecimalFormat twoDigits = new DecimalFormat("00");
64 
65     public static final Set<Integer> allOffsets = new TreeSet<Integer>();
66 
67     static {
68         TimeZone GMT = TimeZone.getTimeZone("Etc/GMT");
69         neutralFormat.setTimeZone(GMT);
70         Calendar cal = Calendar.getInstance(GMT, ULocale.US);
71         int year = cal.get(Calendar.YEAR);
cal.clear()72         cal.clear(); // need to clear fractional seconds
73         cal.set(1970, 0, 1, 0, 0, 0);
74         startDate = cal.getTimeInMillis();
75         cal.set(year + 5, 0, 1, 0, 0, 0);
76         endDate = cal.getTimeInMillis();
77         if (startDate != 0) {
IllegalArgumentException()78             throw new IllegalArgumentException();
79         }
80     }
81 
82     private static final UnicodeSet disallowedInSeparator = (UnicodeSet) new UnicodeSet("[:alphabetic:]").freeze();
83     private static final UnicodeSet IGNORABLE = (UnicodeSet) new UnicodeSet("[,[:whitespace:]]").freeze();
84     private static final EnumSet<Type> dateTypes = EnumSet.of(Type.DAY, Type.MONTH, Type.YEAR, Type.WEEKDAY, Type.ERA);
85     private static final EnumSet<Type> timeTypes = EnumSet.of(Type.HOUR, Type.MINUTE, Type.SECOND, Type.AMPM,
86         Type.TIMEZONE);
87     private static final EnumSet<Type> integerTimeTypes = EnumSet.of(Type.HOUR, Type.MINUTE, Type.SECOND);
88 
89     static final int thisYear = new Date().getYear();
90     static final Date june15 = new Date(thisYear, 5, 15, 0, 0, 0);
91 
92     static final Date january15 = new Date(thisYear, 0, 15, 0, 0, 0);
93 
94     public class Parser {
95         final List<Token> tokens = new ArrayList<Token>();
96         final SoFar haveSoFar = new SoFar();
97         Token previous;
98         final BreakIterator breakIterator;
99         Calendar calendar;
100         private int twoDigitYearOffset;
101         {
set2DigitYearStart(new Date(new Date().getYear() - 80, 1, 1))102             set2DigitYearStart(new Date(new Date().getYear() - 80, 1, 1));
103         }
104 
Parser(BreakIterator breakIterator)105         Parser(BreakIterator breakIterator) {
106             this.breakIterator = breakIterator;
107         }
108 
parse(String text, Calendar cal, ParsePosition parsePosition)109         public void parse(String text, Calendar cal, ParsePosition parsePosition) {
110             calendar = cal;
111             parse(new CharUtilities.CharSourceWrapper<CharSequence>(text), parsePosition);
112         }
113 
addSeparator(StringBuilder separatorBuffer)114         private boolean addSeparator(StringBuilder separatorBuffer) {
115             // for now, disallow arbitrary separators
116             return false;
117             // if (separatorBuffer.length() != 0) {
118             // tokens.add(new Token<String>(trim(separatorBuffer.toString()), Type.OTHER));
119             // separatorBuffer.setLength(0);
120             // }
121         }
122 
addToken(Token token)123         boolean addToken(Token token) {
124             if (haveSoFar.contains(token.getType())) {
125                 if (DEBUG) {
126                     System.out.println("Already have: " + token.getType());
127                 }
128                 return false;
129             }
130             switch (token.getType()) {
131             case ERA:
132             case MONTH:
133             case WEEKDAY:
134             case TIMEZONE:
135             case AMPM:
136                 if (!token.checkAllowableTypes(previous, haveSoFar, tokens)) {
137                     return false;
138                 }
139                 break;
140             case INTEGER:
141                 if (!token.checkAllowableTypes(previous, haveSoFar, tokens)) {
142                     return false;
143                 }
144                 break;
145             case SEPARATOR:
146                 EnumSet<Type> beforeTypes = ((SeparatorToken) token).getAllowsBefore();
147                 // see if there is a restriction on the previous type
148                 if (tokens.size() > 0) {
149                     if (!beforeTypes.contains(previous.getType())) {
150                         if (DEBUG) {
151                             System.out.println("Have " + token + ", while previous token is  " + previous);
152                         }
153                         return false;
154                     }
155                 }
156                 if (previous != null && previous.getType() == Type.INTEGER) {
157                     IntegerToken integerToken = (IntegerToken) previous;
158                     if (!integerToken.restrictAndSetCalendarFieldIfPossible(beforeTypes, haveSoFar, tokens)) {
159                         return false; // couldn't add
160                     }
161                 }
162                 // see what first required type is
163                 haveSoFar.setFirstType(beforeTypes);
164                 EnumSet<Type> afterTypes = ((SeparatorToken) token).getAllowsAfter();
165                 haveSoFar.setFirstType(afterTypes);
166 
167                 break;
168             default:
169             }
170             tokens.add(token);
171             previous = token;
172             return true;
173         }
174 
checkPreviousType(Token token)175         private boolean checkPreviousType(Token token) {
176             if (tokens.size() > 0) {
177                 Token previous = tokens.get(tokens.size() - 1);
178                 if (previous.getType() == Type.SEPARATOR) {
179                     Set<Type> allowable = ((SeparatorToken) previous).getAllowsBefore();
180                     if (!allowable.contains(token.getType())) {
181                         if (DEBUG) {
182                             System.out.println("Have " + token + ", while previous token is " + previous);
183                         }
184                         return false;
185                     }
186                 }
187             }
188             return true;
189         }
190 
191         // Type firstType;
192 
parse(CharSource charlist, ParsePosition parsePosition)193         public Date parse(CharSource charlist, ParsePosition parsePosition) {
194             calendar.clear();
195             tokens.clear();
196             previous = null;
197             haveSoFar.clear();
198             parsePosition.setErrorIndex(-1);
199             StringBuilder separatorBuffer = new StringBuilder();
200             matcher.setText(charlist);
201             breakIterator.setText(charlist.toString());
202 
203             boolean haveStringMonth = false;
204 
205             int i = charlist.fromSourceOffset(parsePosition.getIndex());
206             while (charlist.hasCharAt(i)) {
207                 if (DEBUG) {
208                     System.out.println(charlist.subSequence(0, i) + "|" + charlist.charAt(i) + "\t\t" + tokens);
209                 }
210                 Status status = matcher.setOffset(i).next(Filter.LONGEST_UNIQUE);
211                 if (status != Status.NONE) {
212                     addSeparator(separatorBuffer);
213                     if (!breakIterator.isBoundary(i)) {
214                         parsePosition.setErrorIndex(i);
215                         return null;
216                     }
217                     // TODO check for other calendars
218 
219                     final Token matchValue = matcher.getMatchValue();
220                     // if (matchValue.getType() != Type.WEEKDAY) {
221                     if (matchValue.getType() == Type.MONTH) {
222                         haveStringMonth = true;
223                     }
224                     if (!addToken(matchValue)) {
225                         break;
226                     }
227                     // }
228                     i = matcher.getMatchEnd();
229                     continue;
230                 }
231 
232                 // getting char instead of code point is safe, since we only use this
233                 // for digits, and we only care about those on the BMP.
234                 char ch = charlist.charAt(i);
235                 if (UCharacter.isDigit(ch)) {
236                     addSeparator(separatorBuffer);
237                     // the cast is safe, since we are only getting digits.
238                     int result = (int) UCharacter.getUnicodeNumericValue(ch);
239                     // following may advance 1 too far, so we'll correct later
240                     int j = i;
241                     while (charlist.hasCharAt(++j)) {
242                         ch = charlist.charAt(j);
243                         if (!UCharacter.isDigit(ch)) {
244                             break;
245                         }
246                         result *= 10;
247                         result += (int) UCharacter.getUnicodeNumericValue(ch);
248                     }
249                     if (!addToken(new IntegerToken(result))) {
250                         break;
251                     }
252                     i = j; // we are at least (i+1).
253                     // make another pass at same point. Slightly less efficient, but makes the loop easier.
254                 } else if (IGNORABLE.contains(ch)) {
255                     ++i;
256                 } else if (disallowedInSeparator.contains(ch)) {
257                     break;
258                 } else {
259                     break; // for now, disallow arbitrary separators
260                     // separatorBuffer.append(ch);
261                     // ++i;
262                 }
263             }
264             if (DEBUG) {
265                 System.out.println(charlist.subSequence(0, i) + "|" + "\t\t" + tokens);
266             }
267             parsePosition.setIndex(charlist.toSourceOffset(i));
268 
269             // we now have a list of tokens. Figure out what the date is
270 
271             // first get all the string fields, and separators
272 
273             // we use a few facts from CLDR.
274             // All patterns have date then time.
275             // All patterns have the order hour, minute, second.
276             // Date patterns are:
277             // dMy
278 
279             // case INTEGER:
280             // int value = token.getIntValue();
281             // tokenToAllowed.put(token, allowed);
282             // break;
283 
284             // TODO look at the separators
285             // now get the integers
286             Set<Type> ordering = new LinkedHashSet<Type>();
287             ordering.addAll(haveStringMonth ? dateOrdering.yd : dateOrdering.ymd);
288             ordering.addAll(integerTimeTypes);
289 
290             main: for (Token token : tokens) {
291                 if (token.getType() == Type.INTEGER) {
292                     IntegerToken integerToken = (IntegerToken) token;
293                     // pick the first ordering item that fits
294                     EnumSet<Type> possible = integerToken.allowsAt;
295                     for (Iterator<Type> it = ordering.iterator(); it.hasNext();) {
296                         Type item = it.next();
297                         if (haveSoFar.contains(item)) {
298                             continue;
299                         }
300                         if (possible.contains(item)) {
301                             integerToken.restrictAndSetCalendarFieldIfPossible(EnumSet.of(item), haveSoFar, tokens);
302                             continue main;
303                         }
304                     }
305                     // if we get this far, then none of the orderings work; we failed
306                     if (DEBUG) {
307                         System.out.println("failed to find option for " + token + " in " + possible);
308                     }
309                     return null;
310                 }
311             }
312 
313             for (Token token : tokens) {
314                 int value = token.getIntValue();
315                 switch (token.getType()) {
316                 case ERA:
317                     calendar.set(Calendar.ERA, value);
318                     break;
319                 case YEAR:
320                     if (value < 100) {
321                         value = (twoDigitYearOffset / 100) * 100 + value;
322                         if (value < twoDigitYearOffset) {
323                             value += 100;
324                         }
325                     }
326                     calendar.set(Calendar.YEAR, value);
327                     break;
328                 case DAY:
329                     calendar.set(Calendar.DAY_OF_MONTH, value);
330                     break;
331                 case MONTH:
332                     calendar.set(Calendar.MONTH, value - 1);
333                     break;
334                 case HOUR:
335                     calendar.set(Calendar.HOUR, value);
336                     break;
337                 case MINUTE:
338                     calendar.set(Calendar.MINUTE, value);
339                     break;
340                 case SECOND:
341                     calendar.set(Calendar.SECOND, value);
342                     break;
343                 case AMPM:
344                     calendar.set(Calendar.AM_PM, value);
345                     break;
346                 case TIMEZONE:
347                     calendar.setTimeZone(getTimeZone(ZONE_INT_MAP.get(value)));
348                     break;
349                 default:
350                 }
351             }
352             // if (!haveSoFar.contains(Type.YEAR)) {
353             // calendar.set(calendar.YEAR, new Date().getYear() + 1900);
354             // }
355             return calendar.getTime();
356         }
357 
358         @Override
toString()359         public String toString() {
360             return tokens.toString();
361         }
362 
debugShow()363         public String debugShow() {
364             return matcher.getDictionary().toString();
365         }
366 
debugShow2()367         public String debugShow2() {
368             return Dictionary.load(matcher.getDictionary().getMapping(), new TreeMap()).toString();
369         }
370 
371         /**
372          * Sets the 100-year period 2-digit years will be interpreted as being in
373          * to begin on the date the user specifies.
374          *
375          * @param startDate
376          *            During parsing, two digit years will be placed in the range <code>startDate</code> to
377          *            <code>startDate + 100 years</code>.
378          * @stable ICU 2.0
379          */
set2DigitYearStart(Date startDate)380         public void set2DigitYearStart(Date startDate) {
381             twoDigitYearOffset = startDate.getYear() + 1900;
382         }
383     }
384 
385     static class SoFar {
386         final EnumSet<Type> haveSoFarSet = EnumSet.noneOf(Type.class);
387         Type firstType;
388 
clear()389         public void clear() {
390             haveSoFarSet.clear();
391             firstType = null;
392         }
393 
394         @Override
toString()395         public String toString() {
396             return "{" + firstType + ", " + haveSoFarSet + "}";
397         }
398 
setFirstType(EnumSet<Type> set)399         public void setFirstType(EnumSet<Type> set) {
400             if (firstType != null) {
401                 // skip
402             } else {
403                 boolean hasDate = CollectionUtilities.containsSome(dateTypes, set);
404                 boolean hasTime = CollectionUtilities.containsSome(timeTypes, set);
405                 if (hasDate != hasTime) {
406                     firstType = hasDate ? Type.YEAR : Type.HOUR;
407                 }
408             }
409         }
410 
add(Token token)411         public boolean add(Token token) {
412             Type o = token.getType();
413             setFirstType(o);
414             return haveSoFarSet.add(o);
415         }
416 
setFirstType(Type o)417         private void setFirstType(Type o) {
418             if (firstType != null) {
419                 // fall out
420             } else if (dateTypes.contains(o)) {
421                 firstType = Type.YEAR;
422             } else if (timeTypes.contains(o)) {
423                 firstType = Type.HOUR;
424             }
425         }
426 
contains(Object o)427         public boolean contains(Object o) {
428             return haveSoFarSet.contains(o);
429         }
430     }
431 
toShortString(Set<Type> set)432     static String toShortString(Set<Type> set) {
433         StringBuilder result = new StringBuilder();
434         for (Type t : Type.values()) {
435             if (set.contains(t)) {
436                 result.append(t.toString().charAt(0));
437             } else {
438                 result.append("-");
439             }
440         }
441         return result.toString();
442     }
443 
444     /**
445      * Tokens can be integers, separator strings, or date elements (Timezones, Months, Days, Eras)
446      *
447      * @author markdavis
448      *
449      */
450     static class Token {
451 
452         enum Type {
453             ERA, YEAR, MONTH, WEEKDAY, DAY, HOUR, MINUTE, SECOND, AMPM, TIMEZONE, INTEGER, SEPARATOR, UNKNOWN;
454 
getType(Object field)455             static Type getType(Object field) {
456                 char ch = field.toString().charAt(0);
457                 switch (ch) {
458                 case 'G':
459                     return Type.ERA;
460                 case 'y':
461                 case 'Y':
462                 case 'u':
463                     return Type.YEAR;
464                 // case 'Q': return Type.QUARTER;
465                 case 'M':
466                 case 'L':
467                     return Type.MONTH;
468                 // case 'w': case 'W': return Type.WEEK;
469                 case 'e':
470                 case 'E':
471                 case 'c':
472                     return Type.WEEKDAY;
473                 case 'd':
474                 case 'D':
475                 case 'F':
476                 case 'g':
477                     return Type.DAY;
478                 case 'a':
479                     return Type.AMPM;
480                 case 'h':
481                 case 'H':
482                 case 'k':
483                 case 'K':
484                     return Type.HOUR;
485                 case 'm':
486                     return Type.MINUTE;
487                 case 's':
488                 case 'S':
489                 case 'A':
490                     return Type.SECOND;
491                 case 'v':
492                 case 'z':
493                 case 'Z':
494                 case 'V':
495                     return Type.TIMEZONE;
496                 }
497                 return UNKNOWN;
498             }
499         };
500 
501         private final int value;
502         private final Type type;
503 
getType()504         public Type getType() {
505             return type;
506         }
507 
checkAllowableTypes(Token previous, SoFar haveSoFar, Collection<Token> tokensToFix)508         public boolean checkAllowableTypes(Token previous, SoFar haveSoFar, Collection<Token> tokensToFix) {
509             if (haveSoFar.contains(getType())) {
510                 if (DEBUG) {
511                     System.out.println("Have " + this + ", but already had " + haveSoFar);
512                 }
513                 return false;
514             }
515             EnumSet<Type> allowable = null;
516             if (previous != null && previous.getType() == Type.SEPARATOR) {
517                 allowable = ((SeparatorToken) previous).getAllowsAfter();
518                 if (!allowable.contains(getType())) {
519                     if (DEBUG) {
520                         System.out.println("Have " + this + ", while previous token is " + previous);
521                     }
522                     return false;
523                 }
524             }
525 
526             switch (getType()) {
527 
528             case INTEGER:
529                 IntegerToken integerToken = (IntegerToken) this; // slightly kludgy to call subclass, but simpler
530                 return integerToken.restrictAndSetCalendarFieldIfPossible(allowable, haveSoFar, tokensToFix);
531             case SEPARATOR:
532                 return true;
533             default:
534             }
535             // only if set value
536             return haveSoFar == null ? true : haveSoFar.add(this);
537         }
538 
getIntValue()539         public int getIntValue() {
540             return value;
541         }
542 
Token(int value, Type type)543         public Token(int value, Type type) {
544             this.value = value;
545             this.type = type;
546         }
547 
get()548         public int get() {
549             return value;
550         }
551 
552         @Override
toString()553         public String toString() {
554             return "{" + getType() + ":" + value + (getType() == Type.TIMEZONE ? "/" + ZONE_INT_MAP.get(value) : "")
555                 + "}";
556         }
557 
558         @Override
equals(Object obj)559         public boolean equals(Object obj) {
560             Token other = (Token) obj;
561             return getType() == other.getType() && value == other.value;
562         }
563 
564         @Override
hashCode()565         public int hashCode() {
566             return getType().hashCode() ^ value;
567         }
568     }
569 
570     static class SeparatorToken extends Token {
571 
572         final EnumSet<Type> allowsBefore;
573         final EnumSet<Type> allowsAfter;
574 
SeparatorToken(EnumSet<Type> before, EnumSet<Type> after, int value)575         public SeparatorToken(EnumSet<Type> before, EnumSet<Type> after, int value) {
576             this(before, after, value, Type.SEPARATOR);
577         }
578 
SeparatorToken(EnumSet<Type> before, EnumSet<Type> after, int value, Type type)579         protected SeparatorToken(EnumSet<Type> before, EnumSet<Type> after, int value, Type type) {
580             super(value, type);
581             allowsBefore = before.clone();
582             allowsAfter = after.clone();
583         }
584 
getAllowsAfter()585         public EnumSet<Type> getAllowsAfter() {
586             return allowsAfter;
587         }
588 
getAllowsBefore()589         public EnumSet<Type> getAllowsBefore() {
590             return allowsBefore;
591         }
592 
toString()593         public String toString() {
594             return "{" + getType() + ":" + getIntValue() + "/" + toShortString(allowsBefore) + "/"
595                 + toShortString(allowsAfter) + "}";
596         }
597 
598         @Override
equals(Object obj)599         public boolean equals(Object obj) {
600             if (!super.equals(obj)) {
601                 return false;
602             }
603             SeparatorToken other = (SeparatorToken) obj;
604             return allowsBefore.equals(other.allowsBefore) && allowsAfter.equals(other.allowsAfter);
605         }
606         // don't bother with hashcode
607     }
608 
609     // This is the only mutable one
610     static class IntegerToken extends Token {
611 
612         public Type revisedType = null;
613         EnumSet<Type> allowsAt;
614 
IntegerToken(int value)615         public IntegerToken(int value) {
616             super(value, Type.INTEGER);
617             allowsAt = value == 0 ? EnumSet.of(Type.HOUR, Type.MINUTE, Type.SECOND)
618                 : value < 12 ? EnumSet.of(Type.YEAR, Type.MONTH, Type.DAY, Type.HOUR, Type.MINUTE, Type.SECOND)
619                     : value < 25 ? EnumSet.of(Type.YEAR, Type.DAY, Type.HOUR, Type.MINUTE, Type.SECOND)
620                         : value < 32 ? EnumSet.of(Type.YEAR, Type.DAY, Type.MINUTE, Type.SECOND)
621                             : value < 60 ? EnumSet.of(Type.YEAR, Type.MINUTE, Type.SECOND)
622                                 : EnumSet.of(Type.YEAR);
623         }
624 
625         public boolean restrictAndSetCalendarFieldIfPossible(EnumSet<Type> allowable, SoFar haveSoFar,
626             Collection<Token> tokensToFix) {
627             if (getType() != Type.INTEGER) {
628                 throw new IllegalArgumentException();
629             }
630             EnumSet<Type> ok = allowsAt.clone();
631             // TODO optimize the following
632             ok.removeAll(haveSoFar.haveSoFarSet);
633             if (allowable != null) {
634                 ok.retainAll(allowable);
635             }
636             if (ok.size() == 0) {
637                 if (DEBUG) {
638                     System.out.println("No possibilities for " + this + ": " + allowable + "\t" + haveSoFar);
639                 }
640                 return false; // nothing works
641             }
642             allowsAt = ok;
643             if (ok.size() == 1) {
644                 revisedType = ok.iterator().next();
645                 haveSoFar.add(this);
646                 if (revisedType == Type.INTEGER) {
647                     throw new IllegalArgumentException();
648                 }
649                 // now look through all the other values to see if they need fixing
650                 for (Token token : tokensToFix) {
651                     // look at the other tokens to see if they need fixing
652                     if (token != this && token.getType() == Type.INTEGER) {
653                         IntegerToken other = (IntegerToken) token;
654                         if (!other.restrictAndSetCalendarFieldIfPossible(EnumSet.complementOf(ok), haveSoFar,
655                             tokensToFix)) {
656                             return false;
657                         }
658                     }
659                 }
660                 return true;
661             }
662             return true;
663         }
664 
665         public Type getType() {
666             return revisedType == null ? super.getType() : revisedType;
667         }
668 
669         public Set<Type> getAllowsAt() {
670             return allowsAt;
671         }
672 
673         public String toString() {
674             return "{" + getType() + ":" + getIntValue() + "/" + toShortString(allowsAt) + "}";
675         }
676 
677         @Override
678         public boolean equals(Object obj) {
679             if (!super.equals(obj)) {
680                 return false;
681             }
682             IntegerToken other = (IntegerToken) obj;
683             return allowsAt.equals(other.allowsAt);
684         }
685     }
686 
687     private final Matcher<Token> matcher;
688     private final BreakIterator breakIterator;
689     private final DateOrdering dateOrdering;
690 
691     public LenientDateParser(Matcher<Token> matcher, BreakIterator iterator, DateOrdering dateOrdering) {
692         this.matcher = matcher;
693         breakIterator = iterator;
694         this.dateOrdering = dateOrdering;
695     }
696 
697     public static LenientDateParser getInstance(ULocale locale) {
698         DateOrdering dateOrdering = new DateOrdering();
699         // final RuleBasedCollator col = (RuleBasedCollator) Collator.getInstance(locale);
700         // CollationStringByteConverter converter = new CollationStringByteConverter(col, new StringUtf8Converter()); //
701         // new ByteString(true)
702         // Matcher<String> matcher = converter.getDictionary().getMatcher();
703         // later, cache this dictionary
704 
705         Map<CharSequence, Token> map = DEBUG ? new TreeMap<CharSequence, Token>() : new HashMap<CharSequence, Token>();
706         DateFormatSymbols symbols = new DateFormatSymbols(locale);
707         // load the data
708         loadArray(map, symbols.getAmPmStrings(), Type.AMPM);
709         loadArray(map, symbols.getEraNames(), Type.ERA);
710         loadArray(map, symbols.getEras(), Type.ERA);
711         // TODO skip Narrow??
712         for (int context = 0; context < DateFormatSymbols.DT_CONTEXT_COUNT; ++context) {
713             for (int width = 0; width < DateFormatSymbols.DT_WIDTH_COUNT; ++width) {
714                 loadArray(map, symbols.getMonths(context, width), Type.MONTH);
715                 // try {
716                 // loadArray(map, symbols.getQuarters(context, width), Type.QUARTERS);
717                 // } catch (NullPointerException e) {} // skip these
718                 loadArray(map, symbols.getWeekdays(context, width), Type.WEEKDAY);
719             }
720         }
721 
722         Calendar temp = Calendar.getInstance();
723 
724         String[] zoneFormats = { "z", "zzzz", "Z", "ZZZZ", "v", "vvvv", "V", "VVVV" };
725         List<SimpleDateFormat> zoneFormatList = new ArrayList<SimpleDateFormat>();
726         for (String zoneFormat : zoneFormats) {
727             zoneFormatList.add(new SimpleDateFormat(zoneFormat, locale));
728         }
729         final BestTimeZone bestTimeZone = new BestTimeZone(locale);
730         Relation<String, String> stringToZones = Relation.of(new TreeMap<String, Set<String>>(), TreeSet.class, bestTimeZone);
731 
732         // final UTF16.StringComparator stringComparator = new UTF16.StringComparator(true, false, 0);
733         // Set<String[]> zoneRemaps = new TreeSet(new ArrayComparator(new Comparator[] {stringComparator,
734         // stringComparator, stringComparator, stringComparator}));
735 
736         for (String timezone : ZONE_VALUE_MAP.keySet()) {
737             final TimeZone currentTimeZone = getTimeZone(timezone);
738             for (SimpleDateFormat format : zoneFormatList) {
739                 format.setTimeZone(currentTimeZone);
740 
741                 // hack around the fact that non-daylight timezones fail in ICU right now
742                 // the symptom is that v/vvvv format a non-daylight timezone as "Mountain Time"
743                 // when there is a separate zone *with* daylight that formats that way
744 
745                 // if (format.toPattern().charAt(0) == 'v') {
746                 // String formatted2 = format.format(january15);
747                 // int offset = currentTimeZone.getOffset(june15.getTime());
748                 // int offset2 = currentTimeZone.getOffset(january15.getTime());
749                 // boolean noDaylight = offset == offset2;
750                 // if (noDaylight && formatted2.equals(formatted)) {
751                 // backupStringToZones.put(formatted, timezone);
752                 // }
753                 // }
754                 stringToZones.put(format.format(january15), timezone);
755                 stringToZones.put(format.format(june15), timezone);
756                 //
757                 // pos.setIndex(0);
758                 // temp.setTimeZone(unknownZone);
759                 // format.parse(formatted, temp, pos);
760                 // if (pos.getIndex() != formatted.length()) {
761                 // continue; // unable to parse
762                 // }
763                 // TimeZone otherZone = temp.getTimeZone();
764                 // // if (!otherZone.getID().equals(timezone.getID())) {
765                 // // zoneRemaps.add(new String[] {timezone.getID(), format.toPattern(), formatted, otherZone.getID()}
766                 // );
767                 // // }
768                 // if (!otherZone.getID().equals(unknownZone.getID())) {
769                 // stringToZones.put(formatted, timezone);
770                 // }
771             }
772         }
773         for (String formatted : stringToZones.keySet()) {
774             final Set<String> possibilities = stringToZones.getAll(formatted);
775             String status = uniquenessStatus(possibilities);
776             if (!status.startsWith("OK")) {
777                 if (formatted.equals("Australie (Darwin)")) {
778                     String last = null;
779                     for (String zone : possibilities) {
780                         if (last != null) {
781                             bestTimeZone.compare(last, zone);
782                         }
783                         last = zone;
784                     }
785                 }
786                 System.out.println("Parsing \t\"" + formatted + "\"\t gets \t" + status + "\t" + show(possibilities));
787             }
788             String bestValue = possibilities.iterator().next(); // pick first value
789             loadItem(map, formatted, ZONE_VALUE_MAP.get(bestValue), Type.TIMEZONE);
790         }
791         // get separators from formats
792         // we walk through to see what can come before or after a separator, accumulating them all together
793         FormatParser formatParser = new FormatParser();
794         Map<String, EnumSet<Type>> beforeTypes = new HashMap<String, EnumSet<Type>>();
795         Map<String, EnumSet<Type>> afterTypes = new HashMap<String, EnumSet<Type>>();
796         EnumSet<Type> nonDateTypes = EnumSet.allOf(Type.class);
797         nonDateTypes.removeAll(dateTypes);
798         EnumSet<Type> nonTimeTypes = EnumSet.allOf(Type.class);
799         nonTimeTypes.removeAll(timeTypes);
800         for (int style = 0; style < 4; ++style) {
801             addSeparatorInfo((SimpleDateFormat) DateFormat.getDateInstance(style, locale), formatParser, beforeTypes,
802                 afterTypes, nonDateTypes, dateOrdering);
803             addSeparatorInfo((SimpleDateFormat) DateFormat.getTimeInstance(style, locale), formatParser, beforeTypes,
804                 afterTypes, nonTimeTypes, dateOrdering);
805         }
806         // now allow spaces between date and type
807         add(beforeTypes, " ", dateTypes);
808         add(afterTypes, " ", dateTypes);
809         add(beforeTypes, " ", timeTypes);
810         add(afterTypes, " ", timeTypes);
811 
812         Set<String> allSeparators = new HashSet<String>(beforeTypes.keySet());
813         allSeparators.addAll(afterTypes.keySet());
814         for (String item : allSeparators) {
815             loadItem(map, item, beforeTypes.get(item), afterTypes.get(item));
816         }
817 
818         if (dateOrdering.yd.size() == 0) {
819             dateOrdering.yd.addAll(dateOrdering.ymd);
820         }
821 
822         // TODO remove the setByteConverter; it's just for debugging
823         DictionaryBuilder<Token> builder = new StateDictionaryBuilder<Token>()
824             .setByteConverter(new Utf8StringByteConverter());
825         if (DEBUG) {
826             System.out.println(map);
827         }
828 
829         Dictionary<Token> dict = builder.make(map);
830         // System.out.println(dict.debugShow());
831         // DictionaryCharList x = new DictionaryCharList(converter.getDictionary(), string);
832 
833         LenientDateParser result = new LenientDateParser(dict.getMatcher(), BreakIterator.getWordInstance(locale),
834             dateOrdering);
835         return result;
836     }
837 
838     static final Pattern GMT_ZONE_MATCHER = PatternCache.get("Etc/GMT([-+])([0-9]{1,2})(?::([0-9]{2}))(?::([0-9]{2}))?");
839 
840     private static TimeZone getTimeZone(String timezone) {
841         // this really ought to be done in the inverse order: try the normal timezone, then if it fails try this.
842         // Unfortunately, getTimeZone doesn't give a failure value.
843         if (timezone.startsWith("Etc/GMT")) {
844             java.util.regex.Matcher matcher = GMT_ZONE_MATCHER.matcher(timezone);
845             if (matcher.matches()) {
846                 int offset = Integer.parseInt(matcher.group(2)) * HOUR;
847                 if (matcher.group(3) != null) {
848                     offset += Integer.parseInt(matcher.group(3)) * MINUTE;
849                     if (matcher.group(4) != null) {
850                         offset += Integer.parseInt(matcher.group(3)) * SECOND;
851                     }
852                 }
853                 if (matcher.group(1).equals("+")) { // IMPORTANT: the TZDB offsets are the inverse of everyone elses!
854                     offset = -offset;
855                 }
856                 return new SimpleTimeZone(offset, timezone);
857             }
858         }
859         return TimeZone.getTimeZone(timezone);
860     }
861 
862     private static String show(Set<String> zones) {
863         StringBuilder result = new StringBuilder();
864         result.append("{");
865         for (String zone : zones) {
866             if (result.length() > 1) {
867                 result.append(", ");
868             }
869             result.append(getCountry(zone)).append(":").append(zone);
870         }
871         result.append("}");
872         return result.toString();
873     }
874 
875     private static String uniquenessStatus(Set<String> possibilities) {
876         int count = 0;
877         for (String zone : possibilities) {
878             if (supplementalData.isCanonicalZone(zone)) {
879                 count++;
880             }
881         }
882         return count == 0 ? "ZERO!!" : count == 1 ? "OK" : "AMBIGUOUS:" + count;
883     }
884 
885     static final IntMap<String> ZONE_INT_MAP;
886     static final Map<String, Integer> ZONE_VALUE_MAP;
887     final static SupplementalDataInfo supplementalData = SupplementalDataInfo
888         .getInstance("C:/cvsdata/unicode/cldr/common/supplemental/");
889 
890     private static final boolean SHOW_ZONE_INFO = false;
891     static {
892         Set<String> canonicalZones = supplementalData.getCanonicalZones();
893         // get all the CLDR IDs
894         Set<String> allCLDRZones = new TreeSet<String>(canonicalZones);
895         for (String canonicalZone : canonicalZones) {
896             allCLDRZones.addAll(supplementalData.getZone_aliases(canonicalZone));
897         }
898         // get all the ICU IDs
899         Set<String> allIcuZones = new TreeSet<String>();
900         for (String canonicalZone : TimeZone.getAvailableIDs()) {
901             allIcuZones.add(canonicalZone);
902             for (int i = 0; i < TimeZone.countEquivalentIDs(canonicalZone); ++i) {
903                 allIcuZones.add(TimeZone.getEquivalentID(canonicalZone, i));
904             }
905         }
906 
907         if (SHOW_ZONE_INFO)
908             System.out.println("Zones in CLDR but not ICU:" + getFirstMinusSecond(allCLDRZones, allIcuZones));
909         final Set<String> icuMinusCldr_all = getFirstMinusSecond(allIcuZones, allCLDRZones);
910         if (SHOW_ZONE_INFO) System.out.println("Zones in ICU but not CLDR:" + icuMinusCldr_all);
911 
912         for (String canonicalZone : canonicalZones) {
913             Set<String> aliases = supplementalData.getZone_aliases(canonicalZone);
914             LinkedHashSet<String> icuAliases = getIcuEquivalentZones(canonicalZone);
915             icuAliases.remove(canonicalZone); // difference in APIs
916             icuAliases.removeAll(icuMinusCldr_all);
917             if (SHOW_ZONE_INFO && !aliases.equals(icuAliases)) {
918                 System.out.println("Difference in Aliases for: " + canonicalZone);
919                 Set<String> cldrMinusIcu = getFirstMinusSecond(aliases, icuAliases);
920                 if (cldrMinusIcu.size() != 0) {
921                     System.out.println("\tCLDR - ICU: " + cldrMinusIcu);
922                 }
923                 Set<String> icuMinusCldr = getFirstMinusSecond(icuAliases, aliases);
924                 if (icuMinusCldr.size() != 0) {
925                     System.out.println("\tICU - CLDR: " + icuMinusCldr);
926                 }
927             }
928         }
929 
930         // add missing Etc zones
931         canonicalZones = new TreeSet<String>(supplementalData.getCanonicalZones());
932         Set<String> zones = getAllGmtZones();
933         zones.removeAll(canonicalZones);
934         System.out.println("Missing GMT Zones: " + zones);
935         canonicalZones.addAll(zones);
936         canonicalZones = Collections.unmodifiableSet(canonicalZones);
937 
938         List<String> values = new ArrayList<String>();
939         for (String id : canonicalZones) { // TimeZone.getAvailableIDs() has extraneous values
940             values.add(id);
941         }
942         ZONE_INT_MAP = new IntMap.BasicIntMapFactory<String>().make(values);
943         ZONE_VALUE_MAP = Collections.unmodifiableMap(ZONE_INT_MAP.getValueMap());
944     }
945 
946     private static Set<String> getFirstMinusSecond(Set<String> first, Set<String> second) {
947         Set<String> difference = new TreeSet<String>(first);
948         difference.removeAll(second);
949         return difference;
950     }
951 
952     /**
953      * The best timezone is the lower one.
954      */
955     static class BestTimeZone implements Comparator<String> {
956         // TODO replace by HashMap once done debugging
957         Map<String, Integer> regionToRank = new TreeMap<String, Integer>();
958         Map<String, Map<String, Integer>> regionToZoneToRank = new TreeMap<String, Map<String, Integer>>();
959 
960         public BestTimeZone(ULocale locale) {
961             // build the two maps that we'll use later.
962             int count = 0;
963             String region = locale.getCountry();
964             if (region.length() != 0) { // add the explicit region if there is one
965                 regionToRank.put(region, count++);
966             }
967             // now find the other regions
968             String language = locale.getLanguage();
969             String script = locale.getScript();
970             if (script.length() != 0) {
971                 count = add(language + "_" + script, count);
972             }
973             count = add(language, count);
974 
975             // first do 001
976             Map<String, Map<String, String>> map = supplementalData.getMetazoneToRegionToZone();
977             for (String mzone : map.keySet()) {
978                 Map<String, String> regionToZone = map.get(mzone);
979                 String zone = regionToZone.get("001");
980                 if (zone == null) {
981                     continue;
982                 }
983                 String region3 = supplementalData.getZone_territory(zone);
984                 if (region3 == null) {
985                     continue;
986                 }
987                 addRank(region3, zone);
988             }
989             for (String mzone : map.keySet()) {
990                 Map<String, String> regionToZone = map.get(mzone);
991                 for (String region2 : regionToZone.keySet()) {
992                     String zone = regionToZone.get(region2);
993                     addRank(region2, zone);
994                     String region3 = supplementalData.getZone_territory(zone);
995                     if (region3 != null && !region3.equals(region2)) {
996                         addRank(region3, zone);
997                     }
998                 }
999             }
1000             System.out.println(regionToZoneToRank);
1001         }
1002 
1003         private void addRank(String region2, String zone) {
1004             Map<String, Integer> zoneToRank = regionToZoneToRank.get(region2);
1005             if (zoneToRank == null) regionToZoneToRank.put(region2, zoneToRank = new TreeMap<String, Integer>());
1006             if (!zoneToRank.containsKey(zone)) {
1007                 zoneToRank.put(zone, zoneToRank.size()); // earlier is better.
1008             }
1009         }
1010 
1011         private int add(String language, int count) {
1012             Set<String> data = supplementalData
1013                 .getTerritoriesForPopulationData(language);
1014             // get direct language
1015             if (data != null) {
1016                 System.out.println("???" + language + "\t" + data);
1017                 for (String region : data) {
1018                     regionToRank.put(region, count++);
1019                 }
1020             } else { // add scripts
1021                 String languageSeparator = language + "_";
1022                 for (String language2 : supplementalData
1023                     .getLanguagesForTerritoriesPopulationData()) {
1024                     if (language2.startsWith(languageSeparator)) {
1025                         data = supplementalData.getTerritoriesForPopulationData(language2);
1026                         System.out.println("???" + language2 + "\t" + data);
1027                         for (String region : data) {
1028                             regionToRank.put(region, count++);
1029                         }
1030                     }
1031                 }
1032             }
1033             return count;
1034         }
1035 
1036         public int compare(String z1, String z2) {
1037             // Etc/GMT.* is lower
1038             if (z1.startsWith("Etc/GMT")) {
1039                 if (!z2.startsWith("Etc/GMT")) {
1040                     return -1;
1041                 }
1042             } else if (z2.startsWith("Etc/GMT")) {
1043                 return 1;
1044             }
1045 
1046             // canonical is lower (-1)
1047             boolean c1 = supplementalData.isCanonicalZone(z1);
1048             boolean c2 = supplementalData.isCanonicalZone(z2);
1049             if (c1 != c2) {
1050                 return c1 ? -1 : 1;
1051             }
1052 
1053             // either both are canonical or neither
1054             String zone1 = supplementalData.getZoneFromAlias(z1);
1055             String zone2 = supplementalData.getZoneFromAlias(z2);
1056             // handle in case not even alias
1057             if (zone1 == null) zone1 = z1;
1058             if (zone2 == null) zone2 = z2;
1059 
1060             final String region1 = supplementalData.getZone_territory(zone1);
1061             final String region2 = supplementalData.getZone_territory(zone2);
1062 
1063             if (region1 == region2 || region1 != null && region1.equals(region2)) {
1064                 // regions are both null, or otherwise equal
1065                 if (region1 != null) {
1066                     Map<String, Integer> rankInRegion = regionToZoneToRank.get(region1);
1067                     if (rankInRegion != null) {
1068                         int comparison = getRank(rankInRegion, zone1, zone2);
1069                         if (comparison != 0) {
1070                             return comparison;
1071                         }
1072                     }
1073                 }
1074             } else {
1075                 // regions are not equal
1076                 // get the best region, based on population
1077                 if (region1 == null) {
1078                     return 1; // null is higher than everything
1079                 } else if (region2 == null) {
1080                     return -1;
1081                 }
1082                 int comparison = getRank(regionToRank, region1, region2);
1083                 if (comparison != 0) {
1084                     return comparison;
1085                 }
1086                 // otherwise compare
1087                 return region1.compareTo(region2);
1088             }
1089             // if all else fails, return string ordering
1090             return zone1.compareTo(zone2);
1091         }
1092 
1093         private int getRank(Map<String, Integer> map, final String region1, final String region2) {
1094             Integer w1 = map.get(region1);
1095             Integer w2 = map.get(region2);
1096             if (w1 == null) w1 = 9999;
1097             if (w2 == null) w2 = 9999;
1098             int comparison = w1.compareTo(w2);
1099             return comparison;
1100         }
1101     };
1102 
1103     private static LinkedHashSet<String> getIcuEquivalentZones(String zoneID) {
1104         LinkedHashSet<String> result = new LinkedHashSet<String>();
1105         final int count = TimeZone.countEquivalentIDs(zoneID);
1106         for (int i = 0; i < count; ++i) {
1107             result.add(TimeZone.getEquivalentID(zoneID, i));
1108         }
1109         return result;
1110     }
1111 
1112     static class DateOrdering {
1113         LinkedHashSet ymd = new LinkedHashSet();
1114         LinkedHashSet yd = new LinkedHashSet();
1115     }
1116 
1117     private static void addSeparatorInfo(SimpleDateFormat d, FormatParser formatParser,
1118         Map<String, EnumSet<Type>> beforeTypes, Map<String, EnumSet<Type>> afterTypes, EnumSet<Type> allowedContext,
1119         DateOrdering dateOrdering) {
1120         String pattern = d.toPattern();
1121         if (DEBUG) {
1122             System.out.println("Adding Pattern:\t" + pattern);
1123         }
1124         formatParser.set(pattern);
1125         List<Object> list = formatParser.getItems();
1126         List<Type> temp = new ArrayList<Type>();
1127         for (int i = 0; i < list.size(); ++i) {
1128             Object item = list.get(i);
1129             if (item instanceof String) {
1130                 String sItem = trim((String) item);
1131                 if (i == 0) {
1132                     add(beforeTypes, sItem, allowedContext);
1133                 } else {
1134                     add(beforeTypes, sItem, Type.getType(list.get(i - 1)));
1135                     add(beforeTypes, sItem, Type.INTEGER);
1136                 }
1137                 if (i >= list.size() - 1) {
1138                     add(afterTypes, sItem, allowedContext);
1139                 } else {
1140                     add(afterTypes, sItem, Type.getType(list.get(i + 1)));
1141                     add(afterTypes, sItem, Type.INTEGER);
1142                 }
1143             } else {
1144                 String var = item.toString();
1145                 Type type = Type.getType(var);
1146                 switch (type) {
1147                 case MONTH:
1148                     if (var.length() < 3) {
1149                         temp.add(type);
1150                     }
1151                     break;
1152                 case DAY:
1153                 case YEAR:
1154                     temp.add(type);
1155                     break;
1156                 default:
1157                 }
1158             }
1159         }
1160         if (temp.contains(Type.MONTH)) {
1161             dateOrdering.ymd.addAll(temp);
1162         } else if (temp.size() != 0) {
1163             dateOrdering.yd.addAll(temp);
1164         }
1165     }
1166 
1167     private static void add(Map<String, EnumSet<Type>> stringToTypes, String item, Type type) {
1168         Set<Type> set = stringToTypes.get(item);
1169         if (set == null) {
1170             stringToTypes.put(item, EnumSet.of(type));
1171         } else {
1172             set.add(type);
1173         }
1174     }
1175 
1176     private static void add(Map<String, EnumSet<Type>> stringToTypes, String item, EnumSet<Type> types) {
1177         Set<Type> set = stringToTypes.get(item);
1178         if (set == null) {
1179             stringToTypes.put(item, EnumSet.copyOf(types));
1180         } else {
1181             set.addAll(types);
1182         }
1183     }
1184 
1185     static String trim(String source) {
1186         if (source.length() == 0) return source;
1187         int start;
1188         for (start = 0; start < source.length(); ++start) {
1189             if (!IGNORABLE.contains(source.charAt(start))) {
1190                 break;
1191             }
1192         }
1193         int end;
1194         for (end = source.length(); end > start; --end) {
1195             if (!IGNORABLE.contains(source.charAt(end - 1))) {
1196                 break;
1197             }
1198         }
1199         source = source.substring(start, end);
1200         if (source.length() == 0) source = " ";
1201         return source;
1202     }
1203 
1204     private static void loadItem(Map<CharSequence, Token> map, String item, EnumSet<Type> before, EnumSet<Type> after) {
1205         map.put(item, new SeparatorToken(before, after, -1, Type.SEPARATOR));
1206     }
1207 
1208     private static void loadArray(Map<CharSequence, Token> map, final String[] array, Type type) {
1209         int i = type == Type.MONTH ? 1 : 0; // special case months
1210         for (String item : array) {
1211             // exclude digit-only fields, like in Chinese
1212             if (item != null && item.length() != 0 && !DIGITS.containsSome(item)) {
1213                 loadItem(map, item, i++, type);
1214             }
1215         }
1216     }
1217 
1218     private static final UnicodeSet DIGITS = (UnicodeSet) new UnicodeSet("[:nd:]").freeze();
1219 
1220     private static void loadItem(Map<CharSequence, Token> map, String item, int i, Type type) {
1221         map.put(item, new Token(i, type));
1222     }
1223 
1224     public Parser getParser() {
1225         return new Parser((BreakIterator) breakIterator.clone());
1226     }
1227 
1228     public static String getCountry(String zone) {
1229         return supplementalData.getZone_territory(zone);
1230     }
1231 
1232     /*
1233      *
1234      * Parsing "-1100" gets AMBIGUOUS:5 {001:Etc/GMT+11, AS:Pacific/Pago_Pago, NU:Pacific/Niue, UM:Pacific/Midway,
1235      * WS:Pacific/Apia}
1236      *
1237      * Parsing "+0530" gets AMBIGUOUS:2 {IN:Asia/Calcutta, LK:Asia/Colombo}
1238      * Parsing "+0630" gets AMBIGUOUS:2 {CC:Indian/Cocos, MM:Asia/Rangoon}
1239      * Parsing "+0930" gets AMBIGUOUS:3 {AU:Australia/Adelaide, AU:Australia/Broken_Hill, AU:Australia/Darwin}
1240      * Parsing "+1030" gets AMBIGUOUS:3 {AU:Australia/Adelaide, AU:Australia/Lord_Howe, AU:Australia/Broken_Hill}
1241      * Parsing "GMT+05:30" gets AMBIGUOUS:2 {IN:Asia/Calcutta, LK:Asia/Colombo}
1242      * Parsing "GMT+06:30" gets AMBIGUOUS:2 {CC:Indian/Cocos, MM:Asia/Rangoon}
1243      * Parsing "GMT+09:30" gets AMBIGUOUS:3 {AU:Australia/Adelaide, AU:Australia/Broken_Hill, AU:Australia/Darwin}
1244      * Parsing "GMT+10:30" gets AMBIGUOUS:3 {AU:Australia/Adelaide, AU:Australia/Lord_Howe, AU:Australia/Broken_Hill}
1245      */
1246 
1247     public static Set<String> getAllGmtZones() {
1248         Set<Integer> offsets = new TreeSet<Integer>();
1249         for (String tzid : supplementalData.getCanonicalZones()) {
1250             TimeZone zone = TimeZone.getTimeZone(tzid);
1251             for (long date = startDate; date < endDate; date = getTransitionAfter(
1252                 zone, date)) {
1253                 offsets.add(zone.getOffset(date));
1254             }
1255         }
1256         Set<String> result = new LinkedHashSet<String>();
1257         for (int offset : offsets) {
1258             String zone = "Etc/GMT";
1259             if (offset != 0) {
1260                 // IMPORTANT: the TZDB offsets are the inverses of everyone else's
1261                 if (offset < 0) {
1262                     zone += "+";
1263                     offset = -offset;
1264                 } else {
1265                     zone += "-";
1266                 }
1267                 int hours = offset / HOUR;
1268                 zone += hours; // no leading zero
1269                 offset = offset % HOUR;
1270                 if (offset > 0) {
1271                     int minutes = offset / MINUTE;
1272                     zone += ":" + twoDigits.format(minutes);
1273                     // comment this out for now, since getTimeZone doesn't handle seconds
1274                     // offset = offset % MINUTE;
1275                     // if (offset > 0) {
1276                     // int seconds = (offset + SECOND / 2) / SECOND;
1277                     // zone += ":" + twoDigits.format(seconds);
1278                     // }
1279                 }
1280                 result.add(zone);
1281             }
1282         }
1283         return result;
1284     }
1285 
1286     public static long getTransitionAfter(TimeZone zone, long date) {
1287         TimeZoneTransition transition = ((OlsonTimeZone) zone).getNextTransition(
1288             date, false);
1289         if (transition == null) {
1290             return Long.MAX_VALUE;
1291         }
1292         date = transition.getTime();
1293         return date;
1294     }
1295 
1296 }
1297