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