View Javadoc

1   /*
2    *  Copyright 2009-2013 Stephen Colebourne
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  package org.joda.money.format;
17  
18  import java.io.Serializable;
19  import java.lang.reflect.Method;
20  import java.text.DecimalFormat;
21  import java.text.DecimalFormatSymbols;
22  import java.text.NumberFormat;
23  import java.util.Locale;
24  import java.util.concurrent.ConcurrentHashMap;
25  import java.util.concurrent.ConcurrentMap;
26  
27  /**
28   * Defines the style that the amount of a monetary value will be formatted with.
29   * <p>
30   * The style contains a number of fields that may be configured based on the locale:
31   * <ul>
32   * <li>character used for zero, which defined all the numbers from zero to nine
33   * <li>character used for positive and negative symbols
34   * <li>character used for the decimal point
35   * <li>whether and how to group the amount
36   * <li>character used for grouping, such as grouping thousands
37   * <li>size for each group, such as 3 for thousands
38   * <li>whether to always use a decimal point
39   * </ul>
40   * <p>
41   * The style can be used in three basic ways.
42   * <ul>
43   * <li>set all the fields manually, resulting in the same amount style for all locales
44   * <li>call {@link #localize} manually and optionally adjust to set as required
45   * <li>leave the localized fields as {@code null} and let the locale in the
46   *  formatter to determine the style
47   * </ul>
48   * <p>
49   * This class is immutable and thread-safe.
50   */
51  public final class MoneyAmountStyle implements Serializable {
52  
53      /**
54       * A style that uses ASCII digits/negative sign, the decimal point
55       * and groups large amounts in 3's using a comma.
56       * Forced decimal point is disabled.
57       */
58      public static final MoneyAmountStyle ASCII_DECIMAL_POINT_GROUP3_COMMA =
59          new MoneyAmountStyle('0', '+', '-', '.', GroupingStyle.FULL, ',', 3, false);
60      /**
61       * A style that uses ASCII digits/negative sign, the decimal point
62       * and groups large amounts in 3's using a space.
63       * Forced decimal point is disabled.
64       */
65      public static final MoneyAmountStyle ASCII_DECIMAL_POINT_GROUP3_SPACE =
66          new MoneyAmountStyle('0', '+', '-', '.', GroupingStyle.FULL, ' ', 3, false);
67      /**
68       * A style that uses ASCII digits/negative sign, the decimal point
69       * and no grouping of large amounts.
70       * Forced decimal point is disabled.
71       */
72      public static final MoneyAmountStyle ASCII_DECIMAL_POINT_NO_GROUPING =
73          new MoneyAmountStyle('0', '+', '-', '.', GroupingStyle.NONE, ',', 3, false);
74      /**
75       * A style that uses ASCII digits/negative sign, the decimal comma
76       * and groups large amounts in 3's using a dot.
77       * Forced decimal point is disabled.
78       */
79      public static final MoneyAmountStyle ASCII_DECIMAL_COMMA_GROUP3_DOT =
80          new MoneyAmountStyle('0', '+', '-', ',', GroupingStyle.FULL, '.', 3, false);
81      /**
82       * A style that uses ASCII digits/negative sign, the decimal comma
83       * and groups large amounts in 3's using a space.
84       * Forced decimal point is disabled.
85       */
86      public static final MoneyAmountStyle ASCII_DECIMAL_COMMA_GROUP3_SPACE =
87          new MoneyAmountStyle('0', '+', '-', ',', GroupingStyle.FULL, ' ', 3, false);
88      /**
89       * A style that uses ASCII digits/negative sign, the decimal point
90       * and no grouping of large amounts.
91       * Forced decimal point is disabled.
92       */
93      public static final MoneyAmountStyle ASCII_DECIMAL_COMMA_NO_GROUPING =
94          new MoneyAmountStyle('0', '+', '-', ',', GroupingStyle.NONE, '.', 3, false);
95      /**
96       * A style that will be filled in with localized values using the locale of the formatter.
97       * Grouping is enabled. Forced decimal point is disabled.
98       */
99      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 }