1 // © 2016 and later: Unicode, Inc. and others.
2 // License & terms of use: http://www.unicode.org/copyright.html#License
3 /*
4  *******************************************************************************
5  * Copyright (C) 2012-2016, Google, International Business Machines Corporation and
6  * others. All Rights Reserved.
7  *******************************************************************************
8  */
9 package com.ibm.icu.text;
10 
11 import java.io.IOException;
12 import java.util.ArrayList;
13 import java.util.Arrays;
14 import java.util.Collection;
15 import java.util.Iterator;
16 import java.util.Locale;
17 
18 import com.ibm.icu.impl.ICUCache;
19 import com.ibm.icu.impl.ICUData;
20 import com.ibm.icu.impl.ICUResourceBundle;
21 import com.ibm.icu.impl.SimpleCache;
22 import com.ibm.icu.impl.SimpleFormatterImpl;
23 import com.ibm.icu.util.ICUUncheckedIOException;
24 import com.ibm.icu.util.ULocale;
25 import com.ibm.icu.util.UResourceBundle;
26 
27 /**
28  * Immutable class for formatting a list, using data from CLDR (or supplied
29  * separately). The class is not subclassable.
30  *
31  * @author Mark Davis
32  * @stable ICU 50
33  */
34 final public class ListFormatter {
35     // Compiled SimpleFormatter patterns.
36     private final String two;
37     private final String start;
38     private final String middle;
39     private final String end;
40     private final ULocale locale;
41 
42     /**
43      * Indicates the style of Listformatter
44      * @internal
45      * @deprecated This API is ICU internal only.
46      */
47     @Deprecated
48     public enum Style {
49         /**
50          * Standard style.
51          * @internal
52          * @deprecated This API is ICU internal only.
53          */
54         @Deprecated
55         STANDARD("standard"),
56         /**
57          * Style for full durations
58          * @internal
59          * @deprecated This API is ICU internal only.
60          */
61         @Deprecated
62         DURATION("unit"),
63         /**
64          * Style for durations in abbrevated form
65          * @internal
66          * @deprecated This API is ICU internal only.
67          */
68         @Deprecated
69         DURATION_SHORT("unit-short"),
70         /**
71          * Style for durations in narrow form
72          * @internal
73          * @deprecated This API is ICU internal only.
74          */
75         @Deprecated
76         DURATION_NARROW("unit-narrow");
77 
78         private final String name;
79 
Style(String name)80         Style(String name) {
81             this.name = name;
82         }
83         /**
84          * @internal
85          * @deprecated This API is ICU internal only.
86          */
87         @Deprecated
getName()88         public String getName() {
89             return name;
90         }
91 
92     }
93 
94     /**
95      * <b>Internal:</b> Create a ListFormatter from component strings,
96      * with definitions as in LDML.
97      *
98      * @param two
99      *            string for two items, containing {0} for the first, and {1}
100      *            for the second.
101      * @param start
102      *            string for the start of a list items, containing {0} for the
103      *            first, and {1} for the rest.
104      * @param middle
105      *            string for the start of a list items, containing {0} for the
106      *            first part of the list, and {1} for the rest of the list.
107      * @param end
108      *            string for the end of a list items, containing {0} for the
109      *            first part of the list, and {1} for the last item.
110      * @internal
111      * @deprecated This API is ICU internal only.
112      */
113     @Deprecated
ListFormatter(String two, String start, String middle, String end)114     public ListFormatter(String two, String start, String middle, String end) {
115         this(
116                 compilePattern(two, new StringBuilder()),
117                 compilePattern(start, new StringBuilder()),
118                 compilePattern(middle, new StringBuilder()),
119                 compilePattern(end, new StringBuilder()),
120                 null);
121     }
122 
ListFormatter(String two, String start, String middle, String end, ULocale locale)123     private ListFormatter(String two, String start, String middle, String end, ULocale locale) {
124         this.two = two;
125         this.start = start;
126         this.middle = middle;
127         this.end = end;
128         this.locale = locale;
129     }
130 
compilePattern(String pattern, StringBuilder sb)131     private static String compilePattern(String pattern, StringBuilder sb) {
132         return SimpleFormatterImpl.compileToStringMinMaxArguments(pattern, sb, 2, 2);
133     }
134 
135     /**
136      * Create a list formatter that is appropriate for a locale.
137      *
138      * @param locale
139      *            the locale in question.
140      * @return ListFormatter
141      * @stable ICU 50
142      */
getInstance(ULocale locale)143     public static ListFormatter getInstance(ULocale locale) {
144       return getInstance(locale, Style.STANDARD);
145     }
146 
147     /**
148      * Create a list formatter that is appropriate for a locale.
149      *
150      * @param locale
151      *            the locale in question.
152      * @return ListFormatter
153      * @stable ICU 50
154      */
getInstance(Locale locale)155     public static ListFormatter getInstance(Locale locale) {
156         return getInstance(ULocale.forLocale(locale), Style.STANDARD);
157     }
158 
159     /**
160      * Create a list formatter that is appropriate for a locale and style.
161      *
162      * @param locale the locale in question.
163      * @param style the style
164      * @return ListFormatter
165      * @internal
166      * @deprecated This API is ICU internal only.
167      */
168     @Deprecated
getInstance(ULocale locale, Style style)169     public static ListFormatter getInstance(ULocale locale, Style style) {
170         return cache.get(locale, style.getName());
171     }
172 
173     /**
174      * Create a list formatter that is appropriate for the default FORMAT locale.
175      *
176      * @return ListFormatter
177      * @stable ICU 50
178      */
getInstance()179     public static ListFormatter getInstance() {
180         return getInstance(ULocale.getDefault(ULocale.Category.FORMAT));
181     }
182 
183     /**
184      * Format a list of objects.
185      *
186      * @param items
187      *            items to format. The toString() method is called on each.
188      * @return items formatted into a string
189      * @stable ICU 50
190      */
format(Object... items)191     public String format(Object... items) {
192         return format(Arrays.asList(items));
193     }
194 
195     /**
196      * Format a collection of objects. The toString() method is called on each.
197      *
198      * @param items
199      *            items to format. The toString() method is called on each.
200      * @return items formatted into a string
201      * @stable ICU 50
202      */
format(Collection<?> items)203     public String format(Collection<?> items) {
204         return format(items, -1).toString();
205     }
206 
207     // Formats a collection of objects and returns the formatted string plus the offset
208     // in the string where the index th element appears. index is zero based. If index is
209     // negative or greater than or equal to the size of items then this function returns -1 for
210     // the offset.
format(Collection<?> items, int index)211     FormattedListBuilder format(Collection<?> items, int index) {
212         Iterator<?> it = items.iterator();
213         int count = items.size();
214         switch (count) {
215         case 0:
216             return new FormattedListBuilder("", false);
217         case 1:
218             return new FormattedListBuilder(it.next(), index == 0);
219         case 2:
220             return new FormattedListBuilder(it.next(), index == 0).append(two, it.next(), index == 1);
221         }
222         FormattedListBuilder builder = new FormattedListBuilder(it.next(), index == 0);
223         builder.append(start, it.next(), index == 1);
224         for (int idx = 2; idx < count - 1; ++idx) {
225             builder.append(middle, it.next(), index == idx);
226         }
227         return builder.append(end, it.next(), index == count - 1);
228     }
229 
230     /**
231      * Returns the pattern to use for a particular item count.
232      * @param count the item count.
233      * @return the pattern with {0}, {1}, {2}, etc. For English,
234      * getPatternForNumItems(3) == "{0}, {1}, and {2}"
235      * @throws IllegalArgumentException when count is 0 or negative.
236      * @stable ICU 52
237      */
getPatternForNumItems(int count)238     public String getPatternForNumItems(int count) {
239         if (count <= 0) {
240             throw new IllegalArgumentException("count must be > 0");
241         }
242         ArrayList<String> list = new ArrayList<String>();
243         for (int i = 0; i < count; i++) {
244             list.add(String.format("{%d}", i));
245         }
246         return format(list);
247     }
248 
249     /**
250      * Returns the locale of this object.
251      * @internal
252      * @deprecated This API is ICU internal only.
253      */
254     @Deprecated
getLocale()255     public ULocale getLocale() {
256         return locale;
257     }
258 
259     // Builds a formatted list
260     static class FormattedListBuilder {
261         private StringBuilder current;
262         private int offset;
263 
264         // Start is the first object in the list; If recordOffset is true, records the offset of
265         // this first object.
FormattedListBuilder(Object start, boolean recordOffset)266         public FormattedListBuilder(Object start, boolean recordOffset) {
267             this.current = new StringBuilder(start.toString());
268             this.offset = recordOffset ? 0 : -1;
269         }
270 
271         // Appends additional object. pattern is a template indicating where the new object gets
272         // added in relation to the rest of the list. {0} represents the rest of the list; {1}
273         // represents the new object in pattern. next is the object to be added. If recordOffset
274         // is true, records the offset of next in the formatted string.
append(String pattern, Object next, boolean recordOffset)275         public FormattedListBuilder append(String pattern, Object next, boolean recordOffset) {
276             int[] offsets = (recordOffset || offsetRecorded()) ? new int[2] : null;
277             SimpleFormatterImpl.formatAndReplace(
278                     pattern, current, offsets, current, next.toString());
279             if (offsets != null) {
280                 if (offsets[0] == -1 || offsets[1] == -1) {
281                     throw new IllegalArgumentException(
282                             "{0} or {1} missing from pattern " + pattern);
283                 }
284                 if (recordOffset) {
285                     offset = offsets[1];
286                 } else {
287                     offset += offsets[0];
288                 }
289             }
290             return this;
291         }
292 
appendTo(Appendable appendable)293         public void appendTo(Appendable appendable) {
294             try {
295                 appendable.append(current);
296             } catch(IOException e) {
297                 throw new ICUUncheckedIOException(e);
298             }
299         }
300 
301         @Override
toString()302         public String toString() {
303             return current.toString();
304         }
305 
306         // Gets the last recorded offset or -1 if no offset recorded.
getOffset()307         public int getOffset() {
308             return offset;
309         }
310 
offsetRecorded()311         private boolean offsetRecorded() {
312             return offset >= 0;
313         }
314     }
315 
316     private static class Cache {
317         private final ICUCache<String, ListFormatter> cache =
318             new SimpleCache<String, ListFormatter>();
319 
get(ULocale locale, String style)320         public ListFormatter get(ULocale locale, String style) {
321             String key = String.format("%s:%s", locale.toString(), style);
322             ListFormatter result = cache.get(key);
323             if (result == null) {
324                 result = load(locale, style);
325                 cache.put(key, result);
326             }
327             return result;
328         }
329 
load(ULocale ulocale, String style)330         private static ListFormatter load(ULocale ulocale, String style) {
331             ICUResourceBundle r = (ICUResourceBundle)UResourceBundle.
332                     getBundleInstance(ICUData.ICU_BASE_NAME, ulocale);
333             StringBuilder sb = new StringBuilder();
334             return new ListFormatter(
335                 compilePattern(r.getWithFallback("listPattern/" + style + "/2").getString(), sb),
336                 compilePattern(r.getWithFallback("listPattern/" + style + "/start").getString(), sb),
337                 compilePattern(r.getWithFallback("listPattern/" + style + "/middle").getString(), sb),
338                 compilePattern(r.getWithFallback("listPattern/" + style + "/end").getString(), sb),
339                 ulocale);
340         }
341     }
342 
343     static Cache cache = new Cache();
344 }
345