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