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
18package java.text;
19
20import java.io.IOException;
21import java.io.ObjectInputStream;
22import java.io.ObjectOutputStream;
23import java.io.Serializable;
24import java.util.Arrays;
25import java.util.Locale;
26import java.util.TimeZone;
27import libcore.icu.ICU;
28import libcore.icu.LocaleData;
29import 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 */
53public 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     */
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     */
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     */
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     */
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     */
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     */
147    public static Locale[] getAvailableLocales() {
148        return ICU.getAvailableDateFormatSymbolsLocales();
149    }
150
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
162    private void writeObject(ObjectOutputStream oos) throws IOException {
163        internalZoneStrings();
164        oos.defaultWriteObject();
165    }
166
167    @Override
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
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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