1 package org.unicode.cldr.test; 2 3 import com.google.common.base.Joiner; 4 import com.ibm.icu.impl.Relation; 5 import com.ibm.icu.text.BreakIterator; 6 import com.ibm.icu.text.DateIntervalInfo; 7 import com.ibm.icu.text.DateIntervalInfo.PatternInfo; 8 import com.ibm.icu.text.DateTimePatternGenerator; 9 import com.ibm.icu.text.DateTimePatternGenerator.VariableField; 10 import com.ibm.icu.text.MessageFormat; 11 import com.ibm.icu.text.NumberFormat; 12 import com.ibm.icu.text.SimpleDateFormat; 13 import com.ibm.icu.text.UnicodeSet; 14 import com.ibm.icu.util.Output; 15 import com.ibm.icu.util.ULocale; 16 import java.text.ParseException; 17 import java.util.ArrayList; 18 import java.util.Arrays; 19 import java.util.Calendar; 20 import java.util.Collection; 21 import java.util.Date; 22 import java.util.EnumMap; 23 import java.util.HashMap; 24 import java.util.HashSet; 25 import java.util.Iterator; 26 import java.util.LinkedHashSet; 27 import java.util.List; 28 import java.util.Locale; 29 import java.util.Map; 30 import java.util.Random; 31 import java.util.Set; 32 import java.util.TreeSet; 33 import java.util.regex.Matcher; 34 import java.util.regex.Pattern; 35 import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype; 36 import org.unicode.cldr.util.ApproximateWidth; 37 import org.unicode.cldr.util.CLDRFile; 38 import org.unicode.cldr.util.CLDRFile.Status; 39 import org.unicode.cldr.util.CLDRLocale; 40 import org.unicode.cldr.util.CLDRURLS; 41 import org.unicode.cldr.util.CldrUtility; 42 import org.unicode.cldr.util.DateTimeCanonicalizer.DateTimePatternType; 43 import org.unicode.cldr.util.DayPeriodInfo; 44 import org.unicode.cldr.util.DayPeriodInfo.DayPeriod; 45 import org.unicode.cldr.util.DayPeriodInfo.Type; 46 import org.unicode.cldr.util.Factory; 47 import org.unicode.cldr.util.ICUServiceBuilder; 48 import org.unicode.cldr.util.Level; 49 import org.unicode.cldr.util.LocaleIDParser; 50 import org.unicode.cldr.util.LogicalGrouping; 51 import org.unicode.cldr.util.PathHeader; 52 import org.unicode.cldr.util.PathStarrer; 53 import org.unicode.cldr.util.PatternCache; 54 import org.unicode.cldr.util.PreferredAndAllowedHour; 55 import org.unicode.cldr.util.RegexUtilities; 56 import org.unicode.cldr.util.SupplementalDataInfo; 57 import org.unicode.cldr.util.XPathParts; 58 import org.unicode.cldr.util.props.UnicodeProperty.PatternMatcher; 59 60 public class CheckDates extends FactoryCheckCLDR { 61 static boolean GREGORIAN_ONLY = CldrUtility.getProperty("GREGORIAN", false); 62 63 ICUServiceBuilder icuServiceBuilder = new ICUServiceBuilder(); 64 NumberFormat english = NumberFormat.getNumberInstance(ULocale.ENGLISH); 65 PatternMatcher m; 66 DateTimePatternGenerator.FormatParser formatParser = 67 new DateTimePatternGenerator.FormatParser(); 68 DateTimePatternGenerator dateTimePatternGenerator = DateTimePatternGenerator.getEmptyInstance(); 69 private CoverageLevel2 coverageLevel; 70 private SupplementalDataInfo sdi = SupplementalDataInfo.getInstance(); 71 // Ordered list of this CLDRFile and parent CLDRFiles up to root 72 List<CLDRFile> parentCLDRFiles = new ArrayList<>(); 73 // Map from calendar type (i.e. "gregorian", "generic", "chinese") to DateTimePatternGenerator 74 // instance for that type 75 Map<String, DateTimePatternGenerator> dtpgForType = new HashMap<>(); 76 77 // Use the width of the character "0" as the basic unit for checking widths 78 // It's not perfect, but I'm not sure that anything can be. This helps us 79 // weed out some false positives in width checking, like 10月 vs. 十月 80 // in Chinese, which although technically longer, shouldn't trigger an 81 // error. 82 private static final int REFCHAR = ApproximateWidth.getWidth("0"); 83 84 private Level requiredLevel; 85 private String language; 86 private String territory; 87 88 private DayPeriodInfo dateFormatInfoFormat; 89 90 static String[] samples = { 91 // "AD 1970-01-01T00:00:00Z", 92 // "BC 4004-10-23T07:00:00Z", // try a BC date: creation according to Ussher & Lightfoot. 93 // Assuming garden of 94 // eden 2 hours ahead of UTC 95 "2005-12-02 12:15:16", 96 // "AD 2100-07-11T10:15:16Z", 97 }; // keep aligned with following 98 static String SampleList = "{0}" 99 // + Utility.LINE_SEPARATOR + "\t\u200E{1}\u200E" + Utility.LINE_SEPARATOR + 100 // "\t\u200E{2}\u200E" + 101 // Utility.LINE_SEPARATOR + "\t\u200E{3}\u200E" 102 ; // keep aligned with previous 103 104 private static final String DECIMAL_XPATH = 105 "//ldml/numbers/symbols[@numberSystem='latn']/decimal"; 106 private static final Pattern HOUR_SYMBOL = PatternCache.get("H{1,2}"); 107 private static final Pattern MINUTE_SYMBOL = PatternCache.get("mm"); 108 private static final Pattern YEAR_FIELDS = PatternCache.get("(y|Y|u|U|r){1,5}"); 109 110 private static String CALENDAR_ID_PREFIX = "/calendar[@type=\""; 111 112 static String[] calTypePathsToCheck = { 113 "//ldml/dates/calendars/calendar[@type=\"buddhist\"]", 114 "//ldml/dates/calendars/calendar[@type=\"gregorian\"]", 115 "//ldml/dates/calendars/calendar[@type=\"hebrew\"]", 116 "//ldml/dates/calendars/calendar[@type=\"islamic\"]", 117 "//ldml/dates/calendars/calendar[@type=\"japanese\"]", 118 "//ldml/dates/calendars/calendar[@type=\"roc\"]", 119 }; 120 static String[] calSymbolPathsWhichNeedDistinctValues = { 121 // === for months, days, quarters - format wide & abbrev sets must have distinct values === 122 "/months/monthContext[@type=\"format\"]/monthWidth[@type=\"abbreviated\"]/month", 123 "/months/monthContext[@type=\"format\"]/monthWidth[@type=\"wide\"]/month", 124 "/days/dayContext[@type=\"format\"]/dayWidth[@type=\"abbreviated\"]/day", 125 "/days/dayContext[@type=\"format\"]/dayWidth[@type=\"short\"]/day", 126 "/days/dayContext[@type=\"format\"]/dayWidth[@type=\"wide\"]/day", 127 "/quarters/quarterContext[@type=\"format\"]/quarterWidth[@type=\"abbreviated\"]/quarter", 128 "/quarters/quarterContext[@type=\"format\"]/quarterWidth[@type=\"wide\"]/quarter", 129 // === for dayPeriods - all values for a given context/width must be distinct === 130 "/dayPeriods/dayPeriodContext[@type=\"format\"]/dayPeriodWidth[@type=\"abbreviated\"]/dayPeriod", 131 "/dayPeriods/dayPeriodContext[@type=\"format\"]/dayPeriodWidth[@type=\"narrow\"]/dayPeriod", 132 "/dayPeriods/dayPeriodContext[@type=\"format\"]/dayPeriodWidth[@type=\"wide\"]/dayPeriod", 133 "/dayPeriods/dayPeriodContext[@type=\"stand-alone\"]/dayPeriodWidth[@type=\"abbreviated\"]/dayPeriod", 134 "/dayPeriods/dayPeriodContext[@type=\"stand-alone\"]/dayPeriodWidth[@type=\"narrow\"]/dayPeriod", 135 "/dayPeriods/dayPeriodContext[@type=\"stand-alone\"]/dayPeriodWidth[@type=\"wide\"]/dayPeriod", 136 // === for eras - all values for a given context/width should be distinct (warning) === 137 "/eras/eraNames/era", 138 "/eras/eraAbbr/era", // Hmm, root eraAbbr for japanese has many dups, should we change them 139 // or drop this test? 140 "/eras/eraNarrow/era", // We may need to allow dups here too 141 }; 142 143 // The following calendar symbol sets need not have distinct values 144 // "/months/monthContext[@type=\"format\"]/monthWidth[@type=\"narrow\"]/month", 145 // "/months/monthContext[@type=\"stand-alone\"]/monthWidth[@type=\"abbreviated\"]/month", 146 // "/months/monthContext[@type=\"stand-alone\"]/monthWidth[@type=\"narrow\"]/month", 147 // "/months/monthContext[@type=\"stand-alone\"]/monthWidth[@type=\"wide\"]/month", 148 // "/days/dayContext[@type=\"format\"]/dayWidth[@type=\"narrow\"]/day", 149 // "/days/dayContext[@type=\"stand-alone\"]/dayWidth[@type=\"abbreviated\"]/day", 150 // "/days/dayContext[@type=\"stand-alone\"]/dayWidth[@type=\"narrow\"]/day", 151 // "/days/dayContext[@type=\"stand-alone\"]/dayWidth[@type=\"wide\"]/day", 152 // "/quarters/quarterContext[@type=\"format\"]/quarterWidth[@type=\"narrow\"]/quarter", 153 // "/quarters/quarterContext[@type=\"stand-alone\"]/quarterWidth[@type=\"abbreviated\"]/quarter", 154 // "/quarters/quarterContext[@type=\"stand-alone\"]/quarterWidth[@type=\"narrow\"]/quarter", 155 // "/quarters/quarterContext[@type=\"stand-alone\"]/quarterWidth[@type=\"wide\"]/quarter", 156 157 // The above are followed by trailing pieces such as 158 // "[@type=\"am\"]", 159 // "[@type=\"sun\"]", 160 // "[@type=\"0\"]", 161 // "[@type=\"1\"]", 162 // "[@type=\"12\"]", 163 164 // Map<String, Set<String>> calPathsToSymbolSets; 165 // Map<String, Map<String, String>> calPathsToSymbolMaps = new HashMap<String, Map<String, 166 // String>>(); 167 CheckDates(Factory factory)168 public CheckDates(Factory factory) { 169 super(factory); 170 } 171 172 @Override handleSetCldrFileToCheck( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)173 public CheckCLDR handleSetCldrFileToCheck( 174 CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) { 175 if (cldrFileToCheck == null) return this; 176 super.handleSetCldrFileToCheck(cldrFileToCheck, options, possibleErrors); 177 178 icuServiceBuilder.setCldrFile(getResolvedCldrFileToCheck()); 179 // the following is a hack to work around a bug in ICU4J (the snapshot, not the released 180 // version). 181 try { 182 bi = BreakIterator.getCharacterInstance(new ULocale(cldrFileToCheck.getLocaleID())); 183 } catch (RuntimeException e) { 184 bi = BreakIterator.getCharacterInstance(new ULocale("")); 185 } 186 CLDRFile resolved = getResolvedCldrFileToCheck(); 187 flexInfo = new FlexibleDateFromCLDR(); // ought to just clear(), but not available. 188 flexInfo.set(resolved); 189 190 // load decimal path specially 191 String decimal = resolved.getWinningValue(DECIMAL_XPATH); 192 if (decimal != null) { 193 flexInfo.checkFlexibles(DECIMAL_XPATH, decimal, DECIMAL_XPATH); 194 } 195 196 String localeID = cldrFileToCheck.getLocaleID(); 197 LocaleIDParser lp = new LocaleIDParser(); 198 territory = lp.set(localeID).getRegion(); 199 language = lp.getLanguage(); 200 if (territory == null || territory.length() == 0) { 201 if (language.equals("root")) { 202 territory = "001"; 203 } else { 204 CLDRLocale loc = CLDRLocale.getInstance(localeID); 205 CLDRLocale defContent = sdi.getDefaultContentFromBase(loc); 206 if (defContent == null) { 207 territory = "001"; 208 } else { 209 territory = defContent.getCountry(); 210 } 211 // Set territory for 12/24 hour clock to Egypt (12 hr) for ar_001 212 // instead of 24 hour (exception). 213 if (territory.equals("001") && language.equals("ar")) { 214 territory = "EG"; 215 } 216 } 217 } 218 coverageLevel = CoverageLevel2.getInstance(sdi, localeID); 219 requiredLevel = options.getRequiredLevel(localeID); 220 221 // load gregorian appendItems 222 for (Iterator<String> it = 223 resolved.iterator("//ldml/dates/calendars/calendar[@type=\"gregorian\"]"); 224 it.hasNext(); ) { 225 String path = it.next(); 226 String value = resolved.getWinningValue(path); 227 String fullPath = resolved.getFullXPath(path); 228 try { 229 flexInfo.checkFlexibles(path, value, fullPath); 230 } catch (Exception e) { 231 final String message = e.getMessage(); 232 CheckStatus item = 233 new CheckStatus() 234 .setCause(this) 235 .setMainType(CheckStatus.errorType) 236 .setSubtype( 237 message.contains("Conflicting fields") 238 ? Subtype.dateSymbolCollision 239 : Subtype.internalError) 240 .setMessage(message); 241 possibleErrors.add(item); 242 } 243 // possibleErrors.add(flexInfo.getFailurePath(path)); 244 } 245 redundants.clear(); 246 /* 247 * TODO: NullPointerException may be thrown in ICU here during cldr-unittest TestAll 248 */ 249 flexInfo.getRedundants(redundants); 250 // Set baseSkeletons = flexInfo.gen.getBaseSkeletons(new TreeSet()); 251 // Set notCovered = new TreeSet(neededFormats); 252 // if (flexInfo.preferred12Hour()) { 253 // notCovered.addAll(neededHours12); 254 // } else { 255 // notCovered.addAll(neededHours24); 256 // } 257 // notCovered.removeAll(baseSkeletons); 258 // if (notCovered.size() != 0) { 259 // possibleErrors.add(new CheckStatus().setCause(this).setType(CheckCLDR.finalErrorType) 260 // .setCheckOnSubmit(false) 261 // .setMessage("Missing availableFormats: {0}", new Object[]{notCovered.toString()})); 262 // } 263 pathsWithConflictingOrder2sample = 264 DateOrder.getOrderingInfo(cldrFileToCheck, resolved, flexInfo.fp); 265 if (pathsWithConflictingOrder2sample == null) { 266 CheckStatus item = 267 new CheckStatus() 268 .setCause(this) 269 .setMainType(CheckStatus.errorType) 270 .setSubtype(Subtype.internalError) 271 .setMessage("DateOrder.getOrderingInfo fails"); 272 possibleErrors.add(item); 273 } 274 275 // calPathsToSymbolMaps.clear(); 276 // for (String calTypePath: calTypePathsToCheck) { 277 // for (String calSymbolPath: calSymbolPathsWhichNeedDistinctValues) { 278 // calPathsToSymbolMaps.put(calTypePath.concat(calSymbolPath), null); 279 // } 280 // } 281 282 dateFormatInfoFormat = sdi.getDayPeriods(Type.format, cldrFileToCheck.getLocaleID()); 283 284 // Make new list of parent CLDRFiles 285 parentCLDRFiles.clear(); 286 parentCLDRFiles.add(cldrFileToCheck); 287 while ((localeID = LocaleIDParser.getParent(localeID)) != null) { 288 CLDRFile resolvedParentCLDRFile = getFactory().make(localeID, true); 289 parentCLDRFiles.add(resolvedParentCLDRFile); 290 } 291 // Clear out map of DateTimePatternGenerators for calendarType 292 dtpgForType.clear(); 293 294 return this; 295 } 296 297 Map<String, Map<DateOrder, String>> pathsWithConflictingOrder2sample; 298 299 // Set neededFormats = new TreeSet(Arrays.asList(new String[]{ 300 // "yM", "yMMM", "yMd", "yMMMd", "Md", "MMMd","yQ" 301 // })); 302 // Set neededHours12 = new TreeSet(Arrays.asList(new String[]{ 303 // "hm", "hms" 304 // })); 305 // Set neededHours24 = new TreeSet(Arrays.asList(new String[]{ 306 // "Hm", "Hms" 307 // })); 308 /** 309 * hour+minute, hour+minute+second (12 & 24) year+month, year+month+day (numeric & string) 310 * month+day (numeric & string) year+quarter 311 */ 312 BreakIterator bi; 313 314 FlexibleDateFromCLDR flexInfo; 315 Collection<String> redundants = new HashSet<>(); 316 Status status = new Status(); 317 PathStarrer pathStarrer = new PathStarrer(); 318 stripPrefix(String s)319 private String stripPrefix(String s) { 320 if (s != null) { 321 int prefEnd = s.lastIndexOf(" "); 322 if (prefEnd < 0 || prefEnd >= 3) { 323 prefEnd = s.lastIndexOf("\u2019"); // as in d’ 324 } 325 if (prefEnd >= 0 && prefEnd < 3) { 326 return s.substring(prefEnd + 1); 327 } 328 } 329 return s; 330 } 331 332 @Override handleCheck( String path, String fullPath, String value, Options options, List<CheckStatus> result)333 public CheckCLDR handleCheck( 334 String path, String fullPath, String value, Options options, List<CheckStatus> result) { 335 336 if (fullPath == null) { 337 return this; // skip paths that we don't have 338 } 339 340 if (path.indexOf("/dates") < 0 || path.endsWith("/default") || path.endsWith("/alias")) { 341 return this; 342 } 343 344 if (!accept(result)) return this; 345 346 String sourceLocale = getCldrFileToCheck().getSourceLocaleID(path, status); 347 348 if (!path.equals(status.pathWhereFound) 349 || !sourceLocale.equals(getCldrFileToCheck().getLocaleID())) { 350 return this; 351 } 352 353 if (value == null) { 354 return this; 355 } 356 357 if (pathsWithConflictingOrder2sample != null) { 358 Map<DateOrder, String> problem = pathsWithConflictingOrder2sample.get(path); 359 if (problem != null) { 360 CheckStatus item = 361 new CheckStatus() 362 .setCause(this) 363 .setMainType(CheckStatus.warningType) 364 .setSubtype(Subtype.incorrectDatePattern) 365 .setMessage( 366 "The ordering of date fields is inconsistent with others: {0}", 367 getValues(getResolvedCldrFileToCheck(), problem.values())); 368 result.add(item); 369 } 370 } 371 372 try { 373 if (path.indexOf("[@type=\"abbreviated\"]") >= 0) { 374 // ensures abbreviated <= wide for quarters, months, days, dayPeriods 375 String pathToWide = path.replace("[@type=\"abbreviated\"]", "[@type=\"wide\"]"); 376 String wideValue = getCldrFileToCheck().getWinningValueWithBailey(pathToWide); 377 if (wideValue != null && isTooMuchWiderThan(value, wideValue)) { 378 CheckStatus item = 379 new CheckStatus() 380 .setCause(this) 381 .setMainType(errorOrIfBuildWarning()) 382 .setSubtype(Subtype.abbreviatedDateFieldTooWide) 383 .setMessage( 384 "Abbreviated value \"{0}\" can't be longer than the corresponding wide value \"{1}\"", 385 value, wideValue); 386 result.add(item); 387 } 388 Set<String> grouping = LogicalGrouping.getPaths(getCldrFileToCheck(), path); 389 if (grouping != null) { 390 for (String lgPath : grouping) { 391 String lgPathValue = getCldrFileToCheck().getWinningValueWithBailey(lgPath); 392 if (lgPathValue == null) { 393 continue; 394 } 395 String lgPathToWide = 396 lgPath.replace("[@type=\"abbreviated\"]", "[@type=\"wide\"]"); 397 String lgPathWideValue = 398 getCldrFileToCheck().getWinningValueWithBailey(lgPathToWide); 399 // This helps us get around things like "de març" vs. "març" in Catalan 400 String thisValueStripped = stripPrefix(value); 401 String wideValueStripped = stripPrefix(wideValue); 402 String lgPathValueStripped = stripPrefix(lgPathValue); 403 String lgPathWideValueStripped = stripPrefix(lgPathWideValue); 404 boolean thisPathHasPeriod = value.contains("."); 405 boolean lgPathHasPeriod = lgPathValue.contains("."); 406 if (!thisValueStripped.equalsIgnoreCase(wideValueStripped) 407 && !lgPathValueStripped.equalsIgnoreCase(lgPathWideValueStripped) 408 && thisPathHasPeriod != lgPathHasPeriod) { 409 CheckStatus.Type et = CheckStatus.errorType; 410 if (path.contains("dayPeriod")) { 411 et = CheckStatus.warningType; 412 } 413 CheckStatus item = 414 new CheckStatus() 415 .setCause(this) 416 .setMainType(et) 417 .setSubtype(Subtype.inconsistentPeriods) 418 .setMessage( 419 "Inconsistent use of periods in abbreviations for this section."); 420 result.add(item); 421 break; 422 } 423 } 424 } 425 } else if (path.indexOf("[@type=\"narrow\"]") >= 0) { 426 // ensures narrow <= abbreviated for quarters, months, days, dayPeriods 427 String pathToAbbr = path.replace("[@type=\"narrow\"]", "[@type=\"abbreviated\"]"); 428 String abbrValue = getCldrFileToCheck().getWinningValueWithBailey(pathToAbbr); 429 if (abbrValue != null && isTooMuchWiderThan(value, abbrValue)) { 430 CheckStatus item = 431 new CheckStatus() 432 .setCause(this) 433 .setMainType( 434 CheckStatus.warningType) // Making this just a warning, 435 // because there are some oddball 436 // cases. 437 .setSubtype(Subtype.narrowDateFieldTooWide) 438 .setMessage( 439 "Narrow value \"{0}\" shouldn't be longer than the corresponding abbreviated value \"{1}\"", 440 value, abbrValue); 441 result.add(item); 442 } 443 } else if (path.indexOf("[@type=\"short\"]") >= 0) { 444 // ensures short <= abbreviated and short >= narrow for days 445 String pathToAbbr = path.replace("[@type=\"short\"]", "[@type=\"abbreviated\"]"); 446 String abbrValue = getCldrFileToCheck().getWinningValueWithBailey(pathToAbbr); 447 String pathToNarrow = path.replace("[@type=\"short\"]", "[@type=\"narrow\"]"); 448 String narrowValue = getCldrFileToCheck().getWinningValueWithBailey(pathToNarrow); 449 if ((abbrValue != null 450 && isTooMuchWiderThan(value, abbrValue) 451 && value.length() > abbrValue.length()) 452 || (narrowValue != null 453 && isTooMuchWiderThan(narrowValue, value) 454 && narrowValue.length() > value.length())) { 455 // Without the additional check on length() above, the test is too sensitive 456 // and flags reasonable things like lettercase differences 457 String message; 458 String compareValue; 459 if (abbrValue != null 460 && isTooMuchWiderThan(value, abbrValue) 461 && value.length() > abbrValue.length()) { 462 message = 463 "Short value \"{0}\" can't be longer than the corresponding abbreviated value \"{1}\""; 464 compareValue = abbrValue; 465 } else { 466 message = 467 "Short value \"{0}\" can't be shorter than the corresponding narrow value \"{1}\""; 468 compareValue = narrowValue; 469 } 470 CheckStatus item = 471 new CheckStatus() 472 .setCause(this) 473 .setMainType(errorOrIfBuildWarning()) 474 .setSubtype(Subtype.shortDateFieldInconsistentLength) 475 .setMessage(message, value, compareValue); 476 result.add(item); 477 } 478 } else if (path.indexOf("/eraNarrow") >= 0) { 479 // ensures eraNarrow <= eraAbbr for eras 480 String pathToAbbr = path.replace("/eraNarrow", "/eraAbbr"); 481 String abbrValue = getCldrFileToCheck().getWinningValueWithBailey(pathToAbbr); 482 if (abbrValue != null && isTooMuchWiderThan(value, abbrValue)) { 483 CheckStatus item = 484 new CheckStatus() 485 .setCause(this) 486 .setMainType(errorOrIfBuildWarning()) 487 .setSubtype(Subtype.narrowDateFieldTooWide) 488 .setMessage( 489 "Narrow value \"{0}\" can't be longer than the corresponding abbreviated value \"{1}\"", 490 value, abbrValue); 491 result.add(item); 492 } 493 } else if (path.indexOf("/eraAbbr") >= 0) { 494 // ensures eraAbbr <= eraNames for eras 495 String pathToWide = path.replace("/eraAbbr", "/eraNames"); 496 String wideValue = getCldrFileToCheck().getWinningValueWithBailey(pathToWide); 497 if (wideValue != null && isTooMuchWiderThan(value, wideValue)) { 498 CheckStatus item = 499 new CheckStatus() 500 .setCause(this) 501 .setMainType(errorOrIfBuildWarning()) 502 .setSubtype(Subtype.abbreviatedDateFieldTooWide) 503 .setMessage( 504 "Abbreviated value \"{0}\" can't be longer than the corresponding wide value \"{1}\"", 505 value, wideValue); 506 result.add(item); 507 } 508 } 509 510 String failure = flexInfo.checkValueAgainstSkeleton(path, value); 511 if (failure != null) { 512 result.add( 513 new CheckStatus() 514 .setCause(this) 515 .setMainType(errorOrIfBuildWarning()) 516 .setSubtype(Subtype.illegalDatePattern) 517 .setMessage(failure)); 518 } 519 520 final String collisionPrefix = "//ldml/dates/calendars/calendar"; 521 main: 522 if (path.startsWith(collisionPrefix)) { 523 int pos = path.indexOf("\"]"); // end of first type 524 if (pos < 0 || skipPath(path)) { // skip narrow, no-calendar 525 break main; 526 } 527 pos += 2; 528 String myType = getLastType(path); 529 if (myType == null) { 530 break main; 531 } 532 String myMainType = getMainType(path); 533 534 String calendarPrefix = path.substring(0, pos); 535 boolean endsWithDisplayName = 536 path.endsWith("displayName"); // special hack, these shouldn't be in 537 // calendar. 538 539 Set<String> retrievedPaths = new HashSet<>(); 540 getResolvedCldrFileToCheck() 541 .getPathsWithValue(value, calendarPrefix, null, retrievedPaths); 542 if (retrievedPaths.size() < 2) { 543 break main; 544 } 545 // ldml/dates/calendars/calendar[@type="gregorian"]/eras/eraAbbr/era[@type="0"], 546 // ldml/dates/calendars/calendar[@type="gregorian"]/eras/eraNames/era[@type="0"], 547 // ldml/dates/calendars/calendar[@type="gregorian"]/eras/eraNarrow/era[@type="0"]] 548 Type type = null; 549 DayPeriod dayPeriod = null; 550 final boolean isDayPeriod = path.contains("dayPeriod"); 551 if (isDayPeriod) { 552 XPathParts parts = XPathParts.getFrozenInstance(fullPath); 553 type = 554 Type.fromString( 555 parts.getAttributeValue(5, "type")); // format, stand-alone 556 dayPeriod = DayPeriod.valueOf(parts.getAttributeValue(-1, "type")); 557 } 558 559 // TODO redo above and below in terms of parts instead of searching strings 560 561 Set<String> filteredPaths = new HashSet<>(); 562 Output<Integer> sampleError = new Output<>(); 563 564 for (String item : retrievedPaths) { 565 XPathParts itemParts = XPathParts.getFrozenInstance(item); 566 if (item.equals(path) 567 || skipPath(item) 568 || endsWithDisplayName != item.endsWith("displayName") 569 || itemParts.containsElement("alias")) { 570 continue; 571 } 572 String otherType = getLastType(item); 573 if (myType.equals( 574 otherType)) { // we don't care about items with the same type value 575 continue; 576 } 577 String mainType = getMainType(item); 578 if (!myMainType.equals( 579 mainType)) { // we *only* care about items with the same type value 580 continue; 581 } 582 if (isDayPeriod) { 583 // ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="format"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="am"] 584 Type itemType = 585 Type.fromString( 586 itemParts.getAttributeValue( 587 5, "type")); // format, stand-alone 588 DayPeriod itemDayPeriod = 589 DayPeriod.valueOf(itemParts.getAttributeValue(-1, "type")); 590 591 if (!dateFormatInfoFormat.collisionIsError( 592 type, dayPeriod, itemType, itemDayPeriod, sampleError)) { 593 continue; 594 } 595 } 596 filteredPaths.add(item); 597 } 598 if (filteredPaths.size() == 0) { 599 break main; 600 } 601 Set<String> others = new TreeSet<>(); 602 for (String path2 : filteredPaths) { 603 PathHeader pathHeader = getPathHeaderFactory().fromPath(path2); 604 others.add(pathHeader.getHeaderCode()); 605 } 606 CheckStatus.Type statusType = 607 getPhase() == Phase.SUBMISSION || getPhase() == Phase.BUILD 608 ? CheckStatus.warningType 609 : CheckStatus.errorType; 610 final CheckStatus checkStatus = 611 new CheckStatus() 612 .setCause(this) 613 .setMainType(statusType) 614 .setSubtype(Subtype.dateSymbolCollision); 615 if (sampleError.value == null) { 616 checkStatus.setMessage( 617 "The date value “{0}” is the same as what is used for a different item: {1}", 618 value, others.toString()); 619 } else { 620 checkStatus.setMessage( 621 "The date value “{0}” is the same as what is used for a different item: {1}. Sample problem: {2}", 622 value, others.toString(), sampleError.value / DayPeriodInfo.HOUR); 623 } 624 result.add(checkStatus); 625 } 626 DateTimePatternType dateTypePatternType = DateTimePatternType.fromPath(path); 627 if (DateTimePatternType.STOCK_AVAILABLE_INTERVAL_PATTERNS.contains( 628 dateTypePatternType)) { 629 boolean patternBasicallyOk = false; 630 try { 631 formatParser.set(value); 632 patternBasicallyOk = true; 633 } catch (RuntimeException e) { 634 String message = e.getMessage(); 635 if (message.contains("Illegal datetime field:")) { 636 CheckStatus item = 637 new CheckStatus() 638 .setCause(this) 639 .setMainType(CheckStatus.errorType) 640 .setSubtype(Subtype.illegalDatePattern) 641 .setMessage(message); 642 result.add(item); 643 } else { 644 CheckStatus item = 645 new CheckStatus() 646 .setCause(this) 647 .setMainType(CheckStatus.errorType) 648 .setSubtype(Subtype.illegalDatePattern) 649 .setMessage( 650 "Illegal date format pattern {0}", 651 new Object[] {e}); 652 result.add(item); 653 } 654 } 655 if (patternBasicallyOk) { 656 checkPattern(dateTypePatternType, path, fullPath, value, result); 657 } 658 } else if (path.contains("datetimeSkeleton") 659 && !path.contains("[@alt=")) { // cannot test any alt skeletons 660 // Get calendar type from //ldml/dates/calendars/calendar[@type="..."]/ 661 int startIndex = path.indexOf(CALENDAR_ID_PREFIX); 662 if (startIndex > 0) { 663 startIndex += CALENDAR_ID_PREFIX.length(); 664 int endIndex = path.indexOf("\"]", startIndex); 665 String calendarType = path.substring(startIndex, endIndex); 666 // Get pattern generated from datetimeSkeleton 667 DateTimePatternGenerator dtpg = getDTPGForCalendarType(calendarType); 668 String patternFromSkeleton = dtpg.getBestPattern(value); 669 // Get actual stock pattern 670 String patternPath = 671 path.replace("/datetimeSkeleton", "/pattern[@type=\"standard\"]"); 672 String patternStock = getCldrFileToCheck().getWinningValue(patternPath); 673 // Compare and flag error if mismatch 674 if (!patternFromSkeleton.equals(patternStock)) { 675 CheckStatus item = 676 new CheckStatus() 677 .setCause(this) 678 .setMainType(CheckStatus.warningType) 679 .setSubtype(Subtype.inconsistentDatePattern) 680 .setMessage( 681 "Pattern \"{0}\" from datetimeSkeleton should match corresponding standard pattern \"{1}\", adjust availableFormats to fix.", 682 patternFromSkeleton, patternStock); 683 result.add(item); 684 } 685 } 686 } else if (path.contains("hourFormat")) { 687 int semicolonPos = value.indexOf(';'); 688 if (semicolonPos < 0) { 689 CheckStatus item = 690 new CheckStatus() 691 .setCause(this) 692 .setMainType(CheckStatus.errorType) 693 .setSubtype(Subtype.illegalDatePattern) 694 .setMessage( 695 "Value should contain a positive hour format and a negative hour format separated by a semicolon."); 696 result.add(item); 697 } else { 698 String[] formats = value.split(";"); 699 if (formats[0].equals(formats[1])) { 700 CheckStatus item = 701 new CheckStatus() 702 .setCause(this) 703 .setMainType(CheckStatus.errorType) 704 .setSubtype(Subtype.illegalDatePattern) 705 .setMessage("The hour formats should not be the same."); 706 result.add(item); 707 } else { 708 checkHasHourMinuteSymbols(formats[0], result); 709 checkHasHourMinuteSymbols(formats[1], result); 710 } 711 } 712 } 713 } catch (ParseException e) { 714 CheckStatus item = 715 new CheckStatus() 716 .setCause(this) 717 .setMainType(CheckStatus.errorType) 718 .setSubtype(Subtype.illegalDatePattern) 719 .setMessage( 720 "ParseException in creating date format {0}", new Object[] {e}); 721 result.add(item); 722 } catch (Exception e) { 723 // e.printStackTrace(); 724 // HACK 725 String msg = e.getMessage(); 726 if (msg == null || !HACK_CONFLICTING.matcher(msg).find()) { 727 CheckStatus item = 728 new CheckStatus() 729 .setCause(this) 730 .setMainType(CheckStatus.errorType) 731 .setSubtype(Subtype.illegalDatePattern) 732 .setMessage("Error in creating date format {0}", new Object[] {e}); 733 result.add(item); 734 } 735 } 736 return this; 737 } 738 errorOrIfBuildWarning()739 public org.unicode.cldr.test.CheckCLDR.CheckStatus.Type errorOrIfBuildWarning() { 740 return getPhase() != Phase.BUILD ? CheckStatus.errorType : CheckStatus.warningType; 741 } 742 isTooMuchWiderThan(String shortString, String longString)743 private boolean isTooMuchWiderThan(String shortString, String longString) { 744 // We all 1/3 the width of the reference character as a "fudge factor" in determining the 745 // allowable width 746 return ApproximateWidth.getWidth(shortString) 747 > ApproximateWidth.getWidth(longString) + REFCHAR / 3; 748 } 749 750 /** 751 * Check for the presence of hour and minute symbols. 752 * 753 * @param value the value to be checked 754 * @param result the list to add any errors to. 755 */ checkHasHourMinuteSymbols(String value, List<CheckStatus> result)756 private void checkHasHourMinuteSymbols(String value, List<CheckStatus> result) { 757 boolean hasHourSymbol = HOUR_SYMBOL.matcher(value).find(); 758 boolean hasMinuteSymbol = MINUTE_SYMBOL.matcher(value).find(); 759 if (!hasHourSymbol && !hasMinuteSymbol) { 760 result.add( 761 createErrorCheckStatus() 762 .setMessage( 763 "The hour and minute symbols are missing from {0}.", value)); 764 } else if (!hasHourSymbol) { 765 result.add( 766 createErrorCheckStatus() 767 .setMessage( 768 "The hour symbol (H or HH) should be present in {0}.", value)); 769 } else if (!hasMinuteSymbol) { 770 result.add( 771 createErrorCheckStatus() 772 .setMessage("The minute symbol (mm) should be present in {0}.", value)); 773 } 774 } 775 776 /** 777 * Convenience method for creating errors. 778 * 779 * @return 780 */ createErrorCheckStatus()781 private CheckStatus createErrorCheckStatus() { 782 return new CheckStatus() 783 .setCause(this) 784 .setMainType(CheckStatus.errorType) 785 .setSubtype(Subtype.illegalDatePattern); 786 } 787 skipPath(String path)788 public boolean skipPath(String path) { 789 return path.contains("arrow") 790 || path.contains("/availableFormats") 791 || path.contains("/interval") 792 || path.contains("/dateTimeFormat") 793 // || path.contains("/dayPeriod[") 794 // && !path.endsWith("=\"pm\"]") 795 // && !path.endsWith("=\"am\"]") 796 ; 797 } 798 getLastType(String path)799 public String getLastType(String path) { 800 int secondType = path.lastIndexOf("[@type=\""); 801 if (secondType < 0) { 802 return null; 803 } 804 secondType += 8; 805 int secondEnd = path.indexOf("\"]", secondType); 806 if (secondEnd < 0) { 807 return null; 808 } 809 return path.substring(secondType, secondEnd); 810 } 811 getMainType(String path)812 public String getMainType(String path) { 813 int secondType = path.indexOf("\"]/"); 814 if (secondType < 0) { 815 return null; 816 } 817 secondType += 3; 818 int secondEnd = path.indexOf("/", secondType); 819 if (secondEnd < 0) { 820 return null; 821 } 822 return path.substring(secondType, secondEnd); 823 } 824 getValues(CLDRFile resolvedCldrFileToCheck, Collection<String> values)825 private String getValues(CLDRFile resolvedCldrFileToCheck, Collection<String> values) { 826 Set<String> results = new TreeSet<>(); 827 for (String path : values) { 828 final String stringValue = resolvedCldrFileToCheck.getStringValue(path); 829 if (stringValue != null) { 830 results.add(stringValue); 831 } 832 } 833 return "{" + Joiner.on("},{").join(results) + "}"; 834 } 835 836 static final Pattern HACK_CONFLICTING = PatternCache.get("Conflicting fields:\\s+M+,\\s+l"); 837 838 @Override handleGetExamples( String path, String fullPath, String value, Options options, List<CheckStatus> result)839 public CheckCLDR handleGetExamples( 840 String path, String fullPath, String value, Options options, List<CheckStatus> result) { 841 if (path.indexOf("/dates") < 0 || path.indexOf("gregorian") < 0) return this; 842 try { 843 if (path.indexOf("/pattern") >= 0 && path.indexOf("/dateTimeFormat") < 0 844 || path.indexOf("/dateFormatItem") >= 0) { 845 checkPattern2(path, value, result); 846 } 847 } catch (Exception e) { 848 // don't worry about errors 849 } 850 return this; 851 } 852 853 static final SimpleDateFormat neutralFormat = 854 new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", ULocale.ENGLISH); 855 856 static { 857 neutralFormat.setTimeZone(ExampleGenerator.ZONE_SAMPLE); 858 } 859 860 // Get Date-Time in milliseconds getDateTimeinMillis( int year, int month, int date, int hourOfDay, int minute, int second)861 private static long getDateTimeinMillis( 862 int year, int month, int date, int hourOfDay, int minute, int second) { 863 Calendar cal = Calendar.getInstance(); 864 cal.set(year, month, date, hourOfDay, minute, second); 865 return cal.getTimeInMillis(); 866 } 867 868 static long date1950 = getDateTimeinMillis(1950, 0, 1, 0, 0, 0); 869 static long date2010 = getDateTimeinMillis(2010, 0, 1, 0, 0, 0); 870 static long date4004BC = getDateTimeinMillis(-4004, 9, 23, 2, 0, 0); 871 static Random random = new Random(0); 872 873 // We extend VariableField to implement a proper equals() method so we can use 874 // List methods remove() and get(). 875 private class MyVariableField extends DateTimePatternGenerator.VariableField { MyVariableField(String string)876 public MyVariableField(String string) { 877 super(string); 878 } 879 880 @Override equals(Object object)881 public boolean equals(Object object) { 882 if (!(object instanceof DateTimePatternGenerator.VariableField)) { 883 return false; 884 } 885 return (this.toString().equals(object.toString())); 886 } 887 888 @Override hashCode()889 public int hashCode() { 890 return this.toString().hashCode(); 891 } 892 } 893 894 // In a List, replace DateTimePatternGenerator.VariableField items with MyVariableField updateVariableFieldInList(List<Object> items)895 private List<Object> updateVariableFieldInList(List<Object> items) { 896 for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) { 897 Object object = items.get(itemIndex); 898 if (object instanceof DateTimePatternGenerator.VariableField) { 899 items.set(itemIndex, new MyVariableField(object.toString())); 900 } 901 } 902 return items; 903 } 904 checkPattern( DateTimePatternType dateTypePatternType, String path, String fullPath, String value, List<CheckStatus> result)905 private void checkPattern( 906 DateTimePatternType dateTypePatternType, 907 String path, 908 String fullPath, 909 String value, 910 List<CheckStatus> result) 911 throws ParseException { 912 // Map to skeleton including mapping to canonical pattern chars e.g. LLL -> MMM 913 // (ICU internal, for CLDR?) 914 String skeleton = dateTimePatternGenerator.getSkeletonAllowingDuplicates(value); 915 String skeletonCanonical = 916 dateTimePatternGenerator.getCanonicalSkeletonAllowingDuplicates(value); 917 918 if (value.contains("MMM.") 919 || value.contains("LLL.") 920 || value.contains("E.") 921 || value.contains("eee.") 922 || value.contains("ccc.") 923 || value.contains("QQQ.") 924 || value.contains("qqq.")) { 925 result.add( 926 new CheckStatus() 927 .setCause(this) 928 .setMainType(CheckStatus.warningType) 929 .setSubtype(Subtype.incorrectDatePattern) 930 .setMessage( 931 "Your pattern ({0}) is probably incorrect; abbreviated month/weekday/quarter names that need a period should include it in the name, rather than adding it to the pattern.", 932 value)); 933 } 934 XPathParts pathParts = XPathParts.getFrozenInstance(path); 935 String calendar = pathParts.findAttributeValue("calendar", "type"); 936 String id; 937 switch (dateTypePatternType) { 938 case AVAILABLE: 939 id = pathParts.getAttributeValue(-1, "id"); 940 break; 941 case INTERVAL: 942 id = pathParts.getAttributeValue(-2, "id"); 943 break; 944 case STOCK: 945 id = pathParts.getAttributeValue(-3, "type"); 946 break; 947 default: 948 throw new IllegalArgumentException(); 949 } 950 951 if (dateTypePatternType == DateTimePatternType.AVAILABLE 952 || dateTypePatternType == DateTimePatternType.INTERVAL) { 953 // Map to skeleton including mapping to canonical pattern chars e.g. LLL -> MMM 954 // (ICU internal, for CLDR?) 955 String idCanonical = 956 dateTimePatternGenerator.getCanonicalSkeletonAllowingDuplicates(id); 957 if (skeleton.isEmpty()) { 958 result.add( 959 new CheckStatus() 960 .setCause(this) 961 .setMainType(CheckStatus.errorType) 962 .setSubtype(Subtype.incorrectDatePattern) 963 // "Internal ID ({0}) doesn't match generated ID ({1}) for pattern 964 // ({2}). " + 965 .setMessage( 966 "Your pattern ({1}) is incorrect for ID ({0}). " 967 + "You need to supply a pattern according to " 968 + CLDRURLS.DATE_TIME_PATTERNS_URL 969 + ".", 970 id, 971 value)); 972 } else if (!dateTimePatternGenerator.skeletonsAreSimilar( // ICU internal for CLDR 973 idCanonical, skeletonCanonical)) { 974 // Adjust pattern to match skeleton, but only width and subtype within 975 // canonical categories e.g. MMM -> LLLL, H -> HH. Will not change across 976 // canonical categories e.g. m -> M 977 String fixedValue = dateTimePatternGenerator.replaceFieldTypes(value, id); 978 // check to see if that was enough; if not, may need to do more work. 979 String fixedValueCanonical = 980 dateTimePatternGenerator.getCanonicalSkeletonAllowingDuplicates(fixedValue); 981 String valueFromId = null; 982 if (!dateTimePatternGenerator.skeletonsAreSimilar( 983 idCanonical, fixedValueCanonical)) { 984 // Need to do more work. Try two things to get a reasonable suggestion: 985 // - Getting the winning pattern (valueFromId) from availableFormats for id, 986 // if it is not the same as the bad value we already have. 987 // - Replace a pattern field in fixedValue twhose type does not match the 988 // corresponding field from id. 989 // 990 // Here is the first thing, getting the winning pattern (valueFromId) from 991 // availableFormats for id. 992 String availableFormatPath = 993 "//ldml/dates/calendars/calendar[@type=\"" 994 + calendar 995 + "\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\"" 996 + id 997 + "\"]"; 998 valueFromId = 999 getCldrFileToCheck().getWinningValueWithBailey(availableFormatPath); 1000 if (valueFromId != null 1001 && (valueFromId.equals(value) || valueFromId.equals(fixedValue))) { 1002 valueFromId = null; // not useful in this case 1003 } 1004 // 1005 // Here is the second thing, replacing a pattern field that does not match. 1006 // We compare FormatParser Lists for idCanonical and fixedValueCanonical 1007 // and if a mismatch we update the FormatParser list for fixedValue and 1008 // generate an updated string from the FormatParser. 1009 DateTimePatternGenerator.FormatParser idCanonFormat = 1010 new DateTimePatternGenerator.FormatParser(); 1011 idCanonFormat.set(idCanonical); 1012 List<Object> idCanonItems = updateVariableFieldInList(idCanonFormat.getItems()); 1013 DateTimePatternGenerator.FormatParser fixedValueCanonFormat = 1014 new DateTimePatternGenerator.FormatParser(); 1015 fixedValueCanonFormat.set(fixedValueCanonical); 1016 List<Object> fixedValueCanonItems = 1017 updateVariableFieldInList(fixedValueCanonFormat.getItems()); 1018 DateTimePatternGenerator.FormatParser fixedValueFormat = 1019 new DateTimePatternGenerator.FormatParser(); 1020 fixedValueFormat.set(fixedValue); 1021 List<Object> fixedValueItems = 1022 updateVariableFieldInList(fixedValueFormat.getItems()); 1023 // For idCanonFormat and fixedValueCanonFormat we started with skeletons (no 1024 // literal text), so the items we are comparing will all be MyVariableField. We 1025 // iterate over idCanonItems stripping matching items from fixedValueCanonItems 1026 // until we hopefully have one remaining item in each that do not match each 1027 // other. Then in fixedValueItems we replace the mismatched item with the one 1028 // from idCanonItems. 1029 int itemIndex = idCanonItems.size(); 1030 while (--itemIndex >= 0) { 1031 Object idCanonItem = idCanonItems.get(itemIndex); 1032 if (fixedValueCanonItems.remove(idCanonItem)) { 1033 // we have a match, removed it from fixedValueCanonItems, now remove 1034 // it from idCanonItems (ok since we are iterating backwards). 1035 idCanonItems.remove(itemIndex); 1036 } 1037 } 1038 // Hopefully this leaves us with one item in each list, the mismatch to fix. 1039 if (idCanonItems.size() == 1 && fixedValueCanonItems.size() == 1) { 1040 // In fixedValueItems, replace all occurrences of the single item in 1041 // fixedValueCanonItems (bad value) with the item in idCanonItems. 1042 // There might be more than one for e.g. intervalFormats. 1043 Object fixedValueCanonItem = fixedValueCanonItems.get(0); // replace this 1044 Object idCanonItem = idCanonItems.get(0); // with this 1045 boolean didUpdate = false; 1046 while ((itemIndex = fixedValueItems.indexOf(fixedValueCanonItem)) >= 0) { 1047 fixedValueItems.set(itemIndex, idCanonItem); 1048 didUpdate = true; 1049 } 1050 if (didUpdate) { 1051 // Now get the updated fixedValue with this replacement 1052 fixedValue = fixedValueFormat.toString(); 1053 fixedValueCanonical = 1054 dateTimePatternGenerator.getCanonicalSkeletonAllowingDuplicates( 1055 fixedValue); 1056 } 1057 } 1058 // If this replacement attempt did not work, we give up on fixedValue 1059 if (!dateTimePatternGenerator.skeletonsAreSimilar( 1060 idCanonical, fixedValueCanonical)) { 1061 fixedValue = null; 1062 } 1063 } 1064 // Now report problem and suggested fix 1065 String suggestion = "(no suggestion)"; 1066 if (fixedValue != null) { 1067 suggestion = "(" + fixedValue + ")"; 1068 if (valueFromId != null && !valueFromId.equals(fixedValue)) { 1069 suggestion += " or (" + valueFromId + ")"; 1070 } 1071 } else if (valueFromId != null) { 1072 suggestion = "(" + valueFromId + ")"; 1073 } 1074 result.add( 1075 new CheckStatus() 1076 .setCause(this) 1077 .setMainType(CheckStatus.errorType) 1078 .setSubtype(Subtype.incorrectDatePattern) 1079 // "Internal ID ({0}) doesn't match generated ID ({1}) for pattern 1080 // ({2}). " + 1081 .setMessage( 1082 "Your pattern ({2}) doesn't correspond to what is asked for. Yours would be right for an ID ({1}) but not for the ID ({0}). " 1083 + "Please change your pattern to match what was asked, such as {3}, with the right punctuation and/or ordering for your language. See " 1084 + CLDRURLS.DATE_TIME_PATTERNS_URL 1085 + ".", 1086 id, 1087 skeletonCanonical, 1088 value, 1089 suggestion)); 1090 } 1091 if (dateTypePatternType == DateTimePatternType.AVAILABLE) { 1092 // index y+w+ must correpond to pattern containing only Y+ and w+ 1093 if (idCanonical.matches("y+w+") 1094 && !(skeleton.matches("Y+w+") || skeleton.matches("w+Y+"))) { 1095 result.add( 1096 new CheckStatus() 1097 .setCause(this) 1098 .setMainType(CheckStatus.errorType) 1099 .setSubtype(Subtype.incorrectDatePattern) 1100 .setMessage( 1101 "For id {0}, the pattern ({1}) must contain fields Y and w, and no others.", 1102 id, value)); 1103 } 1104 // index M+W msut correspond to pattern containing only M+/L+ and W 1105 if (idCanonical.matches("M+W") 1106 && !(skeletonCanonical.matches("M+W") 1107 || skeletonCanonical.matches("WM+"))) { 1108 result.add( 1109 new CheckStatus() 1110 .setCause(this) 1111 .setMainType(CheckStatus.errorType) 1112 .setSubtype(Subtype.incorrectDatePattern) 1113 .setMessage( 1114 "For id {0}, the pattern ({1}) must contain fields M or L, plus W, and no others.", 1115 id, value)); 1116 } 1117 } 1118 String failureMessage = (String) flexInfo.getFailurePath(path); 1119 if (failureMessage != null) { 1120 result.add( 1121 new CheckStatus() 1122 .setCause(this) 1123 .setMainType(CheckStatus.errorType) 1124 .setSubtype(Subtype.illegalDatePattern) 1125 .setMessage("{0}", new Object[] {failureMessage})); 1126 } 1127 } 1128 if (dateTypePatternType == DateTimePatternType.STOCK) { 1129 int style = 0; 1130 String len = pathParts.findAttributeValue("timeFormatLength", "type"); 1131 DateOrTime dateOrTime = DateOrTime.time; 1132 if (len == null) { 1133 dateOrTime = DateOrTime.date; 1134 style += 4; 1135 len = pathParts.findAttributeValue("dateFormatLength", "type"); 1136 if (len == null) { 1137 len = pathParts.findAttributeValue("dateTimeFormatLength", "type"); 1138 dateOrTime = DateOrTime.dateTime; 1139 } 1140 } 1141 1142 DateTimeLengths dateTimeLength = 1143 DateTimeLengths.valueOf(len.toUpperCase(Locale.ENGLISH)); 1144 1145 if (calendar.equals("gregorian") 1146 && !"root".equals(getCldrFileToCheck().getLocaleID())) { 1147 checkValue(dateTimeLength, dateOrTime, value, result); 1148 } 1149 if (dateOrTime == DateOrTime.dateTime) { 1150 return; // We don't need to do the rest for date/time combo patterns. 1151 } 1152 style += dateTimeLength.ordinal(); 1153 // do regex match with skeletonCanonical but report errors using skeleton; they have 1154 // corresponding field lengths 1155 if (!dateTimePatterns[style].matcher(skeletonCanonical).matches() 1156 && !calendar.equals("chinese") 1157 && !calendar.equals("hebrew")) { 1158 int i = RegexUtilities.findMismatch(dateTimePatterns[style], skeletonCanonical); 1159 String skeletonPosition = skeleton.substring(0, i) + "☹" + skeleton.substring(i); 1160 result.add( 1161 new CheckStatus() 1162 .setCause(this) 1163 .setMainType(CheckStatus.errorType) 1164 .setSubtype(Subtype.missingOrExtraDateField) 1165 .setMessage( 1166 "Field is missing, extra, or the wrong length. Expected {0} [Internal: {1} / {2}]", 1167 new Object[] { 1168 dateTimeMessage[style], 1169 skeletonPosition, 1170 dateTimePatterns[style].pattern() 1171 })); 1172 } 1173 } else if (dateTypePatternType == DateTimePatternType.INTERVAL) { 1174 if (id.contains("y")) { 1175 String greatestDifference = 1176 pathParts.findAttributeValue("greatestDifference", "id"); 1177 int requiredYearFieldCount = 1; 1178 if ("y".equals(greatestDifference)) { 1179 requiredYearFieldCount = 2; 1180 } 1181 int yearFieldCount = 0; 1182 Matcher yearFieldMatcher = YEAR_FIELDS.matcher(value); 1183 while (yearFieldMatcher.find()) { 1184 yearFieldCount++; 1185 } 1186 if (yearFieldCount < requiredYearFieldCount) { 1187 result.add( 1188 new CheckStatus() 1189 .setCause(this) 1190 .setMainType(CheckStatus.errorType) 1191 .setSubtype(Subtype.missingOrExtraDateField) 1192 .setMessage( 1193 "Not enough year fields in interval pattern. Must have {0} but only found {1}", 1194 new Object[] {requiredYearFieldCount, yearFieldCount})); 1195 } 1196 } 1197 // check PatternInfo, for CLDR-17827 1198 // ICU-22835, DateIntervalInfo.genPatternInfo fails for intervals like LLL - MMM (in fa) 1199 if (!(value.contains("LLL") && value.contains("MMM"))) { 1200 PatternInfo pattern = DateIntervalInfo.genPatternInfo(value, false); 1201 try { 1202 String first = pattern.getFirstPart(); 1203 String second = pattern.getSecondPart(); 1204 if (first == null || second == null) { 1205 result.add( 1206 new CheckStatus() 1207 .setCause(this) 1208 .setMainType(CheckStatus.errorType) 1209 .setSubtype(Subtype.incorrectDatePattern) 1210 .setMessage( 1211 "DateIntervalInfo.PatternInfo returns null for first or second part")); 1212 } 1213 } catch (Exception e) { 1214 result.add( 1215 new CheckStatus() 1216 .setCause(this) 1217 .setMainType(CheckStatus.errorType) 1218 .setSubtype(Subtype.incorrectDatePattern) 1219 .setMessage( 1220 "DateIntervalInfo.PatternInfo exception {0}", 1221 new Object[] {e})); 1222 } 1223 } 1224 } 1225 1226 if (value.contains("G") && calendar.equals("gregorian")) { 1227 GyState actual = GyState.forPattern(value); 1228 GyState expected = getExpectedGy(getCldrFileToCheck().getLocaleID()); 1229 if (actual != expected) { 1230 result.add( 1231 new CheckStatus() 1232 .setCause(this) 1233 .setMainType(CheckStatus.warningType) 1234 .setSubtype(Subtype.unexpectedOrderOfEraYear) 1235 .setMessage( 1236 "Unexpected order of era/year. Expected {0}, but got {1} in 〈{2}〉 for {3}/{4}", 1237 expected, actual, value, calendar, id)); 1238 } 1239 } 1240 } 1241 1242 enum DateOrTime { 1243 date, 1244 time, 1245 dateTime 1246 } 1247 1248 static final Map<DateOrTime, Relation<DateTimeLengths, String>> STOCK_PATTERNS = 1249 new EnumMap<>(DateOrTime.class); 1250 1251 // add( Map<DateOrTime, Relation<DateTimeLengths, String>> stockPatterns, DateOrTime dateOrTime, DateTimeLengths dateTimeLength, String... keys)1252 private static void add( 1253 Map<DateOrTime, Relation<DateTimeLengths, String>> stockPatterns, 1254 DateOrTime dateOrTime, 1255 DateTimeLengths dateTimeLength, 1256 String... keys) { 1257 Relation<DateTimeLengths, String> rel = STOCK_PATTERNS.get(dateOrTime); 1258 if (rel == null) { 1259 STOCK_PATTERNS.put( 1260 dateOrTime, 1261 rel = 1262 Relation.of( 1263 new EnumMap<DateTimeLengths, Set<String>>( 1264 DateTimeLengths.class), 1265 LinkedHashSet.class)); 1266 } 1267 rel.putAll(dateTimeLength, Arrays.asList(keys)); 1268 } 1269 1270 /* Ticket #4936 1271 value(short time) = value(hm) or value(Hm) 1272 value(medium time) = value(hms) or value(Hms) 1273 value(long time) = value(medium time+z) 1274 value(full time) = value(medium time+zzzz) 1275 */ 1276 static { add(STOCK_PATTERNS, DateOrTime.time, DateTimeLengths.SHORT, "hm", "Hm")1277 add(STOCK_PATTERNS, DateOrTime.time, DateTimeLengths.SHORT, "hm", "Hm"); add(STOCK_PATTERNS, DateOrTime.time, DateTimeLengths.MEDIUM, "hms", "Hms")1278 add(STOCK_PATTERNS, DateOrTime.time, DateTimeLengths.MEDIUM, "hms", "Hms"); add(STOCK_PATTERNS, DateOrTime.time, DateTimeLengths.LONG, "hms*z", "Hms*z")1279 add(STOCK_PATTERNS, DateOrTime.time, DateTimeLengths.LONG, "hms*z", "Hms*z"); add(STOCK_PATTERNS, DateOrTime.time, DateTimeLengths.FULL, "hms*zzzz", "Hms*zzzz")1280 add(STOCK_PATTERNS, DateOrTime.time, DateTimeLengths.FULL, "hms*zzzz", "Hms*zzzz"); add(STOCK_PATTERNS, DateOrTime.date, DateTimeLengths.SHORT, "yMd")1281 add(STOCK_PATTERNS, DateOrTime.date, DateTimeLengths.SHORT, "yMd"); add(STOCK_PATTERNS, DateOrTime.date, DateTimeLengths.MEDIUM, "yMMMd")1282 add(STOCK_PATTERNS, DateOrTime.date, DateTimeLengths.MEDIUM, "yMMMd"); add(STOCK_PATTERNS, DateOrTime.date, DateTimeLengths.LONG, "yMMMMd", "yMMMd")1283 add(STOCK_PATTERNS, DateOrTime.date, DateTimeLengths.LONG, "yMMMMd", "yMMMd"); add(STOCK_PATTERNS, DateOrTime.date, DateTimeLengths.FULL, "yMMMMEd", "yMMMEd")1284 add(STOCK_PATTERNS, DateOrTime.date, DateTimeLengths.FULL, "yMMMMEd", "yMMMEd"); 1285 } 1286 1287 static final String AVAILABLE_PREFIX = 1288 "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/availableFormats/dateFormatItem[@id=\""; 1289 static final String AVAILABLE_SUFFIX = "\"]"; 1290 static final String APPEND_TIMEZONE = 1291 "//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dateTimeFormats/appendItems/appendItem[@request=\"Timezone\"]"; 1292 checkValue( DateTimeLengths dateTimeLength, DateOrTime dateOrTime, String value, List<CheckStatus> result)1293 private void checkValue( 1294 DateTimeLengths dateTimeLength, 1295 DateOrTime dateOrTime, 1296 String value, 1297 List<CheckStatus> result) { 1298 // Check consistency of the pattern vs. supplemental wrt 12 vs. 24 hour clock. 1299 if (dateOrTime == DateOrTime.time) { 1300 PreferredAndAllowedHour pref = sdi.getTimeData().get(territory); 1301 if (pref == null) { 1302 pref = sdi.getTimeData().get("001"); 1303 } 1304 String checkForHour, clockType; 1305 if (pref.preferred.equals(PreferredAndAllowedHour.HourStyle.h)) { 1306 checkForHour = "h"; 1307 clockType = "12"; 1308 } else { 1309 checkForHour = "H"; 1310 clockType = "24"; 1311 } 1312 if (!value.contains(checkForHour)) { 1313 CheckStatus.Type errType = CheckStatus.errorType; 1314 // French/Canada is strange, they use 24 hr clock while en_CA uses 12. 1315 if (language.equals("fr") && territory.equals("CA")) { 1316 errType = CheckStatus.warningType; 1317 } 1318 1319 result.add( 1320 new CheckStatus() 1321 .setCause(this) 1322 .setMainType(errType) 1323 .setSubtype(Subtype.inconsistentTimePattern) 1324 .setMessage( 1325 "Time format inconsistent with supplemental time data for territory \"" 1326 + territory 1327 + "\"." 1328 + " Use '" 1329 + checkForHour 1330 + "' for " 1331 + clockType 1332 + " hour clock.")); 1333 } 1334 } 1335 if (dateOrTime == DateOrTime.dateTime) { 1336 boolean inQuotes = false; 1337 for (int i = 0; i < value.length(); i++) { 1338 char ch = value.charAt(i); 1339 if (ch == '\'') { 1340 inQuotes = !inQuotes; 1341 } 1342 if (!inQuotes && (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) { 1343 result.add( 1344 new CheckStatus() 1345 .setCause(this) 1346 .setMainType(CheckStatus.errorType) 1347 .setSubtype(Subtype.patternContainsInvalidCharacters) 1348 .setMessage("Unquoted letter \"{0}\" in dateTime format.", ch)); 1349 } 1350 } 1351 } else { 1352 Set<String> keys = STOCK_PATTERNS.get(dateOrTime).get(dateTimeLength); 1353 StringBuilder b = new StringBuilder(); 1354 boolean onlyNulls = true; 1355 int countMismatches = 0; 1356 boolean errorOnMissing = false; 1357 String timezonePattern = null; 1358 Set<String> bases = new LinkedHashSet<>(); 1359 for (String key : keys) { 1360 int star = key.indexOf('*'); 1361 boolean hasStar = star >= 0; 1362 String base = !hasStar ? key : key.substring(0, star); 1363 bases.add(base); 1364 String xpath = AVAILABLE_PREFIX + base + AVAILABLE_SUFFIX; 1365 String value1 = getCldrFileToCheck().getStringValue(xpath); 1366 // String localeFound = getCldrFileToCheck().getSourceLocaleID(xpath, null); && 1367 // !localeFound.equals("root") && !localeFound.equals("code-fallback") 1368 if (value1 != null) { 1369 onlyNulls = false; 1370 if (hasStar) { 1371 String zone = key.substring(star + 1); 1372 timezonePattern = 1373 getResolvedCldrFileToCheck().getStringValue(APPEND_TIMEZONE); 1374 value1 = MessageFormat.format(timezonePattern, value1, zone); 1375 } 1376 if (equalsExceptWidth(value, value1)) { 1377 return; 1378 } 1379 } else { 1380 // Example, if the requiredLevel for the locale is moderate, 1381 // and the level for the path is modern, then we'll skip the error, 1382 // but if the level for the path is basic, then we won't 1383 Level pathLevel = coverageLevel.getLevel(xpath); 1384 if (requiredLevel.compareTo(pathLevel) >= 0) { 1385 errorOnMissing = true; 1386 } 1387 } 1388 add(b, base, value1); 1389 countMismatches++; 1390 } 1391 if (!onlyNulls) { 1392 if (timezonePattern != null) { 1393 b.append(" (with appendZonePattern: “" + timezonePattern + "”)"); 1394 } 1395 String msg = 1396 countMismatches != 1 1397 ? "{1}-{0} → “{2}” didn't match any of the corresponding flexible skeletons: [{3}]. This or the flexible patterns needs to be changed." 1398 : "{1}-{0} → “{2}” didn't match the corresponding flexible skeleton: {3}. This or the flexible pattern needs to be changed."; 1399 result.add( 1400 new CheckStatus() 1401 .setCause(this) 1402 .setMainType(CheckStatus.warningType) 1403 .setSubtype(Subtype.inconsistentDatePattern) 1404 .setMessage(msg, dateTimeLength, dateOrTime, value, b)); 1405 } else { 1406 if (errorOnMissing) { 1407 String msg = 1408 countMismatches != 1 1409 ? "{1}-{0} → “{2}” doesn't have at least one value for a corresponding flexible skeleton {3}, which needs to be added." 1410 : "{1}-{0} → “{2}” doesn't have a value for the corresponding flexible skeleton {3}, which needs to be added."; 1411 result.add( 1412 new CheckStatus() 1413 .setCause(this) 1414 .setMainType(CheckStatus.warningType) 1415 .setSubtype(Subtype.missingDatePattern) 1416 .setMessage( 1417 msg, 1418 dateTimeLength, 1419 dateOrTime, 1420 value, 1421 Joiner.on(", ").join(bases))); 1422 } 1423 } 1424 } 1425 } 1426 add(StringBuilder b, String key, String value1)1427 private void add(StringBuilder b, String key, String value1) { 1428 if (value1 == null) { 1429 return; 1430 } 1431 if (b.length() != 0) { 1432 b.append(" or "); 1433 } 1434 b.append(key + (value1 == null ? " - missing" : " → “" + value1 + "”")); 1435 } 1436 equalsExceptWidth(String value1, String value2)1437 private boolean equalsExceptWidth(String value1, String value2) { 1438 if (value1.equals(value2)) { 1439 return true; 1440 } else if (value2 == null) { 1441 return false; 1442 } 1443 1444 List<Object> items1 = new ArrayList<>(formatParser.set(value1).getItems()); // clone 1445 List<Object> items2 = formatParser.set(value2).getItems(); 1446 if (items1.size() != items2.size()) { 1447 return false; 1448 } 1449 Iterator<Object> it2 = items2.iterator(); 1450 for (Object item1 : items1) { 1451 Object item2 = it2.next(); 1452 if (item1.equals(item2)) { 1453 continue; 1454 } 1455 if (item1 instanceof VariableField && item2 instanceof VariableField) { 1456 // simple test for now, ignore widths 1457 if (item1.toString().charAt(0) == item2.toString().charAt(0)) { 1458 continue; 1459 } 1460 } 1461 return false; 1462 } 1463 return true; 1464 } 1465 1466 static final Set<String> YgLanguages = 1467 new HashSet<>( 1468 Arrays.asList( 1469 "ar", "cs", "da", "de", "en", "es", "fa", "fi", "fr", "he", "hr", "id", 1470 "it", "nl", "no", "pt", "ru", "sv", "tr")); 1471 getExpectedGy(String localeID)1472 private GyState getExpectedGy(String localeID) { 1473 // hack for now 1474 int firstBar = localeID.indexOf('_'); 1475 String lang = firstBar < 0 ? localeID : localeID.substring(0, firstBar); 1476 return YgLanguages.contains(lang) ? GyState.YEAR_ERA : GyState.ERA_YEAR; 1477 } 1478 1479 enum GyState { 1480 YEAR_ERA, 1481 ERA_YEAR, 1482 OTHER; 1483 static DateTimePatternGenerator.FormatParser formatParser = 1484 new DateTimePatternGenerator.FormatParser(); 1485 1486 static synchronized GyState forPattern(String value) { 1487 formatParser.set(value); 1488 int last = -1; 1489 for (Object x : formatParser.getItems()) { 1490 if (x instanceof VariableField) { 1491 int type = ((VariableField) x).getType(); 1492 if (type == DateTimePatternGenerator.ERA 1493 && last == DateTimePatternGenerator.YEAR) { 1494 return GyState.YEAR_ERA; 1495 } else if (type == DateTimePatternGenerator.YEAR 1496 && last == DateTimePatternGenerator.ERA) { 1497 return GyState.ERA_YEAR; 1498 } 1499 last = type; 1500 } 1501 } 1502 return GyState.OTHER; 1503 } 1504 } 1505 1506 enum DateTimeLengths { 1507 SHORT, 1508 MEDIUM, 1509 LONG, 1510 FULL 1511 } 1512 1513 // The patterns below should only use the *canonical* characters for each field type: 1514 // y (not Y, u, U) 1515 // Q (not q) 1516 // M (not L) 1517 // E (not e, c) 1518 // a (not b, B) 1519 // H or h (not k or K) 1520 // v (not z, Z, V) 1521 static final Pattern[] dateTimePatterns = { 1522 PatternCache.get("a*(h|hh|H|HH)(m|mm)"), // time-short 1523 PatternCache.get("a*(h|hh|H|HH)(m|mm)(s|ss)"), // time-medium 1524 PatternCache.get("a*(h|hh|H|HH)(m|mm)(s|ss)(v+)"), // time-long 1525 PatternCache.get("a*(h|hh|H|HH)(m|mm)(s|ss)(v+)"), // time-full 1526 PatternCache.get("G*y{1,4}M{1,2}(d|dd)"), // date-short; allow yyy for Minguo/ROC calendar 1527 PatternCache.get("G*y(yyy)?M{1,3}(d|dd)"), // date-medium 1528 PatternCache.get("G*y(yyy)?M{1,4}(d|dd)"), // date-long 1529 PatternCache.get("G*y(yyy)?M{1,4}E*(d|dd)"), // date-full 1530 PatternCache.get(".*"), // datetime-short 1531 PatternCache.get(".*"), // datetime-medium 1532 PatternCache.get(".*"), // datetime-long 1533 PatternCache.get(".*"), // datetime-full 1534 }; 1535 1536 static final String[] dateTimeMessage = { 1537 "hours (H, HH, h, or hh), and minutes (m or mm)", // time-short 1538 "hours (H, HH, h, or hh), minutes (m or mm), and seconds (s or ss)", // time-medium 1539 "hours (H, HH, h, or hh), minutes (m or mm), and seconds (s or ss); optionally timezone (z, zzzz, v, vvvv)", // time-long 1540 "hours (H, HH, h, or hh), minutes (m or mm), seconds (s or ss), and timezone (z, zzzz, v, vvvv)", // time-full 1541 "year (y, yy, yyyy), month (M or MM), and day (d or dd); optionally era (G)", // date-short 1542 "year (y), month (M, MM, or MMM), and day (d or dd); optionally era (G)", // date-medium 1543 "year (y), month (M, ... MMMM), and day (d or dd); optionally era (G)", // date-long 1544 "year (y), month (M, ... MMMM), and day (d or dd); optionally day of week (EEEE or cccc) or era (G)", // date-full 1545 }; 1546 1547 public String toString(DateTimePatternGenerator.FormatParser formatParser) { 1548 StringBuffer result = new StringBuffer(); 1549 for (Object x : formatParser.getItems()) { 1550 if (x instanceof DateTimePatternGenerator.VariableField) { 1551 result.append(x.toString()); 1552 } else { 1553 result.append(formatParser.quoteLiteral(x.toString())); 1554 } 1555 } 1556 return result.toString(); 1557 } 1558 1559 private void checkPattern2(String path, String value, List<CheckStatus> result) 1560 throws ParseException { 1561 XPathParts pathParts = XPathParts.getFrozenInstance(path); 1562 String calendar = pathParts.findAttributeValue("calendar", "type"); 1563 SimpleDateFormat x = icuServiceBuilder.getDateFormat(calendar, value); 1564 x.setTimeZone(ExampleGenerator.ZONE_SAMPLE); 1565 result.add( 1566 new MyCheckStatus().setFormat(x).setCause(this).setMainType(CheckStatus.demoType)); 1567 } 1568 1569 private DateTimePatternGenerator getDTPGForCalendarType(String calendarType) { 1570 DateTimePatternGenerator dtpg = dtpgForType.get(calendarType); 1571 if (dtpg == null) { 1572 dtpg = flexInfo.getDTPGForCalendarType(calendarType, parentCLDRFiles); 1573 dtpgForType.put(calendarType, dtpg); 1574 } 1575 return dtpg; 1576 } 1577 1578 static final UnicodeSet XGRAPHEME = 1579 new UnicodeSet("[[:mark:][:grapheme_extend:][:punctuation:]]"); 1580 static final UnicodeSet DIGIT = new UnicodeSet("[:decimal_number:]"); 1581 1582 public static class MyCheckStatus extends CheckStatus { 1583 private SimpleDateFormat df; 1584 1585 public MyCheckStatus setFormat(SimpleDateFormat df) { 1586 this.df = df; 1587 return this; 1588 } 1589 1590 @Override 1591 public SimpleDemo getDemo() { 1592 return new MyDemo().setFormat(df); 1593 } 1594 } 1595 1596 static class MyDemo extends FormatDemo { 1597 private SimpleDateFormat df; 1598 1599 @Override 1600 protected String getPattern() { 1601 return df.toPattern(); 1602 } 1603 1604 @Override 1605 protected String getSampleInput() { 1606 return neutralFormat.format(ExampleGenerator.DATE_SAMPLE); 1607 } 1608 1609 public MyDemo setFormat(SimpleDateFormat df) { 1610 this.df = df; 1611 return this; 1612 } 1613 1614 @Override 1615 protected void getArguments(Map<String, String> inout) { 1616 currentPattern = currentInput = currentFormatted = currentReparsed = "?"; 1617 Date d; 1618 try { 1619 currentPattern = inout.get("pattern"); 1620 if (currentPattern != null) df.applyPattern(currentPattern); 1621 else currentPattern = getPattern(); 1622 } catch (Exception e) { 1623 currentPattern = "Use format like: ##,###.##"; 1624 return; 1625 } 1626 try { 1627 currentInput = inout.get("input"); 1628 if (currentInput == null) { 1629 currentInput = getSampleInput(); 1630 } 1631 d = neutralFormat.parse(currentInput); 1632 } catch (Exception e) { 1633 currentInput = "Use neutral format like: 1993-11-31 13:49:02"; 1634 return; 1635 } 1636 try { 1637 currentFormatted = df.format(d); 1638 } catch (Exception e) { 1639 currentFormatted = "Can't format: " + e.getMessage(); 1640 return; 1641 } 1642 try { 1643 parsePosition.setIndex(0); 1644 Date n = df.parse(currentFormatted, parsePosition); 1645 if (parsePosition.getIndex() != currentFormatted.length()) { 1646 currentReparsed = 1647 "Couldn't parse past: " 1648 + "\u200E" 1649 + currentFormatted.substring(0, parsePosition.getIndex()) 1650 + "\u200E"; 1651 } else { 1652 currentReparsed = neutralFormat.format(n); 1653 } 1654 } catch (Exception e) { 1655 currentReparsed = "Can't parse: " + e.getMessage(); 1656 } 1657 } 1658 } 1659 } 1660