Home | History | Annotate | Download | only in test
      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 
     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
     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
    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      */
    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
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    546         public MyCheckStatus setFormat(DecimalFormat df, String context) {
    547             this.df = df;
    548             this.context = context;
    549             return this;
    550         }
    551 
    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 
    564         protected String getPattern() {
    565             return df.toPattern();
    566         }
    567 
    568         protected String getSampleInput() {
    569             return String.valueOf(ExampleGenerator.NUMBER_SAMPLE);
    570         }
    571 
    572         public MyDemo setFormat(DecimalFormat df) {
    573             this.df = df;
    574             return this;
    575         }
    576 
    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