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