• 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 (path.contains("noCurrency")) {
227                     if (patternPart.indexOf("\u00a4") >= 0) {
228                        result.add(new CheckStatus().setCause(this).setMainType(CheckStatus.errorType)
229                            .setSubtype(Subtype.currencyPatternUnexpectedCurrencySymbol)
230                            .setMessage("noCurrency formatting pattern must not contain a currency symbol."));
231                     }
232                 } else if (patternPart.indexOf("\u00a4") < 0) {
233                     // check for compact format
234                     result.add(new CheckStatus().setCause(this).setMainType(CheckStatus.errorType)
235                         .setSubtype(Subtype.currencyPatternMissingCurrencySymbol)
236                         .setMessage("Currency formatting pattern must contain a currency symbol."));
237                 }
238             }
239 
240             // Make sure percent formatting patterns contain a percent symbol, in each part
241             if (type == NumericType.PERCENT) {
242                 if (patternPart.indexOf("%") < 0)
243                     result.add(new CheckStatus().setCause(this).setMainType(CheckStatus.errorType)
244                         .setSubtype(Subtype.percentPatternMissingPercentSymbol)
245                         .setMessage("Percentage formatting pattern must contain a % symbol."));
246             }
247             isPositive = false;
248         }
249 
250         // check all
251         if (FORBIDDEN_NUMERIC_PATTERN_CHARS.containsSome(value)) {
252             UnicodeSet chars = new UnicodeSet().addAll(value);
253             chars.retainAll(FORBIDDEN_NUMERIC_PATTERN_CHARS);
254             result.add(new CheckStatus()
255                 .setCause(this)
256                 .setMainType(CheckStatus.errorType)
257                 .setSubtype(Subtype.illegalCharactersInNumberPattern)
258                 .setMessage("Pattern contains forbidden characters: \u200E{0}\u200E",
259                     new Object[] { chars.toPattern(false) }));
260         }
261 
262         // get the final type
263         String lastType = parts.getAttributeValue(-1, "type");
264         int zeroCount = 0;
265         // it can only be null or an integer of the form 10+
266         if (lastType != null && !lastType.equals("standard")) {
267             Matcher matcher = ALLOWED_INTEGER.matcher(lastType);
268             if (matcher.matches()) {
269                 zeroCount = matcher.end(1) - matcher.start(1); // number of ascii zeros
270             } else {
271                 result.add(new CheckStatus().setCause(this).setMainType(CheckStatus.errorType)
272                     .setSubtype(Subtype.badNumericType)
273                     .setMessage("The type of a numeric pattern must be missing or of the form 10...."));
274             }
275         }
276 
277         // Check the validity of the pattern. If this check fails, all other checks
278         // after it will fail, so exit early.
279         UnicodeSet illegalChars = findUnquotedChars(type, value);
280         if (illegalChars != null) {
281             result.add(new CheckStatus().setCause(this)
282                 .setMainType(CheckStatus.errorType)
283                 .setSubtype(Subtype.illegalCharactersInNumberPattern)
284                 .setMessage("Pattern contains characters that must be escaped or removed: {0}", new Object[] { illegalChars }));
285             return this;
286         }
287 
288         // Tests that assume that the value is a valid number pattern.
289         // Notice that we pick up any exceptions, so that we can
290         // give a reasonable error message.
291         parts = parts.cloneAsThawed();
292         try {
293             if (type == NumericType.DECIMAL_ABBREVIATED || type == NumericType.CURRENCY_ABBREVIATED) {
294                 // Check for consistency in short/long decimal formats.
295                 checkDecimalFormatConsistency(parts, path, value, result, type);
296             } else {
297                 checkPattern(path, fullPath, value, result, false);
298             }
299 
300             // Check for sane usage of grouping separators.
301             if (COMMA_ABUSE.matcher(value).find()) {
302                 result
303                 .add(new CheckStatus()
304                     .setCause(this)
305                     .setMainType(CheckStatus.errorType)
306                     .setSubtype(Subtype.tooManyGroupingSeparators)
307                     .setMessage(
308                         "Grouping separator (,) should not be used to group tens. Check if a decimal symbol (.) should have been used instead."));
309             } else {
310                 // check that we have a canonical pattern
311                 String pattern = getCanonicalPattern(value, type, zeroCount, isPOSIX);
312                 if (!pattern.equals(value)) {
313                     result.add(new CheckStatus()
314                         .setCause(this).setMainType(CheckStatus.errorType)
315                         .setSubtype(Subtype.numberPatternNotCanonical)
316                         .setMessage("Value should be \u200E{0}\u200E", new Object[] { pattern }));
317                 }
318             }
319 
320         } catch (Exception e) {
321             result.add(new CheckStatus().setCause(this).setMainType(CheckStatus.errorType)
322                 .setSubtype(Subtype.illegalNumberFormat)
323                 .setMessage(e.getMessage() == null ? e.toString() : e.getMessage()));
324         }
325         return this;
326     }
327 
328     /**
329      * Looks for any unquoted non-pattern characters in the specified string
330      * which would make the pattern invalid.
331      * @param type the type of the pattern
332      * @param value the string containing the number pattern
333      * @return the set of unquoted chars in the pattern
334      */
findUnquotedChars(NumericType type, String value)335     private static UnicodeSet findUnquotedChars(NumericType type, String value) {
336         UnicodeSet chars = new UnicodeSet();
337         UnicodeSet allowedChars = null;
338         // Allow the digits 1-9 here because they're already checked in another test.
339         if (type == NumericType.DECIMAL_ABBREVIATED) {
340             allowedChars = new UnicodeSet("[0-9]");
341         } else {
342             allowedChars = new UnicodeSet("[0-9#@.,E+]");
343         }
344         for (String subPattern : value.split(";")) {
345             // Any unquoted non-special chars are allowed in front of or behind the numerical
346             // symbols, but not in between, e.g. " 0000" is okay but "0 000" is not.
347             int firstIdx = -1;
348             for (int i = 0, len = subPattern.length(); i < len; i++) {
349                 char c = subPattern.charAt(i);
350                 if (c == '0' || c == '#') {
351                     firstIdx = i;
352                     break;
353                 }
354             }
355             if (firstIdx == -1) {
356                 continue;
357             }
358             int lastIdx = Math.max(subPattern.lastIndexOf("0"), subPattern.lastIndexOf('#'));
359             chars.addAll(subPattern.substring(firstIdx, lastIdx));
360         }
361         chars.removeAll(allowedChars);
362         return chars.size() > 0 ? chars : null;
363     }
364 
365     /**
366      * Override this method if you are going to provide examples of usage.
367      * Only needed for more complicated cases, like number patterns.
368      */
369     @Override
handleGetExamples(String path, String fullPath, String value, Options options, List result)370     public CheckCLDR handleGetExamples(String path, String fullPath, String value, Options options, List result) {
371         if (path.indexOf("/numbers") < 0) return this;
372         try {
373             if (path.indexOf("/pattern") >= 0 && path.indexOf("/patternDigit") < 0) {
374                 checkPattern(path, fullPath, value, result, true);
375             }
376             if (path.indexOf("/currencies") >= 0 && path.endsWith("/symbol")) {
377                 checkCurrencyFormats(path, fullPath, value, result, true);
378             }
379         } catch (Exception e) {
380             // don't worry about errors here, they'll be caught above.
381         }
382         return this;
383     }
384 
385     /**
386      * 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.
387      * (The latter are only allowed for "singular" plural forms).
388      */
checkDecimalFormatConsistency(XPathParts parts, String path, String value, List<CheckStatus> result, NumericType type)389     private void checkDecimalFormatConsistency(XPathParts parts, String path, String value,
390         List<CheckStatus> result, NumericType type) {
391         // Look for duplicates of decimal formats with the same number
392         // system and type.
393         // Decimal formats of the same type should have the same number
394         // of integer digits in all the available plural forms.
395         DecimalFormat format = new DecimalFormat(value);
396         int numIntegerDigits = format.getMinimumIntegerDigits();
397         String countString = parts.getAttributeValue(-1, "count");
398         Count thisCount = null;
399         try {
400             thisCount = Count.valueOf(countString);
401         } catch (Exception e) {
402             // can happen if count is numeric literal, like "1"
403         }
404         CLDRFile resolvedFile = getResolvedCldrFileToCheck();
405         Set<String> inconsistentItems = new TreeSet<>();
406         Set<Count> otherCounts = new HashSet<>(pluralTypes);
407         if (thisCount != null) {
408             Set<Double> pe = pluralExamples.get(thisCount);
409             if (pe == null) {
410                 /*
411                  * This can happen for unknown reasons when path =
412                  * //ldml/numbers/currencyFormats[@numberSystem="latn"]/currencyFormatLength[@type="short"]/currencyFormat[@type="standard"]/pattern[@type="1000"][@count="one"]
413                  * TODO: something? At least don't throw NullPointerException, as happened when the code
414                  * was "... pluralExamples.get(thisCount).size() ..."; never assume get() returns non-null
415                  */
416                 return;
417             }
418             if (!value.contains("0")) {
419                 switch (pe.size()) {
420                 case 0:  // do nothing, shouldn't ever happen
421                     break;
422                 case 1:
423                     // If a plural case corresponds to a single double value, the format is
424                     // allowed to not include a numeric value and in this way be inconsistent
425                     // with the numeric formats used for other plural cases.
426                     return;
427                 default: // we have too many digits
428                     result.add(new CheckStatus().setCause(this)
429                         .setMainType(CheckStatus.errorType)
430                         .setSubtype(Subtype.missingZeros)
431                         .setMessage("Values without a zero must only be used where there is only one possible numeric form, but this has multiple: {0} ",
432                             pe.toString()));
433                 }
434             }
435             otherCounts.remove(thisCount);
436         }
437         for (Count count : otherCounts) {
438             // System.out.println("## double examples for count " + count + ": " + pluralExamples.get(count));
439             parts.setAttribute("pattern", "count", count.toString());
440             String otherPattern = resolvedFile.getWinningValue(parts.toString());
441             // Ignore the type="other" pattern if not present or invalid.
442             if (otherPattern == null || findUnquotedChars(type, otherPattern) != null) continue;
443             format = new DecimalFormat(otherPattern);
444             int numIntegerDigitsOther = format.getMinimumIntegerDigits();
445             if (pluralExamples.get(count).size() == 1 && numIntegerDigitsOther <= 0) {
446                 // If a plural case corresponds to a single double value, the format is
447                 // allowed to not include a numeric value and in this way be inconsistent
448                 // with the numeric formats used for other plural cases.
449                 continue;
450             }
451             // skip special cases where the count=many is optional
452             if (count == Count.many
453                 && PluralRulesUtil.LOCALES_WITH_OPTIONAL_MANY.contains(LocaleIDParser.getSimpleBaseLanguage(resolvedFile.getLocaleID()) )) {
454                 continue;
455             }
456             if (numIntegerDigitsOther != numIntegerDigits) {
457                 PathHeader pathHeader = getPathHeaderFactory().fromPath(parts.toString());
458                 inconsistentItems.add(pathHeader.getHeaderCode());
459             }
460         }
461         if (inconsistentItems.size() > 0) {
462             // Get label for items of this type by removing the count.
463             PathHeader pathHeader = getPathHeaderFactory().fromPath(path.substring(0, path.lastIndexOf('[')));
464             String groupHeaderString = pathHeader.getHeaderCode();
465             boolean isWinningValue = resolvedFile.getWinningValue(path).equals(value);
466             result.add(new CheckStatus().setCause(this)
467                 .setMainType(isWinningValue ? CheckStatus.errorType : CheckStatus.warningType)
468                 .setSubtype(Subtype.inconsistentPluralFormat)
469                 .setMessage("All values for {0} must have the same number of digits. " +
470                     "The number of zeros in this pattern is inconsistent with the following: {1}.",
471                     groupHeaderString,
472                     inconsistentItems.toString()));
473         }
474     }
475 
476     /**
477      * This method builds a decimal format (based on whether the pattern is for currencies or not)
478      * and tests samples.
479      */
checkPattern(String path, String fullPath, String value, List result, boolean generateExamples)480     private void checkPattern(String path, String fullPath, String value, List result, boolean generateExamples)
481         throws ParseException {
482         if (value.indexOf('\u00a4') >= 0) { // currency pattern
483             DecimalFormat x = icuServiceBuilder.getCurrencyFormat("XXX");
484             addOrTestSamples(x, x.toPattern(), value, result, generateExamples);
485         } else {
486             DecimalFormat x = icuServiceBuilder.getNumberFormat(value);
487             addOrTestSamples(x, value, "", result, generateExamples);
488         }
489     }
490 
491     /**
492      * Check some currency patterns.
493      */
checkCurrencyFormats(String path, String fullPath, String value, List result, boolean generateExamples)494     private void checkCurrencyFormats(String path, String fullPath, String value, List result, boolean generateExamples)
495         throws ParseException {
496         DecimalFormat x = icuServiceBuilder.getCurrencyFormat(CLDRFile.getCode(path));
497         addOrTestSamples(x, x.toPattern(), value, result, generateExamples);
498     }
499 
500     /**
501      * Generates some samples. If we are producing examples, these are used for that; otherwise
502      * they are just tested.
503      */
addOrTestSamples(DecimalFormat x, String pattern, String context, List result, boolean generateExamples)504     private void addOrTestSamples(DecimalFormat x, String pattern, String context, List result, boolean generateExamples)
505         throws ParseException {
506         // Object[] arguments = new Object[3];
507         //
508         // double sample = getRandomNumber();
509         // arguments[0] = String.valueOf(sample);
510         // String formatted = x.format(sample);
511         // arguments[1] = formatted;
512         // boolean gotFailure = false;
513         // try {
514         // parsePosition.setIndex(0);
515         // double parsed = x.parse(formatted, parsePosition).doubleValue();
516         // if (parsePosition.getIndex() != formatted.length()) {
517         // arguments[2] = "Couldn't parse past: " + "\u200E" + formatted.substring(0,parsePosition.getIndex()) +
518         // "\u200E";
519         // gotFailure = true;
520         // } else {
521         // arguments[2] = String.valueOf(parsed);
522         // }
523         // } catch (Exception e) {
524         // arguments[2] = e.getMessage();
525         // gotFailure = true;
526         // }
527         // htmlMessage.append(pattern1)
528         // .append(TransliteratorUtilities.toXML.transliterate(String.valueOf(sample)))
529         // .append(pattern2)
530         // .append(TransliteratorUtilities.toXML.transliterate(formatted))
531         // .append(pattern3)
532         // .append(TransliteratorUtilities.toXML.transliterate(String.valueOf(parsed)))
533         // .append(pattern4);
534         // if (generateExamples || gotFailure) {
535         // result.add(new CheckStatus()
536         // .setCause(this).setType(CheckStatus.exampleType)
537         // .setMessage(SampleList, arguments));
538         // }
539         if (generateExamples) {
540             result.add(new MyCheckStatus()
541                 .setFormat(x, context)
542                 .setCause(this).setMainType(CheckStatus.demoType));
543         }
544     }
545 
546     /**
547      * Generate a randome number for testing, with a certain number of decimal places, and
548      * half the time negative
549      */
getRandomNumber()550     private static double getRandomNumber() {
551         // min = 12345.678
552         double rand = random.nextDouble();
553         // System.out.println(rand);
554         double sample = Math.round(rand * 100000.0 * 1000.0) / 1000.0 + 10000.0;
555         if (random.nextBoolean()) sample = -sample;
556         return sample;
557     }
558 
559     /*
560      * static String pattern1 =
561      * "<table border='1' cellpadding='2' cellspacing='0' style='border-collapse: collapse' style='width: 100%'>"
562      * + "<tr>"
563      * + "<td nowrap width='1%'>Input:</td>"
564      * + "<td><input type='text' name='T1' size='50' style='width: 100%' value='";
565      * static String pattern2 = "'></td>"
566      * + "<td nowrap width='1%'><input type='submit' value='Test' name='B1'></td>"
567      * + "<td nowrap width='1%'>Formatted:</td>"
568      * + "<td><input type='text' name='T2' size='50' style='width: 100%' value='";
569      * static String pattern3 = "'></td>"
570      * + "<td nowrap width='1%'>Parsed:</td>"
571      * + "<td><input type='text' name='T3' size='50' style='width: 100%' value='";
572      * static String pattern4 = "'></td>"
573      * + "</tr>"
574      * + "</table>";
575      */
576 
577     /**
578      * Produce a canonical pattern, which will vary according to type and whether it is posix or not.
579      * @param count
580      *
581      * @param path
582      */
getCanonicalPattern(String inpattern, NumericType type, int zeroCount, boolean isPOSIX)583     public static String getCanonicalPattern(String inpattern, NumericType type, int zeroCount, boolean isPOSIX) {
584         // TODO fix later to properly handle quoted ;
585         DecimalFormat df = new DecimalFormat(inpattern);
586         String pattern;
587 
588         if (zeroCount == 0) {
589             int[] digits = isPOSIX ? type.getPosixDigitCount() : type.getDigitCount();
590             df.setMinimumIntegerDigits(digits[0]);
591             df.setMinimumFractionDigits(digits[1]);
592             df.setMaximumFractionDigits(digits[2]);
593             pattern = df.toPattern();
594         } else { // of form 1000. Result must be 0+(.0+)?
595             if (type == NumericType.CURRENCY_ABBREVIATED || type == NumericType.DECIMAL_ABBREVIATED) {
596                 if (!inpattern.contains("0")) {
597                     return inpattern; // we check in checkDecimalFormatConsistency to make sure that the "no number" case is allowed.
598                 }
599                 if (!inpattern.contains("0.0")) {
600                     df.setMinimumFractionDigits(0); // correct the current rewrite
601                 }
602             }
603             df.setMaximumFractionDigits(df.getMinimumFractionDigits());
604             int minimumIntegerDigits = df.getMinimumIntegerDigits();
605             if (minimumIntegerDigits < 1) minimumIntegerDigits = 1;
606             df.setMaximumIntegerDigits(minimumIntegerDigits);
607             pattern = df.toPattern();
608         }
609 
610         // int pos = pattern.indexOf(';');
611         // if (pos < 0) return pattern + ";-" + pattern;
612         return pattern;
613     }
614 
615     /**
616      * You don't normally need this, unless you are doing a demo also.
617      */
618     static public class MyCheckStatus extends CheckStatus {
619         private DecimalFormat df;
620         String context;
621 
setFormat(DecimalFormat df, String context)622         public MyCheckStatus setFormat(DecimalFormat df, String context) {
623             this.df = df;
624             this.context = context;
625             return this;
626         }
627 
628         @Override
getDemo()629         public SimpleDemo getDemo() {
630             return new MyDemo().setFormat(df);
631         }
632     }
633 
634     /**
635      * Here is how to do a demo.
636      * You provide the function getArguments that takes in-and-out parameters.
637      */
638     static class MyDemo extends FormatDemo {
639         private DecimalFormat df;
640 
641         @Override
getPattern()642         protected String getPattern() {
643             return df.toPattern();
644         }
645 
646         @Override
getSampleInput()647         protected String getSampleInput() {
648             return String.valueOf(ExampleGenerator.NUMBER_SAMPLE);
649         }
650 
setFormat(DecimalFormat df)651         public MyDemo setFormat(DecimalFormat df) {
652             this.df = df;
653             return this;
654         }
655 
656         @Override
getArguments(Map<String, String> inout)657         protected void getArguments(Map<String, String> inout) {
658             currentPattern = currentInput = currentFormatted = currentReparsed = "?";
659             double d;
660             try {
661                 currentPattern = inout.get("pattern");
662                 if (currentPattern != null)
663                     df.applyPattern(currentPattern);
664                 else
665                     currentPattern = getPattern();
666             } catch (Exception e) {
667                 currentPattern = "Use format like: ##,###.##";
668                 return;
669             }
670             try {
671                 currentInput = inout.get("input");
672                 if (currentInput == null) {
673                     currentInput = getSampleInput();
674                 }
675                 d = Double.parseDouble(currentInput);
676             } catch (Exception e) {
677                 currentInput = "Use English format: 1234.56";
678                 return;
679             }
680             try {
681                 currentFormatted = df.format(d);
682             } catch (Exception e) {
683                 currentFormatted = "Can't format: " + e.getMessage();
684                 return;
685             }
686             try {
687                 parsePosition.setIndex(0);
688                 Number n = df.parse(currentFormatted, parsePosition);
689                 if (parsePosition.getIndex() != currentFormatted.length()) {
690                     currentReparsed = "Couldn't parse past: \u200E"
691                         + currentFormatted.substring(0, parsePosition.getIndex()) + "\u200E";
692                 } else {
693                     currentReparsed = n.toString();
694                 }
695             } catch (Exception e) {
696                 currentReparsed = "Can't parse: " + e.getMessage();
697             }
698         }
699 
700     }
701 }
702