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