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