xref: /aosp_15_r20/external/threetenbp/src/main/java/org/threeten/bp/format/SimpleDateTimeTextProvider.java (revision 761b3f507e07ae42b4ad4333aa5dc559de53e1fb)
1 /*
2  * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  *  * Redistributions of source code must retain the above copyright notice,
10  *    this list of conditions and the following disclaimer.
11  *
12  *  * Redistributions in binary form must reproduce the above copyright notice,
13  *    this list of conditions and the following disclaimer in the documentation
14  *    and/or other materials provided with the distribution.
15  *
16  *  * Neither the name of JSR-310 nor the names of its contributors
17  *    may be used to endorse or promote products derived from this software
18  *    without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 package org.threeten.bp.format;
33 
34 import static org.threeten.bp.temporal.ChronoField.AMPM_OF_DAY;
35 import static org.threeten.bp.temporal.ChronoField.DAY_OF_WEEK;
36 import static org.threeten.bp.temporal.ChronoField.ERA;
37 import static org.threeten.bp.temporal.ChronoField.MONTH_OF_YEAR;
38 
39 import java.text.DateFormatSymbols;
40 import java.util.AbstractMap.SimpleImmutableEntry;
41 import java.util.ArrayList;
42 import java.util.Calendar;
43 import java.util.Collections;
44 import java.util.Comparator;
45 import java.util.GregorianCalendar;
46 import java.util.HashMap;
47 import java.util.Iterator;
48 import java.util.List;
49 import java.util.Locale;
50 import java.util.Map;
51 import java.util.Map.Entry;
52 import java.util.concurrent.ConcurrentHashMap;
53 import java.util.concurrent.ConcurrentMap;
54 
55 import org.threeten.bp.temporal.IsoFields;
56 import org.threeten.bp.temporal.TemporalField;
57 
58 /**
59  * The Service Provider Implementation to obtain date-time text for a field.
60  * <p>
61  * This implementation is based on extraction of data from a {@link DateFormatSymbols}.
62  *
63  * <h3>Specification for implementors</h3>
64  * This class is immutable and thread-safe.
65  */
66 final class SimpleDateTimeTextProvider extends DateTimeTextProvider {
67      // TODO: Better implementation based on CLDR
68 
69     /** Comparator. */
70     private static final Comparator<Entry<String, Long>> COMPARATOR = new Comparator<Entry<String, Long>>() {
71         @Override
72         public int compare(Entry<String, Long> obj1, Entry<String, Long> obj2) {
73             return obj2.getKey().length() - obj1.getKey().length();  // longest to shortest
74         }
75     };
76 
77     /** Cache. */
78     private final ConcurrentMap<Entry<TemporalField, Locale>, Object> cache =
79             new ConcurrentHashMap<Entry<TemporalField, Locale>, Object>(16, 0.75f, 2);
80 
81     //-----------------------------------------------------------------------
82     @Override
getText(TemporalField field, long value, TextStyle style, Locale locale)83     public String getText(TemporalField field, long value, TextStyle style, Locale locale) {
84         Object store = findStore(field, locale);
85         if (store instanceof LocaleStore) {
86             return ((LocaleStore) store).getText(value, style);
87         }
88         return null;
89     }
90 
91     @Override
getTextIterator(TemporalField field, TextStyle style, Locale locale)92     public Iterator<Entry<String, Long>> getTextIterator(TemporalField field, TextStyle style, Locale locale) {
93         Object store = findStore(field, locale);
94         if (store instanceof LocaleStore) {
95             return ((LocaleStore) store).getTextIterator(style);
96         }
97         return null;
98     }
99 
100     //-----------------------------------------------------------------------
findStore(TemporalField field, Locale locale)101     private Object findStore(TemporalField field, Locale locale) {
102         Entry<TemporalField, Locale> key = createEntry(field, locale);
103         Object store = cache.get(key);
104         if (store == null) {
105             store = createStore(field, locale);
106             cache.putIfAbsent(key, store);
107             store = cache.get(key);
108         }
109         return store;
110     }
111 
createStore(TemporalField field, Locale locale)112     private Object createStore(TemporalField field, Locale locale) {
113         if (field == MONTH_OF_YEAR) {
114             DateFormatSymbols oldSymbols = DateFormatSymbols.getInstance(locale);
115             Map<TextStyle, Map<Long, String>> styleMap = new HashMap<TextStyle, Map<Long,String>>();
116             Long f1 = 1L;
117             Long f2 = 2L;
118             Long f3 = 3L;
119             Long f4 = 4L;
120             Long f5 = 5L;
121             Long f6 = 6L;
122             Long f7 = 7L;
123             Long f8 = 8L;
124             Long f9 = 9L;
125             Long f10 = 10L;
126             Long f11 = 11L;
127             Long f12 = 12L;
128             String[] array = oldSymbols.getMonths();
129             Map<Long, String> map = new HashMap<Long, String>();
130             map.put(f1, array[Calendar.JANUARY]);
131             map.put(f2, array[Calendar.FEBRUARY]);
132             map.put(f3, array[Calendar.MARCH]);
133             map.put(f4, array[Calendar.APRIL]);
134             map.put(f5, array[Calendar.MAY]);
135             map.put(f6, array[Calendar.JUNE]);
136             map.put(f7, array[Calendar.JULY]);
137             map.put(f8, array[Calendar.AUGUST]);
138             map.put(f9, array[Calendar.SEPTEMBER]);
139             map.put(f10, array[Calendar.OCTOBER]);
140             map.put(f11, array[Calendar.NOVEMBER]);
141             map.put(f12, array[Calendar.DECEMBER]);
142             styleMap.put(TextStyle.FULL, map);
143 
144             map = new HashMap<Long, String>();
145             map.put(f1, narrowMonth(1, array[Calendar.JANUARY], locale));
146             map.put(f2, narrowMonth(2, array[Calendar.FEBRUARY], locale));
147             map.put(f3, narrowMonth(3, array[Calendar.MARCH], locale));
148             map.put(f4, narrowMonth(4, array[Calendar.APRIL], locale));
149             map.put(f5, narrowMonth(5, array[Calendar.MAY], locale));
150             map.put(f6, narrowMonth(6, array[Calendar.JUNE], locale));
151             map.put(f7, narrowMonth(7, array[Calendar.JULY], locale));
152             map.put(f8, narrowMonth(8, array[Calendar.AUGUST], locale));
153             map.put(f9, narrowMonth(9, array[Calendar.SEPTEMBER], locale));
154             map.put(f10, narrowMonth(10, array[Calendar.OCTOBER], locale));
155             map.put(f11, narrowMonth(11, array[Calendar.NOVEMBER], locale));
156             map.put(f12, narrowMonth(12, array[Calendar.DECEMBER], locale));
157             styleMap.put(TextStyle.NARROW, map);
158 
159             array = oldSymbols.getShortMonths();
160             map = new HashMap<Long, String>();
161             map.put(f1, array[Calendar.JANUARY]);
162             map.put(f2, array[Calendar.FEBRUARY]);
163             map.put(f3, array[Calendar.MARCH]);
164             map.put(f4, array[Calendar.APRIL]);
165             map.put(f5, array[Calendar.MAY]);
166             map.put(f6, array[Calendar.JUNE]);
167             map.put(f7, array[Calendar.JULY]);
168             map.put(f8, array[Calendar.AUGUST]);
169             map.put(f9, array[Calendar.SEPTEMBER]);
170             map.put(f10, array[Calendar.OCTOBER]);
171             map.put(f11, array[Calendar.NOVEMBER]);
172             map.put(f12, array[Calendar.DECEMBER]);
173             styleMap.put(TextStyle.SHORT, map);
174             return createLocaleStore(styleMap);
175         }
176         if (field == DAY_OF_WEEK) {
177             DateFormatSymbols oldSymbols = DateFormatSymbols.getInstance(locale);
178             Map<TextStyle, Map<Long, String>> styleMap = new HashMap<TextStyle, Map<Long,String>>();
179             Long f1 = 1L;
180             Long f2 = 2L;
181             Long f3 = 3L;
182             Long f4 = 4L;
183             Long f5 = 5L;
184             Long f6 = 6L;
185             Long f7 = 7L;
186             String[] array = oldSymbols.getWeekdays();
187             Map<Long, String> map = new HashMap<Long, String>();
188             map.put(f1, array[Calendar.MONDAY]);
189             map.put(f2, array[Calendar.TUESDAY]);
190             map.put(f3, array[Calendar.WEDNESDAY]);
191             map.put(f4, array[Calendar.THURSDAY]);
192             map.put(f5, array[Calendar.FRIDAY]);
193             map.put(f6, array[Calendar.SATURDAY]);
194             map.put(f7, array[Calendar.SUNDAY]);
195             styleMap.put(TextStyle.FULL, map);
196 
197             map = new HashMap<Long, String>();
198             map.put(f1, narrowDayOfWeek(1, array[Calendar.MONDAY], locale));
199             map.put(f2, narrowDayOfWeek(2, array[Calendar.TUESDAY], locale));
200             map.put(f3, narrowDayOfWeek(3, array[Calendar.WEDNESDAY], locale));
201             map.put(f4, narrowDayOfWeek(4, array[Calendar.THURSDAY], locale));
202             map.put(f5, narrowDayOfWeek(5, array[Calendar.FRIDAY], locale));
203             map.put(f6, narrowDayOfWeek(6, array[Calendar.SATURDAY], locale));
204             map.put(f7, narrowDayOfWeek(7, array[Calendar.SUNDAY], locale));
205             styleMap.put(TextStyle.NARROW, map);
206 
207             array = oldSymbols.getShortWeekdays();
208             map = new HashMap<Long, String>();
209             map.put(f1, array[Calendar.MONDAY]);
210             map.put(f2, array[Calendar.TUESDAY]);
211             map.put(f3, array[Calendar.WEDNESDAY]);
212             map.put(f4, array[Calendar.THURSDAY]);
213             map.put(f5, array[Calendar.FRIDAY]);
214             map.put(f6, array[Calendar.SATURDAY]);
215             map.put(f7, array[Calendar.SUNDAY]);
216             styleMap.put(TextStyle.SHORT, map);
217             return createLocaleStore(styleMap);
218         }
219         if (field == AMPM_OF_DAY) {
220             DateFormatSymbols oldSymbols = DateFormatSymbols.getInstance(locale);
221             Map<TextStyle, Map<Long, String>> styleMap = new HashMap<TextStyle, Map<Long,String>>();
222             String[] array = oldSymbols.getAmPmStrings();
223             Map<Long, String> map = new HashMap<Long, String>();
224             map.put(0L, array[Calendar.AM]);
225             map.put(1L, array[Calendar.PM]);
226             styleMap.put(TextStyle.FULL, map);
227             styleMap.put(TextStyle.SHORT, map);  // re-use, as we don't have different data
228             return createLocaleStore(styleMap);
229         }
230         if (field == ERA) {
231             DateFormatSymbols oldSymbols = DateFormatSymbols.getInstance(locale);
232             Map<TextStyle, Map<Long, String>> styleMap = new HashMap<TextStyle, Map<Long,String>>();
233             String[] array = oldSymbols.getEras();
234             Map<Long, String> map = new HashMap<Long, String>();
235             map.put(0L, array[GregorianCalendar.BC]);
236             map.put(1L, array[GregorianCalendar.AD]);
237             styleMap.put(TextStyle.SHORT, map);
238             if (locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
239                 map = new HashMap<Long, String>();
240                 map.put(0L, "Before Christ");
241                 map.put(1L, "Anno Domini");
242                 styleMap.put(TextStyle.FULL, map);
243             } else {
244                 // re-use, as we don't have different data
245                 styleMap.put(TextStyle.FULL, map);
246             }
247             map = new HashMap<Long, String>();
248             map.put(0L, array[GregorianCalendar.BC].substring(0, 1));
249             map.put(1L, array[GregorianCalendar.AD].substring(0, 1));
250             styleMap.put(TextStyle.NARROW, map);
251             return createLocaleStore(styleMap);
252         }
253         // hard code English quarter text
254         if (field == IsoFields.QUARTER_OF_YEAR) {
255             Map<TextStyle, Map<Long, String>> styleMap = new HashMap<TextStyle, Map<Long,String>>();
256             Map<Long, String> map = new HashMap<Long, String>();
257             map.put(1L, "Q1");
258             map.put(2L, "Q2");
259             map.put(3L, "Q3");
260             map.put(4L, "Q4");
261             styleMap.put(TextStyle.SHORT, map);
262             map = new HashMap<Long, String>();
263             map.put(1L, "1st quarter");
264             map.put(2L, "2nd quarter");
265             map.put(3L, "3rd quarter");
266             map.put(4L, "4th quarter");
267             styleMap.put(TextStyle.FULL, map);
268             return createLocaleStore(styleMap);
269         }
270         return "";  // null marker for map
271     }
272 
273     // for China/Japan we need special behaviour
narrowMonth(int month, String text, Locale locale)274     private String narrowMonth(int month, String text, Locale locale) {
275         if (locale.getLanguage().equals("zh") && locale.getCountry().equals("CN")) {
276             switch (month) {
277                 case 1:
278                     return "\u4e00";
279                 case 2:
280                     return "\u4e8c";
281                 case 3:
282                     return "\u4e09";
283                 case 4:
284                     return "\u56db";
285                 case 5:
286                     return "\u4e94";
287                 case 6:
288                     return "\u516d";
289                 case 7:
290                     return "\u4e03";
291                 case 8:
292                     return "\u516b";
293                 case 9:
294                     return "\u4e5d";
295                 case 10:
296                     return "\u5341";
297                 case 11:
298                     return "\u5341\u4e00";
299                 case 12:
300                     return "\u5341\u4e8c";
301             }
302         }
303         if (locale.getLanguage().equals("ar")) {
304             switch (month) {
305                 case 1:
306                     return "\u064a";
307                 case 2:
308                     return "\u0641";
309                 case 3:
310                     return "\u0645";
311                 case 4:
312                     return "\u0623";
313                 case 5:
314                     return "\u0648";
315                 case 6:
316                     return "\u0646";
317                 case 7:
318                     return "\u0644";
319                 case 8:
320                     return "\u063a";
321                 case 9:
322                     return "\u0633";
323                 case 10:
324                     return "\u0643";
325                 case 11:
326                     return "\u0628";
327                 case 12:
328                     return "\u062f";
329             }
330         }
331         if (locale.getLanguage().equals("ja") && locale.getCountry().equals("JP")) {
332             return Integer.toString(month);
333         }
334         return text.substring(0, 1);
335     }
336 
337     // for China we need to select the last character
narrowDayOfWeek(int dow, String text, Locale locale)338     private String narrowDayOfWeek(int dow, String text, Locale locale) {
339         if (locale.getLanguage().equals("zh") && locale.getCountry().equals("CN")) {
340             switch (dow) {
341                 case 1:
342                     return "\u4e00";
343                 case 2:
344                     return "\u4e8c";
345                 case 3:
346                     return "\u4e09";
347                 case 4:
348                     return "\u56db";
349                 case 5:
350                     return "\u4e94";
351                 case 6:
352                     return "\u516d";
353                 case 7:
354                     return "\u65e5";
355             }
356         }
357         if (locale.getLanguage().equals("ar")) {
358             switch (dow) {
359                 case 1:
360                     return "\u0646";
361                 case 2:
362                     return "\u062b";
363                 case 3:
364                     return "\u0631";
365                 case 4:
366                     return "\u062e";
367                 case 5:
368                     return "\u062c";
369                 case 6:
370                     return "\u0633";
371                 case 7:
372                     return "\u062d";
373             }
374         }
375         return text.substring(0, 1);
376     }
377 
378     //-----------------------------------------------------------------------
379     /**
380      * Helper method to create an immutable entry.
381      *
382      * @param text  the text, not null
383      * @param field  the field, not null
384      * @return the entry, not null
385      */
createEntry(A text, B field)386     private static <A, B> Entry<A, B> createEntry(A text, B field) {
387         return new SimpleImmutableEntry<A, B>(text, field);
388     }
389 
390     //-----------------------------------------------------------------------
createLocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap)391     private static LocaleStore createLocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) {
392         valueTextMap.put(TextStyle.FULL_STANDALONE, valueTextMap.get(TextStyle.FULL));
393         valueTextMap.put(TextStyle.SHORT_STANDALONE, valueTextMap.get(TextStyle.SHORT));
394         if (valueTextMap.containsKey(TextStyle.NARROW) && valueTextMap.containsKey(TextStyle.NARROW_STANDALONE) == false) {
395             valueTextMap.put(TextStyle.NARROW_STANDALONE, valueTextMap.get(TextStyle.NARROW));
396         }
397         return new LocaleStore(valueTextMap);
398     }
399 
400     /**
401      * Stores the text for a single locale.
402      * <p>
403      * Some fields have a textual representation, such as day-of-week or month-of-year.
404      * These textual representations can be captured in this class for printing
405      * and parsing.
406      * <p>
407      * This class is immutable and thread-safe.
408      */
409     static final class LocaleStore {
410         /**
411          * Map of value to text.
412          */
413         private final Map<TextStyle, Map<Long, String>> valueTextMap;
414         /**
415          * Parsable data.
416          */
417         private final Map<TextStyle, List<Entry<String, Long>>> parsable;
418 
419         //-----------------------------------------------------------------------
420         /**
421          * Constructor.
422          *
423          * @param valueTextMap  the map of values to text to store, assigned and not altered, not null
424          */
LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap)425         LocaleStore(Map<TextStyle, Map<Long, String>> valueTextMap) {
426             this.valueTextMap = valueTextMap;
427             Map<TextStyle, List<Entry<String, Long>>> map = new HashMap<TextStyle, List<Entry<String,Long>>>();
428             List<Entry<String, Long>> allList = new ArrayList<Map.Entry<String,Long>>();
429             for (TextStyle style : valueTextMap.keySet()) {
430                 Map<String, Entry<String, Long>> reverse = new HashMap<String, Map.Entry<String,Long>>();
431                 for (Map.Entry<Long, String> entry : valueTextMap.get(style).entrySet()) {
432                     if (reverse.put(entry.getValue(), createEntry(entry.getValue(), entry.getKey())) != null) {
433                         continue;  // not parsable, try next style
434                     }
435                 }
436                 List<Entry<String, Long>> list = new ArrayList<Map.Entry<String,Long>>(reverse.values());
437                 Collections.sort(list, COMPARATOR);
438                 map.put(style, list);
439                 allList.addAll(list);
440                 map.put(null, allList);
441             }
442             Collections.sort(allList, COMPARATOR);
443             this.parsable = map;
444         }
445 
446         //-----------------------------------------------------------------------
447         /**
448          * Gets the text for the specified field value, locale and style
449          * for the purpose of printing.
450          *
451          * @param value  the value to get text for, not null
452          * @param style  the style to get text for, not null
453          * @return the text for the field value, null if no text found
454          */
getText(long value, TextStyle style)455         String getText(long value, TextStyle style) {
456             Map<Long, String> map = valueTextMap.get(style);
457             return map != null ? map.get(value) : null;
458         }
459 
460         /**
461          * Gets an iterator of text to field for the specified style for the purpose of parsing.
462          * <p>
463          * The iterator must be returned in order from the longest text to the shortest.
464          *
465          * @param style  the style to get text for, null for all parsable text
466          * @return the iterator of text to field pairs, in order from longest text to shortest text,
467          *  null if the style is not parsable
468          */
getTextIterator(TextStyle style)469         Iterator<Entry<String, Long>> getTextIterator(TextStyle style) {
470             List<Entry<String, Long>> list = parsable.get(style);
471             return list != null ? list.iterator() : null;
472         }
473     }
474 
475 }
476