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