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