1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one or more
3  * contributor license agreements.  See the NOTICE file distributed with
4  * this work for additional information regarding copyright ownership.
5  * The ASF licenses this file to You under the Apache License, Version 2.0
6  * (the "License"); you may not use this file except in compliance with
7  * the License.  You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package java.text;
19 
20 import java.io.IOException;
21 import java.io.ObjectInputStream;
22 import java.io.ObjectOutputStream;
23 import java.io.Serializable;
24 import java.util.Arrays;
25 import java.util.Locale;
26 import java.util.TimeZone;
27 import libcore.icu.ICU;
28 import libcore.icu.LocaleData;
29 import libcore.icu.TimeZoneNames;
30 
31 /**
32  * Encapsulates localized date-time formatting data, such as the names of the
33  * months, the names of the days of the week, and the time zone data.
34  * {@code DateFormat} and {@code SimpleDateFormat} both use
35  * {@code DateFormatSymbols} to encapsulate this information.
36  *
37  * <p>Typically you shouldn't use {@code DateFormatSymbols} directly. Rather, you
38  * are encouraged to create a date/time formatter with the {@code DateFormat}
39  * class's factory methods: {@code getTimeInstance}, {@code getDateInstance},
40  * or {@code getDateTimeInstance}. These methods automatically create a
41  * {@code DateFormatSymbols} for the formatter so that you don't have to. After
42  * the formatter is created, you may modify its format pattern using the
43  * {@code setPattern} method. For more information about creating formatters
44  * using {@code DateFormat}'s factory methods, see {@link DateFormat}.
45  *
46  * <p>Direct use of {@code DateFormatSymbols} is likely to be less efficient
47  * because the implementation cannot make assumptions about user-supplied/user-modifiable data
48  * to the same extent that it can with its own built-in data.
49  *
50  * @see DateFormat
51  * @see SimpleDateFormat
52  */
53 public class DateFormatSymbols implements Serializable, Cloneable {
54 
55     private static final long serialVersionUID = -5987973545549424702L;
56 
57     private String localPatternChars;
58 
59     String[] ampms, eras, months, shortMonths, shortWeekdays, weekdays;
60 
61     // This is used to implement parts of Unicode UTS #35 not historically supported.
62     transient LocaleData localeData;
63 
64     // Localized display names.
65     private String[][] zoneStrings;
66 
67     /*
68      * Locale, necessary to lazily load time zone strings. Added to the serialized form for
69      * Android's L release. May be null if the object was deserialized using data from older
70      * releases. See b/16502916.
71      */
72     private final Locale locale;
73 
74     /**
75      * Gets zone strings, initializing them if necessary. Does not create
76      * a defensive copy, so make sure you do so before exposing the returned
77      * arrays to clients.
78      */
internalZoneStrings()79     synchronized String[][] internalZoneStrings() {
80         if (zoneStrings == null) {
81             zoneStrings = TimeZoneNames.getZoneStrings(locale);
82         }
83         return zoneStrings;
84     }
85 
86     /**
87      * Constructs a new {@code DateFormatSymbols} instance containing the
88      * symbols for the user's default locale.
89      * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>".
90      */
DateFormatSymbols()91     public DateFormatSymbols() {
92         this(Locale.getDefault());
93     }
94 
95     /**
96      * Constructs a new {@code DateFormatSymbols} instance containing the
97      * symbols for the specified locale.
98      *
99      * @param locale
100      *            the locale.
101      */
DateFormatSymbols(Locale locale)102     public DateFormatSymbols(Locale locale) {
103         this.locale = LocaleData.mapInvalidAndNullLocales(locale);
104         this.localPatternChars = SimpleDateFormat.PATTERN_CHARS;
105 
106         this.localeData = LocaleData.get(locale);
107         this.ampms = localeData.amPm;
108         this.eras = localeData.eras;
109         this.months = localeData.longMonthNames;
110         this.shortMonths = localeData.shortMonthNames;
111         this.weekdays = localeData.longWeekdayNames;
112         this.shortWeekdays = localeData.shortWeekdayNames;
113     }
114 
115     /**
116      * Returns a new {@code DateFormatSymbols} instance for the user's default locale.
117      * See "<a href="../util/Locale.html#default_locale">Be wary of the default locale</a>".
118      *
119      * @return an instance of {@code DateFormatSymbols}
120      * @since 1.6
121      */
getInstance()122     public static final DateFormatSymbols getInstance() {
123         return getInstance(Locale.getDefault());
124     }
125 
126     /**
127      * Returns a new {@code DateFormatSymbols} for the given locale.
128      *
129      * @param locale the locale
130      * @return an instance of {@code DateFormatSymbols}
131      * @throws NullPointerException if {@code locale == null}
132      * @since 1.6
133      */
getInstance(Locale locale)134     public static final DateFormatSymbols getInstance(Locale locale) {
135         if (locale == null) {
136             throw new NullPointerException("locale == null");
137         }
138         return new DateFormatSymbols(locale);
139     }
140 
141     /**
142      * Returns an array of locales for which custom {@code DateFormatSymbols} instances
143      * are available.
144      * <p>Note that Android does not support user-supplied locale service providers.
145      * @since 1.6
146      */
getAvailableLocales()147     public static Locale[] getAvailableLocales() {
148         return ICU.getAvailableDateFormatSymbolsLocales();
149     }
150 
readObject(ObjectInputStream ois)151     private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
152         ois.defaultReadObject();
153 
154         Locale locale = this.locale;
155         if (locale == null) {
156             // Before the L release Android did not serialize the locale. Handle its absence.
157             locale = Locale.getDefault();
158         }
159         this.localeData = LocaleData.get(locale);
160     }
161 
writeObject(ObjectOutputStream oos)162     private void writeObject(ObjectOutputStream oos) throws IOException {
163         internalZoneStrings();
164         oos.defaultWriteObject();
165     }
166 
167     @Override
clone()168     public Object clone() {
169         try {
170             return super.clone();
171         } catch (CloneNotSupportedException e) {
172             throw new AssertionError();
173         }
174     }
175 
176     /**
177      * Compares this object with the specified object and indicates if they are
178      * equal.
179      *
180      * @param object
181      *            the object to compare with this object.
182      * @return {@code true} if {@code object} is an instance of
183      *         {@code DateFormatSymbols} and has the same symbols as this
184      *         object, {@code false} otherwise.
185      * @see #hashCode
186      */
187     @Override
equals(Object object)188     public boolean equals(Object object) {
189         if (this == object) {
190             return true;
191         }
192         if (!(object instanceof DateFormatSymbols)) {
193             return false;
194         }
195         DateFormatSymbols rhs = (DateFormatSymbols) object;
196         return localPatternChars.equals(rhs.localPatternChars) &&
197                 Arrays.equals(ampms, rhs.ampms) &&
198                 Arrays.equals(eras, rhs.eras) &&
199                 Arrays.equals(months, rhs.months) &&
200                 Arrays.equals(shortMonths, rhs.shortMonths) &&
201                 Arrays.equals(shortWeekdays, rhs.shortWeekdays) &&
202                 Arrays.equals(weekdays, rhs.weekdays) &&
203                 timeZoneStringsEqual(this, rhs);
204     }
205 
timeZoneStringsEqual(DateFormatSymbols lhs, DateFormatSymbols rhs)206     private static boolean timeZoneStringsEqual(DateFormatSymbols lhs, DateFormatSymbols rhs) {
207         // Quick check that may keep us from having to load the zone strings.
208         // Note that different locales may have the same strings, so the opposite check isn't valid.
209         if (lhs.zoneStrings == null && rhs.zoneStrings == null && lhs.locale.equals(rhs.locale)) {
210             return true;
211         }
212         // Make sure zone strings are loaded, then check.
213         return Arrays.deepEquals(lhs.internalZoneStrings(), rhs.internalZoneStrings());
214     }
215 
216     @Override
toString()217     public String toString() {
218         // 'locale' isn't part of the externally-visible state.
219         // 'zoneStrings' is so large, we just print a representative value.
220         return getClass().getName() +
221                 "[amPmStrings=" + Arrays.toString(ampms) +
222                 ",eras=" + Arrays.toString(eras) +
223                 ",localPatternChars=" + localPatternChars +
224                 ",months=" + Arrays.toString(months) +
225                 ",shortMonths=" + Arrays.toString(shortMonths) +
226                 ",shortWeekdays=" + Arrays.toString(shortWeekdays) +
227                 ",weekdays=" + Arrays.toString(weekdays) +
228                 ",zoneStrings=[" + Arrays.toString(internalZoneStrings()[0]) + "...]" +
229                 "]";
230     }
231 
232     /**
233      * Returns the array of strings which represent AM and PM. Use the
234      * {@link java.util.Calendar} constants {@code Calendar.AM} and
235      * {@code Calendar.PM} as indices for the array.
236      *
237      * @return an array of strings.
238      */
getAmPmStrings()239     public String[] getAmPmStrings() {
240         return ampms.clone();
241     }
242 
243     /**
244      * Returns the array of strings which represent BC and AD. Use the
245      * {@link java.util.Calendar} constants {@code GregorianCalendar.BC} and
246      * {@code GregorianCalendar.AD} as indices for the array.
247      *
248      * @return an array of strings.
249      */
getEras()250     public String[] getEras() {
251         return eras.clone();
252     }
253 
254     /**
255      * Returns the pattern characters used by {@link SimpleDateFormat} to
256      * specify date and time fields.
257      */
getLocalPatternChars()258     public String getLocalPatternChars() {
259         return localPatternChars;
260     }
261 
262     /**
263      * Returns the array of strings containing the full names of the months. Use
264      * the {@link java.util.Calendar} constants {@code Calendar.JANUARY} etc. as
265      * indices for the array.
266      *
267      * @return an array of strings.
268      */
getMonths()269     public String[] getMonths() {
270         return months.clone();
271     }
272 
273     /**
274      * Returns the array of strings containing the abbreviated names of the
275      * months. Use the {@link java.util.Calendar} constants
276      * {@code Calendar.JANUARY} etc. as indices for the array.
277      *
278      * @return an array of strings.
279      */
getShortMonths()280     public String[] getShortMonths() {
281         return shortMonths.clone();
282     }
283 
284     /**
285      * Returns the array of strings containing the abbreviated names of the days
286      * of the week. Use the {@link java.util.Calendar} constants
287      * {@code Calendar.SUNDAY} etc. as indices for the array.
288      *
289      * @return an array of strings.
290      */
getShortWeekdays()291     public String[] getShortWeekdays() {
292         return shortWeekdays.clone();
293     }
294 
295     /**
296      * Returns the array of strings containing the full names of the days of the
297      * week. Use the {@link java.util.Calendar} constants
298      * {@code Calendar.SUNDAY} etc. as indices for the array.
299      *
300      * @return an array of strings.
301      */
getWeekdays()302     public String[] getWeekdays() {
303         return weekdays.clone();
304     }
305 
306     /**
307      * Returns the two-dimensional array of strings containing localized names for time zones.
308      * Each row is an array of five strings:
309      * <ul>
310      * <li>The time zone ID, for example "America/Los_Angeles".
311      *     This is not localized, and is used as a key into the table.
312      * <li>The long display name, for example "Pacific Standard Time".
313      * <li>The short display name, for example "PST".
314      * <li>The long display name for DST, for example "Pacific Daylight Time".
315      *     This is the non-DST long name for zones that have never had DST, for
316      *     example "Central Standard Time" for "Canada/Saskatchewan".
317      * <li>The short display name for DST, for example "PDT". This is the
318      *     non-DST short name for zones that have never had DST, for example
319      *     "CST" for "Canada/Saskatchewan".
320      * </ul>
321      */
getZoneStrings()322     public String[][] getZoneStrings() {
323         String[][] result = clone2dStringArray(internalZoneStrings());
324         // If icu4c doesn't have a name, our array contains a null. TimeZone.getDisplayName
325         // knows how to format GMT offsets (and, unlike icu4c, has accurate data). http://b/8128460.
326         for (String[] zone : result) {
327             String id = zone[0];
328             if (zone[1] == null) {
329                 zone[1] = TimeZone.getTimeZone(id).getDisplayName(false, TimeZone.LONG, locale);
330             }
331             if (zone[2] == null) {
332                 zone[2] = TimeZone.getTimeZone(id).getDisplayName(false, TimeZone.SHORT, locale);
333             }
334             if (zone[3] == null) {
335                 zone[3] = TimeZone.getTimeZone(id).getDisplayName(true, TimeZone.LONG, locale);
336             }
337             if (zone[4] == null) {
338                 zone[4] = TimeZone.getTimeZone(id).getDisplayName(true, TimeZone.SHORT, locale);
339             }
340         }
341         return result;
342     }
343 
clone2dStringArray(String[][] array)344     private static String[][] clone2dStringArray(String[][] array) {
345         String[][] result = new String[array.length][];
346         for (int i = 0; i < array.length; ++i) {
347             result[i] = array[i].clone();
348         }
349         return result;
350     }
351 
352     @Override
hashCode()353     public int hashCode() {
354         String[][] zoneStrings = internalZoneStrings();
355         int hashCode;
356         hashCode = localPatternChars.hashCode();
357         for (String element : ampms) {
358             hashCode += element.hashCode();
359         }
360         for (String element : eras) {
361             hashCode += element.hashCode();
362         }
363         for (String element : months) {
364             hashCode += element.hashCode();
365         }
366         for (String element : shortMonths) {
367             hashCode += element.hashCode();
368         }
369         for (String element : shortWeekdays) {
370             hashCode += element.hashCode();
371         }
372         for (String element : weekdays) {
373             hashCode += element.hashCode();
374         }
375         for (String[] element : zoneStrings) {
376             for (int j = 0; j < element.length; j++) {
377                 if (element[j] != null) {
378                     hashCode += element[j].hashCode();
379                 }
380             }
381         }
382         return hashCode;
383     }
384 
385     /**
386      * Sets the array of strings which represent AM and PM. Use the
387      * {@link java.util.Calendar} constants {@code Calendar.AM} and
388      * {@code Calendar.PM} as indices for the array.
389      *
390      * @param data
391      *            the array of strings for AM and PM.
392      */
setAmPmStrings(String[] data)393     public void setAmPmStrings(String[] data) {
394         ampms = data.clone();
395     }
396 
397     /**
398      * Sets the array of Strings which represent BC and AD. Use the
399      * {@link java.util.Calendar} constants {@code GregorianCalendar.BC} and
400      * {@code GregorianCalendar.AD} as indices for the array.
401      *
402      * @param data
403      *            the array of strings for BC and AD.
404      */
setEras(String[] data)405     public void setEras(String[] data) {
406         eras = data.clone();
407     }
408 
409     /**
410      * Sets the pattern characters used by {@link SimpleDateFormat} to specify
411      * date and time fields.
412      *
413      * @param data
414      *            the string containing the pattern characters.
415      * @throws NullPointerException
416      *            if {@code data} is null
417      */
setLocalPatternChars(String data)418     public void setLocalPatternChars(String data) {
419         if (data == null) {
420             throw new NullPointerException("data == null");
421         }
422         localPatternChars = data;
423     }
424 
425     /**
426      * Sets the array of strings containing the full names of the months. Use
427      * the {@link java.util.Calendar} constants {@code Calendar.JANUARY} etc. as
428      * indices for the array.
429      *
430      * @param data
431      *            the array of strings.
432      */
setMonths(String[] data)433     public void setMonths(String[] data) {
434         months = data.clone();
435     }
436 
437     /**
438      * Sets the array of strings containing the abbreviated names of the months.
439      * Use the {@link java.util.Calendar} constants {@code Calendar.JANUARY}
440      * etc. as indices for the array.
441      *
442      * @param data
443      *            the array of strings.
444      */
setShortMonths(String[] data)445     public void setShortMonths(String[] data) {
446         shortMonths = data.clone();
447     }
448 
449     /**
450      * Sets the array of strings containing the abbreviated names of the days of
451      * the week. Use the {@link java.util.Calendar} constants
452      * {@code Calendar.SUNDAY} etc. as indices for the array.
453      *
454      * @param data
455      *            the array of strings.
456      */
setShortWeekdays(String[] data)457     public void setShortWeekdays(String[] data) {
458         shortWeekdays = data.clone();
459     }
460 
461     /**
462      * Sets the array of strings containing the full names of the days of the
463      * week. Use the {@link java.util.Calendar} constants
464      * {@code Calendar.SUNDAY} etc. as indices for the array.
465      *
466      * @param data
467      *            the array of strings.
468      */
setWeekdays(String[] data)469     public void setWeekdays(String[] data) {
470         weekdays = data.clone();
471     }
472 
473     /**
474      * Sets the two-dimensional array of strings containing localized names for time zones.
475      * See {@link #getZoneStrings} for details.
476      * @throws IllegalArgumentException if any row has fewer than 5 elements.
477      * @throws NullPointerException if {@code zoneStrings == null}.
478      */
setZoneStrings(String[][] zoneStrings)479     public void setZoneStrings(String[][] zoneStrings) {
480         if (zoneStrings == null) {
481             throw new NullPointerException("zoneStrings == null");
482         }
483         for (String[] row : zoneStrings) {
484             if (row.length < 5) {
485                 throw new IllegalArgumentException(Arrays.toString(row) + ".length < 5");
486             }
487         }
488         this.zoneStrings = clone2dStringArray(zoneStrings);
489     }
490 
491     /**
492      * Returns the display name of the timezone specified. Returns null if no name was found in the
493      * zone strings.
494      *
495      * @param daylight whether to return the daylight savings or the standard name
496      * @param style one of the {@link TimeZone} styles such as {@link TimeZone#SHORT}
497      *
498      * @hide used internally
499      */
getTimeZoneDisplayName(TimeZone tz, boolean daylight, int style)500     String getTimeZoneDisplayName(TimeZone tz, boolean daylight, int style) {
501         if (style != TimeZone.SHORT && style != TimeZone.LONG) {
502             throw new IllegalArgumentException("Bad style: " + style);
503         }
504 
505         // If custom zone strings have been set with setZoneStrings() we use those. Otherwise we
506         // use the ones from LocaleData.
507         String[][] zoneStrings = internalZoneStrings();
508         return TimeZoneNames.getDisplayName(zoneStrings, tz.getID(), daylight, style);
509     }
510 }
511