1 package org.unicode.cldr.util; 2 3 import com.google.common.base.Joiner; 4 import com.google.common.base.Splitter; 5 import com.google.common.collect.ImmutableList; 6 import com.google.common.collect.ImmutableMap; 7 import com.google.common.collect.ImmutableSet; 8 import com.ibm.icu.impl.Relation; 9 import com.ibm.icu.impl.Row; 10 import com.ibm.icu.impl.Row.R2; 11 import com.ibm.icu.text.SimpleDateFormat; 12 import com.ibm.icu.text.UnicodeSet; 13 import com.ibm.icu.text.UnicodeSet.SpanCondition; 14 import com.ibm.icu.util.ULocale; 15 import com.ibm.icu.util.VersionInfo; 16 import com.vdurmont.semver4j.Semver; 17 import com.vdurmont.semver4j.Semver.SemverType; 18 import com.vdurmont.semver4j.SemverException; 19 import java.text.ParseException; 20 import java.util.Date; 21 import java.util.EnumSet; 22 import java.util.HashMap; 23 import java.util.HashSet; 24 import java.util.Iterator; 25 import java.util.List; 26 import java.util.Locale; 27 import java.util.Map; 28 import java.util.Map.Entry; 29 import java.util.Set; 30 import java.util.TreeMap; 31 import java.util.TreeSet; 32 import java.util.regex.Pattern; 33 import org.unicode.cldr.util.StandardCodes.LstrType; 34 import org.unicode.cldr.util.Validity.Status; 35 36 public abstract class MatchValue implements Predicate<String> { 37 public static final String DEFAULT_SAMPLE = "❓"; 38 39 @Override is(String item)40 public abstract boolean is(String item); 41 getName()42 public abstract String getName(); 43 getSample()44 public String getSample() { 45 return DEFAULT_SAMPLE; 46 } 47 48 @Override toString()49 public String toString() { 50 return getName(); 51 } 52 of(String command)53 public static MatchValue of(String command) { 54 String originalArg = command; 55 int colonPos = command.indexOf('/'); 56 String subargument = null; 57 if (colonPos >= 0) { 58 subargument = command.substring(colonPos + 1); 59 command = command.substring(0, colonPos); 60 } 61 try { 62 MatchValue result = null; 63 switch (command) { 64 case "any": 65 result = AnyMatchValue.of(subargument); 66 break; 67 case "set": 68 result = SetMatchValue.of(subargument); 69 break; 70 case "validity": 71 result = ValidityMatchValue.of(subargument); 72 break; 73 case "bcp47": 74 result = Bcp47MatchValue.of(subargument); 75 break; 76 case "range": 77 result = RangeMatchValue.of(subargument); 78 break; 79 case "literal": 80 result = LiteralMatchValue.of(subargument); 81 break; 82 case "regex": 83 result = RegexMatchValue.of(subargument); 84 break; 85 case "semver": 86 result = SemverMatchValue.of(subargument); 87 break; 88 case "metazone": 89 result = MetazoneMatchValue.of(subargument); 90 break; 91 case "version": 92 result = VersionMatchValue.of(subargument); 93 break; 94 case "time": 95 result = TimeMatchValue.of(subargument); 96 break; 97 case "or": 98 result = OrMatchValue.of(subargument); 99 break; 100 case "unicodeset": 101 result = UnicodeSpanMatchValue.of(subargument); 102 break; 103 default: 104 throw new IllegalArgumentException( 105 "Illegal/Unimplemented match type: " + originalArg); 106 } 107 if (!originalArg.equals(result.getName())) { 108 System.err.println( 109 "Non-standard form or error: " + originalArg + " ==> " + result.getName()); 110 } 111 return result; 112 } catch (Exception e) { 113 throw new IllegalArgumentException("Problem with: " + originalArg, e); 114 } 115 } 116 117 /** Check that a bcp47 locale ID is well-formed. Does not check validity. */ 118 public static class BCP47LocaleWellFormedMatchValue extends MatchValue { 119 static final UnicodeSet basechars = new UnicodeSet("[A-Za-z0-9_]"); 120 BCP47LocaleWellFormedMatchValue()121 public BCP47LocaleWellFormedMatchValue() {} 122 123 @Override getName()124 public String getName() { 125 return "validity/bcp47-wellformed"; 126 } 127 128 @Override is(String item)129 public boolean is(String item) { 130 if (item.equals("und")) return true; // special case because of the matcher 131 if (item.contains("_")) return false; // reject any underscores 132 try { 133 ULocale l = ULocale.forLanguageTag(item); 134 if (l == null || l.getBaseName().isEmpty()) { 135 return false; // failed to parse 136 } 137 138 // check with lstr parser 139 LanguageTagParser ltp = new LanguageTagParser(); 140 ltp.set(item); 141 } catch (Throwable t) { 142 return false; // string failed 143 } 144 return true; 145 } 146 147 @Override getSample()148 public String getSample() { 149 return "de-u-nu-ethi"; 150 } 151 } 152 153 public static class LocaleMatchValue extends MatchValue { 154 private final Predicate<String> lang; 155 private final Predicate<String> script; 156 private final Predicate<String> region; 157 private final Predicate<String> variant; 158 LocaleMatchValue()159 public LocaleMatchValue() { 160 this(null); 161 } 162 LocaleMatchValue(Set<Status> statuses)163 public LocaleMatchValue(Set<Status> statuses) { 164 lang = new ValidityMatchValue(LstrType.language, statuses, false); 165 script = new ValidityMatchValue(LstrType.script, statuses, false); 166 region = new ValidityMatchValue(LstrType.region, statuses, false); 167 variant = new ValidityMatchValue(LstrType.variant, statuses, false); 168 } 169 170 @Override getName()171 public String getName() { 172 return "validity/locale"; 173 } 174 175 @Override is(String item)176 public boolean is(String item) { 177 if (!item.contains("_")) { 178 return lang.is(item); 179 } 180 LanguageTagParser ltp; 181 try { 182 ltp = new LanguageTagParser().set(item); 183 } catch (Exception e) { 184 return false; 185 } 186 return lang.is(ltp.getLanguage()) 187 && (ltp.getScript().isEmpty() || script.is(ltp.getScript())) 188 && (ltp.getRegion().isEmpty() || region.is(ltp.getRegion())) 189 && (ltp.getVariants().isEmpty() || and(variant, ltp.getVariants())) 190 && ltp.getExtensions().isEmpty() 191 && ltp.getLocaleExtensions().isEmpty(); 192 } 193 194 @Override getSample()195 public String getSample() { 196 return "de"; 197 } 198 } 199 200 // TODO remove these if possible — ticket/10120 201 static final Set<String> SCRIPT_HACK = 202 ImmutableSet.of( 203 "Afak", "Blis", "Cirt", "Cyrs", "Egyd", "Egyh", "Geok", "Inds", "Jurc", "Kpel", 204 "Latf", "Latg", "Loma", "Maya", "Moon", "Nkgb", "Phlv", "Roro", "Sara", "Syre", 205 "Syrj", "Syrn", "Teng", "Visp", "Wole"); 206 static final Set<String> VARIANT_HACK = ImmutableSet.of("POSIX", "REVISED", "SAAHO"); 207 208 /** 209 * Returns true if ALL items match the predicate 210 * 211 * @param <T> 212 * @param predicate predicate to check 213 * @param items items to be tested with the predicate 214 * @return 215 */ and(Predicate<T> predicate, Iterable<T> items)216 public static <T> boolean and(Predicate<T> predicate, Iterable<T> items) { 217 for (T item : items) { 218 if (!predicate.is(item)) { 219 return false; 220 } 221 } 222 return true; 223 } 224 225 /** 226 * Returns true if ANY items match the predicate 227 * 228 * @param <T> 229 * @param predicate predicate to check 230 * @param items items to be tested with the predicate 231 * @return 232 */ or(Predicate<T> predicate, Iterable<T> items)233 public static <T> boolean or(Predicate<T> predicate, Iterable<T> items) { 234 for (T item : items) { 235 if (predicate.is(item)) { 236 return true; 237 } 238 } 239 return false; 240 } 241 242 public static class EnumParser<T extends Enum> { 243 private final Class<T> aClass; 244 private final Set<T> all; 245 EnumParser(Class<T> aClass)246 private EnumParser(Class<T> aClass) { 247 this.aClass = aClass; 248 all = ImmutableSet.copyOf(EnumSet.allOf(aClass)); 249 } 250 of(Class<T> aClass)251 public static <T> EnumParser of(Class<T> aClass) { 252 return new EnumParser(aClass); 253 } 254 parse(String text)255 public Set<T> parse(String text) { 256 Set<T> statuses = EnumSet.noneOf(aClass); 257 boolean negative = text.startsWith("!"); 258 if (negative) { 259 text = text.substring(1); 260 } 261 for (String item : SPLIT_SPACE_OR_COMMA.split(text)) { 262 statuses.add(getItem(item)); 263 } 264 if (negative) { 265 TreeSet<T> temp = new TreeSet<>(all); 266 temp.removeAll(statuses); 267 statuses = temp; 268 } 269 return ImmutableSet.copyOf(statuses); 270 } 271 getItem(String text)272 private T getItem(String text) { 273 try { 274 return (T) aClass.getMethod("valueOf", String.class).invoke(null, text); 275 } catch (Exception e) { 276 throw new IllegalArgumentException(e); 277 } 278 } 279 format(Set<?> set)280 public String format(Set<?> set) { 281 if (set.size() > all.size() / 2) { 282 TreeSet<T> temp = new TreeSet<>(all); 283 temp.removeAll(set); 284 return "!" + Joiner.on(' ').join(temp); 285 } else { 286 return Joiner.on(' ').join(set); 287 } 288 } 289 isAll(Set<Status> statuses)290 public boolean isAll(Set<Status> statuses) { 291 return statuses.equals(all); 292 } 293 } 294 295 public static class ValidityMatchValue extends MatchValue { 296 private final LstrType type; 297 private final boolean shortId; 298 private final Set<Status> statuses; 299 private static Map<String, Status> shortCodeToStatus; 300 private static final EnumParser<Status> enumParser = EnumParser.of(Status.class); 301 302 @Override getName()303 public String getName() { 304 return "validity/" 305 + (shortId ? "short-" : "") 306 + type.toString() 307 + (enumParser.isAll(statuses) ? "" : "/" + enumParser.format(statuses)); 308 } 309 ValidityMatchValue(LstrType type)310 private ValidityMatchValue(LstrType type) { 311 this(type, null, false); 312 } 313 ValidityMatchValue(LstrType type, Set<Status> statuses, boolean shortId)314 private ValidityMatchValue(LstrType type, Set<Status> statuses, boolean shortId) { 315 this.type = type; 316 if (type != LstrType.unit && shortId) { 317 throw new IllegalArgumentException("short- not supported except for units"); 318 } 319 this.shortId = shortId; 320 this.statuses = 321 statuses == null ? EnumSet.allOf(Status.class) : ImmutableSet.copyOf(statuses); 322 } 323 of(String typeName)324 public static MatchValue of(String typeName) { 325 if (typeName.equals("locale")) { 326 return new LocaleMatchValue(); 327 } 328 if (typeName.equals("bcp47-wellformed")) { 329 return new BCP47LocaleWellFormedMatchValue(); 330 } 331 int slashPos = typeName.indexOf('/'); 332 Set<Status> statuses = null; 333 if (slashPos > 0) { 334 statuses = enumParser.parse(typeName.substring(slashPos + 1)); 335 typeName = typeName.substring(0, slashPos); 336 } 337 boolean shortId = typeName.startsWith("short-"); 338 if (shortId) { 339 typeName = typeName.substring(6); 340 } 341 LstrType type = LstrType.fromString(typeName); 342 return new ValidityMatchValue(type, statuses, shortId); 343 } 344 345 @Override is(String item)346 public boolean is(String item) { 347 // TODO handle deprecated 348 switch (type) { 349 case script: 350 if (SCRIPT_HACK.contains(item)) { 351 return true; 352 } 353 break; 354 case variant: 355 if (VARIANT_HACK.contains(item)) { 356 return true; 357 } 358 item = item.toLowerCase(Locale.ROOT); 359 break; 360 case language: 361 item = item.equals("root") ? "und" : item; 362 break; 363 case unit: 364 if (shortId) { 365 if (shortCodeToStatus 366 == null) { // lazy evaluation to avoid circular dependencies 367 Map<String, Status> _shortCodeToStatus = new TreeMap<>(); 368 for (Entry<String, Status> entry : 369 Validity.getInstance() 370 .getCodeToStatus(LstrType.unit) 371 .entrySet()) { 372 String key = entry.getKey(); 373 Status status = entry.getValue(); 374 final String shortKey = key.substring(key.indexOf('-') + 1); 375 Status old = _shortCodeToStatus.get(shortKey); 376 if (old == null) { 377 _shortCodeToStatus.put(shortKey, status); 378 // } else { 379 // System.out.println("Skipping 380 // duplicate status: " + key + " old: " + old + " new: " + 381 // status); 382 } 383 } 384 shortCodeToStatus = ImmutableMap.copyOf(_shortCodeToStatus); 385 } 386 final Status status = shortCodeToStatus.get(item); 387 return status != null && statuses.contains(status); 388 } 389 default: 390 break; 391 } 392 final Status status = Validity.getInstance().getCodeToStatus(type).get(item); 393 return status != null && statuses.contains(status); 394 } 395 396 @Override getSample()397 public String getSample() { 398 return Validity.getInstance().getCodeToStatus(type).keySet().iterator().next(); 399 } 400 } 401 402 public static class Bcp47MatchValue extends MatchValue { 403 private final String key; 404 private Set<String> valid; 405 406 @Override getName()407 public String getName() { 408 return "bcp47/" + key; 409 } 410 Bcp47MatchValue(String key)411 private Bcp47MatchValue(String key) { 412 this.key = key; 413 } 414 of(String key)415 public static Bcp47MatchValue of(String key) { 416 return new Bcp47MatchValue(key); 417 } 418 419 @Override is(String item)420 public synchronized boolean is(String item) { 421 if (valid == null) { // must lazy-eval 422 SupplementalDataInfo sdi = SupplementalDataInfo.getInstance(); 423 Relation<String, String> keyToSubtypes = sdi.getBcp47Keys(); 424 Relation<R2<String, String>, String> keySubtypeToAliases = sdi.getBcp47Aliases(); 425 Map<String, String> aliasesToKey = new HashMap<>(); 426 for (String key : keyToSubtypes.keySet()) { 427 Set<String> aliases = keySubtypeToAliases.get(Row.of(key, "")); 428 if (aliases != null) { 429 for (String alias : aliases) { 430 aliasesToKey.put(alias, key); 431 } 432 } 433 } 434 Set<String> keyList; 435 Set<String> subtypeList; 436 // TODO handle deprecated 437 // fix data to remove aliases, then narrow this 438 switch (key) { 439 case "anykey": 440 keyList = keyToSubtypes.keySet(); 441 valid = new TreeSet<>(keyList); 442 for (String keyItem : keyList) { 443 addAliases(keySubtypeToAliases, keyItem, ""); 444 } 445 valid.add("x"); // TODO: investigate adding to bcp47 data files 446 break; 447 case "anyvalue": 448 valid = new TreeSet<>(keyToSubtypes.values()); 449 for (String keyItem : keyToSubtypes.keySet()) { 450 subtypeList = keyToSubtypes.get(keyItem); 451 // if (subtypeList == null) { 452 // continue; 453 // } 454 for (String subtypeItem : subtypeList) { 455 addAliases(keySubtypeToAliases, keyItem, subtypeItem); 456 } 457 } 458 valid.add("generic"); // TODO: investigate adding to bcp47 data files 459 break; 460 default: 461 subtypeList = keyToSubtypes.get(key); 462 if (subtypeList == null) { 463 String key2 = aliasesToKey.get(key); 464 if (key2 != null) { 465 subtypeList = keyToSubtypes.get(key2); 466 } 467 } 468 try { 469 valid = new TreeSet<>(subtypeList); 470 } catch (Exception e) { 471 throw new IllegalArgumentException("Illegal keyValue: " + getName()); 472 } 473 for (String subtypeItem : subtypeList) { 474 addAliases(keySubtypeToAliases, key, subtypeItem); 475 } 476 switch (key) { 477 case "ca": 478 valid.add( 479 "generic"); // TODO: investigate adding to bcp47 data files 480 break; 481 } 482 break; 483 } 484 valid = ImmutableSet.copyOf(valid); 485 } 486 // <key name="tz" description="Time zone key" alias="timezone"> 487 // <type name="adalv" description="Andorra" alias="Europe/Andorra"/> 488 // <key name="nu" description="Numbering system type key" alias="numbers"> 489 // <type name="adlm" description="Adlam digits" since="30"/> 490 return valid.contains(item); 491 } 492 addAliases( Relation<R2<String, String>, String> keySubtypeToAliases, String keyItem, String subtype)493 private void addAliases( 494 Relation<R2<String, String>, String> keySubtypeToAliases, 495 String keyItem, 496 String subtype) { 497 Set<String> aliases = keySubtypeToAliases.get(Row.of(keyItem, subtype)); 498 if (aliases != null && !aliases.isEmpty()) { 499 valid.addAll(aliases); 500 } 501 } 502 503 @Override getSample()504 public String getSample() { 505 is("X"); // force load data 506 return valid == null ? "XX" : valid.iterator().next(); 507 } 508 } 509 510 static final Splitter RANGE = Splitter.on('~').trimResults(); 511 512 // TODO: have Range that can be ints, doubles, or versions 513 public static class RangeMatchValue extends MatchValue { 514 private final double start; 515 private final double end; 516 private final boolean isInt; 517 518 @Override getName()519 public String getName() { 520 return "range/" + (isInt ? (long) start + "~" + (long) end : start + "~" + end); 521 } 522 RangeMatchValue(String key)523 private RangeMatchValue(String key) { 524 Iterator<String> parts = RANGE.split(key).iterator(); 525 start = Double.parseDouble(parts.next()); 526 end = Double.parseDouble(parts.next()); 527 isInt = !key.contains("."); 528 if (parts.hasNext()) { 529 throw new IllegalArgumentException("Range must be of form <int>~<int>"); 530 } 531 } 532 of(String key)533 public static RangeMatchValue of(String key) { 534 return new RangeMatchValue(key); 535 } 536 537 @Override is(String item)538 public boolean is(String item) { 539 if (isInt && item.contains(".")) { 540 return false; 541 } 542 double value; 543 try { 544 value = Double.parseDouble(item); 545 } catch (NumberFormatException e) { 546 return false; 547 } 548 return start <= value && value <= end; 549 } 550 551 @Override getSample()552 public String getSample() { 553 return String.valueOf((int) (start + end) / 2); 554 } 555 } 556 557 static final Splitter LIST = Splitter.on(", ").trimResults(); 558 static final Splitter SPLIT_SPACE_OR_COMMA = 559 Splitter.on(Pattern.compile("[, ]")).omitEmptyStrings().trimResults(); 560 561 public static class LiteralMatchValue extends MatchValue { 562 private final Set<String> items; 563 564 @Override getName()565 public String getName() { 566 return "literal/" + Joiner.on(", ").join(items); 567 } 568 LiteralMatchValue(String key)569 private LiteralMatchValue(String key) { 570 items = ImmutableSet.copyOf(LIST.splitToList(key)); 571 } 572 of(String key)573 public static LiteralMatchValue of(String key) { 574 return new LiteralMatchValue(key); 575 } 576 577 @Override is(String item)578 public boolean is(String item) { 579 return items.contains(item); 580 } 581 582 @Override getSample()583 public String getSample() { 584 return items.iterator().next(); 585 } 586 587 /** 588 * Return immutable set of items 589 * 590 * @return 591 */ getItems()592 public Set<String> getItems() { 593 return items; 594 } 595 } 596 597 public static class RegexMatchValue extends MatchValue { 598 private final Pattern pattern; 599 600 @Override getName()601 public String getName() { 602 return "regex/" + pattern; 603 } 604 RegexMatchValue(String key)605 protected RegexMatchValue(String key) { 606 pattern = Pattern.compile(key); 607 } 608 of(String key)609 public static RegexMatchValue of(String key) { 610 return new RegexMatchValue(key); 611 } 612 613 @Override is(String item)614 public boolean is(String item) { 615 return pattern.matcher(item).matches(); 616 } 617 } 618 619 public static class SemverMatchValue extends MatchValue { 620 @Override getName()621 public String getName() { 622 return "semver"; 623 } 624 SemverMatchValue(String key)625 protected SemverMatchValue(String key) { 626 super(); 627 } 628 of(String key)629 public static SemverMatchValue of(String key) { 630 if (key != null) { 631 throw new IllegalArgumentException("No parameter allowed"); 632 } 633 return new SemverMatchValue(key); 634 } 635 636 @Override is(String item)637 public boolean is(String item) { 638 try { 639 new Semver(item, SemverType.STRICT); 640 return true; 641 } catch (SemverException e) { 642 return false; 643 } 644 } 645 } 646 647 public static class VersionMatchValue extends MatchValue { 648 649 @Override getName()650 public String getName() { 651 return "version"; 652 } 653 VersionMatchValue(String key)654 private VersionMatchValue(String key) {} 655 of(String key)656 public static VersionMatchValue of(String key) { 657 if (key != null) { 658 throw new IllegalArgumentException("No parameter allowed"); 659 } 660 return new VersionMatchValue(key); 661 } 662 663 @Override is(String item)664 public boolean is(String item) { 665 try { 666 VersionInfo.getInstance(item); 667 } catch (Exception e) { 668 return false; 669 } 670 return true; 671 } 672 } 673 674 public static class MetazoneMatchValue extends MatchValue { 675 private Set<String> valid; 676 677 @Override getName()678 public String getName() { 679 return "metazone"; 680 } 681 of(String key)682 public static MetazoneMatchValue of(String key) { 683 if (key != null) { 684 throw new IllegalArgumentException("No parameter allowed"); 685 } 686 return new MetazoneMatchValue(); 687 } 688 689 @Override is(String item)690 public synchronized boolean is(String item) { 691 // must lazy-eval 692 if (valid == null) { 693 SupplementalDataInfo sdi = SupplementalDataInfo.getInstance(); 694 valid = sdi.getAllMetazones(); 695 } 696 return valid.contains(item); 697 } 698 } 699 700 public static class AnyMatchValue extends MatchValue { 701 final String key; 702 AnyMatchValue(String key)703 public AnyMatchValue(String key) { 704 this.key = key; 705 } 706 707 @Override getName()708 public String getName() { 709 return "any" + (key == null ? "" : "/" + key); 710 } 711 of(String key)712 public static AnyMatchValue of(String key) { 713 return new AnyMatchValue(key); 714 } 715 716 @Override is(String item)717 public boolean is(String item) { 718 return true; 719 } 720 } 721 722 static final Splitter SPACE_SPLITTER = Splitter.on(' ').omitEmptyStrings(); 723 724 public static class SetMatchValue extends MatchValue { 725 final MatchValue subtest; 726 SetMatchValue(MatchValue subtest)727 public SetMatchValue(MatchValue subtest) { 728 this.subtest = subtest; 729 } 730 731 @Override getName()732 public String getName() { 733 return "set/" + subtest.getName(); 734 } 735 of(String key)736 public static SetMatchValue of(String key) { 737 return new SetMatchValue(MatchValue.of(key)); 738 } 739 740 @Override is(String items)741 public boolean is(String items) { 742 List<String> splitItems = SPACE_SPLITTER.splitToList(items); 743 if ((new HashSet<>(splitItems)).size() != splitItems.size()) { 744 throw new IllegalArgumentException("Set contains duplicates: " + items); 745 } 746 return and(subtest, splitItems); 747 } 748 749 @Override getSample()750 public String getSample() { 751 return subtest.getSample(); 752 } 753 } 754 755 static final Splitter BARS_SPLITTER = Splitter.on("||").omitEmptyStrings(); 756 757 public static class OrMatchValue extends MatchValue { 758 final List<MatchValue> subtests; 759 OrMatchValue(Iterator<MatchValue> iterator)760 private OrMatchValue(Iterator<MatchValue> iterator) { 761 this.subtests = ImmutableList.copyOf(iterator); 762 } 763 764 @Override getName()765 public String getName() { 766 return "or/" + Joiner.on("||").join(subtests); 767 } 768 of(String key)769 public static OrMatchValue of(String key) { 770 return new OrMatchValue( 771 BARS_SPLITTER.splitToList(key).stream().map(k -> MatchValue.of(k)).iterator()); 772 } 773 774 @Override is(String item)775 public boolean is(String item) { 776 for (MatchValue subtest : subtests) { 777 if (subtest.is(item)) { 778 return true; 779 } 780 } 781 return false; 782 } 783 784 @Override getSample()785 public String getSample() { 786 for (MatchValue subtest : subtests) { 787 String result = subtest.getSample(); 788 if (!result.equals(DEFAULT_SAMPLE)) { 789 return result; 790 } 791 } 792 return DEFAULT_SAMPLE; 793 } 794 } 795 796 public static class TimeMatchValue extends MatchValue { 797 final String sample; 798 final SimpleDateFormat formatter; 799 TimeMatchValue(String key)800 public TimeMatchValue(String key) { 801 formatter = new SimpleDateFormat(key, ULocale.ROOT); 802 sample = formatter.format(new Date()); 803 } 804 805 @Override getName()806 public String getName() { 807 return "time/" + formatter.toPattern(); 808 } 809 of(String key)810 public static TimeMatchValue of(String key) { 811 return new TimeMatchValue(key); 812 } 813 814 @Override is(String item)815 public boolean is(String item) { 816 try { 817 formatter.parse(item); 818 return true; 819 } catch (ParseException e) { 820 return false; 821 } 822 } 823 824 @Override getSample()825 public String getSample() { 826 return sample; 827 } 828 } 829 830 public static class UnicodeSpanMatchValue extends MatchValue { 831 final String sample; 832 final UnicodeSet uset; 833 UnicodeSpanMatchValue(String key)834 public UnicodeSpanMatchValue(String key) { 835 UnicodeSet temp; 836 try { 837 temp = new UnicodeSet(key); 838 } catch (Exception e) { 839 temp = UnicodeSet.EMPTY; 840 int debug = 0; 841 } 842 uset = temp.freeze(); 843 sample = new StringBuilder().appendCodePoint(uset.getRangeStart(0)).toString(); 844 } 845 846 @Override getName()847 public String getName() { 848 return "unicodeset/" + uset; 849 } 850 of(String key)851 public static UnicodeSpanMatchValue of(String key) { 852 return new UnicodeSpanMatchValue(key); 853 } 854 855 @Override is(String item)856 public boolean is(String item) { 857 return uset.span(item, SpanCondition.CONTAINED) == item.length(); 858 } 859 860 @Override getSample()861 public String getSample() { 862 return sample; 863 } 864 } 865 } 866