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 }