xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/util/MatchValue.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
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