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 }