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      */
 221     public DecimalStyle withZeroDigit(char zeroDigit) {
 222         if (zeroDigit == this.zeroDigit) {
 223             return this;
 224         }
 225         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
 226     }
 227 
 228     //-----------------------------------------------------------------------
 229     /**
 230      * Gets the character that represents the positive sign.
 231      * <p>
 232      * The character used to represent a positive number may vary by culture.
 233      * This method specifies the character to use.
 234      *
 235      * @return the character for the positive sign
 236      */
 237     public char getPositiveSign() {
 238         return positiveSign;
 239     }
 240 
 241     /**
 242      * Returns a copy of the info with a new character that represents the positive sign.
 243      * <p>
 244      * The character used to represent a positive number may vary by culture.
 245      * This method specifies the character to use.
 246      *
 247      * @param positiveSign  the character for the positive sign
 248      * @return  a copy with a new character that represents the positive sign, not null
 249      */
 250     public DecimalStyle withPositiveSign(char positiveSign) {
 251         if (positiveSign == this.positiveSign) {
 252             return this;
 253         }
 254         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
 255     }
 256 
 257     //-----------------------------------------------------------------------
 258     /**
 259      * Gets the character that represents the negative sign.
 260      * <p>
 261      * The character used to represent a negative number may vary by culture.
 262      * This method specifies the character to use.
 263      *
 264      * @return the character for the negative sign
 265      */
 266     public char getNegativeSign() {
 267         return negativeSign;
 268     }
 269 
 270     /**
 271      * Returns a copy of the info with a new character that represents the negative sign.
 272      * <p>
 273      * The character used to represent a negative number may vary by culture.
 274      * This method specifies the character to use.
 275      *
 276      * @param negativeSign  the character for the negative sign
 277      * @return  a copy with a new character that represents the negative sign, not null
 278      */
 279     public DecimalStyle withNegativeSign(char negativeSign) {
 280         if (negativeSign == this.negativeSign) {
 281             return this;
 282         }
 283         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
 284     }
 285 
 286     //-----------------------------------------------------------------------
 287     /**
 288      * Gets the character that represents the decimal point.
 289      * <p>
 290      * The character used to represent a decimal point may vary by culture.
 291      * This method specifies the character to use.
 292      *
 293      * @return the character for the decimal point
 294      */
 295     public char getDecimalSeparator() {
 296         return decimalSeparator;
 297     }
 298 
 299     /**
 300      * Returns a copy of the info with a new character that represents the decimal point.
 301      * <p>
 302      * The character used to represent a decimal point may vary by culture.
 303      * This method specifies the character to use.
 304      *
 305      * @param decimalSeparator  the character for the decimal point
 306      * @return  a copy with a new character that represents the decimal point, not null
 307      */
 308     public DecimalStyle withDecimalSeparator(char decimalSeparator) {
 309         if (decimalSeparator == this.decimalSeparator) {
 310             return this;
 311         }
 312         return new DecimalStyle(zeroDigit, positiveSign, negativeSign, decimalSeparator);
 313     }
 314 
 315     //-----------------------------------------------------------------------
 316     /**
 317      * Checks whether the character is a digit, based on the currently set zero character.
 318      *
 319      * @param ch  the character to check
 320      * @return the value, 0 to 9, of the character, or -1 if not a digit
 321      */
 322     int convertToDigit(char ch) {
 323         int val = ch - zeroDigit;
 324         return (val >= 0 && val <= 9) ? val : -1;
 325     }
 326 
 327     /**
 328      * Converts the input numeric text to the internationalized form using the zero character.
 329      *
 330      * @param numericText  the text, consisting of digits 0 to 9, to convert, not null
 331      * @return the internationalized text, not null
 332      */
 333     String convertNumberToI18N(String numericText) {
 334         if (zeroDigit == '0') {
 335             return numericText;
 336         }
 337         int diff = zeroDigit - '0';
 338         char[] array = numericText.toCharArray();
 339         for (int i = 0; i < array.length; i++) {
 340             array[i] = (char) (array[i] + diff);
 341         }
 342         return new String(array);
 343     }
 344 
 345     //-----------------------------------------------------------------------
 346     /**
 347      * Checks if this DecimalStyle is equal to another DecimalStyle.
 348      *
 349      * @param obj  the object to check, null returns false
 350      * @return true if this is equal to the other date
 351      */
 352     @Override
 353     public boolean equals(Object obj) {
 354         if (this == obj) {
 355             return true;
 356         }
 357         if (obj instanceof DecimalStyle) {
 358             DecimalStyle other = (DecimalStyle) obj;
 359             return (zeroDigit == other.zeroDigit && positiveSign == other.positiveSign &&
 360                     negativeSign == other.negativeSign && decimalSeparator == other.decimalSeparator);
 361         }
 362         return false;
 363     }
 364 
 365     /**
 366      * A hash code for this DecimalStyle.
 367      *
 368      * @return a suitable hash code
 369      */
 370     @Override
 371     public int hashCode() {
 372         return zeroDigit + positiveSign + negativeSign + decimalSeparator;
 373     }
 374 
 375     //-----------------------------------------------------------------------
 376     /**
 377      * Returns a string describing this DecimalStyle.
 378      *
 379      * @return a string description, not null
 380      */
 381     @Override
 382     public String toString() {
 383         return "DecimalStyle[" + zeroDigit + positiveSign + negativeSign + decimalSeparator + "]";
 384     }
 385 
 386 }