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 (path.contains("noCurrency")) { 227 if (patternPart.indexOf("\u00a4") >= 0) { 228 result.add(new CheckStatus().setCause(this).setMainType(CheckStatus.errorType) 229 .setSubtype(Subtype.currencyPatternUnexpectedCurrencySymbol) 230 .setMessage("noCurrency formatting pattern must not contain a currency symbol.")); 231 } 232 } else if (patternPart.indexOf("\u00a4") < 0) { 233 // check for compact format 234 result.add(new CheckStatus().setCause(this).setMainType(CheckStatus.errorType) 235 .setSubtype(Subtype.currencyPatternMissingCurrencySymbol) 236 .setMessage("Currency formatting pattern must contain a currency symbol.")); 237 } 238 } 239 240 // Make sure percent formatting patterns contain a percent symbol, in each part 241 if (type == NumericType.PERCENT) { 242 if (patternPart.indexOf("%") < 0) 243 result.add(new CheckStatus().setCause(this).setMainType(CheckStatus.errorType) 244 .setSubtype(Subtype.percentPatternMissingPercentSymbol) 245 .setMessage("Percentage formatting pattern must contain a % symbol.")); 246 } 247 isPositive = false; 248 } 249 250 // check all 251 if (FORBIDDEN_NUMERIC_PATTERN_CHARS.containsSome(value)) { 252 UnicodeSet chars = new UnicodeSet().addAll(value); 253 chars.retainAll(FORBIDDEN_NUMERIC_PATTERN_CHARS); 254 result.add(new CheckStatus() 255 .setCause(this) 256 .setMainType(CheckStatus.errorType) 257 .setSubtype(Subtype.illegalCharactersInNumberPattern) 258 .setMessage("Pattern contains forbidden characters: \u200E{0}\u200E", 259 new Object[] { chars.toPattern(false) })); 260 } 261 262 // get the final type 263 String lastType = parts.getAttributeValue(-1, "type"); 264 int zeroCount = 0; 265 // it can only be null or an integer of the form 10+ 266 if (lastType != null && !lastType.equals("standard")) { 267 Matcher matcher = ALLOWED_INTEGER.matcher(lastType); 268 if (matcher.matches()) { 269 zeroCount = matcher.end(1) - matcher.start(1); // number of ascii zeros 270 } else { 271 result.add(new CheckStatus().setCause(this).setMainType(CheckStatus.errorType) 272 .setSubtype(Subtype.badNumericType) 273 .setMessage("The type of a numeric pattern must be missing or of the form 10....")); 274 } 275 } 276 277 // Check the validity of the pattern. If this check fails, all other checks 278 // after it will fail, so exit early. 279 UnicodeSet illegalChars = findUnquotedChars(type, value); 280 if (illegalChars != null) { 281 result.add(new CheckStatus().setCause(this) 282 .setMainType(CheckStatus.errorType) 283 .setSubtype(Subtype.illegalCharactersInNumberPattern) 284 .setMessage("Pattern contains characters that must be escaped or removed: {0}", new Object[] { illegalChars })); 285 return this; 286 } 287 288 // Tests that assume that the value is a valid number pattern. 289 // Notice that we pick up any exceptions, so that we can 290 // give a reasonable error message. 291 parts = parts.cloneAsThawed(); 292 try { 293 if (type == NumericType.DECIMAL_ABBREVIATED || type == NumericType.CURRENCY_ABBREVIATED) { 294 // Check for consistency in short/long decimal formats. 295 checkDecimalFormatConsistency(parts, path, value, result, type); 296 } else { 297 checkPattern(path, fullPath, value, result, false); 298 } 299 300 // Check for sane usage of grouping separators. 301 if (COMMA_ABUSE.matcher(value).find()) { 302 result 303 .add(new CheckStatus() 304 .setCause(this) 305 .setMainType(CheckStatus.errorType) 306 .setSubtype(Subtype.tooManyGroupingSeparators) 307 .setMessage( 308 "Grouping separator (,) should not be used to group tens. Check if a decimal symbol (.) should have been used instead.")); 309 } else { 310 // check that we have a canonical pattern 311 String pattern = getCanonicalPattern(value, type, zeroCount, isPOSIX); 312 if (!pattern.equals(value)) { 313 result.add(new CheckStatus() 314 .setCause(this).setMainType(CheckStatus.errorType) 315 .setSubtype(Subtype.numberPatternNotCanonical) 316 .setMessage("Value should be \u200E{0}\u200E", new Object[] { pattern })); 317 } 318 } 319 320 } catch (Exception e) { 321 result.add(new CheckStatus().setCause(this).setMainType(CheckStatus.errorType) 322 .setSubtype(Subtype.illegalNumberFormat) 323 .setMessage(e.getMessage() == null ? e.toString() : e.getMessage())); 324 } 325 return this; 326 } 327 328 /** 329 * Looks for any unquoted non-pattern characters in the specified string 330 * which would make the pattern invalid. 331 * @param type the type of the pattern 332 * @param value the string containing the number pattern 333 * @return the set of unquoted chars in the pattern 334 */ findUnquotedChars(NumericType type, String value)335 private static UnicodeSet findUnquotedChars(NumericType type, String value) { 336 UnicodeSet chars = new UnicodeSet(); 337 UnicodeSet allowedChars = null; 338 // Allow the digits 1-9 here because they're already checked in another test. 339 if (type == NumericType.DECIMAL_ABBREVIATED) { 340 allowedChars = new UnicodeSet("[0-9]"); 341 } else { 342 allowedChars = new UnicodeSet("[0-9#@.,E+]"); 343 } 344 for (String subPattern : value.split(";")) { 345 // Any unquoted non-special chars are allowed in front of or behind the numerical 346 // symbols, but not in between, e.g. " 0000" is okay but "0 000" is not. 347 int firstIdx = -1; 348 for (int i = 0, len = subPattern.length(); i < len; i++) { 349 char c = subPattern.charAt(i); 350 if (c == '0' || c == '#') { 351 firstIdx = i; 352 break; 353 } 354 } 355 if (firstIdx == -1) { 356 continue; 357 } 358 int lastIdx = Math.max(subPattern.lastIndexOf("0"), subPattern.lastIndexOf('#')); 359 chars.addAll(subPattern.substring(firstIdx, lastIdx)); 360 } 361 chars.removeAll(allowedChars); 362 return chars.size() > 0 ? chars : null; 363 } 364 365 /** 366 * Override this method if you are going to provide examples of usage. 367 * Only needed for more complicated cases, like number patterns. 368 */ 369 @Override handleGetExamples(String path, String fullPath, String value, Options options, List result)370 public CheckCLDR handleGetExamples(String path, String fullPath, String value, Options options, List result) { 371 if (path.indexOf("/numbers") < 0) return this; 372 try { 373 if (path.indexOf("/pattern") >= 0 && path.indexOf("/patternDigit") < 0) { 374 checkPattern(path, fullPath, value, result, true); 375 } 376 if (path.indexOf("/currencies") >= 0 && path.endsWith("/symbol")) { 377 checkCurrencyFormats(path, fullPath, value, result, true); 378 } 379 } catch (Exception e) { 380 // don't worry about errors here, they'll be caught above. 381 } 382 return this; 383 } 384 385 /** 386 * 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. 387 * (The latter are only allowed for "singular" plural forms). 388 */ checkDecimalFormatConsistency(XPathParts parts, String path, String value, List<CheckStatus> result, NumericType type)389 private void checkDecimalFormatConsistency(XPathParts parts, String path, String value, 390 List<CheckStatus> result, NumericType type) { 391 // Look for duplicates of decimal formats with the same number 392 // system and type. 393 // Decimal formats of the same type should have the same number 394 // of integer digits in all the available plural forms. 395 DecimalFormat format = new DecimalFormat(value); 396 int numIntegerDigits = format.getMinimumIntegerDigits(); 397 String countString = parts.getAttributeValue(-1, "count"); 398 Count thisCount = null; 399 try { 400 thisCount = Count.valueOf(countString); 401 } catch (Exception e) { 402 // can happen if count is numeric literal, like "1" 403 } 404 CLDRFile resolvedFile = getResolvedCldrFileToCheck(); 405 Set<String> inconsistentItems = new TreeSet<>(); 406 Set<Count> otherCounts = new HashSet<>(pluralTypes); 407 if (thisCount != null) { 408 Set<Double> pe = pluralExamples.get(thisCount); 409 if (pe == null) { 410 /* 411 * This can happen for unknown reasons when path = 412 * //ldml/numbers/currencyFormats[@numberSystem="latn"]/currencyFormatLength[@type="short"]/currencyFormat[@type="standard"]/pattern[@type="1000"][@count="one"] 413 * TODO: something? At least don't throw NullPointerException, as happened when the code 414 * was "... pluralExamples.get(thisCount).size() ..."; never assume get() returns non-null 415 */ 416 return; 417 } 418 if (!value.contains("0")) { 419 switch (pe.size()) { 420 case 0: // do nothing, shouldn't ever happen 421 break; 422 case 1: 423 // If a plural case corresponds to a single double value, the format is 424 // allowed to not include a numeric value and in this way be inconsistent 425 // with the numeric formats used for other plural cases. 426 return; 427 default: // we have too many digits 428 result.add(new CheckStatus().setCause(this) 429 .setMainType(CheckStatus.errorType) 430 .setSubtype(Subtype.missingZeros) 431 .setMessage("Values without a zero must only be used where there is only one possible numeric form, but this has multiple: {0} ", 432 pe.toString())); 433 } 434 } 435 otherCounts.remove(thisCount); 436 } 437 for (Count count : otherCounts) { 438 // System.out.println("## double examples for count " + count + ": " + pluralExamples.get(count)); 439 parts.setAttribute("pattern", "count", count.toString()); 440 String otherPattern = resolvedFile.getWinningValue(parts.toString()); 441 // Ignore the type="other" pattern if not present or invalid. 442 if (otherPattern == null || findUnquotedChars(type, otherPattern) != null) continue; 443 format = new DecimalFormat(otherPattern); 444 int numIntegerDigitsOther = format.getMinimumIntegerDigits(); 445 if (pluralExamples.get(count).size() == 1 && numIntegerDigitsOther <= 0) { 446 // If a plural case corresponds to a single double value, the format is 447 // allowed to not include a numeric value and in this way be inconsistent 448 // with the numeric formats used for other plural cases. 449 continue; 450 } 451 // skip special cases where the count=many is optional 452 if (count == Count.many 453 && PluralRulesUtil.LOCALES_WITH_OPTIONAL_MANY.contains(LocaleIDParser.getSimpleBaseLanguage(resolvedFile.getLocaleID()) )) { 454 continue; 455 } 456 if (numIntegerDigitsOther != numIntegerDigits) { 457 PathHeader pathHeader = getPathHeaderFactory().fromPath(parts.toString()); 458 inconsistentItems.add(pathHeader.getHeaderCode()); 459 } 460 } 461 if (inconsistentItems.size() > 0) { 462 // Get label for items of this type by removing the count. 463 PathHeader pathHeader = getPathHeaderFactory().fromPath(path.substring(0, path.lastIndexOf('['))); 464 String groupHeaderString = pathHeader.getHeaderCode(); 465 boolean isWinningValue = resolvedFile.getWinningValue(path).equals(value); 466 result.add(new CheckStatus().setCause(this) 467 .setMainType(isWinningValue ? CheckStatus.errorType : CheckStatus.warningType) 468 .setSubtype(Subtype.inconsistentPluralFormat) 469 .setMessage("All values for {0} must have the same number of digits. " + 470 "The number of zeros in this pattern is inconsistent with the following: {1}.", 471 groupHeaderString, 472 inconsistentItems.toString())); 473 } 474 } 475 476 /** 477 * This method builds a decimal format (based on whether the pattern is for currencies or not) 478 * and tests samples. 479 */ checkPattern(String path, String fullPath, String value, List result, boolean generateExamples)480 private void checkPattern(String path, String fullPath, String value, List result, boolean generateExamples) 481 throws ParseException { 482 if (value.indexOf('\u00a4') >= 0) { // currency pattern 483 DecimalFormat x = icuServiceBuilder.getCurrencyFormat("XXX"); 484 addOrTestSamples(x, x.toPattern(), value, result, generateExamples); 485 } else { 486 DecimalFormat x = icuServiceBuilder.getNumberFormat(value); 487 addOrTestSamples(x, value, "", result, generateExamples); 488 } 489 } 490 491 /** 492 * Check some currency patterns. 493 */ checkCurrencyFormats(String path, String fullPath, String value, List result, boolean generateExamples)494 private void checkCurrencyFormats(String path, String fullPath, String value, List result, boolean generateExamples) 495 throws ParseException { 496 DecimalFormat x = icuServiceBuilder.getCurrencyFormat(CLDRFile.getCode(path)); 497 addOrTestSamples(x, x.toPattern(), value, result, generateExamples); 498 } 499 500 /** 501 * Generates some samples. If we are producing examples, these are used for that; otherwise 502 * they are just tested. 503 */ addOrTestSamples(DecimalFormat x, String pattern, String context, List result, boolean generateExamples)504 private void addOrTestSamples(DecimalFormat x, String pattern, String context, List result, boolean generateExamples) 505 throws ParseException { 506 // Object[] arguments = new Object[3]; 507 // 508 // double sample = getRandomNumber(); 509 // arguments[0] = String.valueOf(sample); 510 // String formatted = x.format(sample); 511 // arguments[1] = formatted; 512 // boolean gotFailure = false; 513 // try { 514 // parsePosition.setIndex(0); 515 // double parsed = x.parse(formatted, parsePosition).doubleValue(); 516 // if (parsePosition.getIndex() != formatted.length()) { 517 // arguments[2] = "Couldn't parse past: " + "\u200E" + formatted.substring(0,parsePosition.getIndex()) + 518 // "\u200E"; 519 // gotFailure = true; 520 // } else { 521 // arguments[2] = String.valueOf(parsed); 522 // } 523 // } catch (Exception e) { 524 // arguments[2] = e.getMessage(); 525 // gotFailure = true; 526 // } 527 // htmlMessage.append(pattern1) 528 // .append(TransliteratorUtilities.toXML.transliterate(String.valueOf(sample))) 529 // .append(pattern2) 530 // .append(TransliteratorUtilities.toXML.transliterate(formatted)) 531 // .append(pattern3) 532 // .append(TransliteratorUtilities.toXML.transliterate(String.valueOf(parsed))) 533 // .append(pattern4); 534 // if (generateExamples || gotFailure) { 535 // result.add(new CheckStatus() 536 // .setCause(this).setType(CheckStatus.exampleType) 537 // .setMessage(SampleList, arguments)); 538 // } 539 if (generateExamples) { 540 result.add(new MyCheckStatus() 541 .setFormat(x, context) 542 .setCause(this).setMainType(CheckStatus.demoType)); 543 } 544 } 545 546 /** 547 * Generate a randome number for testing, with a certain number of decimal places, and 548 * half the time negative 549 */ getRandomNumber()550 private static double getRandomNumber() { 551 // min = 12345.678 552 double rand = random.nextDouble(); 553 // System.out.println(rand); 554 double sample = Math.round(rand * 100000.0 * 1000.0) / 1000.0 + 10000.0; 555 if (random.nextBoolean()) sample = -sample; 556 return sample; 557 } 558 559 /* 560 * static String pattern1 = 561 * "<table border='1' cellpadding='2' cellspacing='0' style='border-collapse: collapse' style='width: 100%'>" 562 * + "<tr>" 563 * + "<td nowrap width='1%'>Input:</td>" 564 * + "<td><input type='text' name='T1' size='50' style='width: 100%' value='"; 565 * static String pattern2 = "'></td>" 566 * + "<td nowrap width='1%'><input type='submit' value='Test' name='B1'></td>" 567 * + "<td nowrap width='1%'>Formatted:</td>" 568 * + "<td><input type='text' name='T2' size='50' style='width: 100%' value='"; 569 * static String pattern3 = "'></td>" 570 * + "<td nowrap width='1%'>Parsed:</td>" 571 * + "<td><input type='text' name='T3' size='50' style='width: 100%' value='"; 572 * static String pattern4 = "'></td>" 573 * + "</tr>" 574 * + "</table>"; 575 */ 576 577 /** 578 * Produce a canonical pattern, which will vary according to type and whether it is posix or not. 579 * @param count 580 * 581 * @param path 582 */ getCanonicalPattern(String inpattern, NumericType type, int zeroCount, boolean isPOSIX)583 public static String getCanonicalPattern(String inpattern, NumericType type, int zeroCount, boolean isPOSIX) { 584 // TODO fix later to properly handle quoted ; 585 DecimalFormat df = new DecimalFormat(inpattern); 586 String pattern; 587 588 if (zeroCount == 0) { 589 int[] digits = isPOSIX ? type.getPosixDigitCount() : type.getDigitCount(); 590 df.setMinimumIntegerDigits(digits[0]); 591 df.setMinimumFractionDigits(digits[1]); 592 df.setMaximumFractionDigits(digits[2]); 593 pattern = df.toPattern(); 594 } else { // of form 1000. Result must be 0+(.0+)? 595 if (type == NumericType.CURRENCY_ABBREVIATED || type == NumericType.DECIMAL_ABBREVIATED) { 596 if (!inpattern.contains("0")) { 597 return inpattern; // we check in checkDecimalFormatConsistency to make sure that the "no number" case is allowed. 598 } 599 if (!inpattern.contains("0.0")) { 600 df.setMinimumFractionDigits(0); // correct the current rewrite 601 } 602 } 603 df.setMaximumFractionDigits(df.getMinimumFractionDigits()); 604 int minimumIntegerDigits = df.getMinimumIntegerDigits(); 605 if (minimumIntegerDigits < 1) minimumIntegerDigits = 1; 606 df.setMaximumIntegerDigits(minimumIntegerDigits); 607 pattern = df.toPattern(); 608 } 609 610 // int pos = pattern.indexOf(';'); 611 // if (pos < 0) return pattern + ";-" + pattern; 612 return pattern; 613 } 614 615 /** 616 * You don't normally need this, unless you are doing a demo also. 617 */ 618 static public class MyCheckStatus extends CheckStatus { 619 private DecimalFormat df; 620 String context; 621 setFormat(DecimalFormat df, String context)622 public MyCheckStatus setFormat(DecimalFormat df, String context) { 623 this.df = df; 624 this.context = context; 625 return this; 626 } 627 628 @Override getDemo()629 public SimpleDemo getDemo() { 630 return new MyDemo().setFormat(df); 631 } 632 } 633 634 /** 635 * Here is how to do a demo. 636 * You provide the function getArguments that takes in-and-out parameters. 637 */ 638 static class MyDemo extends FormatDemo { 639 private DecimalFormat df; 640 641 @Override getPattern()642 protected String getPattern() { 643 return df.toPattern(); 644 } 645 646 @Override getSampleInput()647 protected String getSampleInput() { 648 return String.valueOf(ExampleGenerator.NUMBER_SAMPLE); 649 } 650 setFormat(DecimalFormat df)651 public MyDemo setFormat(DecimalFormat df) { 652 this.df = df; 653 return this; 654 } 655 656 @Override getArguments(Map<String, String> inout)657 protected void getArguments(Map<String, String> inout) { 658 currentPattern = currentInput = currentFormatted = currentReparsed = "?"; 659 double d; 660 try { 661 currentPattern = inout.get("pattern"); 662 if (currentPattern != null) 663 df.applyPattern(currentPattern); 664 else 665 currentPattern = getPattern(); 666 } catch (Exception e) { 667 currentPattern = "Use format like: ##,###.##"; 668 return; 669 } 670 try { 671 currentInput = inout.get("input"); 672 if (currentInput == null) { 673 currentInput = getSampleInput(); 674 } 675 d = Double.parseDouble(currentInput); 676 } catch (Exception e) { 677 currentInput = "Use English format: 1234.56"; 678 return; 679 } 680 try { 681 currentFormatted = df.format(d); 682 } catch (Exception e) { 683 currentFormatted = "Can't format: " + e.getMessage(); 684 return; 685 } 686 try { 687 parsePosition.setIndex(0); 688 Number n = df.parse(currentFormatted, parsePosition); 689 if (parsePosition.getIndex() != currentFormatted.length()) { 690 currentReparsed = "Couldn't parse past: \u200E" 691 + currentFormatted.substring(0, parsePosition.getIndex()) + "\u200E"; 692 } else { 693 currentReparsed = n.toString(); 694 } 695 } catch (Exception e) { 696 currentReparsed = "Can't parse: " + e.getMessage(); 697 } 698 } 699 700 } 701 } 702