• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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