1 /* GENERATED SOURCE. DO NOT MODIFY. */
2 // © 2016 and later: Unicode, Inc. and others.
3 // License & terms of use: http://www.unicode.org/copyright.html#License
4 /*
5  *******************************************************************************
6  * Copyright (C) 2008-2016, Google Inc, International Business Machines Corporation
7  * and others. All Rights Reserved.
8  *******************************************************************************
9  */
10 package android.icu.text;
11 
12 import java.util.ArrayList;
13 import java.util.Collections;
14 import java.util.Comparator;
15 import java.util.Iterator;
16 import java.util.List;
17 import java.util.Locale;
18 
19 import android.icu.lang.UCharacter;
20 import android.icu.text.AlphabeticIndex.Bucket;
21 import android.icu.text.AlphabeticIndex.Bucket.LabelType;
22 import android.icu.util.LocaleData;
23 import android.icu.util.ULocale;
24 
25 /**
26  * AlphabeticIndex supports the creation of a UI index appropriate for a given language.
27  * It can support either direct use, or use with a client that doesn't support localized collation.
28  * The following is an example of what an index might look like in a UI:
29  *
30  * <pre>
31  *  <b>... A B C D E F G H I J K L M N O P Q R S T U V W X Y Z  ...</b>
32  *
33  *  <b>A</b>
34  *     Addison
35  *     Albertson
36  *     Azensky
37  *  <b>B</b>
38  *     Baecker
39  *  ...
40  * </pre>
41  *
42  * The class can generate a list of labels for use as a UI "index", that is, a list of
43  * clickable characters (or character sequences) that allow the user to see a segment
44  * (bucket) of a larger "target" list. That is, each label corresponds to a bucket in
45  * the target list, where everything in the bucket is greater than or equal to the character
46  * (according to the locale's collation). Strings can be added to the index;
47  * they will be in sorted order in the right bucket.
48  * <p>
49  * The class also supports having buckets for strings before the first (underflow),
50  * after the last (overflow), and between scripts (inflow). For example, if the index
51  * is constructed with labels for Russian and English, Greek characters would fall
52  * into an inflow bucket between the other two scripts.
53  *
54  * <p><em>Note:</em> If you expect to have a lot of ASCII or Latin characters
55  * as well as characters from the user's language,
56  * then it is a good idea to call addLabels(ULocale.English).
57  *
58  * <h2>Direct Use</h2>
59  * <p>The following shows an example of building an index directly.
60  *  The "show..." methods below are just to illustrate usage.
61  *
62  * <pre>
63  * // Create a simple index where the values for the strings are Integers, and add the strings
64  *
65  * AlphabeticIndex&lt;Integer&gt; index = new AlphabeticIndex&lt;Integer&gt;(desiredLocale).addLabels(additionalLocale);
66  * int counter = 0;
67  * for (String item : test) {
68  *     index.addRecord(item, counter++);
69  * }
70  * ...
71  * // Show index at top. We could skip or gray out empty buckets
72  *
73  * for (AlphabeticIndex.Bucket&lt;Integer&gt; bucket : index) {
74  *     if (showAll || bucket.size() != 0) {
75  *         showLabelAtTop(UI, bucket.getLabel());
76  *     }
77  * }
78  *  ...
79  * // Show the buckets with their contents, skipping empty buckets
80  *
81  * for (AlphabeticIndex.Bucket&lt;Integer&gt; bucket : index) {
82  *     if (bucket.size() != 0) {
83  *         showLabelInList(UI, bucket.getLabel());
84  *         for (AlphabeticIndex.Record&lt;Integer&gt; item : bucket) {
85  *             showIndexedItem(UI, item.getName(), item.getData());
86  *         }
87  * </pre>
88  *
89  * The caller can build different UIs using this class.
90  * For example, an index character could be omitted or grayed-out
91  * if its bucket is empty. Small buckets could also be combined based on size, such as:
92  *
93  * <pre>
94  * <b>... A-F G-N O-Z ...</b>
95  * </pre>
96  *
97  * <h2>Client Support</h2>
98  * <p>Callers can also use the {@link AlphabeticIndex.ImmutableIndex}, or the AlphabeticIndex itself,
99  * to support sorting on a client that doesn't support AlphabeticIndex functionality.
100  *
101  * <p>The ImmutableIndex is both immutable and thread-safe.
102  * The corresponding AlphabeticIndex methods are not thread-safe because
103  * they "lazily" build the index buckets.
104  * <ul>
105  * <li>ImmutableIndex.getBucket(index) provides random access to all
106  *     buckets and their labels and label types.
107  * <li>AlphabeticIndex.getBucketLabels() or the bucket iterator on either class
108  *     can be used to get a list of the labels,
109  *     such as "...", "A", "B",..., and send that list to the client.
110  * <li>When the client has a new name, it sends that name to the server.
111  * The server needs to call the following methods,
112  * and communicate the bucketIndex and collationKey back to the client.
113  *
114  * <pre>
115  * int bucketIndex = index.getBucketIndex(name);
116  * String label = immutableIndex.getBucket(bucketIndex).getLabel();  // optional
117  * RawCollationKey collationKey = collator.getRawCollationKey(name, null);
118  * </pre>
119  *
120  * <li>The client would put the name (and associated information) into its bucket for bucketIndex. The collationKey is a
121  * sequence of bytes that can be compared with a binary compare, and produce the right localized result.</li>
122  * </ul>
123  *
124  * @author Mark Davis
125  */
126 public final class AlphabeticIndex<V> implements Iterable<Bucket<V>> {
127     /**
128      * Prefix string for Chinese index buckets.
129      * See http://unicode.org/repos/cldr/trunk/specs/ldml/tr35-collation.html#Collation_Indexes
130      */
131     private static final String BASE = "\uFDD0";
132 
133     private static final char CGJ = '\u034F';
134 
135     private static final Comparator<String> binaryCmp = new UTF16.StringComparator(true, false, 0);
136 
137     private final RuleBasedCollator collatorOriginal;
138     private final RuleBasedCollator collatorPrimaryOnly;
139     private RuleBasedCollator collatorExternal;
140 
141     // Comparator for records, so that the Record class can be static.
142     private final Comparator<Record<V>> recordComparator = new Comparator<Record<V>>() {
143         @Override
144         public int compare(Record<V> o1, Record<V> o2) {
145             return collatorOriginal.compare(o1.name, o2.name);
146         }
147     };
148 
149     private final List<String> firstCharsInScripts;
150 
151     // We accumulate these as we build up the input parameters
152     private final UnicodeSet initialLabels = new UnicodeSet();
153     private List<Record<V>> inputList;
154 
155     // Lazy evaluated: null means that we have not built yet.
156     private BucketList<V> buckets;
157 
158     private String overflowLabel = "\u2026";
159     private String underflowLabel = "\u2026";
160     private String inflowLabel = "\u2026";
161 
162     /**
163      * Immutable, thread-safe version of {@link AlphabeticIndex}.
164      * This class provides thread-safe methods for bucketing,
165      * and random access to buckets and their properties,
166      * but does not offer adding records to the index.
167      *
168      * @param <V> The Record value type is unused. It can be omitted for this class
169      * if it was omitted for the AlphabeticIndex that built it.
170      */
171     public static final class ImmutableIndex<V> implements Iterable<Bucket<V>> {
172         private final BucketList<V> buckets;
173         private final Collator collatorPrimaryOnly;
174 
ImmutableIndex(BucketList<V> bucketList, Collator collatorPrimaryOnly)175         private ImmutableIndex(BucketList<V> bucketList, Collator collatorPrimaryOnly) {
176             this.buckets = bucketList;
177             this.collatorPrimaryOnly = collatorPrimaryOnly;
178         }
179 
180         /**
181          * Returns the number of index buckets and labels, including underflow/inflow/overflow.
182          *
183          * @return the number of index buckets
184          */
getBucketCount()185         public int getBucketCount() {
186             return buckets.getBucketCount();
187         }
188 
189         /**
190          * Finds the index bucket for the given name and returns the number of that bucket.
191          * Use {@link #getBucket(int)} to get the bucket's properties.
192          *
193          * @param name the string to be sorted into an index bucket
194          * @return the bucket number for the name
195          */
getBucketIndex(CharSequence name)196         public int getBucketIndex(CharSequence name) {
197             return buckets.getBucketIndex(name, collatorPrimaryOnly);
198         }
199 
200         /**
201          * Returns the index-th bucket. Returns null if the index is out of range.
202          *
203          * @param index bucket number
204          * @return the index-th bucket
205          */
getBucket(int index)206         public Bucket<V> getBucket(int index) {
207             if (0 <= index && index < buckets.getBucketCount()) {
208                 return buckets.immutableVisibleList.get(index);
209             } else {
210                 return null;
211             }
212         }
213 
214         /**
215          * {@inheritDoc}
216          */
217         @Override
iterator()218         public Iterator<Bucket<V>> iterator() {
219             return buckets.iterator();
220         }
221     }
222 
223     /**
224      * Create the index object.
225      *
226      * @param locale
227      *            The locale for the index.
228      */
AlphabeticIndex(ULocale locale)229     public AlphabeticIndex(ULocale locale) {
230         this(locale, null);
231     }
232 
233     /**
234      * Create the index object.
235      *
236      * @param locale
237      *            The locale for the index.
238      */
AlphabeticIndex(Locale locale)239     public AlphabeticIndex(Locale locale) {
240         this(ULocale.forLocale(locale), null);
241     }
242 
243     /**
244      * Create an AlphabeticIndex that uses a specific collator.
245      *
246      * <p>The index will be created with no labels; the addLabels() function must be called
247      * after creation to add the desired labels to the index.
248      *
249      * <p>The index will work directly with the supplied collator. If the caller will need to
250      * continue working with the collator it should be cloned first, so that the
251      * collator provided to the AlphabeticIndex remains unchanged after creation of the index.
252      *
253      * @param collator The collator to use to order the contents of this index.
254      */
AlphabeticIndex(RuleBasedCollator collator)255     public AlphabeticIndex(RuleBasedCollator collator) {
256         this(null, collator);
257     }
258 
259     /**
260      * Internal constructor containing implementation used by public constructors.
261      */
AlphabeticIndex(ULocale locale, RuleBasedCollator collator)262     private AlphabeticIndex(ULocale locale, RuleBasedCollator collator) {
263         collatorOriginal = collator != null ? collator : (RuleBasedCollator) Collator.getInstance(locale);
264         try {
265             collatorPrimaryOnly = collatorOriginal.cloneAsThawed();
266         } catch (Exception e) {
267             // should never happen
268             throw new IllegalStateException("Collator cannot be cloned", e);
269         }
270         collatorPrimaryOnly.setStrength(Collator.PRIMARY);
271         collatorPrimaryOnly.freeze();
272 
273         firstCharsInScripts = getFirstCharactersInScripts();
274         Collections.sort(firstCharsInScripts, collatorPrimaryOnly);
275         // Guard against a degenerate collator where
276         // some script boundary strings are primary ignorable.
277         for (;;) {
278             if (firstCharsInScripts.isEmpty()) {
279                 throw new IllegalArgumentException(
280                         "AlphabeticIndex requires some non-ignorable script boundary strings");
281             }
282             if (collatorPrimaryOnly.compare(firstCharsInScripts.get(0), "") == 0) {
283                 firstCharsInScripts.remove(0);
284             } else {
285                 break;
286             }
287         }
288 
289         // Chinese index characters, which are specific to each of the several Chinese tailorings,
290         // take precedence over the single locale data exemplar set per language.
291         if (!addChineseIndexCharacters() && locale != null) {
292             addIndexExemplars(locale);
293         }
294     }
295 
296     /**
297      * Add more index characters (aside from what are in the locale)
298      * @param additions additional characters to add to the index, such as A-Z.
299      * @return this, for chaining
300      */
addLabels(UnicodeSet additions)301     public AlphabeticIndex<V> addLabels(UnicodeSet additions) {
302         initialLabels.addAll(additions);
303         buckets = null;
304         return this;
305     }
306 
307     /**
308      * Add more index characters (aside from what are in the locale)
309      * @param additions additional characters to add to the index, such as those in Swedish.
310      * @return this, for chaining
311      */
addLabels(ULocale... additions)312     public AlphabeticIndex<V> addLabels(ULocale... additions) {
313         for (ULocale addition : additions) {
314             addIndexExemplars(addition);
315         }
316         buckets = null;
317         return this;
318     }
319 
320     /**
321      * Add more index characters (aside from what are in the locale)
322      * @param additions additional characters to add to the index, such as those in Swedish.
323      * @return this, for chaining
324      */
addLabels(Locale... additions)325     public AlphabeticIndex<V> addLabels(Locale... additions) {
326         for (Locale addition : additions) {
327             addIndexExemplars(ULocale.forLocale(addition));
328         }
329         buckets = null;
330         return this;
331     }
332 
333     /**
334      * Set the overflow label
335      * @param overflowLabel see class description
336      * @return this, for chaining
337      */
setOverflowLabel(String overflowLabel)338     public AlphabeticIndex<V> setOverflowLabel(String overflowLabel) {
339         this.overflowLabel = overflowLabel;
340         buckets = null;
341         return this;
342     }
343 
344     /**
345      * Get the default label used in the IndexCharacters' locale for underflow, eg the last item in: X Y Z ...
346      *
347      * @return underflow label
348      */
getUnderflowLabel()349     public String getUnderflowLabel() {
350         return underflowLabel; // TODO get localized version
351     }
352 
353 
354     /**
355      * Set the underflowLabel label
356      * @param underflowLabel see class description
357      * @return this, for chaining
358      */
setUnderflowLabel(String underflowLabel)359     public AlphabeticIndex<V> setUnderflowLabel(String underflowLabel) {
360         this.underflowLabel = underflowLabel;
361         buckets = null;
362         return this;
363     }
364 
365     /**
366      * Get the default label used in the IndexCharacters' locale for overflow, eg the first item in: ... A B C
367      *
368      * @return overflow label
369      */
getOverflowLabel()370     public String getOverflowLabel() {
371         return overflowLabel; // TODO get localized version
372     }
373 
374 
375     /**
376      * Set the inflowLabel label
377      * @param inflowLabel see class description
378      * @return this, for chaining
379      */
setInflowLabel(String inflowLabel)380     public AlphabeticIndex<V> setInflowLabel(String inflowLabel) {
381         this.inflowLabel = inflowLabel;
382         buckets = null;
383         return this;
384     }
385 
386     /**
387      * Get the default label used for abbreviated buckets <i>between</i> other labels. For example, consider the labels
388      * for Latin and Greek are used: X Y Z ... &#x0391; &#x0392; &#x0393;.
389      *
390      * @return inflow label
391      */
getInflowLabel()392     public String getInflowLabel() {
393         return inflowLabel; // TODO get localized version
394     }
395 
396 
397     /**
398      * Get the limit on the number of labels in the index. The number of buckets can be slightly larger: see getBucketCount().
399      *
400      * @return maxLabelCount maximum number of labels.
401      */
getMaxLabelCount()402     public int getMaxLabelCount() {
403         return maxLabelCount;
404     }
405 
406     /**
407      * Set a limit on the number of labels in the index. The number of buckets can be slightly larger: see
408      * getBucketCount().
409      *
410      * @param maxLabelCount Set the maximum number of labels. Currently, if the number is exceeded, then every
411      *         nth item is removed to bring the count down. A more sophisticated mechanism may be available in the
412      *         future.
413      * @return this, for chaining
414      */
setMaxLabelCount(int maxLabelCount)415     public AlphabeticIndex<V> setMaxLabelCount(int maxLabelCount) {
416         this.maxLabelCount = maxLabelCount;
417         buckets = null;
418         return this;
419     }
420 
421     /**
422      * Determine the best labels to use. This is based on the exemplars, but we also process to make sure that they are unique,
423      * and sort differently, and that the overall list is small enough.
424      */
initLabels()425     private List<String> initLabels() {
426         Normalizer2 nfkdNormalizer = Normalizer2.getNFKDInstance();
427         List<String> indexCharacters = new ArrayList<String>();
428 
429         String firstScriptBoundary = firstCharsInScripts.get(0);
430         String overflowBoundary = firstCharsInScripts.get(firstCharsInScripts.size() - 1);
431 
432         // We make a sorted array of elements.
433         // Some of the input may be redundant.
434         // That is, we might have c, ch, d, where "ch" sorts just like "c", "h".
435         // We filter out those cases.
436         for (String item : initialLabels) {
437             boolean checkDistinct;
438             if (!UTF16.hasMoreCodePointsThan(item, 1)) {
439                 checkDistinct = false;
440             } else if(item.charAt(item.length() - 1) == '*' &&
441                     item.charAt(item.length() - 2) != '*') {
442                 // Use a label if it is marked with one trailing star,
443                 // even if the label string sorts the same when all contractions are suppressed.
444                 item = item.substring(0, item.length() - 1);
445                 checkDistinct = false;
446             } else {
447                 checkDistinct = true;
448             }
449             if (collatorPrimaryOnly.compare(item, firstScriptBoundary) < 0) {
450                 // Ignore a primary-ignorable or non-alphabetic index character.
451             } else if (collatorPrimaryOnly.compare(item, overflowBoundary) >= 0) {
452                 // Ignore an index character that will land in the overflow bucket.
453             } else if (checkDistinct && collatorPrimaryOnly.compare(item, separated(item)) == 0) {
454                 // Ignore a multi-code point index character that does not sort distinctly
455                 // from the sequence of its separate characters.
456             } else {
457                 int insertionPoint = Collections.binarySearch(indexCharacters, item, collatorPrimaryOnly);
458                 if (insertionPoint < 0) {
459                     indexCharacters.add(~insertionPoint, item);
460                 } else {
461                     String itemAlreadyIn = indexCharacters.get(insertionPoint);
462                     if (isOneLabelBetterThanOther(nfkdNormalizer, item, itemAlreadyIn)) {
463                         indexCharacters.set(insertionPoint, item);
464                     }
465                 }
466             }
467         }
468 
469         // if the result is still too large, cut down to maxLabelCount elements, by removing every nth element
470 
471         final int size = indexCharacters.size() - 1;
472         if (size > maxLabelCount) {
473             int count = 0;
474             int old = -1;
475             for (Iterator<String> it = indexCharacters.iterator(); it.hasNext();) {
476                 ++count;
477                 it.next();
478                 final int bump = count * maxLabelCount / size;
479                 if (bump == old) {
480                     it.remove();
481                 } else {
482                     old = bump;
483                 }
484             }
485         }
486 
487         return indexCharacters;
488     }
489 
fixLabel(String current)490     private static String fixLabel(String current) {
491         if (!current.startsWith(BASE)) {
492             return current;
493         }
494         int rest = current.charAt(BASE.length());
495         if (0x2800 < rest && rest <= 0x28FF) { // stroke count
496             return (rest-0x2800) + "\u5283";
497         }
498         return current.substring(BASE.length());
499     }
500 
501     /**
502      * This method is called to get the index exemplars. Normally these come from the locale directly,
503      * but if they aren't available, we have to synthesize them.
504      */
addIndexExemplars(ULocale locale)505     private void addIndexExemplars(ULocale locale) {
506         UnicodeSet exemplars = LocaleData.getExemplarSet(locale, 0, LocaleData.ES_INDEX);
507         if (exemplars != null && !exemplars.isEmpty()) {
508             initialLabels.addAll(exemplars);
509             return;
510         }
511 
512         // The locale data did not include explicit Index characters.
513         // Synthesize a set of them from the locale's standard exemplar characters.
514         exemplars = LocaleData.getExemplarSet(locale, 0, LocaleData.ES_STANDARD);
515 
516         exemplars = exemplars.cloneAsThawed();
517         // question: should we add auxiliary exemplars?
518         if (exemplars.containsSome('a', 'z') || exemplars.isEmpty()) {
519             exemplars.addAll('a', 'z');
520         }
521         if (exemplars.containsSome(0xAC00, 0xD7A3)) {  // Hangul syllables
522             // cut down to small list
523             exemplars.remove(0xAC00, 0xD7A3).
524                 add(0xAC00).add(0xB098).add(0xB2E4).add(0xB77C).
525                 add(0xB9C8).add(0xBC14).add(0xC0AC).add(0xC544).
526                 add(0xC790).add(0xCC28).add(0xCE74).add(0xD0C0).
527                 add(0xD30C).add(0xD558);
528         }
529         if (exemplars.containsSome(0x1200, 0x137F)) {  // Ethiopic block
530             // cut down to small list
531             // make use of the fact that Ethiopic is allocated in 8's, where
532             // the base is 0 mod 8.
533             UnicodeSet ethiopic = new UnicodeSet("[ሀለሐመሠረሰሸቀቈቐቘበቨተቸኀኈነኘአከኰኸዀወዐዘዠየደዸጀገጐጘጠጨጰጸፀፈፐፘ]");
534             ethiopic.retainAll(exemplars);
535             exemplars.remove('ሀ', 0x137F).addAll(ethiopic);
536         }
537 
538         // Upper-case any that aren't already so.
539         //   (We only do this for synthesized index characters.)
540         for (String item : exemplars) {
541             initialLabels.add(UCharacter.toUpperCase(locale, item));
542         }
543     }
544 
545     /**
546      * Add Chinese index characters from the tailoring.
547      */
addChineseIndexCharacters()548     private boolean addChineseIndexCharacters() {
549         UnicodeSet contractions = new UnicodeSet();
550         try {
551             collatorPrimaryOnly.internalAddContractions(BASE.charAt(0), contractions);
552         } catch (Exception e) {
553             return false;
554         }
555         if (contractions.isEmpty()) { return false; }
556         initialLabels.addAll(contractions);
557         for (String s : contractions) {
558             assert(s.startsWith(BASE));
559             char c = s.charAt(s.length() - 1);
560             if (0x41 <= c && c <= 0x5A) {  // A-Z
561                 // There are Pinyin labels, add ASCII A-Z labels as well.
562                 initialLabels.add(0x41, 0x5A);  // A-Z
563                 break;
564             }
565         }
566         return true;
567     }
568 
569     /**
570      * Return the string with interspersed CGJs. Input must have more than 2 codepoints.
571      * <p>This is used to test whether contractions sort differently from their components.
572      */
separated(String item)573     private String separated(String item) {
574         StringBuilder result = new StringBuilder();
575         // add a CGJ except within surrogates
576         char last = item.charAt(0);
577         result.append(last);
578         for (int i = 1; i < item.length(); ++i) {
579             char ch = item.charAt(i);
580             if (!UCharacter.isHighSurrogate(last) || !UCharacter.isLowSurrogate(ch)) {
581                 result.append(CGJ);
582             }
583             result.append(ch);
584             last = ch;
585         }
586         return result.toString();
587     }
588 
589     /**
590      * Builds an immutable, thread-safe version of this instance, without data records.
591      *
592      * @return an immutable index instance
593      */
buildImmutableIndex()594     public ImmutableIndex<V> buildImmutableIndex() {
595         // The current AlphabeticIndex Java code never modifies the bucket list once built.
596         // If it contains no records, we can use it.
597         // addRecord() sets buckets=null rather than inserting the new record into it.
598         BucketList<V> immutableBucketList;
599         if (inputList != null && !inputList.isEmpty()) {
600             // We need a bucket list with no records.
601             immutableBucketList = createBucketList();
602         } else {
603             if (buckets == null) {
604                 buckets = createBucketList();
605             }
606             immutableBucketList = buckets;
607         }
608         return new ImmutableIndex<V>(immutableBucketList, collatorPrimaryOnly);
609     }
610 
611     /**
612      * Get the labels.
613      *
614      * @return The list of bucket labels, after processing.
615      */
getBucketLabels()616     public List<String> getBucketLabels() {
617         initBuckets();
618         ArrayList<String> result = new ArrayList<String>();
619         for (Bucket<V> bucket : buckets) {
620             result.add(bucket.getLabel());
621         }
622         return result;
623     }
624 
625     /**
626      * Get a clone of the collator used internally. Note that for performance reasons, the clone is only done once, and
627      * then stored. The next time it is accessed, the same instance is returned.
628      * <p>
629      * <b><i>Don't use this method across threads if you are changing the settings on the collator, at least not without
630      * synchronizing.</i></b>
631      *
632      * @return a clone of the collator used internally
633      */
getCollator()634     public RuleBasedCollator getCollator() {
635         if (collatorExternal == null) {
636             try {
637                 collatorExternal = (RuleBasedCollator) (collatorOriginal.clone());
638             } catch (Exception e) {
639                 // should never happen
640                 throw new IllegalStateException("Collator cannot be cloned", e);
641             }
642         }
643         return collatorExternal;
644     }
645 
646     /**
647      * Add a record (name and data) to the index. The name will be used to sort the items into buckets, and to sort
648      * within the bucket. Two records may have the same name. When they do, the sort order is according to the order added:
649      * the first added comes first.
650      *
651      * @param name
652      *            Name, such as a name
653      * @param data
654      *            Data, such as an address or link
655      * @return this, for chaining
656      */
addRecord(CharSequence name, V data)657     public AlphabeticIndex<V> addRecord(CharSequence name, V data) {
658         // TODO instead of invalidating, just add to unprocessed list.
659         buckets = null; // invalidate old bucketlist
660         if (inputList == null) {
661             inputList = new ArrayList<Record<V>>();
662         }
663         inputList.add(new Record<V>(name, data));
664         return this;
665     }
666 
667     /**
668      * Get the bucket number for the given name. This routine permits callers to implement their own bucket handling
669      * mechanisms, including client-server handling. For example, when a new name is created on the client, it can ask
670      * the server for the bucket for that name, and the sortkey (using getCollator). Once the client has that
671      * information, it can put the name into the right bucket, and sort it within that bucket, without having access to
672      * the index or collator.
673      * <p>
674      * Note that the bucket number (and sort key) are only valid for the settings of the current AlphabeticIndex; if
675      * those are changed, then the bucket number and sort key must be regenerated.
676      *
677      * @param name
678      *            Name, such as a name
679      * @return the bucket index for the name
680      */
getBucketIndex(CharSequence name)681     public int getBucketIndex(CharSequence name) {
682         initBuckets();
683         return buckets.getBucketIndex(name, collatorPrimaryOnly);
684     }
685 
686     /**
687      * Clear the index.
688      *
689      * @return this, for chaining
690      */
clearRecords()691     public AlphabeticIndex<V> clearRecords() {
692         if (inputList != null && !inputList.isEmpty()) {
693             inputList.clear();
694             buckets = null;
695         }
696         return this;
697     }
698 
699     /**
700      * Return the number of buckets in the index. This will be the same as the number of labels, plus buckets for the underflow, overflow, and inflow(s).
701      *
702      * @return number of buckets
703      */
getBucketCount()704     public int getBucketCount() {
705         initBuckets();
706         return buckets.getBucketCount();
707     }
708 
709     /**
710      * Return the number of records in the index: that is, the total number of distinct &lt;name,data&gt; pairs added with addRecord(...), over all the buckets.
711      *
712      * @return total number of records in buckets
713      */
getRecordCount()714     public int getRecordCount() {
715         return inputList != null ? inputList.size() : 0;
716     }
717 
718     /**
719      * Return an iterator over the buckets.
720      *
721      * @return iterator over buckets.
722      */
723     @Override
iterator()724     public Iterator<Bucket<V>> iterator() {
725         initBuckets();
726         return buckets.iterator();
727     }
728 
729     /**
730      * Creates an index, and buckets and sorts the list of records into the index.
731      */
initBuckets()732     private void initBuckets() {
733         if (buckets != null) {
734             return;
735         }
736         buckets = createBucketList();
737         if (inputList == null || inputList.isEmpty()) {
738             return;
739         }
740 
741         // Sort the records by name.
742         // Stable sort preserves input order of collation duplicates.
743         Collections.sort(inputList, recordComparator);
744 
745         // Now, we traverse all of the input, which is now sorted.
746         // If the item doesn't go in the current bucket, we find the next bucket that contains it.
747         // This makes the process order n*log(n), since we just sort the list and then do a linear process.
748         // However, if the user adds an item at a time and then gets the buckets, this isn't efficient, so
749         // we need to improve it for that case.
750 
751         Iterator<Bucket<V>> bucketIterator = buckets.fullIterator();
752         Bucket<V> currentBucket = bucketIterator.next();
753         Bucket<V> nextBucket;
754         String upperBoundary;
755         if (bucketIterator.hasNext()) {
756             nextBucket = bucketIterator.next();
757             upperBoundary = nextBucket.lowerBoundary;
758         } else {
759             nextBucket = null;
760             upperBoundary = null;
761         }
762         for (Record<V> r : inputList) {
763             // if the current bucket isn't the right one, find the one that is
764             // We have a special flag for the last bucket so that we don't look any further
765             while (upperBoundary != null &&
766                     collatorPrimaryOnly.compare(r.name, upperBoundary) >= 0) {
767                 currentBucket = nextBucket;
768                 // now reset the boundary that we compare against
769                 if (bucketIterator.hasNext()) {
770                     nextBucket = bucketIterator.next();
771                     upperBoundary = nextBucket.lowerBoundary;
772                 } else {
773                     upperBoundary = null;
774                 }
775             }
776             // now put the record into the bucket.
777             Bucket<V> bucket = currentBucket;
778             if (bucket.displayBucket != null) {
779                 bucket = bucket.displayBucket;
780             }
781             if (bucket.records == null) {
782                 bucket.records = new ArrayList<Record<V>>();
783             }
784             bucket.records.add(r);
785         }
786     }
787 
788     private int maxLabelCount = 99;
789 
790     /**
791      * Returns true if one index character string is "better" than the other.
792      * Shorter NFKD is better, and otherwise NFKD-binary-less-than is
793      * better, and otherwise binary-less-than is better.
794      */
isOneLabelBetterThanOther(Normalizer2 nfkdNormalizer, String one, String other)795     private static boolean isOneLabelBetterThanOther(Normalizer2 nfkdNormalizer, String one, String other) {
796         // This is called with primary-equal strings, but never with one.equals(other).
797         String n1 = nfkdNormalizer.normalize(one);
798         String n2 = nfkdNormalizer.normalize(other);
799         int result = n1.codePointCount(0, n1.length()) - n2.codePointCount(0, n2.length());
800         if (result != 0) {
801             return result < 0;
802         }
803         result = binaryCmp.compare(n1, n2);
804         if (result != 0) {
805             return result < 0;
806         }
807         return binaryCmp.compare(one, other) < 0;
808     }
809 
810     /**
811      * A (name, data) pair, to be sorted by name into one of the index buckets.
812      * The user data is not used by the index implementation.
813      */
814     public static class Record<V> {
815         private final CharSequence name;
816         private final V data;
817 
Record(CharSequence name, V data)818         private Record(CharSequence name, V data) {
819             this.name = name;
820             this.data = data;
821         }
822 
823         /**
824          * Get the name
825          *
826          * @return the name
827          */
getName()828         public CharSequence getName() {
829             return name;
830         }
831 
832         /**
833          * Get the data
834          *
835          * @return the data
836          */
getData()837         public V getData() {
838             return data;
839         }
840 
841         /**
842          * Standard toString()
843          */
844         @Override
toString()845         public String toString() {
846             return name + "=" + data;
847         }
848     }
849 
850     /**
851      * An index "bucket" with a label string and type.
852      * It is referenced by {@link AlphabeticIndex#getBucketIndex(CharSequence)}
853      * and {@link AlphabeticIndex.ImmutableIndex#getBucketIndex(CharSequence)},
854      * returned by {@link AlphabeticIndex.ImmutableIndex#getBucket(int)},
855      * and {@link AlphabeticIndex#addRecord(CharSequence, Object)} adds a record
856      * into a bucket according to the record's name.
857      *
858      * @param <V>
859      *            Data type
860      */
861     public static class Bucket<V> implements Iterable<Record<V>> {
862         private final String label;
863         private final String lowerBoundary;
864         private final LabelType labelType;
865         private Bucket<V> displayBucket;
866         private int displayIndex;
867         private List<Record<V>> records;
868 
869         /**
870          * Type of the label
871          */
872         public enum LabelType {
873             /**
874              * Normal
875              */
876             NORMAL,
877             /**
878              * Underflow (before the first)
879              */
880             UNDERFLOW,
881             /**
882              * Inflow (between scripts)
883              */
884             INFLOW,
885             /**
886              * Overflow (after the last)
887              */
888             OVERFLOW
889         }
890 
891         /**
892          * Set up the bucket.
893          *
894          * @param label
895          *            label for the bucket
896          * @param labelType
897          *            is an underflow, overflow, or inflow bucket
898          */
Bucket(String label, String lowerBoundary, LabelType labelType)899         private Bucket(String label, String lowerBoundary, LabelType labelType) {
900             this.label = label;
901             this.lowerBoundary = lowerBoundary;
902             this.labelType = labelType;
903         }
904 
905         /**
906          * Get the label
907          *
908          * @return label for the bucket
909          */
getLabel()910         public String getLabel() {
911             return label;
912         }
913 
914         /**
915          * Is a normal, underflow, overflow, or inflow bucket
916          *
917          * @return is an underflow, overflow, or inflow bucket
918          */
getLabelType()919         public LabelType getLabelType() {
920             return labelType;
921         }
922 
923         /**
924          * Get the number of records in the bucket.
925          *
926          * @return number of records in bucket
927          */
size()928         public int size() {
929             return records == null ? 0 : records.size();
930         }
931 
932         /**
933          * Iterator over the records in the bucket
934          */
935         @Override
iterator()936         public Iterator<Record<V>> iterator() {
937             if (records == null) {
938                 return Collections.<Record<V>>emptyList().iterator();
939             }
940             return records.iterator();
941         }
942 
943         /**
944          * Standard toString()
945          */
946         @Override
toString()947         public String toString() {
948             return "{" +
949             "labelType=" + labelType
950             + ", " +
951             "lowerBoundary=" + lowerBoundary
952             + ", " +
953             "label=" + label
954             + "}"
955             ;
956         }
957     }
958 
createBucketList()959     private BucketList<V> createBucketList() {
960         // Initialize indexCharacters.
961         List<String> indexCharacters = initLabels();
962 
963         // Variables for hasMultiplePrimaryWeights().
964         long variableTop;
965         if (collatorPrimaryOnly.isAlternateHandlingShifted()) {
966             variableTop = collatorPrimaryOnly.getVariableTop() & 0xffffffffL;
967         } else {
968             variableTop = 0;
969         }
970         boolean hasInvisibleBuckets = false;
971 
972         // Helper arrays for Chinese Pinyin collation.
973         @SuppressWarnings({ "rawtypes", "unchecked" })
974         Bucket<V>[] asciiBuckets = new Bucket[26];
975         @SuppressWarnings({ "rawtypes", "unchecked" })
976         Bucket<V>[] pinyinBuckets = new Bucket[26];
977         boolean hasPinyin = false;
978 
979         ArrayList<Bucket<V>> bucketList = new ArrayList<Bucket<V>>();
980 
981         // underflow bucket
982         bucketList.add(new Bucket<V>(getUnderflowLabel(), "", LabelType.UNDERFLOW));
983 
984         // fix up the list, adding underflow, additions, overflow
985         // Insert inflow labels as needed.
986         int scriptIndex = -1;
987         String scriptUpperBoundary = "";
988         for (String current : indexCharacters) {
989             if (collatorPrimaryOnly.compare(current, scriptUpperBoundary) >= 0) {
990                 // We crossed the script boundary into a new script.
991                 String inflowBoundary = scriptUpperBoundary;
992                 boolean skippedScript = false;
993                 for (;;) {
994                     scriptUpperBoundary = firstCharsInScripts.get(++scriptIndex);
995                     if (collatorPrimaryOnly.compare(current, scriptUpperBoundary) < 0) {
996                         break;
997                     }
998                     skippedScript = true;
999                 }
1000                 if (skippedScript && bucketList.size() > 1) {
1001                     // We are skipping one or more scripts,
1002                     // and we are not just getting out of the underflow label.
1003                     bucketList.add(new Bucket<V>(getInflowLabel(), inflowBoundary,
1004                             LabelType.INFLOW));
1005                 }
1006             }
1007             // Add a bucket with the current label.
1008             Bucket<V> bucket = new Bucket<V>(fixLabel(current), current, LabelType.NORMAL);
1009             bucketList.add(bucket);
1010             // Remember ASCII and Pinyin buckets for Pinyin redirects.
1011             char c;
1012             if (current.length() == 1 && 'A' <= (c = current.charAt(0)) && c <= 'Z') {
1013                 asciiBuckets[c - 'A'] = bucket;
1014             } else if (current.length() == BASE.length() + 1 && current.startsWith(BASE) &&
1015                     'A' <= (c = current.charAt(BASE.length())) && c <= 'Z') {
1016                 pinyinBuckets[c - 'A'] = bucket;
1017                 hasPinyin = true;
1018             }
1019             // Check for multiple primary weights.
1020             if (!current.startsWith(BASE) &&
1021                     hasMultiplePrimaryWeights(collatorPrimaryOnly, variableTop, current) &&
1022                     !current.endsWith("\uffff")) {
1023                 // "Æ" or "Sch" etc.
1024                 for (int i = bucketList.size() - 2;; --i) {
1025                     Bucket<V> singleBucket = bucketList.get(i);
1026                     if (singleBucket.labelType != LabelType.NORMAL) {
1027                         // There is no single-character bucket since the last
1028                         // underflow or inflow label.
1029                         break;
1030                     }
1031                     if (singleBucket.displayBucket == null &&
1032                             !hasMultiplePrimaryWeights(collatorPrimaryOnly, variableTop, singleBucket.lowerBoundary)) {
1033                         // Add an invisible bucket that redirects strings greater than the expansion
1034                         // to the previous single-character bucket.
1035                         // For example, after ... Q R S Sch we add Sch\uFFFF->S
1036                         // and after ... Q R S Sch Sch\uFFFF St we add St\uFFFF->S.
1037                         bucket = new Bucket<V>("", current + "\uFFFF", LabelType.NORMAL);
1038                         bucket.displayBucket = singleBucket;
1039                         bucketList.add(bucket);
1040                         hasInvisibleBuckets = true;
1041                         break;
1042                     }
1043                 }
1044             }
1045         }
1046         if (bucketList.size() == 1) {
1047             // No real labels, show only the underflow label.
1048             return new BucketList<V>(bucketList, bucketList);
1049         }
1050         // overflow bucket
1051         bucketList.add(new Bucket<V>(getOverflowLabel(), scriptUpperBoundary, LabelType.OVERFLOW)); // final
1052 
1053         if (hasPinyin) {
1054             // Redirect Pinyin buckets.
1055             Bucket<V> asciiBucket = null;
1056             for (int i = 0; i < 26; ++i) {
1057                 if (asciiBuckets[i] != null) {
1058                     asciiBucket = asciiBuckets[i];
1059                 }
1060                 if (pinyinBuckets[i] != null && asciiBucket != null) {
1061                     pinyinBuckets[i].displayBucket = asciiBucket;
1062                     hasInvisibleBuckets = true;
1063                 }
1064             }
1065         }
1066 
1067         if (!hasInvisibleBuckets) {
1068             return new BucketList<V>(bucketList, bucketList);
1069         }
1070         // Merge inflow buckets that are visually adjacent.
1071         // Iterate backwards: Merge inflow into overflow rather than the other way around.
1072         int i = bucketList.size() - 1;
1073         Bucket<V> nextBucket = bucketList.get(i);
1074         while (--i > 0) {
1075             Bucket<V> bucket = bucketList.get(i);
1076             if (bucket.displayBucket != null) {
1077                 continue;  // skip invisible buckets
1078             }
1079             if (bucket.labelType == LabelType.INFLOW) {
1080                 if (nextBucket.labelType != LabelType.NORMAL) {
1081                     bucket.displayBucket = nextBucket;
1082                     continue;
1083                 }
1084             }
1085             nextBucket = bucket;
1086         }
1087 
1088         ArrayList<Bucket<V>> publicBucketList = new ArrayList<Bucket<V>>();
1089         for (Bucket<V> bucket : bucketList) {
1090             if (bucket.displayBucket == null) {
1091                 publicBucketList.add(bucket);
1092             }
1093         }
1094         return new BucketList<V>(bucketList, publicBucketList);
1095     }
1096 
1097     private static class BucketList<V> implements Iterable<Bucket<V>> {
1098         private final ArrayList<Bucket<V>> bucketList;
1099         private final List<Bucket<V>> immutableVisibleList;
1100 
BucketList(ArrayList<Bucket<V>> bucketList, ArrayList<Bucket<V>> publicBucketList)1101         private BucketList(ArrayList<Bucket<V>> bucketList, ArrayList<Bucket<V>> publicBucketList) {
1102             this.bucketList = bucketList;
1103 
1104             int displayIndex = 0;
1105             for (Bucket<V> bucket : publicBucketList) {
1106                 bucket.displayIndex = displayIndex++;
1107             }
1108             immutableVisibleList = Collections.unmodifiableList(publicBucketList);
1109         }
1110 
getBucketCount()1111         private int getBucketCount() {
1112             return immutableVisibleList.size();
1113         }
1114 
getBucketIndex(CharSequence name, Collator collatorPrimaryOnly)1115         private int getBucketIndex(CharSequence name, Collator collatorPrimaryOnly) {
1116             // binary search
1117             int start = 0;
1118             int limit = bucketList.size();
1119             while ((start + 1) < limit) {
1120                 int i = (start + limit) / 2;
1121                 Bucket<V> bucket = bucketList.get(i);
1122                 int nameVsBucket = collatorPrimaryOnly.compare(name, bucket.lowerBoundary);
1123                 if (nameVsBucket < 0) {
1124                     limit = i;
1125                 } else {
1126                     start = i;
1127                 }
1128             }
1129             Bucket<V> bucket = bucketList.get(start);
1130             if (bucket.displayBucket != null) {
1131                 bucket = bucket.displayBucket;
1132             }
1133             return bucket.displayIndex;
1134         }
1135 
1136         /**
1137          * Private iterator over all the buckets, visible and invisible
1138          */
fullIterator()1139         private Iterator<Bucket<V>> fullIterator() {
1140             return bucketList.iterator();
1141         }
1142 
1143         /**
1144          * Iterator over just the visible buckets.
1145          */
1146         @Override
iterator()1147         public Iterator<Bucket<V>> iterator() {
1148             return immutableVisibleList.iterator(); // use immutable list to prevent remove().
1149         }
1150     }
1151 
hasMultiplePrimaryWeights( RuleBasedCollator coll, long variableTop, String s)1152     private static boolean hasMultiplePrimaryWeights(
1153             RuleBasedCollator coll, long variableTop, String s) {
1154         long[] ces = coll.internalGetCEs(s);
1155         boolean seenPrimary = false;
1156         for (int i = 0; i < ces.length; ++i) {
1157             long ce = ces[i];
1158             long p = ce >>> 32;
1159             if (p > variableTop) {
1160                 // not primary ignorable
1161                 if (seenPrimary) {
1162                     return true;
1163                 }
1164                 seenPrimary = true;
1165             }
1166         }
1167         return false;
1168     }
1169 
1170     // TODO: Surely we have at least a ticket for porting these mask values to UCharacter.java?!
1171     private static final int GC_LU_MASK = 1 << UCharacter.UPPERCASE_LETTER;
1172     private static final int GC_LL_MASK = 1 << UCharacter.LOWERCASE_LETTER;
1173     private static final int GC_LT_MASK = 1 << UCharacter.TITLECASE_LETTER;
1174     private static final int GC_LM_MASK = 1 << UCharacter.MODIFIER_LETTER;
1175     private static final int GC_LO_MASK = 1 << UCharacter.OTHER_LETTER;
1176     private static final int GC_L_MASK =
1177             GC_LU_MASK|GC_LL_MASK|GC_LT_MASK|GC_LM_MASK|GC_LO_MASK;
1178     private static final int GC_CN_MASK = 1 << UCharacter.GENERAL_OTHER_TYPES;
1179 
1180     /**
1181      * Return a list of the first character in each script. Only exposed for testing.
1182      *
1183      * @return list of first characters in each script
1184      * @deprecated This API is ICU internal, only for testing.
1185      * @hide original deprecated declaration
1186      * @hide draft / provisional / internal are hidden on Android
1187      */
1188     @Deprecated
getFirstCharactersInScripts()1189     public List<String> getFirstCharactersInScripts() {
1190         List<String> dest = new ArrayList<String>(200);
1191         // Fetch the script-first-primary contractions which are defined in the root collator.
1192         // They all start with U+FDD1.
1193         UnicodeSet set = new UnicodeSet();
1194         collatorPrimaryOnly.internalAddContractions(0xFDD1, set);
1195         if (set.isEmpty()) {
1196             throw new UnsupportedOperationException(
1197                     "AlphabeticIndex requires script-first-primary contractions");
1198         }
1199         for (String boundary : set) {
1200             int gcMask = 1 << UCharacter.getType(boundary.codePointAt(1));
1201             if ((gcMask & (GC_L_MASK | GC_CN_MASK)) == 0) {
1202                 // Ignore boundaries for the special reordering groups.
1203                 // Take only those for "real scripts" (where the sample character is a Letter,
1204                 // and the one for unassigned implicit weights (Cn).
1205                 continue;
1206             }
1207             dest.add(boundary);
1208         }
1209         return dest;
1210     }
1211 }
1212