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