1 /*
2  * Copyright (c) 2012, 2013, 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      */

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      */

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      * If the locale contains "nu" (Numbering System) and/or "rg"
151      * (Region Override) <a href="../../util/Locale.html#def_locale_extension">
152      * Unicode extensions</a>, returned instance will reflect the values specified with
153      * those extensions. If both "nu" and "rg" are specified, the value from
154      * the "nu" extension supersedes the implicit one from the "rg" extension.
155      *
156      * @param locale  the locale, not null
157      * @return the decimal style, not null
158      */

159     public static DecimalStyle of(Locale locale) {
160         Objects.requireNonNull(locale, "locale");
161         DecimalStyle info = CACHE.get(locale);
162         if (info == null) {
163             info = create(locale);
164             CACHE.putIfAbsent(locale, info);
165             info = CACHE.get(locale);
166         }
167         return info;
168     }
169
170     private static DecimalStyle create(Locale locale) {
171         DecimalFormatSymbols oldSymbols = DecimalFormatSymbols.getInstance(locale);
172         char zeroDigit = oldSymbols.getZeroDigit();
173         char positiveSign = '+';
174         char negativeSign = oldSymbols.getMinusSign();
175         char decimalSeparator = oldSymbols.getDecimalSeparator();
176         if (zeroDigit == '0' && negativeSign == '-' && decimalSeparator == '.') {
177             return STANDARD;
178         }
179         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
180     }
181
182     //-----------------------------------------------------------------------
183     /**
184      * Restricted constructor.
185      *
186      * @param zeroChar  the character to use for the digit of zero
187      * @param positiveSignChar  the character to use for the positive sign
188      * @param negativeSignChar  the character to use for the negative sign
189      * @param decimalPointChar  the character to use for the decimal point
190      */

191     private DecimalStyle(char zeroChar, char positiveSignChar, char negativeSignChar, char decimalPointChar) {
192         this.zeroDigit = zeroChar;
193         this.positiveSign = positiveSignChar;
194         this.negativeSign = negativeSignChar;
195         this.decimalSeparator = decimalPointChar;
196     }
197
198     //-----------------------------------------------------------------------
199     /**
200      * Gets the character that represents zero.
201      * <p>
202      * The character used to represent digits may vary by culture.
203      * This method specifies the zero character to use, which implies the characters for one to nine.
204      *
205      * @return the character for zero
206      */

207     public char getZeroDigit() {
208         return zeroDigit;
209     }
210
211     /**
212      * Returns a copy of the info with a new character that represents zero.
213      * <p>
214      * The character used to represent digits may vary by culture.
215      * This method specifies the zero character to use, which implies the characters for one to nine.
216      *
217      * @param zeroDigit  the character for zero
218      * @return  a copy with a new character that represents zero, not null
219      */

220     public DecimalStyle withZeroDigit(char zeroDigit) {
221         if (zeroDigit == this.zeroDigit) {
222             return this;
223         }
224         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
225     }
226
227     //-----------------------------------------------------------------------
228     /**
229      * Gets the character that represents the positive sign.
230      * <p>
231      * The character used to represent a positive number may vary by culture.
232      * This method specifies the character to use.
233      *
234      * @return the character for the positive sign
235      */

236     public char getPositiveSign() {
237         return positiveSign;
238     }
239
240     /**
241      * Returns a copy of the info with a new character that represents the positive sign.
242      * <p>
243      * The character used to represent a positive number may vary by culture.
244      * This method specifies the character to use.
245      *
246      * @param positiveSign  the character for the positive sign
247      * @return  a copy with a new character that represents the positive sign, not null
248      */

249     public DecimalStyle withPositiveSign(char positiveSign) {
250         if (positiveSign == this.positiveSign) {
251             return this;
252         }
253         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
254     }
255
256     //-----------------------------------------------------------------------
257     /**
258      * Gets the character that represents the negative sign.
259      * <p>
260      * The character used to represent a negative number may vary by culture.
261      * This method specifies the character to use.
262      *
263      * @return the character for the negative sign
264      */

265     public char getNegativeSign() {
266         return negativeSign;
267     }
268
269     /**
270      * Returns a copy of the info with a new character that represents the negative sign.
271      * <p>
272      * The character used to represent a negative number may vary by culture.
273      * This method specifies the character to use.
274      *
275      * @param negativeSign  the character for the negative sign
276      * @return  a copy with a new character that represents the negative sign, not null
277      */

278     public DecimalStyle withNegativeSign(char negativeSign) {
279         if (negativeSign == this.negativeSign) {
280             return this;
281         }
282         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
283     }
284
285     //-----------------------------------------------------------------------
286     /**
287      * Gets the character that represents the decimal point.
288      * <p>
289      * The character used to represent a decimal point may vary by culture.
290      * This method specifies the character to use.
291      *
292      * @return the character for the decimal point
293      */

294     public char getDecimalSeparator() {
295         return decimalSeparator;
296     }
297
298     /**
299      * Returns a copy of the info with a new character that represents the decimal point.
300      * <p>
301      * The character used to represent a decimal point may vary by culture.
302      * This method specifies the character to use.
303      *
304      * @param decimalSeparator  the character for the decimal point
305      * @return  a copy with a new character that represents the decimal point, not null
306      */

307     public DecimalStyle withDecimalSeparator(char decimalSeparator) {
308         if (decimalSeparator == this.decimalSeparator) {
309             return this;
310         }
311         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
312     }
313
314     //-----------------------------------------------------------------------
315     /**
316      * Checks whether the character is a digit, based on the currently set zero character.
317      *
318      * @param ch  the character to check
319      * @return the value, 0 to 9, of the character, or -1 if not a digit
320      */

321     int convertToDigit(char ch) {
322         int val = ch - zeroDigit;
323         return (val >= 0 && val <= 9) ? val : -1;
324     }
325
326     /**
327      * Converts the input numeric text to the internationalized form using the zero character.
328      *
329      * @param numericText  the text, consisting of digits 0 to 9, to convert, not null
330      * @return the internationalized text, not null
331      */

332     String convertNumberToI18N(String numericText) {
333         if (zeroDigit == '0') {
334             return numericText;
335         }
336         int diff = zeroDigit - '0';
337         char[] array = numericText.toCharArray();
338         for (int i = 0; i < array.length; i++) {
339             array[i] = (char) (array[i] + diff);
340         }
341         return new String(array);
342     }
343
344     //-----------------------------------------------------------------------
345     /**
346      * Checks if this DecimalStyle is equal to another DecimalStyle.
347      *
348      * @param obj  the object to check, null returns false
349      * @return true if this is equal to the other date
350      */

351     @Override
352     public boolean equals(Object obj) {
353         if (this == obj) {
354             return true;
355         }
356         if (obj instanceof DecimalStyle) {
357             DecimalStyle other = (DecimalStyle) obj;
358             return (zeroDigit == other.zeroDigit && positiveSign == other.positiveSign &&
359                     negativeSign == other.negativeSign && decimalSeparator == other.decimalSeparator);
360         }
361         return false;
362     }
363
364     /**
365      * A hash code for this DecimalStyle.
366      *
367      * @return a suitable hash code
368      */

369     @Override
370     public int hashCode() {
371         return zeroDigit + positiveSign + negativeSign + decimalSeparator;
372     }
373
374     //-----------------------------------------------------------------------
375     /**
376      * Returns a string describing this DecimalStyle.
377      *
378      * @return a string description, not null
379      */

380     @Override
381     public String toString() {
382         return "DecimalStyle[" + zeroDigit + positiveSign + negativeSign + decimalSeparator + "]";
383     }
384
385 }
386