1 /*
2  * Copyright (c) 2012, 2015, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 /*
27  * This file is available under and governed by the GNU General Public
28  * License version 2 only, as published by the Free Software Foundation.
29  * However, the following notice accompanied the original version of this
30  * file:
31  *
32  * Copyright (c) 2008-2012, Stephen Colebourne & Michael Nascimento Santos
33  *
34  * All rights reserved.
35  *
36  * Redistribution and use in source and binary forms, with or without
37  * modification, are permitted provided that the following conditions are met:
38  *
39  *  * Redistributions of source code must retain the above copyright notice,
40  *    this list of conditions and the following disclaimer.
41  *
42  *  * Redistributions in binary form must reproduce the above copyright notice,
43  *    this list of conditions and the following disclaimer in the documentation
44  *    and/or other materials provided with the distribution.
45  *
46  *  * Neither the name of JSR-310 nor the names of its contributors
47  *    may be used to endorse or promote products derived from this software
48  *    without specific prior written permission.
49  *
50  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
51  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
52  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
53  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
54  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
55  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
56  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
57  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
58  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
59  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
60  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
61  */
62 package java.time.format;
63 
64 import java.text.DecimalFormatSymbols;
65 import java.util.Collections;
66 import java.util.HashSet;
67 import java.util.Locale;
68 import java.util.Objects;
69 import java.util.Set;
70 import java.util.concurrent.ConcurrentHashMap;
71 import java.util.concurrent.ConcurrentMap;
72 
73 /**
74  * Localized decimal style used in date and time formatting.
75  * <p>
76  * A significant part of dealing with dates and times is the localization.
77  * This class acts as a central point for accessing the information.
78  *
79  * @implSpec
80  * This class is immutable and thread-safe.
81  *
82  * @since 1.8
83  */
84 public final class DecimalStyle {
85 
86     /**
87      * The standard set of non-localized decimal style symbols.
88      * <p>
89      * This uses standard ASCII characters for zero, positive, negative and a dot for the decimal point.
90      */
91     public static final DecimalStyle STANDARD = new DecimalStyle('0', '+', '-', '.');
92     /**
93      * The cache of DecimalStyle instances.
94      */
95     private static final ConcurrentMap<Locale, DecimalStyle> CACHE = new ConcurrentHashMap<>(16, 0.75f, 2);
96 
97     /**
98      * The zero digit.
99      */
100     private final char zeroDigit;
101     /**
102      * The positive sign.
103      */
104     private final char positiveSign;
105     /**
106      * The negative sign.
107      */
108     private final char negativeSign;
109     /**
110      * The decimal separator.
111      */
112     private final char decimalSeparator;
113 
114     //-----------------------------------------------------------------------
115     /**
116      * Lists all the locales that are supported.
117      * <p>
118      * The locale 'en_US' will always be present.
119      *
120      * @return a Set of Locales for which localization is supported
121      */
getAvailableLocales()122     public static Set<Locale> getAvailableLocales() {
123         Locale[] l = DecimalFormatSymbols.getAvailableLocales();
124         Set<Locale> locales = new HashSet<>(l.length);
125         Collections.addAll(locales, l);
126         return locales;
127     }
128 
129     /**
130      * Obtains the DecimalStyle for the default
131      * {@link java.util.Locale.Category#FORMAT FORMAT} locale.
132      * <p>
133      * This method provides access to locale sensitive decimal style symbols.
134      * <p>
135      * This is equivalent to calling
136      * {@link #of(Locale)
137      *     of(Locale.getDefault(Locale.Category.FORMAT))}.
138      *
139      * @see java.util.Locale.Category#FORMAT
140      * @return the decimal style, not null
141      */
ofDefaultLocale()142     public static DecimalStyle ofDefaultLocale() {
143         return of(Locale.getDefault(Locale.Category.FORMAT));
144     }
145 
146     /**
147      * Obtains the DecimalStyle for the specified locale.
148      * <p>
149      * This method provides access to locale sensitive decimal style symbols.
150      *
151      * @param locale  the locale, not null
152      * @return the decimal style, not null
153      */
of(Locale locale)154     public static DecimalStyle of(Locale locale) {
155         Objects.requireNonNull(locale, "locale");
156         DecimalStyle info = CACHE.get(locale);
157         if (info == null) {
158             info = create(locale);
159             CACHE.putIfAbsent(locale, info);
160             info = CACHE.get(locale);
161         }
162         return info;
163     }
164 
create(Locale locale)165     private static DecimalStyle create(Locale locale) {
166         DecimalFormatSymbols oldSymbols = DecimalFormatSymbols.getInstance(locale);
167         char zeroDigit = oldSymbols.getZeroDigit();
168         char positiveSign = '+';
169         char negativeSign = oldSymbols.getMinusSign();
170         char decimalSeparator = oldSymbols.getDecimalSeparator();
171         if (zeroDigit == '0' && negativeSign == '-' && decimalSeparator == '.') {
172             return STANDARD;
173         }
174         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
175     }
176 
177     //-----------------------------------------------------------------------
178     /**
179      * Restricted constructor.
180      *
181      * @param zeroChar  the character to use for the digit of zero
182      * @param positiveSignChar  the character to use for the positive sign
183      * @param negativeSignChar  the character to use for the negative sign
184      * @param decimalPointChar  the character to use for the decimal point
185      */
DecimalStyle(char zeroChar, char positiveSignChar, char negativeSignChar, char decimalPointChar)186     private DecimalStyle(char zeroChar, char positiveSignChar, char negativeSignChar, char decimalPointChar) {
187         this.zeroDigit = zeroChar;
188         this.positiveSign = positiveSignChar;
189         this.negativeSign = negativeSignChar;
190         this.decimalSeparator = decimalPointChar;
191     }
192 
193     //-----------------------------------------------------------------------
194     /**
195      * Gets the character that represents zero.
196      * <p>
197      * The character used to represent digits may vary by culture.
198      * This method specifies the zero character to use, which implies the characters for one to nine.
199      *
200      * @return the character for zero
201      */
getZeroDigit()202     public char getZeroDigit() {
203         return zeroDigit;
204     }
205 
206     /**
207      * Returns a copy of the info with a new character that represents zero.
208      * <p>
209      * The character used to represent digits may vary by culture.
210      * This method specifies the zero character to use, which implies the characters for one to nine.
211      *
212      * @param zeroDigit  the character for zero
213      * @return  a copy with a new character that represents zero, not null
214 
215      */
withZeroDigit(char zeroDigit)216     public DecimalStyle withZeroDigit(char zeroDigit) {
217         if (zeroDigit == this.zeroDigit) {
218             return this;
219         }
220         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
221     }
222 
223     //-----------------------------------------------------------------------
224     /**
225      * Gets the character that represents the positive sign.
226      * <p>
227      * The character used to represent a positive number may vary by culture.
228      * This method specifies the character to use.
229      *
230      * @return the character for the positive sign
231      */
getPositiveSign()232     public char getPositiveSign() {
233         return positiveSign;
234     }
235 
236     /**
237      * Returns a copy of the info with a new character that represents the positive sign.
238      * <p>
239      * The character used to represent a positive number may vary by culture.
240      * This method specifies the character to use.
241      *
242      * @param positiveSign  the character for the positive sign
243      * @return  a copy with a new character that represents the positive sign, not null
244      */
withPositiveSign(char positiveSign)245     public DecimalStyle withPositiveSign(char positiveSign) {
246         if (positiveSign == this.positiveSign) {
247             return this;
248         }
249         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
250     }
251 
252     //-----------------------------------------------------------------------
253     /**
254      * Gets the character that represents the negative sign.
255      * <p>
256      * The character used to represent a negative number may vary by culture.
257      * This method specifies the character to use.
258      *
259      * @return the character for the negative sign
260      */
getNegativeSign()261     public char getNegativeSign() {
262         return negativeSign;
263     }
264 
265     /**
266      * Returns a copy of the info with a new character that represents the negative sign.
267      * <p>
268      * The character used to represent a negative number may vary by culture.
269      * This method specifies the character to use.
270      *
271      * @param negativeSign  the character for the negative sign
272      * @return  a copy with a new character that represents the negative sign, not null
273      */
withNegativeSign(char negativeSign)274     public DecimalStyle withNegativeSign(char negativeSign) {
275         if (negativeSign == this.negativeSign) {
276             return this;
277         }
278         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
279     }
280 
281     //-----------------------------------------------------------------------
282     /**
283      * Gets the character that represents the decimal point.
284      * <p>
285      * The character used to represent a decimal point may vary by culture.
286      * This method specifies the character to use.
287      *
288      * @return the character for the decimal point
289      */
getDecimalSeparator()290     public char getDecimalSeparator() {
291         return decimalSeparator;
292     }
293 
294     /**
295      * Returns a copy of the info with a new character that represents the decimal point.
296      * <p>
297      * The character used to represent a decimal point may vary by culture.
298      * This method specifies the character to use.
299      *
300      * @param decimalSeparator  the character for the decimal point
301      * @return  a copy with a new character that represents the decimal point, not null
302      */
withDecimalSeparator(char decimalSeparator)303     public DecimalStyle withDecimalSeparator(char decimalSeparator) {
304         if (decimalSeparator == this.decimalSeparator) {
305             return this;
306         }
307         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
308     }
309 
310     //-----------------------------------------------------------------------
311     /**
312      * Checks whether the character is a digit, based on the currently set zero character.
313      *
314      * @param ch  the character to check
315      * @return the value, 0 to 9, of the character, or -1 if not a digit
316      */
convertToDigit(char ch)317     int convertToDigit(char ch) {
318         int val = ch - zeroDigit;
319         return (val >= 0 && val <= 9) ? val : -1;
320     }
321 
322     /**
323      * Converts the input numeric text to the internationalized form using the zero character.
324      *
325      * @param numericText  the text, consisting of digits 0 to 9, to convert, not null
326      * @return the internationalized text, not null
327      */
convertNumberToI18N(String numericText)328     String convertNumberToI18N(String numericText) {
329         if (zeroDigit == '0') {
330             return numericText;
331         }
332         int diff = zeroDigit - '0';
333         char[] array = numericText.toCharArray();
334         for (int i = 0; i < array.length; i++) {
335             array[i] = (char) (array[i] + diff);
336         }
337         return new String(array);
338     }
339 
340     //-----------------------------------------------------------------------
341     /**
342      * Checks if this DecimalStyle is equal to another DecimalStyle.
343      *
344      * @param obj  the object to check, null returns false
345      * @return true if this is equal to the other date
346      */
347     @Override
equals(Object obj)348     public boolean equals(Object obj) {
349         if (this == obj) {
350             return true;
351         }
352         if (obj instanceof DecimalStyle) {
353             DecimalStyle other = (DecimalStyle) obj;
354             return (zeroDigit == other.zeroDigit && positiveSign == other.positiveSign &&
355                     negativeSign == other.negativeSign && decimalSeparator == other.decimalSeparator);
356         }
357         return false;
358     }
359 
360     /**
361      * A hash code for this DecimalStyle.
362      *
363      * @return a suitable hash code
364      */
365     @Override
hashCode()366     public int hashCode() {
367         return zeroDigit + positiveSign + negativeSign + decimalSeparator;
368     }
369 
370     //-----------------------------------------------------------------------
371     /**
372      * Returns a string describing this DecimalStyle.
373      *
374      * @return a string description, not null
375      */
376     @Override
toString()377     public String toString() {
378         return "DecimalStyle[" + zeroDigit + positiveSign + negativeSign + decimalSeparator + "]";
379     }
380 
381 }
382