diff --git a/src/main/java/org/javamoney/moneta/internal/JDKObjects.java b/src/main/java/org/javamoney/moneta/internal/JDKObjects.java new file mode 100644 index 0000000..f1c1840 --- /dev/null +++ b/src/main/java/org/javamoney/moneta/internal/JDKObjects.java @@ -0,0 +1,34 @@ +/* + Copyright (c) 2020, Werner Keil and others by the @author tag. + + Licensed under the Apache License, Version 2.0 (the "License"); you may not + use this file except in compliance with the License. You may obtain a copy of + the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + License for the specific language governing permissions and limitations under + the License. + */ +package org.javamoney.moneta.internal; + +public class JDKObjects { + /** + * JDK Drop-in-replacement + * @since 1.4.1 + */ + public static boolean nonNull(Object obj) { + return obj != null; + } + + /** + * JDK Drop-in-replacement + * @since 1.4.1 + */ + public static boolean isNull(Object obj) { + return obj == null; + } +} diff --git a/src/main/java/org/javamoney/moneta/spi/MoneyUtils.java b/src/main/java/org/javamoney/moneta/spi/MoneyUtils.java index c687380..d8e3d82 100644 --- a/src/main/java/org/javamoney/moneta/spi/MoneyUtils.java +++ b/src/main/java/org/javamoney/moneta/spi/MoneyUtils.java @@ -1,5 +1,5 @@ /* - Copyright (c) 2012, 2014, Credit Suisse (Anatole Tresch), Werner Keil and others by the @author tag. + Copyright (c) 2012, 2020, Werner Keil, Otavio Santana and others by the @author tag. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of @@ -15,16 +15,22 @@ */ package org.javamoney.moneta.spi; +import org.javamoney.moneta.internal.JDKObjects; + import javax.money.CurrencyUnit; import javax.money.MonetaryAmount; import javax.money.MonetaryContext; import javax.money.MonetaryException; + import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; -import java.util.Objects; import java.util.logging.Logger; +import static java.math.RoundingMode.HALF_EVEN; +import static java.util.Objects.requireNonNull; +import static java.util.logging.Level.FINEST; + /** * Platform RI: This utility class simplifies implementing {@link MonetaryAmount}, * by providing the common functionality. The different explicitly typed methods @@ -34,13 +40,17 @@ * implement {@link MonetaryAmount} directly. * * @author Anatole Tresch + * @author Werner Keil */ public final class MoneyUtils { - /** - * The logger used. - */ + private static final Logger LOG = Logger.getLogger(MoneyUtils.class.getName()); + public static final String NBSP_STRING = "\u00A0"; + public static final String NNBSP_STRING = "\u202F"; + public static final char NBSP = NBSP_STRING.charAt(0); + public static final char NNBSP = NNBSP_STRING.charAt(0); + private MoneyUtils() { } @@ -94,13 +104,18 @@ public static BigDecimal getBigDecimal(Number num) { * @return the corresponding {@link BigDecimal} */ public static BigDecimal getBigDecimal(Number num, MonetaryContext moneyContext) { - BigDecimal bd = getBigDecimal(num); - if (moneyContext!=null) { - MathContext mc = getMathContext(moneyContext, RoundingMode.HALF_EVEN); + BigDecimal bd = getBigDecimal(num); + if (JDKObjects.nonNull(moneyContext)) { + MathContext mc = getMathContext(moneyContext, HALF_EVEN); bd = new BigDecimal(bd.toString(), mc); - if (moneyContext.getMaxScale() > 0) { - LOG.fine(String.format("Got Max Scale %s", moneyContext.getMaxScale())); - bd = bd.setScale(moneyContext.getMaxScale(), mc.getRoundingMode()); + int maxScale = moneyContext.getMaxScale(); + if (maxScale > 0) { + if (bd.scale() > maxScale) { + if (LOG.isLoggable(FINEST)) { + LOG.log(FINEST, "The number scale is " + bd.scale() + " but Max Scale is " + maxScale); + } + bd = bd.setScale(maxScale, mc.getRoundingMode()); + } } } return bd; @@ -116,15 +131,12 @@ public static BigDecimal getBigDecimal(Number num, MonetaryContext moneyContext) */ public static MathContext getMathContext(MonetaryContext monetaryContext, RoundingMode defaultMode) { MathContext ctx = monetaryContext.get(MathContext.class); - if (ctx!=null) { + if (JDKObjects.nonNull(ctx)) { return ctx; } RoundingMode roundingMode = monetaryContext.get(RoundingMode.class); if (roundingMode == null) { - roundingMode = defaultMode; - } - if (roundingMode == null) { - roundingMode = RoundingMode.HALF_EVEN; + roundingMode = HALF_EVEN; } return new MathContext(monetaryContext.getPrecision(), roundingMode); } @@ -139,9 +151,9 @@ public static MathContext getMathContext(MonetaryContext monetaryContext, Roundi * {@link CurrencyUnit#getCurrencyCode()}). */ public static void checkAmountParameter(MonetaryAmount amount, CurrencyUnit currencyUnit) { - Objects.requireNonNull(amount, "Amount must not be null."); + requireNonNull(amount, "Amount must not be null."); final CurrencyUnit amountCurrency = amount.getCurrency(); - if (!(currencyUnit.getCurrencyCode().equals(amountCurrency.getCurrencyCode()))) { + if (!currencyUnit.getCurrencyCode().equals(amountCurrency.getCurrencyCode())) { throw new MonetaryException("Currency mismatch: " + currencyUnit + '/' + amountCurrency); } } @@ -153,7 +165,14 @@ public static void checkAmountParameter(MonetaryAmount amount, CurrencyUnit curr * @throws IllegalArgumentException If the number is null */ public static void checkNumberParameter(Number number) { - Objects.requireNonNull(number, "Number is required."); + requireNonNull(number, "Number is required."); } + /** + * Replaces the non-breaking space character U+00A0 and Narrow non-breaking space U+202F from the string with usual space. + * https://en.wikipedia.org/wiki/Non-breaking_space} + */ + public static String replaceNbspWithSpace(String s) { + return s.replace(NBSP, ' ').replace(NNBSP, ' '); + } } diff --git a/src/main/java/org/javamoney/moneta/spi/format/AmountNumberToken.java b/src/main/java/org/javamoney/moneta/spi/format/AmountNumberToken.java index 6d33158..f57e034 100644 --- a/src/main/java/org/javamoney/moneta/spi/format/AmountNumberToken.java +++ b/src/main/java/org/javamoney/moneta/spi/format/AmountNumberToken.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2014, Credit Suisse (Anatole Tresch), Werner Keil and others by the @author tag. + * Copyright (c) 2012, 2020, Werner Keil and others by the @author tag. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -15,18 +15,24 @@ */ package org.javamoney.moneta.spi.format; -import org.javamoney.moneta.format.AmountFormatParams; +import org.javamoney.moneta.internal.JDKObjects; +import org.javamoney.moneta.spi.MoneyUtils; +import javax.money.MonetaryAmount; +import javax.money.format.AmountFormatContext; +import javax.money.format.MonetaryParseException; import java.io.IOException; import java.math.BigDecimal; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; +import java.text.ParsePosition; import java.util.Locale; import java.util.logging.Logger; -import javax.money.MonetaryAmount; -import javax.money.format.AmountFormatContext; -import javax.money.format.MonetaryParseException; +import static java.util.Objects.requireNonNull; +import static org.javamoney.moneta.format.AmountFormatParams.GROUPING_GROUPING_SEPARATORS; +import static org.javamoney.moneta.format.AmountFormatParams.GROUPING_SIZES; +import static org.javamoney.moneta.spi.MoneyUtils.replaceNbspWithSpace; /** * {@link FormatToken} which allows to format a {@link MonetaryAmount} type. @@ -37,40 +43,47 @@ final class AmountNumberToken implements FormatToken { private final AmountFormatContext amountFormatContext; - private String partialNumberPattern; + private final String partialNumberPattern; private DecimalFormat parseFormat; private DecimalFormat formatFormat; private StringGrouper numberGroup; - public AmountNumberToken(AmountFormatContext amountFormatContext, String partialNumberPattern) { + AmountNumberToken(AmountFormatContext amountFormatContext, String partialNumberPattern) { + requireNonNull(amountFormatContext, "amountFormatContext is required."); + requireNonNull(partialNumberPattern, "partialNumberPattern is required."); this.amountFormatContext = amountFormatContext; - if(amountFormatContext==null) { - throw new IllegalArgumentException( - "amountFormatContext is required."); - } - this.partialNumberPattern = partialNumberPattern; + this.partialNumberPattern = replaceNbspWithSpace(partialNumberPattern); initDecimalFormats(); } private void initDecimalFormats() { - formatFormat = (DecimalFormat) DecimalFormat.getInstance(amountFormatContext.get(Locale.class)); - parseFormat = (DecimalFormat) DecimalFormat.getInstance(amountFormatContext.get(Locale.class)); + Locale locale = amountFormatContext.get(Locale.class); + formatFormat = (DecimalFormat) DecimalFormat.getInstance(locale); + formatFormat.applyPattern(MoneyUtils.replaceNbspWithSpace(formatFormat.toPattern())); + parseFormat = (DecimalFormat) formatFormat.clone(); DecimalFormatSymbols syms = amountFormatContext.get(DecimalFormatSymbols.class); - if (syms!=null) { - formatFormat.setDecimalFormatSymbols(syms); - parseFormat.setDecimalFormatSymbols(syms); - } - formatFormat.applyPattern(this.partialNumberPattern); - parseFormat.applyPattern(this.partialNumberPattern.trim()); - // Fix for https://github.com/JavaMoney/jsr354-ri/issues/151 - if ("BG".equals(amountFormatContext.getLocale().getCountry())) { - formatFormat.setGroupingSize(3); - formatFormat.setGroupingUsed(true); + if (JDKObjects.nonNull(syms)) { + syms = (DecimalFormatSymbols) syms.clone(); + } else { syms = formatFormat.getDecimalFormatSymbols(); - syms.setDecimalSeparator(','); - syms.setGroupingSeparator(' '); - formatFormat.setDecimalFormatSymbols(syms); - parseFormat.setDecimalFormatSymbols(syms); + } + fixThousandsSeparatorWithSpace(syms); + formatFormat.setDecimalFormatSymbols(syms); + parseFormat.setDecimalFormatSymbols(syms); + + formatFormat.applyPattern(partialNumberPattern); + parseFormat.applyPattern(partialNumberPattern.trim()); + } + + private void fixThousandsSeparatorWithSpace(DecimalFormatSymbols symbols) { + if(Character.isSpaceChar(formatFormat.getDecimalFormatSymbols().getGroupingSeparator())){ + symbols.setGroupingSeparator(' '); + } + if(Character.isWhitespace(formatFormat.getDecimalFormatSymbols().getDecimalSeparator())){ + symbols.setDecimalSeparator(' '); + } + if(Character.isWhitespace(formatFormat.getDecimalFormatSymbols().getMonetaryDecimalSeparator())){ + symbols.setMonetaryDecimalSeparator(' '); } } @@ -90,40 +103,34 @@ public AmountFormatContext getAmountFormatContext() { * @return the number pattern used, never {@code null}. */ public String getNumberPattern() { - return this.partialNumberPattern; + return partialNumberPattern; } @Override public void print(Appendable appendable, MonetaryAmount amount) throws IOException { - if (amountFormatContext.get(AmountFormatParams.GROUPING_SIZES, int[].class) == null || - amountFormatContext.get(AmountFormatParams.GROUPING_SIZES, int[].class).length == 0) { - appendable.append(this.formatFormat.format(amount.getNumber() - .numberValue(BigDecimal.class))); + int[] groupSizes = amountFormatContext.get(GROUPING_SIZES, int[].class); + if (groupSizes == null || groupSizes.length == 0) { + String preformattedValue = formatFormat.format(amount.getNumber().numberValue(BigDecimal.class)); + appendable.append(preformattedValue); return; } - this.formatFormat.setGroupingUsed(false); - String preformattedValue = this.formatFormat.format(amount.getNumber() - .numberValue(BigDecimal.class)); - String[] numberParts = splitNumberParts(this.formatFormat, - preformattedValue); + formatFormat.setGroupingUsed(false); + String preformattedValue = formatFormat.format(amount.getNumber().numberValue(BigDecimal.class)); + String[] numberParts = splitNumberParts(formatFormat, preformattedValue); if (numberParts.length != 2) { appendable.append(preformattedValue); } else { - if (numberGroup==null) { - char[] groupChars = amountFormatContext.get(AmountFormatParams.GROUPING_GROUPING_SEPARATORS, char[].class); + if (JDKObjects.isNull(numberGroup)) { + char[] groupChars = amountFormatContext.get(GROUPING_GROUPING_SEPARATORS, char[].class); if (groupChars == null || groupChars.length == 0) { - groupChars = new char[]{this.formatFormat - .getDecimalFormatSymbols().getGroupingSeparator()}; - } - int[] groupSizes = amountFormatContext.get(AmountFormatParams.GROUPING_SIZES, int[].class); - if (groupSizes == null) { - groupSizes = new int[0]; + char groupingSeparator = formatFormat.getDecimalFormatSymbols().getGroupingSeparator(); + groupChars = new char[]{groupingSeparator}; } numberGroup = new StringGrouper(groupChars, groupSizes); } preformattedValue = numberGroup.group(numberParts[0]) - + this.formatFormat.getDecimalFormatSymbols() + + formatFormat.getDecimalFormatSymbols() .getDecimalSeparator() + numberParts[1]; appendable.append(preformattedValue); } @@ -131,20 +138,21 @@ public void print(Appendable appendable, MonetaryAmount amount) private String[] splitNumberParts(DecimalFormat format, String preformattedValue) { - int index = preformattedValue.indexOf(format.getDecimalFormatSymbols() - .getDecimalSeparator()); + char decimalSeparator = format.getDecimalFormatSymbols().getDecimalSeparator(); + int index = preformattedValue.indexOf(decimalSeparator); if (index < 0) { return new String[]{preformattedValue}; } - return new String[]{preformattedValue.substring(0, index), - preformattedValue.substring(index + 1)}; + String beforeSeparator = preformattedValue.substring(0, index); + String afterSeparator = preformattedValue.substring(index + 1); + return new String[]{beforeSeparator, afterSeparator}; } @Override public void parse(ParseContext context) throws MonetaryParseException { - String token = context.lookupNextToken(); - if (token!=null && !context.isComplete()) { - parseToken(context, token); + context.skipWhitespace(); + if (!context.isFullyParsed()) { + parseToken(context); if (context.hasError()) { throw new MonetaryParseException(context.getErrorMessage(), context.getInput(), context.getIndex()); } @@ -154,19 +162,65 @@ public void parse(ParseContext context) throws MonetaryParseException { } } - private void parseToken(ParseContext context, String token) { - try { - Number number = this.parseFormat.parse(token); - if (number!=null) { - context.setParsedNumber(number); - context.consume(token); - } - } catch (Exception e) { - Logger.getLogger(getClass().getName()).finest( - "Could not parse amount from: " + token); + private void parseToken(ParseContext context) { + ParsePosition pos = new ParsePosition(context.getIndex()); + String consumedInput = context.getInput().toString(); + + // Check for amount with currenccy, so we only parse the amount part... + int[] range = evalNumberRange(consumedInput); // 0: firstDigit, 1: lastDigit + if(range[0]<0){ + context.setError(); + context.setErrorIndex(0); + context.setErrorMessage("No digits found: \"" + context.getOriginalInput() + "\""); + return; + } + consumedInput = consumedInput.substring(0, range[1]+1); + String input = consumedInput.substring(0, range[0]) + // any literal part + consumedInput.substring(range[0]) // number part, without any spaces. + .replace(" ", "") + .replace(MoneyUtils.NBSP_STRING, "") + .replace(MoneyUtils.NNBSP_STRING, ""); + pos = new ParsePosition(0); + Number number = parseFormat.parse(input, pos); + if (JDKObjects.nonNull(number)) { + context.setParsedNumber(number); + context.consume(consumedInput); + } else { + Logger.getLogger(getClass().getName()).finest("Could not parse amount from: " + context.getOriginalInput()); context.setError(); - context.setErrorMessage(e.getMessage()); + context.setErrorIndex(pos.getErrorIndex()); + context.setErrorMessage("Unparseable number: \"" + context.getOriginalInput() + "\""); } } + private int[] evalNumberRange(String input) { + int firstDigit = -1; + int lastDigit = -1; + for(int i=0;i0) { + break; + } + } + } + return new int[]{firstDigit, lastDigit}; + } + + @Override + public String toString() { + Locale locale = amountFormatContext.getLocale(); + return "AmountNumberToken [locale=" + locale + ", partialNumberPattern=" + partialNumberPattern + ']'; + } } diff --git a/src/main/java/org/javamoney/moneta/spi/format/DefaultMonetaryAmountFormat.java b/src/main/java/org/javamoney/moneta/spi/format/DefaultMonetaryAmountFormat.java index 47f022c..c0e4a9a 100644 --- a/src/main/java/org/javamoney/moneta/spi/format/DefaultMonetaryAmountFormat.java +++ b/src/main/java/org/javamoney/moneta/spi/format/DefaultMonetaryAmountFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2014, Credit Suisse (Anatole Tresch), Werner Keil and others by the @author tag. + * Copyright (c) 2012, 2020, Werner Keil and others by the @author tag. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of @@ -19,6 +19,8 @@ import javax.money.MonetaryAmount; import javax.money.MonetaryAmountFactory; import javax.money.Monetary; + +import org.javamoney.moneta.format.AmountFormatParams; import org.javamoney.moneta.format.CurrencyStyle; import java.io.IOException; @@ -32,6 +34,7 @@ import java.util.logging.Logger; import javax.money.format.AmountFormatContext; +import javax.money.format.AmountFormatContextBuilder; import javax.money.format.MonetaryAmountFormat; import javax.money.format.MonetaryParseException; @@ -77,6 +80,25 @@ final class DefaultMonetaryAmountFormat implements MonetaryAmountFormat { * @param amountFormatContext the {@link javax.money.format.AmountFormatContext} to be used, not {@code null}. */ DefaultMonetaryAmountFormat(AmountFormatContext amountFormatContext) { + Locale locale = amountFormatContext.getLocale(); + if(locale != null && "IN".equals(locale.getCountry()) + && amountFormatContext.get(AmountFormatParams.GROUPING_SIZES, int[].class)==null) { + // Fix invalid JDK grouping for rupees... + amountFormatContext = amountFormatContext.toBuilder().set(AmountFormatParams.GROUPING_SIZES, new int[]{3,2}) + .build(); + } + if(locale != null && "BG".equals(locale.getCountry())) { + AmountFormatContextBuilder builder = amountFormatContext.toBuilder(); + if(amountFormatContext.get(AmountFormatParams.GROUPING_SIZES, int[].class)==null) { + // Fix invalid JDK grouping for leva... + builder.set(AmountFormatParams.GROUPING_SIZES, new int[]{3}).build(); + } + if(amountFormatContext.get(AmountFormatParams.GROUPING_GROUPING_SEPARATORS, int[].class)==null) { + // Fix invalid JDK grouping for leva... + builder.set(AmountFormatParams.GROUPING_GROUPING_SEPARATORS, new String[]{"\u00A0"}).build(); + } + amountFormatContext = builder.build(); + } setAmountFormatContext(amountFormatContext); } @@ -232,7 +254,10 @@ private void setAmountFormatContext(AmountFormatContext amountFormatContext) { // Fix for https://github.com/JavaMoney/jsr354-ri/issues/151 if (amountFormatContext.getLocale() != null && "BG".equals(amountFormatContext.getLocale().getCountry())) { pattern = "#,##0.00 ¤"; - }else { + // At least in Java 7 there is no space between INR and the decimal pattern + } else if (amountFormatContext.getLocale() != null && "IN".equals(amountFormatContext.getLocale().getCountry())) { + pattern = "¤ #,##0.00"; + } else { pattern = ((DecimalFormat) DecimalFormat.getCurrencyInstance(amountFormatContext.getLocale())).toPattern(); } } diff --git a/src/test/java/org/javamoney/moneta/format/MonetaryFormatsTest.java b/src/test/java/org/javamoney/moneta/format/MonetaryFormatsTest.java index 5096b57..0d454da 100644 --- a/src/test/java/org/javamoney/moneta/format/MonetaryFormatsTest.java +++ b/src/test/java/org/javamoney/moneta/format/MonetaryFormatsTest.java @@ -36,7 +36,7 @@ public class MonetaryFormatsTest { private static final Locale DANISH = new Locale("da"); private static final Locale BULGARIA = new Locale("bg", "BG"); - public static final Locale INDIA = new Locale("en, IN"); + public static final Locale INDIA = new Locale("en", "IN"); @Test public void testParse_DKK_da() { @@ -121,8 +121,8 @@ public void testParse_INR_en_IN() { @Test public void testFormat_INR_en_IN() { MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(INDIA); - assertMoneyFormat(format, Money.of(67890000000000L, "INR"), "INR 67,890,000,000,000.00"); -// assertMoneyFormat(format, Money.of(67890000000000L, "INR"), "INR 6,78,90,00,00,00,000.00"); TODO: https://github.com/JavaMoney/jsr354-ri-bp/issues/55 + //assertMoneyFormat(format, Money.of(67890000000000L, "INR"), "INR 67,890,000,000,000.00"); + assertMoneyFormat(format, Money.of(67890000000000L, "INR"), "INR 6,78,90,00,00,00,000.00"); //TODO: https://github.com/JavaMoney/jsr354-ri-bp/issues/55 } @Test