diff --git a/moneta-core/src/main/java/org/javamoney/moneta/FastMoney.java b/moneta-core/src/main/java/org/javamoney/moneta/FastMoney.java index a791e62e..f6ed35f1 100644 --- a/moneta-core/src/main/java/org/javamoney/moneta/FastMoney.java +++ b/moneta-core/src/main/java/org/javamoney/moneta/FastMoney.java @@ -194,7 +194,7 @@ public MonetaryContext getContext() { } private long getInternalNumber(Number number) { - BigDecimal bd = MoneyUtils.getBigDecimal(number); + BigDecimal bd = getStrippedBigDecimal(number); if (bd.scale() > SCALE) { throw new ArithmeticException(number + " can not be represented by this class, scale > " + SCALE); } @@ -206,6 +206,9 @@ private long getInternalNumber(Number number) { return bd.movePointRight(SCALE).longValue(); } + private static BigDecimal getStrippedBigDecimal(Number number) { + return MoneyUtils.stripScalingZeroes(MoneyUtils.getBigDecimal(number)); + } /** * Static factory method for creating a new instance of {@link FastMoney}. @@ -333,11 +336,12 @@ public FastMoney add(MonetaryAmount amount) { private void checkAmountParameter(MonetaryAmount amount) { MoneyUtils.checkAmountParameter(amount, this.currency); - // numeric check for overflow... - if (amount.getNumber().getScale() > SCALE) { + // numeric check for overflow, based on the effective (trailing-zero stripped) value + BigDecimal bd = MoneyUtils.stripScalingZeroes(amount.getNumber().numberValue(BigDecimal.class)); + if (bd.scale() > SCALE) { throw new ArithmeticException("Parameter exceeds maximal scale: " + SCALE); } - if (amount.getNumber().getPrecision() > MAX_BD.precision()) { + if (bd.precision() > MAX_BD.precision()) { throw new ArithmeticException("Parameter exceeds maximal precision: " + SCALE); } } @@ -427,7 +431,7 @@ public FastMoney remainder(Number divisor) { } private boolean isOne(Number number) { - BigDecimal bd = MoneyUtils.getBigDecimal(number); + BigDecimal bd = getStrippedBigDecimal(number); try { return bd.scale() == 0 && bd.longValueExact() == 1L; } catch (Exception e) { @@ -566,7 +570,7 @@ public boolean hasSameNumberAs(Number number) { */ @Override public NumberValue getNumber() { - return new DefaultNumberValue(getBigDecimal()); + return new DefaultNumberValue(MoneyUtils.stripScalingZeroes(getBigDecimal())); } @Override @@ -594,7 +598,7 @@ protected void checkNumber(Number number) { if (number.longValue() > MAX_BD.longValue()) { throw new ArithmeticException("Value exceeds maximal value: " + MAX_BD); } - BigDecimal bd = MoneyUtils.getBigDecimal(number); + BigDecimal bd = getStrippedBigDecimal(number); if (bd.precision() > MAX_BD.precision()) { throw new ArithmeticException("Precision exceeds maximal precision: " + MAX_BD.precision()); } diff --git a/moneta-core/src/main/java/org/javamoney/moneta/spi/ConvertBigDecimal.java b/moneta-core/src/main/java/org/javamoney/moneta/spi/ConvertBigDecimal.java index 86c3b47a..10728088 100644 --- a/moneta-core/src/main/java/org/javamoney/moneta/spi/ConvertBigDecimal.java +++ b/moneta-core/src/main/java/org/javamoney/moneta/spi/ConvertBigDecimal.java @@ -60,24 +60,21 @@ BigDecimal getDecimal(Number num) { NUMBERVALUE { @Override BigDecimal getDecimal(Number num) { - BigDecimal result = ((NumberValue)num).numberValue(BigDecimal.class); - return stripScalingZeroes(result); + return ((NumberValue)num).numberValue(BigDecimal.class); } }, /** Conversion from BigDecimal. */ BIGDECIMAL { @Override BigDecimal getDecimal(Number num) { - BigDecimal result = ((BigDecimal)num); - return stripScalingZeroes(result); + return (BigDecimal) num; } }, /** Conversion from BigDecimal, extended. */ BIGDECIMAL_EXTENDS { @Override BigDecimal getDecimal(Number num) { - BigDecimal result = ((BigDecimal)num).stripTrailingZeros(); - return stripScalingZeroes(result); + return (BigDecimal) num; } }, /** Default conversion based on String, if everything else failed. */ @@ -89,9 +86,8 @@ BigDecimal getDecimal(Number num) { result = new BigDecimal(num.toString()); } catch (NumberFormatException ignored) { } - result = Optional.ofNullable(result).orElse( + return Optional.ofNullable(result).orElse( BigDecimal.valueOf(num.doubleValue())); - return stripScalingZeroes(result); } }; @@ -131,14 +127,4 @@ private static ConvertBigDecimal factory(Number num) { private static final List> FLOATINGS = Arrays.asList( Float.class, Double.class); - - private static BigDecimal stripScalingZeroes(BigDecimal result) { - if (result.signum() == 0) { - return BigDecimal.ZERO; - } - if (result.scale() > 0) { - return result.stripTrailingZeros(); - } - return result; - } } diff --git a/moneta-core/src/main/java/org/javamoney/moneta/spi/MoneyUtils.java b/moneta-core/src/main/java/org/javamoney/moneta/spi/MoneyUtils.java index e76b4436..9ccd7a8b 100644 --- a/moneta-core/src/main/java/org/javamoney/moneta/spi/MoneyUtils.java +++ b/moneta-core/src/main/java/org/javamoney/moneta/spi/MoneyUtils.java @@ -120,6 +120,23 @@ public static BigDecimal getBigDecimal(Number num, MonetaryContext moneyContext) return bd; } + /** + * Returns the numeric value with any non-significant trailing zeros removed, mapping any + * zero value (including {@code -0}) to {@link BigDecimal#ZERO}. + * + * @param value the value to normalize, not null. + * @return the value with non-significant trailing zeros stripped. + */ + public static BigDecimal stripScalingZeroes(BigDecimal value) { + if (value.signum() == 0) { + return BigDecimal.ZERO; + } + if (value.scale() > 0) { + return value.stripTrailingZeros(); + } + return value; + } + /** * Evaluates the {@link MathContext} from the given {@link MonetaryContext}. * diff --git a/moneta-core/src/test/java/org/javamoney/moneta/MoneyTest.java b/moneta-core/src/test/java/org/javamoney/moneta/MoneyTest.java index 924cb256..91d4fd59 100644 --- a/moneta-core/src/test/java/org/javamoney/moneta/MoneyTest.java +++ b/moneta-core/src/test/java/org/javamoney/moneta/MoneyTest.java @@ -93,7 +93,7 @@ public void testDivideAndRemainder_BigDecimal() { Money[] divideAndRemainder = money1.divideAndRemainder(new BigDecimal("0.50000000000000000001")); assertEquals(divideAndRemainder[0].getNumber().numberValue(BigDecimal.class), BigDecimal.ONE); assertEquals(divideAndRemainder[1].getNumber().numberValue(BigDecimal.class), - new BigDecimal("0.5")); + new BigDecimal("0.5000000000000000")); } @Test @@ -1084,10 +1084,10 @@ public void testToString_ReverseOrder() { System.setProperty("org.javamoney.toStringFormatOrder", "ca"); assertEquals(Money.of(new BigDecimal("1.23455645"), "XXX").toString(), "XXX 1.23455645"); assertEquals("CHF 1234", Money.of(1234, "CHF").toString()); - assertEquals("CHF 1234", Money.of(new BigDecimal("1234.0"), "CHF").toString()); + assertEquals("CHF 1234.0", Money.of(new BigDecimal("1234.0"), "CHF").toString()); assertEquals("CHF 1234.1", Money.of(new BigDecimal("1234.1"), "CHF").toString()); - assertEquals("CHF 0.01", Money.of(new BigDecimal("0.0100"), "CHF").toString()); - assertEquals("CHF 50", + assertEquals("CHF 0.0100", Money.of(new BigDecimal("0.0100"), "CHF").toString()); + assertEquals("CHF 50.0", Money.of(new BigDecimal("500").multiply(new BigDecimal(".1")), "CHF").toString()); } finally { System.clearProperty("org.javamoney.toStringFormatOrder"); @@ -1105,13 +1105,13 @@ public void testToString() { assertEquals(Money.of(1234, "CHF").toString(), "CHF 1234"); assertEquals(Money.of(new BigDecimal("1234"), "CHF", MonetaryContextBuilder.of().setMaxScale(2).setFixedScale(true).build()).toString(), "CHF 1234.00"); - assertEquals(Money.of(new BigDecimal("1234.0"), "CHF").toString(), "CHF 1234"); + assertEquals(Money.of(new BigDecimal("1234.0"), "CHF").toString(), "CHF 1234.0"); assertEquals("CHF 1234.1", Money.of(new BigDecimal("1234.1"), "CHF").toString()); assertEquals(Money.of(new BigDecimal("1234.1"), "CHF", MonetaryContextBuilder.of().setMaxScale(2).build()).toString(), "CHF 1234.1"); assertEquals(Money.of(new BigDecimal("1234.1"), "CHF", twoDigitFixedContext).toString(), "CHF 1234.10"); - assertEquals("CHF 0.01", Money.of(new BigDecimal("0.0100"), "CHF").toString()); - assertEquals("CHF 50", + assertEquals("CHF 0.0100", Money.of(new BigDecimal("0.0100"), "CHF").toString()); + assertEquals("CHF 50.0", Money.of(new BigDecimal("500").multiply(new BigDecimal(".1")), "CHF").toString()); Money money1 = Money.of(1234567.3456, "EUR"); assertEquals(money1.toString(), "EUR 1234567.3456"); diff --git a/moneta-core/src/test/java/org/javamoney/moneta/MoneyTrailingZerosTest.java b/moneta-core/src/test/java/org/javamoney/moneta/MoneyTrailingZerosTest.java new file mode 100644 index 00000000..2ea4b05d --- /dev/null +++ b/moneta-core/src/test/java/org/javamoney/moneta/MoneyTrailingZerosTest.java @@ -0,0 +1,129 @@ +/* + Copyright (c) 2012, 2026, 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; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.math.BigDecimal; + +import javax.money.*; + +import org.javamoney.moneta.spi.DefaultNumberValue; +import org.testng.annotations.Test; + +public class MoneyTrailingZerosTest { + + private static final CurrencyUnit USD = Monetary.getCurrency("USD"); + + @Test + public void preservesScaleFromBigDecimal() { + Money m = Money.of(new BigDecimal("1.420"), USD); + assertEquals(m.getScale(), 3); + assertEquals(m.getPrecision(), 4); + assertEquals(m.getNumber().getScale(), 3); + assertEquals(m.getNumber().getPrecision(), 4); + assertEquals(m.getNumber().numberValue(BigDecimal.class), new BigDecimal("1.420")); + assertEquals(m.getNumber().numberValueExact(BigDecimal.class), new BigDecimal("1.420")); + } + + @Test + public void preservesScaleThroughParseRoundTrip() { + Money m = Money.of(new BigDecimal("1.420"), USD); + Money parsed = Money.parse(m.toString()); + assertEquals(parsed.getScale(), 3); + assertEquals(parsed.getNumber().numberValue(BigDecimal.class), new BigDecimal("1.420")); + } + + @Test + public void preservesScaleViaNumberOverload() { + Number number = new BigDecimal("1.420"); + Money m = Money.of(number, USD); + assertEquals(m.getNumber().numberValue(BigDecimal.class), new BigDecimal("1.420")); + } + + @Test + public void preservesScaleFromNumberValueInput() { + Money m = Money.of(DefaultNumberValue.of(new BigDecimal("1.420")), USD); + assertEquals(m.getNumber().numberValue(BigDecimal.class), new BigDecimal("1.420")); + } + + @Test + public void preservesScaleViaCurrencyCode() { + Money byCode = Money.of(new BigDecimal("1.420"), "USD"); + assertEquals(byCode.getNumber().numberValue(BigDecimal.class), new BigDecimal("1.420")); + } + + @Test + public void preservesScaleViaExplicitContext() { + MonetaryContext ctx = MonetaryContextBuilder.of(Money.class).setPrecision(64).build(); + Money withCtx = Money.of(new BigDecimal("1.420"), "USD", ctx); + assertEquals(withCtx.getNumber().numberValue(BigDecimal.class), new BigDecimal("1.420")); + } + + @Test + public void preservesScaleViaTypedFactory() { + MonetaryAmountFactory factory = Monetary.getAmountFactory(Money.class); + Money m = factory.setCurrency(USD).setNumber(new BigDecimal("1.420")).create(); + assertEquals(m.getNumber().numberValue(BigDecimal.class), new BigDecimal("1.420")); + } + + @Test + public void preservesScaleThroughGetFactoryRoundTrip() { + Money m = Money.of(new BigDecimal("1.420"), USD); + Money rebuilt = m.getFactory().create(); + assertEquals(rebuilt.getNumber().numberValue(BigDecimal.class), new BigDecimal("1.420")); + } + + @Test + public void preservesScaleFromOtherMonetaryAmount() { + RoundedMoney source = RoundedMoney.of(new BigDecimal("1.420"), USD); + Money m = Money.from(source); + assertEquals(m.getNumber().numberValue(BigDecimal.class), new BigDecimal("1.420")); + } + + @Test + public void preservesScaleFromOfMinor() { + Money m = Money.ofMinor(USD, 1200, 2); + assertEquals(m.getScale(), 2); + assertEquals(m.getNumber().numberValue(BigDecimal.class), new BigDecimal("12.00")); + assertEquals(m.toString(), "USD 12.00"); + } + + @Test + public void preservesScaleOfArithmeticResult() { + Money product = Money.of(new BigDecimal("2.50"), USD).multiply(new BigDecimal("2")); + assertEquals(product.getNumber().numberValue(BigDecimal.class), new BigDecimal("5.00")); + } + + @Test + public void fractionPartsReflectPreservedScale() { + NumberValue value = Money.of(new BigDecimal("1.420"), USD).getNumber(); + assertEquals(value.getAmountFractionNumerator(), 420L); + assertEquals(value.getAmountFractionDenominator(), 1000L); + } + + @Test + public void equalityAndComparisonIgnoreTrailingZeros() { + Money withZeros = Money.of(new BigDecimal("1.420"), USD); + Money withoutZeros = Money.of(new BigDecimal("1.42"), USD); + + assertEquals(withZeros, withoutZeros); + assertEquals(withZeros.hashCode(), withoutZeros.hashCode()); + assertTrue(withZeros.isEqualTo(withoutZeros)); + assertEquals(withZeros.compareTo(withoutZeros), 0); + } +} diff --git a/moneta-core/src/test/java/org/javamoney/moneta/RoundedMoneyTest.java b/moneta-core/src/test/java/org/javamoney/moneta/RoundedMoneyTest.java index 2a928557..669d8fb8 100644 --- a/moneta-core/src/test/java/org/javamoney/moneta/RoundedMoneyTest.java +++ b/moneta-core/src/test/java/org/javamoney/moneta/RoundedMoneyTest.java @@ -88,7 +88,7 @@ public void testDivideAndRemainder_BigDecimal() { RoundedMoney money1 = RoundedMoney.of(BigDecimal.ONE, EURO); RoundedMoney[] divideAndRemainder = money1.divideAndRemainder(new BigDecimal("0.50000001")); assertEquals(divideAndRemainder[0].getNumber().numberValue(BigDecimal.class), BigDecimal.ONE); - assertEquals(divideAndRemainder[1].getNumber().numberValue(BigDecimal.class), new BigDecimal("0.5")); + assertEquals(divideAndRemainder[1].getNumber().numberValue(BigDecimal.class), new BigDecimal("0.50")); } @Test @@ -974,15 +974,15 @@ public void testToString() { Locale.setDefault(Locale.ENGLISH); assertEquals(RoundedMoney.of(new BigDecimal("1.23455645"), "XXX").toString(), "XXX 1.23455645"); assertEquals("CHF 1234", RoundedMoney.of(1234, "CHF").toString()); - assertEquals("CHF 1234", RoundedMoney.of(new BigDecimal("1234.0"), "CHF").toString()); + assertEquals("CHF 1234.0", RoundedMoney.of(new BigDecimal("1234.0"), "CHF").toString()); assertEquals("CHF 1234.1", RoundedMoney.of(new BigDecimal("1234.1"), "CHF").toString()); - assertEquals("CHF 0.01", RoundedMoney.of(new BigDecimal("0.0100"), "CHF").toString()); + assertEquals("CHF 0.0100", RoundedMoney.of(new BigDecimal("0.0100"), "CHF").toString()); assertEquals(RoundedMoney.of(new BigDecimal("1.23455645"), "XXX").toString(), "XXX 1.23455645"); assertEquals("CHF 1234", RoundedMoney.of(1234, "CHF").toString()); - assertEquals("CHF 1234", RoundedMoney.of(new BigDecimal("1234.0"), "CHF").toString()); + assertEquals("CHF 1234.0", RoundedMoney.of(new BigDecimal("1234.0"), "CHF").toString()); assertEquals("CHF 1234.1", RoundedMoney.of(new BigDecimal("1234.1"), "CHF").toString()); - assertEquals("CHF 0.01", RoundedMoney.of(new BigDecimal("0.0100"), "CHF").toString()); + assertEquals("CHF 0.0100", RoundedMoney.of(new BigDecimal("0.0100"), "CHF").toString()); } finally { Locale.setDefault(defaultLocale); } diff --git a/moneta-core/src/test/java/org/javamoney/moneta/spi/CurrencyConversionTest.java b/moneta-core/src/test/java/org/javamoney/moneta/spi/CurrencyConversionTest.java index 64a68506..f4538fb4 100644 --- a/moneta-core/src/test/java/org/javamoney/moneta/spi/CurrencyConversionTest.java +++ b/moneta-core/src/test/java/org/javamoney/moneta/spi/CurrencyConversionTest.java @@ -15,6 +15,7 @@ import java.util.List; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; public class CurrencyConversionTest { @@ -68,58 +69,32 @@ public ExchangeRate getExchangeRate(ConversionQuery conversionQuery) { @Test public void testMoneyConversion() { - testMoneyConversionHelper( Money.of(0, BASE_CURRENCY), Money.of(1000, BASE_CURRENCY), - Money.of(new BigDecimal("1000.345"), BASE_CURRENCY) - ); + Money.of(new BigDecimal("1000.345"), BASE_CURRENCY)); } @Test public void testFastMoneyConversion() { - testMoneyConversionHelper( FastMoney.of(0, BASE_CURRENCY), FastMoney.of(1000, BASE_CURRENCY), - FastMoney.of(new BigDecimal("1000.345"), BASE_CURRENCY) - ); + FastMoney.of(new BigDecimal("1000.345"), BASE_CURRENCY)); } private void testMoneyConversionHelper(MonetaryAmount zeroAmount, MonetaryAmount roundedAmount, MonetaryAmount amountWithCents) { - assertEquals(zeroAmount.with( - exactRateProvider.getCurrencyConversion(TERM_CURRENCY) - ).toString(), - "UAH 0" - ); - - assertEquals( - amountWithCents.with( - exactRateProvider.getCurrencyConversion(TERM_CURRENCY) - ).toString(), - "UAH 28465.81732" - ); - - assertEquals( - roundedAmount.with( - exactRateProvider.getCurrencyConversion(TERM_CURRENCY) - ).toString(), - "UAH 28456" - ); - - assertEquals( - amountWithCents.with( - integerRateProvider.getCurrencyConversion(TERM_CURRENCY) - ).toString(), - "UAH 28009.66" - ); - - assertEquals( - roundedAmount.with( - integerRateProvider.getCurrencyConversion(TERM_CURRENCY) - ).toString(), - "UAH 28000" - ); + assertConverted(zeroAmount.with(exactRateProvider.getCurrencyConversion(TERM_CURRENCY)), "0"); + assertConverted(amountWithCents.with(exactRateProvider.getCurrencyConversion(TERM_CURRENCY)), "28465.81732"); + assertConverted(roundedAmount.with(exactRateProvider.getCurrencyConversion(TERM_CURRENCY)), "28456"); + assertConverted(amountWithCents.with(integerRateProvider.getCurrencyConversion(TERM_CURRENCY)), "28009.66"); + assertConverted(roundedAmount.with(integerRateProvider.getCurrencyConversion(TERM_CURRENCY)), "28000"); + } + + private void assertConverted(MonetaryAmount converted, String expectedValue) { + assertEquals(converted.getCurrency(), Monetary.getCurrency(TERM_CURRENCY)); + assertTrue(converted.isEqualTo(Money.of(new BigDecimal(expectedValue), TERM_CURRENCY)), + converted + " should have numeric value " + expectedValue); } } \ No newline at end of file diff --git a/moneta-core/src/test/java/org/javamoney/moneta/spi/DefaultNumberValueTest.java b/moneta-core/src/test/java/org/javamoney/moneta/spi/DefaultNumberValueTest.java index 310883ed..22bdb634 100644 --- a/moneta-core/src/test/java/org/javamoney/moneta/spi/DefaultNumberValueTest.java +++ b/moneta-core/src/test/java/org/javamoney/moneta/spi/DefaultNumberValueTest.java @@ -142,7 +142,7 @@ public void shouldReturnAmountFractionDenominator() { @Test public void shouldReturnAmountFractionDenominatorForNegativeScale() { - NumberValue numberValue = DefaultNumberValue.of(BigDecimal.valueOf(10.00)); + NumberValue numberValue = DefaultNumberValue.of(new BigDecimal("1E+1")); assertEquals(1, numberValue.getAmountFractionDenominator()); }