xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/test/CheckNumbers.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 package org.unicode.cldr.test;
2 
3 import com.google.common.base.Splitter;
4 import com.google.common.collect.ImmutableSet;
5 import com.ibm.icu.text.DecimalFormat;
6 import com.ibm.icu.text.NumberFormat;
7 import com.ibm.icu.text.UnicodeSet;
8 import com.ibm.icu.util.ULocale;
9 import java.text.ParseException;
10 import java.util.HashSet;
11 import java.util.List;
12 import java.util.Map;
13 import java.util.Random;
14 import java.util.Set;
15 import java.util.TreeSet;
16 import java.util.regex.Matcher;
17 import java.util.regex.Pattern;
18 import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype;
19 import org.unicode.cldr.test.DisplayAndInputProcessor.NumericType;
20 import org.unicode.cldr.util.CLDRFile;
21 import org.unicode.cldr.util.CldrUtility;
22 import org.unicode.cldr.util.Factory;
23 import org.unicode.cldr.util.ICUServiceBuilder;
24 import org.unicode.cldr.util.LocaleIDParser;
25 import org.unicode.cldr.util.PathHeader;
26 import org.unicode.cldr.util.PatternCache;
27 import org.unicode.cldr.util.PluralRulesUtil;
28 import org.unicode.cldr.util.SupplementalDataInfo;
29 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo;
30 import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
31 import org.unicode.cldr.util.SupplementalDataInfo.PluralType;
32 import org.unicode.cldr.util.XPathParts;
33 
34 public class CheckNumbers extends FactoryCheckCLDR {
35     private static final Splitter SEMI_SPLITTER = Splitter.on(';');
36 
37     private static final Set<String> SKIP_TIME_SEPARATOR = ImmutableSet.of("nds", "fr_CA");
38 
39     private static final UnicodeSet FORBIDDEN_NUMERIC_PATTERN_CHARS = new UnicodeSet("[[:n:]-[0]]");
40 
41     /**
42      * If you are going to use ICU services, then ICUServiceBuilder will allow you to create them
43      * entirely from CLDR data, without using the ICU data.
44      */
45     private ICUServiceBuilder icuServiceBuilder = new ICUServiceBuilder();
46 
47     private Set<Count> pluralTypes;
48     private Map<Count, Set<Double>> pluralExamples;
49     private Set<String> validNumberingSystems;
50 
51     private String defaultNumberingSystem;
52     private String defaultTimeSeparatorPath;
53     private String patternForHm;
54 
55     /** A number formatter used to show the English format for comparison. */
56     private static NumberFormat english = NumberFormat.getNumberInstance(ULocale.ENGLISH);
57 
58     static {
59         english.setMaximumFractionDigits(5);
60     }
61 
62     /** Providing random numbers for some of the tests */
63     private static Random random = new Random();
64 
65     private static Pattern ALLOWED_INTEGER = PatternCache.get("1(0+)");
66     private static Pattern COMMA_ABUSE = PatternCache.get(",[0#]([^0#]|$)");
67 
68     /**
69      * A MessageFormat string. For display, anything variable that contains strings that might have
70      * BIDI characters in them needs to be surrounded by \u200E.
71      */
72     static String SampleList = "{0} \u2192 \u201C\u200E{1}\u200E\u201D \u2192 {2}";
73 
74     /** Special flag for POSIX locale. */
75     boolean isPOSIX;
76 
CheckNumbers(Factory factory)77     public CheckNumbers(Factory factory) {
78         super(factory);
79     }
80 
81     /**
82      * Whenever your test needs initialization, override setCldrFileToCheck. It is called for each
83      * new file needing testing. The first two lines will always be the same; checking for null, and
84      * calling the super.
85      */
86     @Override
handleSetCldrFileToCheck( CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors)87     public CheckCLDR handleSetCldrFileToCheck(
88             CLDRFile cldrFileToCheck, Options options, List<CheckStatus> possibleErrors) {
89         if (cldrFileToCheck == null) return this;
90         super.handleSetCldrFileToCheck(cldrFileToCheck, options, possibleErrors);
91         icuServiceBuilder.setCldrFile(getResolvedCldrFileToCheck());
92         isPOSIX = cldrFileToCheck.getLocaleID().indexOf("POSIX") >= 0;
93         SupplementalDataInfo supplementalData =
94                 SupplementalDataInfo.getInstance(getFactory().getSupplementalDirectory());
95         PluralInfo pluralInfo =
96                 supplementalData.getPlurals(PluralType.cardinal, cldrFileToCheck.getLocaleID());
97         pluralTypes = pluralInfo.getCounts();
98         pluralExamples = pluralInfo.getCountToExamplesMap();
99         validNumberingSystems = supplementalData.getNumberingSystems();
100 
101         CLDRFile resolvedFile = getResolvedCldrFileToCheck();
102         defaultNumberingSystem =
103                 resolvedFile.getWinningValue("//ldml/numbers/defaultNumberingSystem");
104         if (defaultNumberingSystem == null
105                 || !validNumberingSystems.contains(defaultNumberingSystem)) {
106             defaultNumberingSystem = "latn";
107         }
108         defaultTimeSeparatorPath =
109                 "//ldml/numbers/symbols[@numberSystem=\""
110                         + defaultNumberingSystem
111                         + "\"]/timeSeparator";
112         // Note for the above, an actual time separator path may add the following after the above:
113         // [@alt='...'] and/or [@draft='...']
114         // Ideally we would get the following for default calendar, here we just use gregorian;
115         // probably OK
116         patternForHm =
117                 resolvedFile.getWinningValue(
118                         "//ldml/dates/calendars/calendar[@type='gregorian']/dateTimeFormats/availableFormats/dateFormatItem[@id='Hm']");
119 
120         return this;
121     }
122 
123     /**
124      * This is the method that does the check. Notice that for performance, you should try to exit
125      * as fast as possible except where the path is one that you are testing.
126      */
127     @Override
handleCheck( String path, String fullPath, String value, Options options, List<CheckStatus> result)128     public CheckCLDR handleCheck(
129             String path, String fullPath, String value, Options options, List<CheckStatus> result) {
130 
131         if (fullPath == null || value == null) return this; // skip paths that we don't have
132 
133         // TODO: could exclude more paths
134         if (!accept(result)) return this;
135 
136         // Do a quick check on the currencyMatch, to make sure that it is a proper UnicodeSet
137         if (path.indexOf("/currencyMatch") >= 0) {
138             try {
139                 new UnicodeSet(value);
140             } catch (Exception e) {
141                 result.add(
142                         new CheckStatus()
143                                 .setCause(this)
144                                 .setMainType(CheckStatus.errorType)
145                                 .setSubtype(Subtype.invalidCurrencyMatchSet)
146                                 .setMessage(
147                                         "Error in creating UnicodeSet {0}; {1}; {2}",
148                                         new Object[] {value, e.getClass().getName(), e}));
149             }
150             return this;
151         }
152 
153         if (path.indexOf("/minimumGroupingDigits") >= 0) {
154             try {
155                 int mgd = Integer.parseInt(value);
156                 if (!CldrUtility.DIGITS.contains(value)) {
157                     result.add(
158                             new CheckStatus()
159                                     .setCause(this)
160                                     .setMainType(CheckStatus.errorType)
161                                     .setSubtype(Subtype.badMinimumGroupingDigits)
162                                     .setMessage(
163                                             "Minimum grouping digits can only contain Western digits [0-9]."));
164                 } else {
165                     if (mgd > 4) {
166                         result.add(
167                                 new CheckStatus()
168                                         .setCause(this)
169                                         .setMainType(CheckStatus.errorType)
170                                         .setSubtype(Subtype.badMinimumGroupingDigits)
171                                         .setMessage(
172                                                 "Minimum grouping digits cannot be greater than 4."));
173 
174                     } else if (mgd < 1) {
175                         result.add(
176                                 new CheckStatus()
177                                         .setCause(this)
178                                         .setMainType(CheckStatus.errorType)
179                                         .setSubtype(Subtype.badMinimumGroupingDigits)
180                                         .setMessage(
181                                                 "Minimum grouping digits cannot be less than 1."));
182 
183                     } else if (mgd > 2) {
184                         result.add(
185                                 new CheckStatus()
186                                         .setCause(this)
187                                         .setMainType(CheckStatus.warningType)
188                                         .setSubtype(Subtype.badMinimumGroupingDigits)
189                                         .setMessage(
190                                                 "Minimum grouping digits > 2 is rare. Please double check this."));
191                     }
192                 }
193             } catch (NumberFormatException e) {
194                 result.add(
195                         new CheckStatus()
196                                 .setCause(this)
197                                 .setMainType(CheckStatus.errorType)
198                                 .setSubtype(Subtype.badMinimumGroupingDigits)
199                                 .setMessage("Minimum grouping digits must be a numeric value."));
200             }
201             return this;
202         }
203 
204         if (path.indexOf("defaultNumberingSystem") >= 0
205                 || path.indexOf("otherNumberingSystems") >= 0) {
206             if (!validNumberingSystems.contains(value)) {
207                 result.add(
208                         new CheckStatus()
209                                 .setCause(this)
210                                 .setMainType(CheckStatus.errorType)
211                                 .setSubtype(Subtype.illegalNumberingSystem)
212                                 .setMessage("Invalid numbering system: " + value));
213             }
214         }
215 
216         if (path.contains(defaultTimeSeparatorPath) && !path.contains("[@alt=") && value != null) {
217             // timeSeparator for default numbering system should be in availableFormats Hm item
218             if (patternForHm != null && !patternForHm.contains(value)) {
219                 // Should be fixed to not require hack, see #11833
220                 if (!SKIP_TIME_SEPARATOR.contains(getCldrFileToCheck().getLocaleID())) {
221                     result.add(
222                             new CheckStatus()
223                                     .setCause(this)
224                                     .setMainType(CheckStatus.errorType)
225                                     .setSubtype(Subtype.invalidSymbol)
226                                     .setMessage(
227                                             "Invalid timeSeparator: "
228                                                     + value
229                                                     + "; must match what is used in Hm time pattern: "
230                                                     + patternForHm));
231                 }
232             }
233         }
234 
235         // quick bail from all other cases
236         NumericType type = NumericType.getNumericType(path);
237         if (type == NumericType.NOT_NUMERIC) {
238             return this; // skip
239         }
240         XPathParts parts = XPathParts.getFrozenInstance(path);
241 
242         boolean isPositive = true;
243         for (String patternPart : SEMI_SPLITTER.split(value)) {
244             if (!isPositive && !"accounting".equals(parts.getAttributeValue(-2, "type"))) {
245                 // must contain the minus sign if not accounting.
246                 // String numberSystem = parts.getAttributeValue(2, "numberSystem");
247                 // String minusSign = "-"; // icuServiceBuilder.getMinusSign(numberSystem == null ?
248                 // "latn" : numberSystem);
249                 if (patternPart.indexOf('-') < 0)
250                     result.add(
251                             new CheckStatus()
252                                     .setCause(this)
253                                     .setMainType(CheckStatus.errorType)
254                                     .setSubtype(Subtype.missingMinusSign)
255                                     .setMessage(
256                                             "Negative format must contain ASCII minus sign (-)."));
257             }
258             // Make sure currency patterns contain a currency symbol
259             if (type == NumericType.CURRENCY || type == NumericType.CURRENCY_ABBREVIATED) {
260                 if (type == NumericType.CURRENCY_ABBREVIATED && value.equals("0")) {
261                     // do nothing, not problem
262                 } else if (path.contains("noCurrency")) {
263                     if (patternPart.indexOf("\u00a4") >= 0) {
264                         result.add(
265                                 new CheckStatus()
266                                         .setCause(this)
267                                         .setMainType(CheckStatus.errorType)
268                                         .setSubtype(Subtype.currencyPatternUnexpectedCurrencySymbol)
269                                         .setMessage(
270                                                 "noCurrency formatting pattern must not contain a currency symbol."));
271                     }
272                 } else if (patternPart.indexOf("\u00a4") < 0) {
273                     // check for compact format
274                     result.add(
275                             new CheckStatus()
276                                     .setCause(this)
277                                     .setMainType(CheckStatus.errorType)
278                                     .setSubtype(Subtype.currencyPatternMissingCurrencySymbol)
279                                     .setMessage(
280                                             "Currency formatting pattern must contain a currency symbol."));
281                 }
282             }
283 
284             // Make sure percent formatting patterns contain a percent symbol, in each part
285             if (type == NumericType.PERCENT) {
286                 if (patternPart.indexOf("%") < 0)
287                     result.add(
288                             new CheckStatus()
289                                     .setCause(this)
290                                     .setMainType(CheckStatus.errorType)
291                                     .setSubtype(Subtype.percentPatternMissingPercentSymbol)
292                                     .setMessage(
293                                             "Percentage formatting pattern must contain a % symbol."));
294             }
295             isPositive = false;
296         }
297 
298         // check all
299         if (FORBIDDEN_NUMERIC_PATTERN_CHARS.containsSome(value)) {
300             UnicodeSet chars = new UnicodeSet().addAll(value);
301             chars.retainAll(FORBIDDEN_NUMERIC_PATTERN_CHARS);
302             result.add(
303                     new CheckStatus()
304                             .setCause(this)
305                             .setMainType(CheckStatus.errorType)
306                             .setSubtype(Subtype.illegalCharactersInNumberPattern)
307                             .setMessage(
308                                     "Pattern contains forbidden characters: \u200E{0}\u200E",
309                                     new Object[] {chars.toPattern(false)}));
310         }
311 
312         // get the final type
313         String lastType = parts.getAttributeValue(-1, "type");
314         int zeroCount = 0;
315         // it can only be null or an integer of the form 10+
316         if (lastType != null && !lastType.equals("standard")) {
317             Matcher matcher = ALLOWED_INTEGER.matcher(lastType);
318             if (matcher.matches()) {
319                 zeroCount = matcher.end(1) - matcher.start(1); // number of ascii zeros
320             } else {
321                 result.add(
322                         new CheckStatus()
323                                 .setCause(this)
324                                 .setMainType(CheckStatus.errorType)
325                                 .setSubtype(Subtype.badNumericType)
326                                 .setMessage(
327                                         "The type of a numeric pattern must be missing or of the form 10...."));
328             }
329         }
330 
331         // Check the validity of the pattern. If this check fails, all other checks
332         // after it will fail, so exit early.
333         UnicodeSet illegalChars = findUnquotedChars(type, value);
334         if (illegalChars != null) {
335             result.add(
336                     new CheckStatus()
337                             .setCause(this)
338                             .setMainType(CheckStatus.errorType)
339                             .setSubtype(Subtype.illegalCharactersInNumberPattern)
340                             .setMessage(
341                                     "Pattern contains characters that must be escaped or removed: {0}",
342                                     new Object[] {illegalChars}));
343             return this;
344         }
345 
346         // Tests that assume that the value is a valid number pattern.
347         // Notice that we pick up any exceptions, so that we can
348         // give a reasonable error message.
349         parts = parts.cloneAsThawed();
350         try {
351             if (type == NumericType.DECIMAL_ABBREVIATED
352                     || type == NumericType.CURRENCY_ABBREVIATED) {
353                 // Check for consistency in short/long decimal formats.
354                 checkDecimalFormatConsistency(parts, path, value, result, type);
355             } else {
356                 checkPattern(path, fullPath, value, result, false);
357             }
358 
359             // Check for sane usage of grouping separators.
360             if (COMMA_ABUSE.matcher(value).find()) {
361                 result.add(
362                         new CheckStatus()
363                                 .setCause(this)
364                                 .setMainType(CheckStatus.errorType)
365                                 .setSubtype(Subtype.tooManyGroupingSeparators)
366                                 .setMessage(
367                                         "Grouping separator (,) should not be used to group tens. Check if a decimal symbol (.) should have been used instead."));
368             } else {
369                 // check that we have a canonical pattern
370                 String pattern = getCanonicalPattern(value, type, zeroCount, isPOSIX);
371                 if (!pattern.equals(value)) {
372                     result.add(
373                             new CheckStatus()
374                                     .setCause(this)
375                                     .setMainType(CheckStatus.errorType)
376                                     .setSubtype(Subtype.numberPatternNotCanonical)
377                                     .setMessage(
378                                             "Value should be \u200E{0}\u200E",
379                                             new Object[] {pattern}));
380                 }
381             }
382 
383         } catch (Exception e) {
384             result.add(
385                     new CheckStatus()
386                             .setCause(this)
387                             .setMainType(CheckStatus.errorType)
388                             .setSubtype(Subtype.illegalNumberFormat)
389                             .setMessage(e.getMessage() == null ? e.toString() : e.getMessage()));
390         }
391         return this;
392     }
393 
394     /**
395      * Looks for any unquoted non-pattern characters in the specified string which would make the
396      * pattern invalid.
397      *
398      * @param type the type of the pattern
399      * @param value the string containing the number pattern
400      * @return the set of unquoted chars in the pattern
401      */
findUnquotedChars(NumericType type, String value)402     private static UnicodeSet findUnquotedChars(NumericType type, String value) {
403         UnicodeSet chars = new UnicodeSet();
404         UnicodeSet allowedChars = null;
405         // Allow the digits 1-9 here because they're already checked in another test.
406         if (type == NumericType.DECIMAL_ABBREVIATED) {
407             allowedChars = new UnicodeSet("[0-9]");
408         } else {
409             allowedChars = new UnicodeSet("[0-9#@.,E+]");
410         }
411         for (String subPattern : value.split(";")) {
412             // Any unquoted non-special chars are allowed in front of or behind the numerical
413             // symbols, but not in between, e.g. " 0000" is okay but "0 000" is not.
414             int firstIdx = -1;
415             for (int i = 0, len = subPattern.length(); i < len; i++) {
416                 char c = subPattern.charAt(i);
417                 if (c == '0' || c == '#') {
418                     firstIdx = i;
419                     break;
420                 }
421             }
422             if (firstIdx == -1) {
423                 continue;
424             }
425             int lastIdx = Math.max(subPattern.lastIndexOf("0"), subPattern.lastIndexOf('#'));
426             chars.addAll(subPattern.substring(firstIdx, lastIdx));
427         }
428         chars.removeAll(allowedChars);
429         return chars.size() > 0 ? chars : null;
430     }
431 
432     /**
433      * Override this method if you are going to provide examples of usage. Only needed for more
434      * complicated cases, like number patterns.
435      */
436     @Override
handleGetExamples( String path, String fullPath, String value, Options options, List result)437     public CheckCLDR handleGetExamples(
438             String path, String fullPath, String value, Options options, List result) {
439         if (path.indexOf("/numbers") < 0) return this;
440         try {
441             if (path.indexOf("/pattern") >= 0 && path.indexOf("/patternDigit") < 0) {
442                 checkPattern(path, fullPath, value, result, true);
443             }
444             if (path.indexOf("/currencies") >= 0 && path.endsWith("/symbol")) {
445                 checkCurrencyFormats(path, fullPath, value, result, true);
446             }
447         } catch (Exception e) {
448             // don't worry about errors here, they'll be caught above.
449         }
450         return this;
451     }
452 
453     /**
454      * Only called when we are looking at compact decimals. Make sure that we have a consistent
455      * number of 0's at each level, and check for missing 0's. (The latter are only allowed for
456      * "singular" plural forms).
457      */
checkDecimalFormatConsistency( XPathParts parts, String path, String value, List<CheckStatus> result, NumericType type)458     private void checkDecimalFormatConsistency(
459             XPathParts parts,
460             String path,
461             String value,
462             List<CheckStatus> result,
463             NumericType type) {
464         // Look for duplicates of decimal formats with the same number
465         // system and type.
466         // Decimal formats of the same type should have the same number
467         // of integer digits in all the available plural forms.
468         DecimalFormat format = new DecimalFormat(value);
469         int numIntegerDigits = format.getMinimumIntegerDigits();
470         String countString = parts.getAttributeValue(-1, "count");
471         Count thisCount = null;
472         try {
473             thisCount = Count.valueOf(countString);
474         } catch (Exception e) {
475             // can happen if count is numeric literal, like "1"
476         }
477         CLDRFile resolvedFile = getResolvedCldrFileToCheck();
478         Set<String> inconsistentItems = new TreeSet<>();
479         Set<Count> otherCounts = new HashSet<>(pluralTypes);
480         if (thisCount != null) {
481             Set<Double> pe = pluralExamples.get(thisCount);
482             if (pe == null) {
483                 /*
484                  * This can happen for unknown reasons when path =
485                  * //ldml/numbers/currencyFormats[@numberSystem="latn"]/currencyFormatLength[@type="short"]/currencyFormat[@type="standard"]/pattern[@type="1000"][@count="one"]
486                  * TODO: something? At least don't throw NullPointerException, as happened when the code
487                  * was "... pluralExamples.get(thisCount).size() ..."; never assume get() returns non-null
488                  */
489                 return;
490             }
491             if (!value.contains("0")) {
492                 switch (pe.size()) {
493                     case 0: // do nothing, shouldn't ever happen
494                         break;
495                     case 1:
496                         // If a plural case corresponds to a single double value, the format is
497                         // allowed to not include a numeric value and in this way be inconsistent
498                         // with the numeric formats used for other plural cases.
499                         return;
500                     default: // we have too many digits
501                         result.add(
502                                 new CheckStatus()
503                                         .setCause(this)
504                                         .setMainType(CheckStatus.errorType)
505                                         .setSubtype(Subtype.missingZeros)
506                                         .setMessage(
507                                                 "Values without a zero must only be used where there is only one possible numeric form, but this has multiple: {0} ",
508                                                 pe.toString()));
509                 }
510             }
511             otherCounts.remove(thisCount);
512         }
513         for (Count count : otherCounts) {
514             // System.out.println("## double examples for count " + count + ": " +
515             // pluralExamples.get(count));
516             parts.setAttribute("pattern", "count", count.toString());
517             String otherPattern = resolvedFile.getWinningValue(parts.toString());
518             // Ignore the type="other" pattern if not present or invalid.
519             if (otherPattern == null || findUnquotedChars(type, otherPattern) != null) continue;
520             format = new DecimalFormat(otherPattern);
521             int numIntegerDigitsOther = format.getMinimumIntegerDigits();
522             if (pluralExamples.get(count).size() == 1 && numIntegerDigitsOther <= 0) {
523                 // If a plural case corresponds to a single double value, the format is
524                 // allowed to not include a numeric value and in this way be inconsistent
525                 // with the numeric formats used for other plural cases.
526                 continue;
527             }
528             // skip special cases where the count=many is optional
529             if (count == Count.many
530                     && PluralRulesUtil.LOCALES_WITH_OPTIONAL_MANY.contains(
531                             LocaleIDParser.getSimpleBaseLanguage(resolvedFile.getLocaleID()))) {
532                 continue;
533             }
534             if (numIntegerDigitsOther != numIntegerDigits) {
535                 PathHeader pathHeader = getPathHeaderFactory().fromPath(parts.toString());
536                 inconsistentItems.add(pathHeader.getHeaderCode());
537             }
538         }
539         if (inconsistentItems.size() > 0) {
540             // Get label for items of this type by removing the count.
541             PathHeader pathHeader =
542                     getPathHeaderFactory().fromPath(path.substring(0, path.lastIndexOf('[')));
543             String groupHeaderString = pathHeader.getHeaderCode();
544             boolean isWinningValue = resolvedFile.getWinningValue(path).equals(value);
545             result.add(
546                     new CheckStatus()
547                             .setCause(this)
548                             .setMainType(
549                                     isWinningValue
550                                             ? CheckStatus.errorType
551                                             : CheckStatus.warningType)
552                             .setSubtype(Subtype.inconsistentPluralFormat)
553                             .setMessage(
554                                     "All values for {0} must have the same number of digits. "
555                                             + "The number of zeros in this pattern is inconsistent with the following: {1}.",
556                                     groupHeaderString, inconsistentItems.toString()));
557         }
558     }
559 
560     /**
561      * This method builds a decimal format (based on whether the pattern is for currencies or not)
562      * and tests samples.
563      */
checkPattern( String path, String fullPath, String value, List result, boolean generateExamples)564     private void checkPattern(
565             String path, String fullPath, String value, List result, boolean generateExamples)
566             throws ParseException {
567         if (value.indexOf('\u00a4') >= 0) { // currency pattern
568             DecimalFormat x = icuServiceBuilder.getCurrencyFormat("XXX");
569             addOrTestSamples(x, x.toPattern(), value, result, generateExamples);
570         } else {
571             DecimalFormat x = icuServiceBuilder.getNumberFormat(value);
572             addOrTestSamples(x, value, "", result, generateExamples);
573         }
574     }
575 
576     /** Check some currency patterns. */
checkCurrencyFormats( String path, String fullPath, String value, List result, boolean generateExamples)577     private void checkCurrencyFormats(
578             String path, String fullPath, String value, List result, boolean generateExamples)
579             throws ParseException {
580         DecimalFormat x = icuServiceBuilder.getCurrencyFormat(CLDRFile.getCode(path));
581         addOrTestSamples(x, x.toPattern(), value, result, generateExamples);
582     }
583 
584     /**
585      * Generates some samples. If we are producing examples, these are used for that; otherwise they
586      * are just tested.
587      */
addOrTestSamples( DecimalFormat x, String pattern, String context, List result, boolean generateExamples)588     private void addOrTestSamples(
589             DecimalFormat x, String pattern, String context, List result, boolean generateExamples)
590             throws ParseException {
591         // Object[] arguments = new Object[3];
592         //
593         // double sample = getRandomNumber();
594         // arguments[0] = String.valueOf(sample);
595         // String formatted = x.format(sample);
596         // arguments[1] = formatted;
597         // boolean gotFailure = false;
598         // try {
599         // parsePosition.setIndex(0);
600         // double parsed = x.parse(formatted, parsePosition).doubleValue();
601         // if (parsePosition.getIndex() != formatted.length()) {
602         // arguments[2] = "Couldn't parse past: " + "\u200E" +
603         // formatted.substring(0,parsePosition.getIndex()) +
604         // "\u200E";
605         // gotFailure = true;
606         // } else {
607         // arguments[2] = String.valueOf(parsed);
608         // }
609         // } catch (Exception e) {
610         // arguments[2] = e.getMessage();
611         // gotFailure = true;
612         // }
613         // htmlMessage.append(pattern1)
614         // .append(TransliteratorUtilities.toXML.transliterate(String.valueOf(sample)))
615         // .append(pattern2)
616         // .append(TransliteratorUtilities.toXML.transliterate(formatted))
617         // .append(pattern3)
618         // .append(TransliteratorUtilities.toXML.transliterate(String.valueOf(parsed)))
619         // .append(pattern4);
620         // if (generateExamples || gotFailure) {
621         // result.add(new CheckStatus()
622         // .setCause(this).setType(CheckStatus.exampleType)
623         // .setMessage(SampleList, arguments));
624         // }
625         if (generateExamples) {
626             result.add(
627                     new MyCheckStatus()
628                             .setFormat(x, context)
629                             .setCause(this)
630                             .setMainType(CheckStatus.demoType));
631         }
632     }
633 
634     /**
635      * Generate a randome number for testing, with a certain number of decimal places, and half the
636      * time negative
637      */
getRandomNumber()638     private static double getRandomNumber() {
639         // min = 12345.678
640         double rand = random.nextDouble();
641         // System.out.println(rand);
642         double sample = Math.round(rand * 100000.0 * 1000.0) / 1000.0 + 10000.0;
643         if (random.nextBoolean()) sample = -sample;
644         return sample;
645     }
646 
647     /*
648      * static String pattern1 =
649      * "<table border='1' cellpadding='2' cellspacing='0' style='border-collapse: collapse' style='width: 100%'>"
650      * + "<tr>"
651      * + "<td nowrap width='1%'>Input:</td>"
652      * + "<td><input type='text' name='T1' size='50' style='width: 100%' value='";
653      * static String pattern2 = "'></td>"
654      * + "<td nowrap width='1%'><input type='submit' value='Test' name='B1'></td>"
655      * + "<td nowrap width='1%'>Formatted:</td>"
656      * + "<td><input type='text' name='T2' size='50' style='width: 100%' value='";
657      * static String pattern3 = "'></td>"
658      * + "<td nowrap width='1%'>Parsed:</td>"
659      * + "<td><input type='text' name='T3' size='50' style='width: 100%' value='";
660      * static String pattern4 = "'></td>"
661      * + "</tr>"
662      * + "</table>";
663      */
664 
665     /**
666      * Produce a canonical pattern, which will vary according to type and whether it is posix or
667      * not.
668      *
669      * @param count
670      * @param path
671      */
getCanonicalPattern( String inpattern, NumericType type, int zeroCount, boolean isPOSIX)672     public static String getCanonicalPattern(
673             String inpattern, NumericType type, int zeroCount, boolean isPOSIX) {
674         // TODO fix later to properly handle quoted ;
675         DecimalFormat df = new DecimalFormat(inpattern);
676         String pattern;
677 
678         if (zeroCount == 0) {
679             int[] digits = isPOSIX ? type.getPosixDigitCount() : type.getDigitCount();
680             df.setMinimumIntegerDigits(digits[0]);
681             df.setMinimumFractionDigits(digits[1]);
682             df.setMaximumFractionDigits(digits[2]);
683             pattern = df.toPattern();
684         } else { // of form 1000. Result must be 0+(.0+)?
685             if (type == NumericType.CURRENCY_ABBREVIATED
686                     || type == NumericType.DECIMAL_ABBREVIATED) {
687                 if (!inpattern.contains("0")) {
688                     return inpattern; // we check in checkDecimalFormatConsistency to make sure that
689                     // the "no number" case is allowed.
690                 }
691                 if (!inpattern.contains("0.0")) {
692                     df.setMinimumFractionDigits(0); // correct the current rewrite
693                 }
694             }
695             df.setMaximumFractionDigits(df.getMinimumFractionDigits());
696             int minimumIntegerDigits = df.getMinimumIntegerDigits();
697             if (minimumIntegerDigits < 1) minimumIntegerDigits = 1;
698             df.setMaximumIntegerDigits(minimumIntegerDigits);
699             pattern = df.toPattern();
700         }
701 
702         // int pos = pattern.indexOf(';');
703         // if (pos < 0) return pattern + ";-" + pattern;
704         return pattern;
705     }
706 
707     /** You don't normally need this, unless you are doing a demo also. */
708     public static class MyCheckStatus extends CheckStatus {
709         private DecimalFormat df;
710         String context;
711 
setFormat(DecimalFormat df, String context)712         public MyCheckStatus setFormat(DecimalFormat df, String context) {
713             this.df = df;
714             this.context = context;
715             return this;
716         }
717 
718         @Override
getDemo()719         public SimpleDemo getDemo() {
720             return new MyDemo().setFormat(df);
721         }
722     }
723 
724     /**
725      * Here is how to do a demo. You provide the function getArguments that takes in-and-out
726      * parameters.
727      */
728     static class MyDemo extends FormatDemo {
729         private DecimalFormat df;
730 
731         @Override
getPattern()732         protected String getPattern() {
733             return df.toPattern();
734         }
735 
736         @Override
getSampleInput()737         protected String getSampleInput() {
738             return String.valueOf(ExampleGenerator.NUMBER_SAMPLE);
739         }
740 
setFormat(DecimalFormat df)741         public MyDemo setFormat(DecimalFormat df) {
742             this.df = df;
743             return this;
744         }
745 
746         @Override
getArguments(Map<String, String> inout)747         protected void getArguments(Map<String, String> inout) {
748             currentPattern = currentInput = currentFormatted = currentReparsed = "?";
749             double d;
750             try {
751                 currentPattern = inout.get("pattern");
752                 if (currentPattern != null) df.applyPattern(currentPattern);
753                 else currentPattern = getPattern();
754             } catch (Exception e) {
755                 currentPattern = "Use format like: ##,###.##";
756                 return;
757             }
758             try {
759                 currentInput = inout.get("input");
760                 if (currentInput == null) {
761                     currentInput = getSampleInput();
762                 }
763                 d = Double.parseDouble(currentInput);
764             } catch (Exception e) {
765                 currentInput = "Use English format: 1234.56";
766                 return;
767             }
768             try {
769                 currentFormatted = df.format(d);
770             } catch (Exception e) {
771                 currentFormatted = "Can't format: " + e.getMessage();
772                 return;
773             }
774             try {
775                 parsePosition.setIndex(0);
776                 Number n = df.parse(currentFormatted, parsePosition);
777                 if (parsePosition.getIndex() != currentFormatted.length()) {
778                     currentReparsed =
779                             "Couldn't parse past: \u200E"
780                                     + currentFormatted.substring(0, parsePosition.getIndex())
781                                     + "\u200E";
782                 } else {
783                     currentReparsed = n.toString();
784                 }
785             } catch (Exception e) {
786                 currentReparsed = "Can't parse: " + e.getMessage();
787             }
788         }
789     }
790 }
791