001    /*
002     *  Copyright 2009-2013 Stephen Colebourne
003     *
004     *  Licensed under the Apache License, Version 2.0 (the "License");
005     *  you may not use this file except in compliance with the License.
006     *  You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     *  Unless required by applicable law or agreed to in writing, software
011     *  distributed under the License is distributed on an "AS IS" BASIS,
012     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     *  See the License for the specific language governing permissions and
014     *  limitations under the License.
015     */
016    package org.joda.money.format;
017    
018    import java.io.Serializable;
019    import java.lang.reflect.Method;
020    import java.text.DecimalFormat;
021    import java.text.DecimalFormatSymbols;
022    import java.text.NumberFormat;
023    import java.util.Locale;
024    import java.util.concurrent.ConcurrentHashMap;
025    import java.util.concurrent.ConcurrentMap;
026    
027    /**
028     * Defines the style that the amount of a monetary value will be formatted with.
029     * <p>
030     * The style contains a number of fields that may be configured based on the locale:
031     * <ul>
032     * <li>character used for zero, which defined all the numbers from zero to nine
033     * <li>character used for positive and negative symbols
034     * <li>character used for the decimal point
035     * <li>whether and how to group the amount
036     * <li>character used for grouping, such as grouping thousands
037     * <li>size for each group, such as 3 for thousands
038     * <li>whether to always use a decimal point
039     * </ul>
040     * <p>
041     * The style can be used in three basic ways.
042     * <ul>
043     * <li>set all the fields manually, resulting in the same amount style for all locales
044     * <li>call {@link #localize} manually and optionally adjust to set as required
045     * <li>leave the localized fields as {@code null} and let the locale in the
046     *  formatter to determine the style
047     * </ul>
048     * <p>
049     * This class is immutable and thread-safe.
050     */
051    public final class MoneyAmountStyle implements Serializable {
052    
053        /**
054         * A style that uses ASCII digits/negative sign, the decimal point
055         * and groups large amounts in 3's using a comma.
056         * Forced decimal point is disabled.
057         */
058        public static final MoneyAmountStyle ASCII_DECIMAL_POINT_GROUP3_COMMA =
059            new MoneyAmountStyle('0', '+', '-', '.', GroupingStyle.FULL, ',', 3, false);
060        /**
061         * A style that uses ASCII digits/negative sign, the decimal point
062         * and groups large amounts in 3's using a space.
063         * Forced decimal point is disabled.
064         */
065        public static final MoneyAmountStyle ASCII_DECIMAL_POINT_GROUP3_SPACE =
066            new MoneyAmountStyle('0', '+', '-', '.', GroupingStyle.FULL, ' ', 3, false);
067        /**
068         * A style that uses ASCII digits/negative sign, the decimal point
069         * and no grouping of large amounts.
070         * Forced decimal point is disabled.
071         */
072        public static final MoneyAmountStyle ASCII_DECIMAL_POINT_NO_GROUPING =
073            new MoneyAmountStyle('0', '+', '-', '.', GroupingStyle.NONE, ',', 3, false);
074        /**
075         * A style that uses ASCII digits/negative sign, the decimal comma
076         * and groups large amounts in 3's using a dot.
077         * Forced decimal point is disabled.
078         */
079        public static final MoneyAmountStyle ASCII_DECIMAL_COMMA_GROUP3_DOT =
080            new MoneyAmountStyle('0', '+', '-', ',', GroupingStyle.FULL, '.', 3, false);
081        /**
082         * A style that uses ASCII digits/negative sign, the decimal comma
083         * and groups large amounts in 3's using a space.
084         * Forced decimal point is disabled.
085         */
086        public static final MoneyAmountStyle ASCII_DECIMAL_COMMA_GROUP3_SPACE =
087            new MoneyAmountStyle('0', '+', '-', ',', GroupingStyle.FULL, ' ', 3, false);
088        /**
089         * A style that uses ASCII digits/negative sign, the decimal point
090         * and no grouping of large amounts.
091         * Forced decimal point is disabled.
092         */
093        public static final MoneyAmountStyle ASCII_DECIMAL_COMMA_NO_GROUPING =
094            new MoneyAmountStyle('0', '+', '-', ',', GroupingStyle.NONE, '.', 3, false);
095        /**
096         * A style that will be filled in with localized values using the locale of the formatter.
097         * Grouping is enabled. Forced decimal point is disabled.
098         */
099        public static final MoneyAmountStyle LOCALIZED_GROUPING =
100            new MoneyAmountStyle(-1, -1, -1, -1, GroupingStyle.FULL, -1, -1, false);
101        /**
102         * A style that will be filled in with localized values using the locale of the formatter.
103         * Grouping is disabled. Forced decimal point is disabled.
104         */
105        public static final MoneyAmountStyle LOCALIZED_NO_GROUPING =
106            new MoneyAmountStyle(-1, -1, -1, -1, GroupingStyle.NONE, -1, -1, false);
107        /**
108         * Cache of localized styles.
109         */
110        private static final ConcurrentMap<Locale, MoneyAmountStyle> LOCALIZED_CACHE = new ConcurrentHashMap<Locale, MoneyAmountStyle>();
111        /**
112         * Serialization version.
113         */
114        private static final long serialVersionUID = 1L;
115    
116        /**
117         * The character defining zero, and thus the numbers zero to nine.
118         */
119        private final int zeroCharacter;
120        /**
121         * The character representing the positive sign.
122         */
123        private final int positiveCharacter;
124        /**
125         * The prefix string when the amount is negative.
126         */
127        private final int negativeCharacter;
128        /**
129         * The character used for the decimal point.
130         */
131        private final int decimalPointCharacter;
132        /**
133         * Whether to group or not.
134         */
135        private final GroupingStyle groupingStyle;
136        /**
137         * The character used for grouping.
138         */
139        private final int groupingCharacter;
140        /**
141         * The size of each group.
142         */
143        private final int groupingSize;
144        /**
145         * Whether to always require the decimal point to be visible.
146         */
147        private final boolean forceDecimalPoint;
148    
149        //-----------------------------------------------------------------------
150        /**
151         * Gets a localized style.
152         * <p>
153         * This creates a localized style for the specified locale.
154         * Grouping will be enabled, forced decimal point will be disabled.
155         *
156         * @param locale  the locale to use, not null
157         * @return the new instance, never null
158         */
159        public static MoneyAmountStyle of(Locale locale) {
160            return getLocalizedStyle(locale);
161        }
162    
163        //-----------------------------------------------------------------------
164        /**
165         * Constructor, creating a new monetary instance.
166         * 
167         * @param zeroCharacter  the zero character
168         * @param negativeCharacter  the negative sign
169         * @param decimalPointCharacter  the decimal point character
170         * @param groupingStyle  the grouping style, not null
171         * @param groupingCharacter  the grouping character
172         * @param groupingSize  the grouping size
173         * @param forceDecimalPoint  whether to always use the decimal point character
174         * @param postiveCharacter  the positive sign
175         */
176        private MoneyAmountStyle(
177                    int zeroCharacter,
178                    int positiveCharacter, int negativeCharacter,
179                    int decimalPointCharacter, GroupingStyle groupingStyle,
180                    int groupingCharacter, int groupingSize, boolean forceDecimalPoint) {
181            this.zeroCharacter = zeroCharacter;
182            this.positiveCharacter = positiveCharacter;
183            this.negativeCharacter = negativeCharacter;
184            this.decimalPointCharacter = decimalPointCharacter;
185            this.groupingStyle = groupingStyle;
186            this.groupingCharacter = groupingCharacter;
187            this.groupingSize = groupingSize;
188            this.forceDecimalPoint = forceDecimalPoint;
189        }
190    
191        //-----------------------------------------------------------------------
192        /**
193         * Returns a {@code MoneyAmountStyle} instance configured for the specified locale.
194         * <p>
195         * This method will return a new instance where each field that was defined
196         * to be localized (by being set to {@code null}) is filled in.
197         * If this instance is fully defined (with all fields non-null), then this
198         * method has no effect. Once this method is called, no method will return null.
199         * <p>
200         * The settings for the locale are pulled from {@link DecimalFormatSymbols} and
201         * {@link DecimalFormat}.
202         * 
203         * @param locale  the locale to use, not null
204         * @return the new instance for chaining, never null
205         */
206        public MoneyAmountStyle localize(Locale locale) {
207            MoneyFormatter.checkNotNull(locale, "Locale must not be null");
208            MoneyAmountStyle result = this;
209            MoneyAmountStyle protoStyle = null;
210            if (zeroCharacter < 0) {
211                protoStyle = getLocalizedStyle(locale);
212                result = result.withZeroCharacter(protoStyle.getZeroCharacter());
213            }
214            if (positiveCharacter < 0) {
215                protoStyle = getLocalizedStyle(locale);
216                result = result.withPositiveSignCharacter(protoStyle.getPositiveSignCharacter());
217            }
218            if (negativeCharacter < 0) {
219                protoStyle = getLocalizedStyle(locale);
220                result = result.withNegativeSignCharacter(protoStyle.getNegativeSignCharacter());
221            }
222            if (decimalPointCharacter < 0) {
223                protoStyle = (protoStyle == null ? getLocalizedStyle(locale) : protoStyle);
224                result = result.withDecimalPointCharacter(protoStyle.getDecimalPointCharacter());
225            }
226            if (groupingCharacter < 0) {
227                protoStyle = (protoStyle == null ? getLocalizedStyle(locale) : protoStyle);
228                result = result.withGroupingCharacter(protoStyle.getGroupingCharacter());
229            }
230            if (groupingSize < 0) {
231                protoStyle = (protoStyle == null ? getLocalizedStyle(locale) : protoStyle);
232                result = result.withGroupingSize(protoStyle.getGroupingSize());
233            }
234            return result;
235        }
236    
237        //-----------------------------------------------------------------------
238        /**
239         * Gets the prototype localized style for the given locale.
240         * <p>
241         * This uses {@link DecimalFormatSymbols} and {@link NumberFormat}.
242         * <p>
243         * If JDK 6 or newer is being used, {@code DecimalFormatSymbols.getInstance(locale)}
244         * will be used in order to allow the use of locales defined as extensions.
245         * Otherwise, {@code new DecimalFormatSymbols(locale)} will be used.
246         * 
247         * @param locale  the {@link Locale} used to get the correct {@link DecimalFormatSymbols}
248         * @return the symbols, never null
249         */
250        private static MoneyAmountStyle getLocalizedStyle(Locale locale) {
251            MoneyAmountStyle protoStyle = LOCALIZED_CACHE.get(locale);
252            if (protoStyle == null) {
253                DecimalFormatSymbols symbols;
254                try {
255                    Method method = DecimalFormatSymbols.class.getMethod("getInstance", new Class[] {Locale.class});
256                    symbols = (DecimalFormatSymbols) method.invoke(null, new Object[] {locale});  // handle JDK 6
257                } catch (Exception ex) {
258                    symbols = new DecimalFormatSymbols(locale);  // handle JDK 5
259                }
260                NumberFormat format = NumberFormat.getCurrencyInstance(locale);
261                int size = (format instanceof DecimalFormat ? ((DecimalFormat) format).getGroupingSize() : 3);
262                protoStyle = new MoneyAmountStyle(
263                        symbols.getZeroDigit(),
264                        '+', symbols.getMinusSign(),
265                        symbols.getMonetaryDecimalSeparator(),
266                        GroupingStyle.FULL, symbols.getGroupingSeparator(), size, false);
267                LOCALIZED_CACHE.putIfAbsent(locale, protoStyle);
268            }
269            return protoStyle;
270        }
271    
272        //-----------------------------------------------------------------------
273        /**
274         * Gets the character used for zero, and defining the characters zero to nine.
275         * <p>
276         * The UTF-8 standard supports a number of different numeric scripts.
277         * Each script has the characters in order from zero to nine.
278         * This method returns the zero character, which therefore also defines one to nine.
279         * 
280         * @return the zero character, null if to be determined by locale
281         */
282        public Character getZeroCharacter() {
283            return zeroCharacter < 0 ? null : (char) zeroCharacter;
284        }
285    
286        /**
287         * Returns a copy of this style with the specified zero character.
288         * <p>
289         * The UTF-8 standard supports a number of different numeric scripts.
290         * Each script has the characters in order from zero to nine.
291         * This method sets the zero character, which therefore also defines one to nine.
292         * <p>
293         * For English, this is a '0'. Some other scripts use different characters
294         * for the numbers zero to nine.
295         * 
296         * @param zeroCharacter  the zero character, null if to be determined by locale
297         * @return the new instance for chaining, never null
298         */
299        public MoneyAmountStyle withZeroCharacter(Character zeroCharacter) {
300            int zeroVal = (zeroCharacter == null ? -1 : zeroCharacter);
301            if (zeroVal == this.zeroCharacter) {
302                return this;
303            }
304            return new MoneyAmountStyle(
305                    zeroVal,
306                    positiveCharacter, negativeCharacter,
307                    decimalPointCharacter, groupingStyle,
308                    groupingCharacter, groupingSize, forceDecimalPoint);
309        }
310    
311        //-----------------------------------------------------------------------
312        /**
313         * Gets the character used for the positive sign character.
314         * <p>
315         * The standard ASCII symbol is '+'.
316         * 
317         * @return the format for positive amounts, null if to be determined by locale
318         */
319        public Character getPositiveSignCharacter() {
320            return positiveCharacter < 0 ? null : (char) positiveCharacter;
321        }
322    
323        /**
324         * Returns a copy of this style with the specified positive sign character.
325         * <p>
326         * The standard ASCII symbol is '+'.
327         * 
328         * @param positiveCharacter  the positive character, null if to be determined by locale
329         * @return the new instance for chaining, never null
330         */
331        public MoneyAmountStyle withPositiveSignCharacter(Character positiveCharacter) {
332            int positiveVal = (positiveCharacter == null ? -1 : positiveCharacter);
333            if (positiveVal == this.positiveCharacter) {
334                return this;
335            }
336            return new MoneyAmountStyle(
337                    zeroCharacter,
338                    positiveVal, negativeCharacter,
339                    decimalPointCharacter, groupingStyle,
340                    groupingCharacter, groupingSize, forceDecimalPoint);
341        }
342    
343        //-----------------------------------------------------------------------
344        /**
345         * Gets the character used for the negative sign character.
346         * <p>
347         * The standard ASCII symbol is '-'.
348         * 
349         * @return the format for negative amounts, null if to be determined by locale
350         */
351        public Character getNegativeSignCharacter() {
352            return negativeCharacter < 0 ? null : (char) negativeCharacter;
353        }
354    
355        /**
356         * Returns a copy of this style with the specified negative sign character.
357         * <p>
358         * The standard ASCII symbol is '-'.
359         * 
360         * @param negativeCharacter  the negative character, null if to be determined by locale
361         * @return the new instance for chaining, never null
362         */
363        public MoneyAmountStyle withNegativeSignCharacter(Character negativeCharacter) {
364            int negativeVal = (negativeCharacter == null ? -1 : negativeCharacter);
365            if (negativeVal == this.negativeCharacter) {
366                return this;
367            }
368            return new MoneyAmountStyle(
369                    zeroCharacter,
370                    positiveCharacter, negativeVal,
371                    decimalPointCharacter, groupingStyle,
372                    groupingCharacter, groupingSize, forceDecimalPoint);
373        }
374    
375        //-----------------------------------------------------------------------
376        /**
377         * Gets the character used for the decimal point.
378         * 
379         * @return the decimal point character, null if to be determined by locale
380         */
381        public Character getDecimalPointCharacter() {
382            return decimalPointCharacter < 0 ? null : (char) decimalPointCharacter;
383        }
384    
385        /**
386         * Returns a copy of this style with the specified decimal point character.
387         * <p>
388         * For English, this is a dot.
389         * 
390         * @param decimalPointCharacter  the decimal point character, null if to be determined by locale
391         * @return the new instance for chaining, never null
392         */
393        public MoneyAmountStyle withDecimalPointCharacter(Character decimalPointCharacter) {
394            int dpVal = (decimalPointCharacter == null ? -1 : decimalPointCharacter);
395            if (dpVal == this.decimalPointCharacter) {
396                return this;
397            }
398            return new MoneyAmountStyle(
399                    zeroCharacter,
400                    positiveCharacter, negativeCharacter,
401                    dpVal, groupingStyle,
402                    groupingCharacter, groupingSize, forceDecimalPoint);
403        }
404    
405        //-----------------------------------------------------------------------
406        /**
407         * Gets the character used to separate groups, typically thousands.
408         * 
409         * @return the grouping character, null if to be determined by locale
410         */
411        public Character getGroupingCharacter() {
412            return groupingCharacter < 0 ? null : (char) groupingCharacter;
413        }
414    
415        /**
416         * Returns a copy of this style with the specified grouping character.
417         * <p>
418         * For English, this is a comma.
419         * 
420         * @param groupingCharacter  the grouping character, null if to be determined by locale
421         * @return the new instance for chaining, never null
422         */
423        public MoneyAmountStyle withGroupingCharacter(Character groupingCharacter) {
424            int groupingVal = (groupingCharacter == null ? -1 : groupingCharacter);
425            if (groupingVal == this.groupingCharacter) {
426                return this;
427            }
428            return new MoneyAmountStyle(
429                    zeroCharacter,
430                    positiveCharacter, negativeCharacter,
431                    decimalPointCharacter, groupingStyle,
432                    groupingVal, groupingSize, forceDecimalPoint);
433        }
434    
435        //-----------------------------------------------------------------------
436        /**
437         * Gets the size of each group, typically 3 for thousands.
438         * 
439         * @return the size of each group, null if to be determined by locale
440         */
441        public Integer getGroupingSize() {
442            return groupingSize < 0 ? null : groupingSize;
443        }
444    
445        /**
446         * Returns a copy of this style with the specified grouping size.
447         * 
448         * @param groupingSize  the size of each group, such as 3 for thousands,
449         *          not zero or negative, null if to be determined by locale
450         * @return the new instance for chaining, never null
451         * @throws IllegalArgumentException if the grouping size is zero or less
452         */
453        public MoneyAmountStyle withGroupingSize(Integer groupingSize) {
454            int sizeVal = (groupingSize == null ? -1 : groupingSize);
455            if (groupingSize != null && sizeVal <= 0) {
456                throw new IllegalArgumentException("Grouping size must be greater than zero");
457            }
458            if (sizeVal == this.groupingSize) {
459                return this;
460            }
461            return new MoneyAmountStyle(
462                    zeroCharacter,
463                    positiveCharacter, negativeCharacter,
464                    decimalPointCharacter, groupingStyle,
465                    groupingCharacter, sizeVal, forceDecimalPoint);
466        }
467    
468        //-----------------------------------------------------------------------
469        /**
470         * Gets whether to use the grouping separator, typically for thousands.
471         * 
472         * @return whether to use the grouping separator
473         * @deprecated Use getGroupingStyle()
474         */
475        @Deprecated
476        public boolean isGrouping() {
477            return getGroupingStyle() == GroupingStyle.FULL;
478        }
479    
480        /**
481         * Returns a copy of this style with the specified grouping setting.
482         * 
483         * @param grouping  true to use the grouping separator, false to not use it
484         * @return the new instance for chaining, never null
485         * @deprecated Use withGroupingStyle(GroupingStyle.FULL)
486         */
487        @Deprecated
488        public MoneyAmountStyle withGrouping(boolean grouping) {
489            return withGroupingStyle(grouping ? GroupingStyle.FULL : GroupingStyle.NONE);
490        }
491    
492        //-----------------------------------------------------------------------
493        /**
494         * Gets the style of grouping required.
495         * 
496         * @return the grouping style, not null
497         */
498        public GroupingStyle getGroupingStyle() {
499            return groupingStyle;
500        }
501    
502        /**
503         * Returns a copy of this style with the specified grouping setting.
504         * 
505         * @param groupingStyle  the grouping style, not null
506         * @return the new instance for chaining, never null
507         */
508        public MoneyAmountStyle withGroupingStyle(GroupingStyle groupingStyle) {
509            MoneyFormatter.checkNotNull(groupingStyle, "groupingStyle");
510            if (this.groupingStyle == groupingStyle) {
511                return this;
512            }
513            return new MoneyAmountStyle(
514                    zeroCharacter,
515                    positiveCharacter, negativeCharacter,
516                    decimalPointCharacter, groupingStyle,
517                    groupingCharacter, groupingSize, forceDecimalPoint);
518        }
519    
520        //-----------------------------------------------------------------------
521        /**
522         * Gets whether to always use the decimal point, even if there is no fraction.
523         * 
524         * @return whether to force the decimal point on output
525         */
526        public boolean isForcedDecimalPoint() {
527            return forceDecimalPoint;
528        }
529    
530        /**
531         * Returns a copy of this style with the specified decimal point setting.
532         * 
533         * @param forceDecimalPoint  true to force the use of the decimal point, false to use it if required
534         * @return the new instance for chaining, never null
535         */
536        public MoneyAmountStyle withForcedDecimalPoint(boolean forceDecimalPoint) {
537            if (this.forceDecimalPoint == forceDecimalPoint) {
538                return this;
539            }
540            return new MoneyAmountStyle(
541                    zeroCharacter,
542                    positiveCharacter, negativeCharacter,
543                    decimalPointCharacter, groupingStyle,
544                    groupingCharacter, groupingSize, forceDecimalPoint);
545        }
546    
547        //-----------------------------------------------------------------------
548        /**
549         * Compares this style with another.
550         * 
551         * @param other  the other style, null returns false
552         * @return true if equal
553         */
554        @Override
555        public boolean equals(Object other) {
556            if (other == this) {
557                return true;
558            }
559            if (other instanceof MoneyAmountStyle == false) {
560                return false;
561            }
562            MoneyAmountStyle otherStyle = (MoneyAmountStyle) other;
563            return (zeroCharacter == otherStyle.zeroCharacter) &&
564                    (positiveCharacter == otherStyle.positiveCharacter) &&
565                    (negativeCharacter == otherStyle.negativeCharacter) &&
566                    (decimalPointCharacter == otherStyle.decimalPointCharacter) &&
567                    (groupingStyle == otherStyle.groupingStyle) &&
568                    (groupingCharacter == otherStyle.groupingCharacter) &&
569                    (groupingSize == otherStyle.groupingSize) &&
570                    (forceDecimalPoint == otherStyle.forceDecimalPoint);
571        }
572    
573        /**
574         * A suitable hash code.
575         * 
576         * @return a hash code
577         */
578        @Override
579        public int hashCode() {
580            int hash = 13;
581            hash += zeroCharacter * 17;
582            hash += positiveCharacter * 17;
583            hash += negativeCharacter * 17;
584            hash += decimalPointCharacter * 17;
585            hash += groupingStyle.hashCode() * 17;
586            hash += groupingCharacter * 17;
587            hash += groupingSize * 17;
588            hash += (forceDecimalPoint ? 2 : 4);
589            return hash;
590        }
591    
592        //-----------------------------------------------------------------------
593        /**
594         * Gets a string summary of the style.
595         * 
596         * @return a string summarising the style, never null
597         */
598        @Override
599        public String toString() {
600            return "MoneyAmountStyle['" + getZeroCharacter() + "','" + getPositiveSignCharacter() + "','" +
601                getNegativeSignCharacter() + "','" + getDecimalPointCharacter() + "','" +
602                getGroupingStyle() + "," + getGroupingCharacter() + "','" + getGroupingSize() + "'," +
603                isForcedDecimalPoint() + "]";
604        }
605    
606    }