1/* GENERATED SOURCE. DO NOT MODIFY. */
2/*
3 ****************************************************************************************
4 * Copyright (C) 2009-2016, Google, Inc.; International Business Machines Corporation   *
5 * and others. All Rights Reserved.                                                     *
6 ****************************************************************************************
7 */
8package android.icu.util;
9
10import java.util.HashMap;
11import java.util.HashSet;
12import java.util.Iterator;
13import java.util.LinkedHashMap;
14import java.util.LinkedHashSet;
15import java.util.Map;
16import java.util.Map.Entry;
17import java.util.Set;
18import java.util.regex.Matcher;
19import java.util.regex.Pattern;
20
21import android.icu.impl.ICUResourceBundle;
22import android.icu.impl.Relation;
23import android.icu.impl.Row;
24import android.icu.impl.Row.R3;
25import android.icu.impl.Utility;
26
27/**
28 * Provides a way to match the languages (locales) supported by a product to the
29 * languages (locales) acceptable to a user, and get the best match. For
30 * example:
31 *
32 * <pre>
33 * LocaleMatcher matcher = new LocaleMatcher("fr, en-GB, en");
34 *
35 * // afterwards:
36 * matcher.getBestMatch("en-US").toLanguageTag() =&gt; "en"
37 * </pre>
38 *
39 * It takes into account when languages are close to one another, such as fil
40 * and tl, and when language regional variants are close, like en-GB and en-AU.
41 * It also handles scripts, like zh-Hant vs zh-TW. For examples, see the test
42 * file.
43 * <p>All classes implementing this interface should be immutable. Often a
44 * product will just need one static instance, built with the languages
45 * that it supports. However, it may want multiple instances with different
46 * default languages based on additional information, such as the domain.
47 *
48 * @author markdavis@google.com
49 * @hide Only a subset of ICU is exposed in Android
50 */
51public class LocaleMatcher {
52
53    /**
54     * @deprecated This API is ICU internal only.
55     * @hide draft / provisional / internal are hidden on Android
56     */
57    @Deprecated
58    public static final boolean DEBUG = false;
59
60    private static final ULocale UNKNOWN_LOCALE = new ULocale("und");
61
62    /**
63     * Threshold for falling back to the default (first) language. May make this
64     * a parameter in the future.
65     */
66    private static final double DEFAULT_THRESHOLD = 0.5;
67
68    /**
69     * The default language, in case the threshold is not met.
70     */
71    private final ULocale defaultLanguage;
72
73    /**
74     * The default language, in case the threshold is not met.
75     */
76    private final double threshold;
77
78    /**
79     * Create a new language matcher. The highest-weighted language is the
80     * default. That means that if no other language is matches closer than a given
81     * threshold, that default language is chosen. Typically the default is English,
82     * but it could be different based on additional information, such as the domain
83     * of the page.
84     *
85     * @param languagePriorityList weighted list
86     */
87    public LocaleMatcher(LocalePriorityList languagePriorityList) {
88        this(languagePriorityList, defaultWritten);
89    }
90
91    /**
92     * Create a new language matcher from a String form. The highest-weighted
93     * language is the default.
94     *
95     * @param languagePriorityListString String form of LanguagePriorityList
96     */
97    public LocaleMatcher(String languagePriorityListString) {
98        this(LocalePriorityList.add(languagePriorityListString).build());
99    }
100
101    /**
102     * Internal testing function; may expose API later.
103     * @param languagePriorityList LocalePriorityList to match
104     * @param matcherData Internal matching data
105     * @deprecated This API is ICU internal only.
106     * @hide draft / provisional / internal are hidden on Android
107     */
108    @Deprecated
109    public LocaleMatcher(LocalePriorityList languagePriorityList, LanguageMatcherData matcherData) {
110        this(languagePriorityList, matcherData, DEFAULT_THRESHOLD);
111    }
112
113    /**
114     * Internal testing function; may expose API later.
115     * @param languagePriorityList LocalePriorityList to match
116     * @param matcherData Internal matching data
117     * @deprecated This API is ICU internal only.
118     * @hide draft / provisional / internal are hidden on Android
119     */
120    @Deprecated
121    public LocaleMatcher(LocalePriorityList languagePriorityList, LanguageMatcherData matcherData, double threshold) {
122        this.matcherData = matcherData == null ? defaultWritten : matcherData.freeze();
123        for (final ULocale language : languagePriorityList) {
124            add(language, languagePriorityList.getWeight(language));
125        }
126        processMapping();
127        Iterator<ULocale> it = languagePriorityList.iterator();
128        defaultLanguage = it.hasNext() ? it.next() : null;
129        this.threshold = threshold;
130    }
131
132
133    /**
134     * Returns a fraction between 0 and 1, where 1 means that the languages are a
135     * perfect match, and 0 means that they are completely different. Note that
136     * the precise values may change over time; no code should be made dependent
137     * on the values remaining constant.
138     * @param desired Desired locale
139     * @param desiredMax Maximized locale (using likely subtags)
140     * @param supported Supported locale
141     * @param supportedMax Maximized locale (using likely subtags)
142     * @return value between 0 and 1, inclusive.
143     */
144    public double match(ULocale desired, ULocale desiredMax, ULocale supported, ULocale supportedMax) {
145        return matcherData.match(desired, desiredMax, supported, supportedMax);
146    }
147
148
149    /**
150     * Canonicalize a locale (language). Note that for now, it is canonicalizing
151     * according to CLDR conventions (he vs iw, etc), since that is what is needed
152     * for likelySubtags.
153     * @param ulocale language/locale code
154     * @return ULocale with remapped subtags.
155     */
156    public ULocale canonicalize(ULocale ulocale) {
157        // TODO Get the data from CLDR, use Java conventions.
158        String lang = ulocale.getLanguage();
159        String lang2 = canonicalMap.get(lang);
160        String script = ulocale.getScript();
161        String script2 = canonicalMap.get(script);
162        String region = ulocale.getCountry();
163        String region2 = canonicalMap.get(region);
164        if (lang2 != null || script2 != null || region2 != null) {
165            return new ULocale(
166                lang2 == null ? lang : lang2,
167                    script2 == null ? script : script2,
168                        region2 == null ? region : region2
169                );
170        }
171        return ulocale;
172    }
173
174    /**
175     * Get the best match for a LanguagePriorityList
176     *
177     * @param languageList list to match
178     * @return best matching language code
179     */
180    public ULocale getBestMatch(LocalePriorityList languageList) {
181        double bestWeight = 0;
182        ULocale bestTableMatch = null;
183        double penalty = 0;
184        OutputDouble matchWeight = new OutputDouble();
185        for (final ULocale language : languageList) {
186            final ULocale matchLocale = getBestMatchInternal(language, matchWeight);
187            final double weight = matchWeight.value * languageList.getWeight(language) - penalty;
188            if (weight > bestWeight) {
189                bestWeight = weight;
190                bestTableMatch = matchLocale;
191            }
192            penalty += 0.07000001;
193        }
194        if (bestWeight < threshold) {
195            bestTableMatch = defaultLanguage;
196        }
197        return bestTableMatch;
198    }
199
200    /**
201     * Convenience method: Get the best match for a LanguagePriorityList
202     *
203     * @param languageList String form of language priority list
204     * @return best matching language code
205     */
206    public ULocale getBestMatch(String languageList) {
207        return getBestMatch(LocalePriorityList.add(languageList).build());
208    }
209
210    /**
211     * Get the best match for an individual language code.
212     *
213     * @param ulocale locale/language code to match
214     * @return best matching language code
215     */
216    public ULocale getBestMatch(ULocale ulocale) {
217        return getBestMatchInternal(ulocale, null);
218    }
219
220    /**
221     * @deprecated This API is ICU internal only.
222     * @hide draft / provisional / internal are hidden on Android
223     */
224    @Deprecated
225    public ULocale getBestMatch(ULocale... ulocales) {
226        return getBestMatch(LocalePriorityList.add(ulocales).build());
227    }
228
229    /**
230     * {@inheritDoc}
231     */
232    @Override
233    public String toString() {
234        return "{" + defaultLanguage + ", "
235            + localeToMaxLocaleAndWeight + "}";
236    }
237    // ================= Privates =====================
238
239    /**
240     * Get the best match for an individual language code.
241     *
242     * @param languageCode
243     * @return best matching language code and weight (as per
244     *         {@link #match(ULocale, ULocale)})
245     */
246    private ULocale getBestMatchInternal(ULocale languageCode, OutputDouble outputWeight) {
247        languageCode = canonicalize(languageCode);
248        final ULocale maximized = addLikelySubtags(languageCode);
249        if (DEBUG) {
250            System.out.println("\ngetBestMatchInternal: " + languageCode + ";\t" + maximized);
251        }
252        double bestWeight = 0;
253        ULocale bestTableMatch = null;
254        String baseLanguage = maximized.getLanguage();
255        Set<R3<ULocale, ULocale, Double>> searchTable = desiredLanguageToPossibleLocalesToMaxLocaleToData.get(baseLanguage);
256        if (searchTable != null) { // we preprocessed the table so as to filter by lanugage
257            if (DEBUG) System.out.println("\tSearching: " + searchTable);
258            for (final R3<ULocale, ULocale, Double> tableKeyValue : searchTable) {
259                ULocale tableKey = tableKeyValue.get0();
260                ULocale maxLocale = tableKeyValue.get1();
261                Double matchedWeight = tableKeyValue.get2();
262                final double match = match(languageCode, maximized, tableKey, maxLocale);
263                if (DEBUG) {
264                    System.out.println("\t" + tableKeyValue + ";\t" + match + "\n");
265                }
266                final double weight = match * matchedWeight;
267                if (weight > bestWeight) {
268                    bestWeight = weight;
269                    bestTableMatch = tableKey;
270                    if (weight > 0.999d) { // bail on good enough match.
271                        break;
272                    }
273                }
274            }
275        }
276        if (bestWeight < threshold) {
277            bestTableMatch = defaultLanguage;
278        }
279        if (outputWeight != null) {
280            outputWeight.value = bestWeight; // only return the weight when needed
281        }
282        return bestTableMatch;
283    }
284
285    /**
286     * @deprecated This API is ICU internal only.
287     * @hide draft / provisional / internal are hidden on Android
288     */
289    @Deprecated
290    private static class OutputDouble { // TODO, move to where OutputInt is
291        double value;
292    }
293
294    private void add(ULocale language, Double weight) {
295        language = canonicalize(language);
296        R3<ULocale, ULocale, Double> row = Row.of(language, addLikelySubtags(language), weight);
297        row.freeze();
298        localeToMaxLocaleAndWeight.add(row);
299    }
300
301    /**
302     * We preprocess the data to get just the possible matches for each desired base language.
303     */
304    private void processMapping() {
305        for (Entry<String, Set<String>> desiredToMatchingLanguages : matcherData.matchingLanguages().keyValuesSet()) {
306            String desired = desiredToMatchingLanguages.getKey();
307            Set<String> supported = desiredToMatchingLanguages.getValue();
308            for (R3<ULocale, ULocale, Double> localeToMaxAndWeight : localeToMaxLocaleAndWeight) {
309                final ULocale key = localeToMaxAndWeight.get0();
310                String lang = key.getLanguage();
311                if (supported.contains(lang)) {
312                    addFiltered(desired, localeToMaxAndWeight);
313                }
314            }
315        }
316        // now put in the values directly, since languages always map to themselves
317        for (R3<ULocale, ULocale, Double> localeToMaxAndWeight : localeToMaxLocaleAndWeight) {
318            final ULocale key = localeToMaxAndWeight.get0();
319            String lang = key.getLanguage();
320            addFiltered(lang, localeToMaxAndWeight);
321        }
322    }
323
324    private void addFiltered(String desired, R3<ULocale, ULocale, Double> localeToMaxAndWeight) {
325        Set<R3<ULocale, ULocale, Double>> map = desiredLanguageToPossibleLocalesToMaxLocaleToData.get(desired);
326        if (map == null) {
327            desiredLanguageToPossibleLocalesToMaxLocaleToData.put(desired, map = new LinkedHashSet<R3<ULocale, ULocale, Double>>());
328        }
329        map.add(localeToMaxAndWeight);
330        if (DEBUG) {
331            System.out.println(desired + ", " + localeToMaxAndWeight);
332        }
333    }
334
335    Set<Row.R3<ULocale, ULocale, Double>> localeToMaxLocaleAndWeight = new LinkedHashSet<Row.R3<ULocale, ULocale, Double>>();
336    Map<String,Set<Row.R3<ULocale, ULocale, Double>>> desiredLanguageToPossibleLocalesToMaxLocaleToData
337    = new LinkedHashMap<String,Set<Row.R3<ULocale, ULocale, Double>>>();
338
339    // =============== Special Mapping Information ==============
340
341    /**
342     * We need to add another method to addLikelySubtags that doesn't return
343     * null, but instead substitutes Zzzz and ZZ if unknown. There are also
344     * a few cases where addLikelySubtags needs to have expanded data, to handle
345     * all deprecated codes.
346     * @param languageCode
347     * @return "fixed" addLikelySubtags
348     */
349    private ULocale addLikelySubtags(ULocale languageCode) {
350        // max("und") = "en_Latn_US", and since matching is based on maximized tags, the undefined
351        // language would normally match English.  But that would produce the counterintuitive results
352        // that getBestMatch("und", LocaleMatcher("it,en")) would be "en", and
353        // getBestMatch("en", LocaleMatcher("it,und")) would be "und".
354        //
355        // To avoid that, we change the matcher's definitions of max (AddLikelySubtagsWithDefaults)
356        // so that max("und")="und". That produces the following, more desirable results:
357        if (languageCode.equals(UNKNOWN_LOCALE)) {
358            return UNKNOWN_LOCALE;
359        }
360        final ULocale result = ULocale.addLikelySubtags(languageCode);
361        // should have method on getLikelySubtags for this
362        if (result == null || result.equals(languageCode)) {
363            final String language = languageCode.getLanguage();
364            final String script = languageCode.getScript();
365            final String region = languageCode.getCountry();
366            return new ULocale((language.length()==0 ? "und"
367                : language)
368                + "_"
369                + (script.length()==0 ? "Zzzz" : script)
370                + "_"
371                + (region.length()==0 ? "ZZ" : region));
372        }
373        return result;
374    }
375
376    private static class LocalePatternMatcher {
377        // a value of null means a wildcard; matches any.
378        private String lang;
379        private String script;
380        private String region;
381        private Level level;
382        static Pattern pattern = Pattern.compile(
383            "([a-z]{1,8}|\\*)"
384                + "(?:[_-]([A-Z][a-z]{3}|\\*))?"
385                + "(?:[_-]([A-Z]{2}|[0-9]{3}|\\*))?");
386
387        public LocalePatternMatcher(String toMatch) {
388            Matcher matcher = pattern.matcher(toMatch);
389            if (!matcher.matches()) {
390                throw new IllegalArgumentException("Bad pattern: " + toMatch);
391            }
392            lang = matcher.group(1);
393            script = matcher.group(2);
394            region = matcher.group(3);
395            level = region != null ? Level.region : script != null ? Level.script : Level.language;
396
397            if (lang.equals("*")) {
398                lang = null;
399            }
400            if (script != null && script.equals("*")) {
401                script = null;
402            }
403            if (region != null && region.equals("*")) {
404                region = null;
405            }
406        }
407
408        boolean matches(ULocale ulocale) {
409            if (lang != null && !lang.equals(ulocale.getLanguage())) {
410                return false;
411            }
412            if (script != null && !script.equals(ulocale.getScript())) {
413                return false;
414            }
415            if (region != null && !region.equals(ulocale.getCountry())) {
416                return false;
417            }
418            return true;
419        }
420
421        public Level getLevel() {
422            return level;
423        }
424
425        public String getLanguage() {
426            return (lang == null ? "*" : lang);
427        }
428
429        public String getScript() {
430            return (script == null ? "*" : script);
431        }
432
433        public String getRegion() {
434            return (region == null ? "*" : region);
435        }
436
437        public String toString() {
438            String result = getLanguage();
439            if (level != Level.language) {
440                result += "-" + getScript();
441                if (level != Level.script) {
442                    result += "-" + getRegion();
443                }
444            }
445            return result;
446        }
447
448        /* (non-Javadoc)
449         * @see java.lang.Object#equals(java.lang.Object)
450         */
451        @Override
452        public boolean equals(Object obj) {
453            LocalePatternMatcher other = (LocalePatternMatcher) obj;
454            return Utility.objectEquals(level, other.level)
455                && Utility.objectEquals(lang, other.lang)
456                && Utility.objectEquals(script, other.script)
457                && Utility.objectEquals(region, other.region);
458        }
459
460        /* (non-Javadoc)
461         * @see java.lang.Object#hashCode()
462         */
463        @Override
464        public int hashCode() {
465            return level.ordinal()
466                ^ (lang == null ? 0 : lang.hashCode())
467                ^ (script == null ? 0 : script.hashCode())
468                ^ (region == null ? 0 : region.hashCode());
469        }
470    }
471
472    enum Level {
473        language(0.99),
474        script(0.2),
475        region(0.04);
476
477        final double worst;
478
479        Level(double d) {
480            worst = d;
481        }
482    }
483
484    private static class ScoreData implements Freezable<ScoreData> {
485        @SuppressWarnings("unused")
486        private static final double maxUnequal_changeD_sameS = 0.5;
487
488        @SuppressWarnings("unused")
489        private static final double maxUnequal_changeEqual = 0.75;
490
491        LinkedHashSet<Row.R3<LocalePatternMatcher,LocalePatternMatcher,Double>> scores = new LinkedHashSet<R3<LocalePatternMatcher, LocalePatternMatcher, Double>>();
492        final Level level;
493
494        public ScoreData(Level level) {
495            this.level = level;
496        }
497
498        void addDataToScores(String desired, String supported, R3<LocalePatternMatcher,LocalePatternMatcher,Double> data) {
499            //            Map<String, Set<R3<LocalePatternMatcher,LocalePatternMatcher,Double>>> lang_result = scores.get(desired);
500            //            if (lang_result == null) {
501            //                scores.put(desired, lang_result = new HashMap());
502            //            }
503            //            Set<R3<LocalePatternMatcher,LocalePatternMatcher,Double>> result = lang_result.get(supported);
504            //            if (result == null) {
505            //                lang_result.put(supported, result = new LinkedHashSet());
506            //            }
507            //            result.add(data);
508            boolean added = scores.add(data);
509            if (!added) {
510                throw new ICUException("trying to add duplicate data: " +  data);
511            }
512        }
513
514        double getScore(ULocale dMax, String desiredRaw, String desiredMax,
515            ULocale sMax, String supportedRaw, String supportedMax) {
516            double distance = 0;
517            if (!desiredMax.equals(supportedMax)) {
518                distance = getRawScore(dMax, sMax);
519            } else if (!desiredRaw.equals(supportedRaw)) { // maxes are equal, changes are equal
520                distance += 0.001;
521            }
522            return distance;
523        }
524
525        private double getRawScore(ULocale desiredLocale, ULocale supportedLocale) {
526            if (DEBUG) {
527                System.out.println("\t\t\t" + level + " Raw Score:\t" + desiredLocale + ";\t" + supportedLocale);
528            }
529            for (R3<LocalePatternMatcher,LocalePatternMatcher,Double> datum : scores) { // : result
530                if (datum.get0().matches(desiredLocale)
531                    && datum.get1().matches(supportedLocale)) {
532                    if (DEBUG) {
533                        System.out.println("\t\t\t\tFOUND\t" + datum);
534                    }
535                    return datum.get2();
536                }
537            }
538            if (DEBUG) {
539                System.out.println("\t\t\t\tNOTFOUND\t" + level.worst);
540            }
541            return level.worst;
542        }
543
544        public String toString() {
545            StringBuilder result = new StringBuilder().append(level);
546            for (R3<LocalePatternMatcher, LocalePatternMatcher, Double> score : scores) {
547                result.append("\n\t\t").append(score);
548            }
549            return result.toString();
550        }
551
552
553        @SuppressWarnings("unchecked")
554        public ScoreData cloneAsThawed() {
555            try {
556                ScoreData result = (ScoreData) clone();
557                result.scores = (LinkedHashSet<R3<LocalePatternMatcher, LocalePatternMatcher, Double>>) result.scores.clone();
558                result.frozen = false;
559                return result;
560            } catch (CloneNotSupportedException e) {
561                throw new ICUCloneNotSupportedException(e); // will never happen
562            }
563
564        }
565
566        private volatile boolean frozen = false;
567
568        public ScoreData freeze() {
569            return this;
570        }
571
572        public boolean isFrozen() {
573            return frozen;
574        }
575
576        public Relation<String,String> getMatchingLanguages() {
577            Relation<String,String> desiredToSupported = Relation.of(new LinkedHashMap<String,Set<String>>(), HashSet.class);
578            for (R3<LocalePatternMatcher, LocalePatternMatcher, Double> item : scores) {
579                LocalePatternMatcher desired = item.get0();
580                LocalePatternMatcher supported = item.get1();
581                if (desired.lang != null && supported.lang != null) { // explicitly mentioned languages must have reasonable distance
582                    desiredToSupported.put(desired.lang, supported.lang);
583                }
584            }
585            desiredToSupported.freeze();
586            return desiredToSupported;
587        }
588    }
589
590    /**
591     * Only for testing and use by tools. Interface may change!!
592     * @deprecated This API is ICU internal only.
593     * @hide draft / provisional / internal are hidden on Android
594     */
595    @Deprecated
596    public static class LanguageMatcherData implements Freezable<LanguageMatcherData> {
597        private ScoreData languageScores = new ScoreData(Level.language);
598        private ScoreData scriptScores = new ScoreData(Level.script);
599        private ScoreData regionScores = new ScoreData(Level.region);
600        private Relation<String, String> matchingLanguages;
601        private volatile boolean frozen = false;
602
603
604        /**
605         * @deprecated This API is ICU internal only.
606         * @hide draft / provisional / internal are hidden on Android
607         */
608        @Deprecated
609        public LanguageMatcherData() {
610        }
611
612        /**
613         * @deprecated This API is ICU internal only.
614         * @hide draft / provisional / internal are hidden on Android
615         */
616        @Deprecated
617        public Relation<String, String> matchingLanguages() {
618            return matchingLanguages;
619        }
620
621        /**
622         * @deprecated This API is ICU internal only.
623         * @hide draft / provisional / internal are hidden on Android
624         */
625        @Deprecated
626        public String toString() {
627            return languageScores + "\n\t" + scriptScores + "\n\t" + regionScores;
628        }
629
630        /**
631         * @deprecated This API is ICU internal only.
632         * @hide draft / provisional / internal are hidden on Android
633         */
634        @Deprecated
635        public double match(ULocale a, ULocale aMax, ULocale b, ULocale bMax) {
636            double diff = 0;
637            diff += languageScores.getScore(aMax, a.getLanguage(), aMax.getLanguage(), bMax, b.getLanguage(), bMax.getLanguage());
638            if (diff > 0.999d) { // with no language match, we bail
639                return 0.0d;
640            }
641            diff += scriptScores.getScore(aMax, a.getScript(), aMax.getScript(), bMax, b.getScript(), bMax.getScript());
642            diff += regionScores.getScore(aMax, a.getCountry(), aMax.getCountry(), bMax, b.getCountry(), bMax.getCountry());
643
644            if (!a.getVariant().equals(b.getVariant())) {
645                diff += 0.01;
646            }
647            if (diff < 0.0d) {
648                diff = 0.0d;
649            } else if (diff > 1.0d) {
650                diff = 1.0d;
651            }
652            if (DEBUG) {
653                System.out.println("\t\t\tTotal Distance\t" + diff);
654            }
655            return 1.0 - diff;
656        }
657
658
659        /**
660         * Add an exceptional distance between languages, typically because regional
661         * dialects were given their own language codes. At this point the code is
662         * symmetric. We don't bother producing an equivalence class because there are
663         * so few cases; this function depends on the other permutations being
664         * added specifically.
665         * @deprecated This API is ICU internal only.
666         * @hide draft / provisional / internal are hidden on Android
667         */
668        @SuppressWarnings("unused")
669        @Deprecated
670        private LanguageMatcherData addDistance(String desired, String supported, int percent) {
671            return addDistance(desired, supported, percent, false, null);
672        }
673        /**
674         * @deprecated This API is ICU internal only.
675         * @hide draft / provisional / internal are hidden on Android
676         */
677        @Deprecated
678        public LanguageMatcherData addDistance(String desired, String supported, int percent, String comment) {
679            return addDistance(desired, supported, percent, false, comment);
680        }
681        /**
682         * @deprecated This API is ICU internal only.
683         * @hide draft / provisional / internal are hidden on Android
684         */
685        @Deprecated
686        public LanguageMatcherData addDistance(String desired, String supported, int percent, boolean oneway) {
687            return addDistance(desired, supported, percent, oneway, null);
688        }
689
690        private LanguageMatcherData addDistance(String desired, String supported, int percent, boolean oneway, String comment) {
691            if (DEBUG) {
692                System.out.println("\t<languageMatch desired=\"" + desired + "\"" +
693                    " supported=\"" + supported + "\"" +
694                    " percent=\"" + percent + "\""
695                    + (oneway ? " oneway=\"true\"" : "")
696                    + "/>"
697                    + (comment == null ? "" : "\t<!-- " + comment + " -->"));
698                //                    //     .addDistance("nn", "nb", 4, true)
699                //                        System.out.println(".addDistance(\"" + desired + "\"" +
700                //                                ", \"" + supported + "\"" +
701                //                                ", " + percent + ""
702                //                                + (oneway ? "" : ", true")
703                //                                + (comment == null ? "" : ", \"" + comment + "\"")
704                //                                + ")"
705                //                        );
706
707            }
708            double score = 1-percent/100.0; // convert from percentage
709            LocalePatternMatcher desiredMatcher = new LocalePatternMatcher(desired);
710            Level desiredLen = desiredMatcher.getLevel();
711            LocalePatternMatcher supportedMatcher = new LocalePatternMatcher(supported);
712            Level supportedLen = supportedMatcher.getLevel();
713            if (desiredLen != supportedLen) {
714                throw new IllegalArgumentException("Lengths unequal: " + desired + ", " + supported);
715            }
716            R3<LocalePatternMatcher,LocalePatternMatcher,Double> data = Row.of(desiredMatcher, supportedMatcher, score);
717            R3<LocalePatternMatcher,LocalePatternMatcher,Double> data2 = oneway ? null : Row.of(supportedMatcher, desiredMatcher, score);
718            boolean desiredEqualsSupported = desiredMatcher.equals(supportedMatcher);
719            switch (desiredLen) {
720            case language:
721                String dlanguage = desiredMatcher.getLanguage();
722                String slanguage = supportedMatcher.getLanguage();
723                languageScores.addDataToScores(dlanguage, slanguage, data);
724                if (!oneway && !desiredEqualsSupported) {
725                    languageScores.addDataToScores(slanguage, dlanguage, data2);
726                }
727                break;
728            case script:
729                String dscript = desiredMatcher.getScript();
730                String sscript = supportedMatcher.getScript();
731                scriptScores.addDataToScores(dscript, sscript, data);
732                if (!oneway && !desiredEqualsSupported) {
733                    scriptScores.addDataToScores(sscript, dscript, data2);
734                }
735                break;
736            case region:
737                String dregion = desiredMatcher.getRegion();
738                String sregion = supportedMatcher.getRegion();
739                regionScores.addDataToScores(dregion, sregion, data);
740                if (!oneway && !desiredEqualsSupported) {
741                    regionScores.addDataToScores(sregion, dregion, data2);
742                }
743                break;
744            }
745            return this;
746        }
747
748        /**
749         * {@inheritDoc}
750         * @deprecated This API is ICU internal only.
751         * @hide draft / provisional / internal are hidden on Android
752         */
753        @Deprecated
754        public LanguageMatcherData cloneAsThawed() {
755            LanguageMatcherData result;
756            try {
757                result = (LanguageMatcherData) clone();
758                result.languageScores = languageScores.cloneAsThawed();
759                result.scriptScores = scriptScores.cloneAsThawed();
760                result.regionScores = regionScores.cloneAsThawed();
761                result.frozen = false;
762                return result;
763            } catch (CloneNotSupportedException e) {
764                throw new ICUCloneNotSupportedException(e); // will never happen
765            }
766        }
767
768        /**
769         * {@inheritDoc}
770         * @deprecated This API is ICU internal only.
771         * @hide draft / provisional / internal are hidden on Android
772         */
773        @Deprecated
774        public LanguageMatcherData freeze() {
775            languageScores.freeze();
776            regionScores.freeze();
777            scriptScores.freeze();
778            matchingLanguages = languageScores.getMatchingLanguages();
779            frozen = true;
780            return this;
781        }
782
783        /**
784         * {@inheritDoc}
785         * @deprecated This API is ICU internal only.
786         * @hide draft / provisional / internal are hidden on Android
787         */
788        @Deprecated
789        public boolean isFrozen() {
790            return frozen;
791        }
792    }
793
794    LanguageMatcherData matcherData;
795
796    private static final LanguageMatcherData defaultWritten;
797
798    private static HashMap<String,String> canonicalMap = new HashMap<String, String>();
799
800
801    static {
802        canonicalMap.put("iw", "he");
803        canonicalMap.put("mo", "ro");
804        canonicalMap.put("tl", "fil");
805
806        ICUResourceBundle suppData = getICUSupplementalData();
807        ICUResourceBundle languageMatching = suppData.findTopLevel("languageMatching");
808        ICUResourceBundle written = (ICUResourceBundle) languageMatching.get("written");
809        defaultWritten = new LanguageMatcherData();
810
811        for(UResourceBundleIterator iter = written.getIterator(); iter.hasNext();) {
812            ICUResourceBundle item = (ICUResourceBundle) iter.next();
813            /*
814            "*_*_*",
815            "*_*_*",
816            "96",
817             */
818            // <languageMatch desired="gsw" supported="de" percent="96" oneway="true" />
819            boolean oneway = item.getSize() > 3 && "1".equals(item.getString(3));
820            defaultWritten.addDistance(item.getString(0), item.getString(1), Integer.parseInt(item.getString(2)), oneway);
821        }
822        defaultWritten.freeze();
823    }
824
825    /**
826     * @deprecated This API is ICU internal only.
827     * @hide draft / provisional / internal are hidden on Android
828     */
829    @Deprecated
830    public static ICUResourceBundle getICUSupplementalData() {
831        ICUResourceBundle suppData = (ICUResourceBundle) UResourceBundle.getBundleInstance(
832            ICUResourceBundle.ICU_BASE_NAME,
833            "supplementalData",
834            ICUResourceBundle.ICU_DATA_CLASS_LOADER);
835        return suppData;
836    }
837
838    /**
839     * @deprecated This API is ICU internal only.
840     * @hide draft / provisional / internal are hidden on Android
841     */
842    @Deprecated
843    public static double match(ULocale a, ULocale b) {
844        final LocaleMatcher matcher = new LocaleMatcher("");
845        return matcher.match(a, matcher.addLikelySubtags(a), b, matcher.addLikelySubtags(b));
846    }
847}
848