/** * Copyright (c) 2012, 2015, Credit Suisse (Anatole Tresch), 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 org.javamoney.moneta.ToStringMonetaryAmountFormat.ToStringMonetaryAmountFormatStyle; import org.javamoney.moneta.internal.FastMoneyAmountBuilder; import org.javamoney.moneta.spi.DefaultNumberValue; import org.javamoney.moneta.spi.MonetaryConfig; import org.javamoney.moneta.spi.MoneyUtils; import javax.money.*; import javax.money.format.MonetaryAmountFormat; import java.io.Serializable; import java.math.BigDecimal; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; /** * <code>long</code> based implementation of {@link MonetaryAmount}.This class internally uses a * single long number as numeric representation, which basically is interpreted as minor units.<p> * It suggested to have a performance advantage of a 10-15 times faster compared to {@link Money}, * which internally uses {@link BigDecimal}. Nevertheless this comes with a price of less precision. * As an example performing the following calculation one million times, results in slightly * different results: * </p> * <pre><code> * Money money1 = money1.add(Money.of(1234567.3444, "EUR")); * money1 = money1.subtract(Money.of(232323, "EUR")); * money1 = money1.multiply(3.4); * money1 = money1.divide(5.456); * </code></pre> * <p> * Executed one million (1000000) times this results in {@code EUR 1657407.962529182}, calculated in * 3680 ms, or roughly 3ns/loop. * <p> * whereas * </p> * <pre><code> * FastMoney money1 = money1.add(FastMoney.of(1234567.3444, "EUR")); * money1 = money1.subtract(FastMoney.of(232323, "EUR")); * money1 = money1.multiply(3.4); * money1 = money1.divide(5.456); * </code></pre> * <p> * executed one million (1000000) times results in {@code EUR 1657407.96251}, calculated in 179 ms, * which is less than 1ns/loop. * </p><p> * Also note than mixing up types my drastically change the performance behavior. E.g. replacing the * code above with the following: * * </p> * <pre><code> * FastMoney money1 = money1.add(Money.of(1234567.3444, "EUR")); * money1 = money1.subtract(FastMoney.of(232323, "EUR")); * money1 = money1.multiply(3.4); * money1 = money1.divide(5.456); * </code></pre> * <p> * executed one million (1000000) times may execute significantly longer, since monetary amount type * conversion is involved. * </p><p> * Basically, when mixing amount implementations, the performance of the amount, on which most of * the operations are operated, has the most significant impact on the overall performance behavior. * * @author Anatole Tresch * @author Werner Keil * @version 1.0 */ public final class FastMoney implements MonetaryAmount, Comparable<MonetaryAmount>, Serializable { private static final long serialVersionUID = 1L; /** * The logger used. */ private static final Logger LOG = Logger.getLogger(FastMoney.class.getName()); /** * The currency of this amount. */ private final CurrencyUnit currency; /** * The numeric part of this amount. */ private final long number; /** * The current scale represented by the number. */ private static final int SCALE = 5; /** * the {@link MonetaryContext} used by this instance, e.g. on division. */ private static final MonetaryContext MONETARY_CONTEXT = MonetaryContextBuilder.of(FastMoney.class).setMaxScale(SCALE).setFixedScale(true).setPrecision(19).build(); /** * Maximum possible value supported, using XX (no currency). */ public static final FastMoney MAX_VALUE = new FastMoney(Long.MAX_VALUE, Monetary.getCurrency("XXX")); /** * Maximum possible numeric value supported. */ private static final BigDecimal MAX_BD = MAX_VALUE.getBigDecimal(); /** * Minimum possible value supported, using XX (no currency). */ public static final FastMoney MIN_VALUE = new FastMoney(Long.MIN_VALUE, Monetary.getCurrency("XXX")); /** * Minimum possible numeric value supported. */ private static final BigDecimal MIN_BD = MIN_VALUE.getBigDecimal(); /** * Creates a new instance os {@link FastMoney}. * * @param currency the currency, not null. * @param number the amount, not null. */ private FastMoney(Number number, CurrencyUnit currency, boolean allowInternalRounding) { Objects.requireNonNull(currency, "Currency is required."); this.currency = currency; Objects.requireNonNull(number, "Number is required."); this.number = getInternalNumber(number, allowInternalRounding); } /** * Creates a new instance os {@link FastMoney}. * * @param currency the currency, not null. * @param numberValue the numeric value, not null. */ private FastMoney(NumberValue numberValue, CurrencyUnit currency, boolean allowInternalRounding) { Objects.requireNonNull(currency, "Currency is required."); this.currency = currency; Objects.requireNonNull(numberValue, "Number is required."); this.number = getInternalNumber(numberValue.numberValue(BigDecimal.class), allowInternalRounding); } /** * Creates a new instance os {@link FastMoney}. * * @param number The format number value * @param currency the currency, not null. */ private FastMoney(long number, CurrencyUnit currency) { Objects.requireNonNull(currency, "Currency is required."); this.currency = currency; this.number = number; } /** * Returns the amount’s currency, modelled as {@link CurrencyUnit}. * Implementations may co-variantly change the return type to a more * specific implementation of {@link CurrencyUnit} if desired. * * @return the currency, never {@code null} * @see javax.money.MonetaryAmount#getCurrency() */ @Override public CurrencyUnit getCurrency() { return currency; } /** * Access the {@link MonetaryContext} used by this instance. * * @return the {@link MonetaryContext} used, never null. * @see javax.money.MonetaryAmount#getContext() */ @Override public MonetaryContext getContext() { return MONETARY_CONTEXT; } private long getInternalNumber(Number number, boolean allowInternalRounding) { BigDecimal bd = MoneyUtils.getBigDecimal(number); if (!allowInternalRounding && bd.scale() > SCALE) { throw new ArithmeticException(number + " can not be represented by this class, scale > " + SCALE); } if (bd.compareTo(MIN_BD) < 0) { throw new ArithmeticException("Overflow: " + number + " < " + MIN_BD); } else if (bd.compareTo(MAX_BD) > 0) { throw new ArithmeticException("Overflow: " + number + " > " + MAX_BD); } return bd.movePointRight(SCALE).longValue(); } /** * Static factory method for creating a new instance of {@link FastMoney}. * * @param currency The target currency, not null. * @param numberBinding The numeric part, not null. * @return A new instance of {@link FastMoney}. */ public static FastMoney of(NumberValue numberBinding, CurrencyUnit currency) { return new FastMoney(numberBinding, currency, false); } /** * Static factory method for creating a new instance of {@link FastMoney}. * * @param currency The target currency, not null. * @param number The numeric part, not null. * @return A new instance of {@link FastMoney}. */ public static FastMoney of(Number number, CurrencyUnit currency) { return new FastMoney(number, currency, false); } /** * Static factory method for creating a new instance of {@link FastMoney}. * * @param currencyCode The target currency as currency code. * @param number The numeric part, not null. * @return A new instance of {@link FastMoney}. */ public static FastMoney of(Number number, String currencyCode) { CurrencyUnit currency = Monetary.getCurrency(currencyCode); return of(number, currency); } /** * Obtains an instance of {@link FastMoney} representing zero. * @param currency * @return an instance of {@link FastMoney} representing zero. * @since 1.0.1 */ public static FastMoney zero(CurrencyUnit currency) { return of(BigDecimal.ZERO, currency); } /** * Obtains an instance of {@code FastMoney} from an amount in minor units. * For example, {@code ofMinor(USD, 1234)} creates the instance {@code USD 12.34}. * @param currency the currency, not null * @param amountMinor the amount of money in the minor division of the currency * @return the monetary amount from minor units * @see {@link CurrencyUnit#getDefaultFractionDigits()} * @see {@link FastMoney#ofMinor(CurrencyUnit, long, int)} * @throws NullPointerException when the currency is null * @throws IllegalArgumentException when {@link CurrencyUnit#getDefaultFractionDigits()} is lesser than zero. * @since 1.0.1 */ public static FastMoney ofMinor(CurrencyUnit currency, long amountMinor) { return ofMinor(currency, amountMinor, currency.getDefaultFractionDigits()); } /** * Obtains an instance of {@code FastMoney} from an amount in minor units. * For example, {@code ofMinor(USD, 1234, 2)} creates the instance {@code USD 12.34}. * @param currency the currency, not null * @param amountMinor the amount of money in the minor division of the currency * @param factionDigits number of digits * @return the monetary amount from minor units * @see {@link CurrencyUnit#getDefaultFractionDigits()} * @see {@link FastMoney#ofMinor(CurrencyUnit, long, int)} * @throws NullPointerException when the currency is null * @throws IllegalArgumentException when the factionDigits is negative * @since 1.0.1 */ public static FastMoney ofMinor(CurrencyUnit currency, long amountMinor, int factionDigits) { if(factionDigits < 0) { throw new IllegalArgumentException("The factionDigits cannot be negative"); } return of(BigDecimal.valueOf(amountMinor, factionDigits), currency); } @Override public int compareTo(MonetaryAmount o) { Objects.requireNonNull(o); int compare = getCurrency().getCurrencyCode().compareTo(o.getCurrency().getCurrencyCode()); if (compare == 0) { compare = getNumber().numberValue(BigDecimal.class).compareTo(o.getNumber().numberValue(BigDecimal.class)); } return compare; } @Override public int hashCode() { return Objects.hash(currency, number); } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof FastMoney) { FastMoney other = (FastMoney) obj; return Objects.equals(currency, other.currency) && Objects.equals(number, other.number); } return false; } @Override public FastMoney abs() { if (this.isPositiveOrZero()) { return this; } return this.negate(); } @Override public FastMoney add(MonetaryAmount amount) { checkAmountParameter(amount); if (amount.isZero()) { return this; } return new FastMoney(Math.addExact(this.number, getInternalNumber(amount.getNumber(), false)), getCurrency()); } private void checkAmountParameter(MonetaryAmount amount) { MoneyUtils.checkAmountParameter(amount, this.currency); // numeric check for overflow... if (amount.getNumber().getScale() > SCALE) { throw new ArithmeticException("Parameter exceeds maximal scale: " + SCALE); } if (amount.getNumber().getPrecision() > MAX_BD.precision()) { throw new ArithmeticException("Parameter exceeds maximal precision: " + SCALE); } } @Override public FastMoney divide(Number divisor) { if (NumberVerifier.isInfinityAndNotNaN(divisor)) { return new FastMoney(0L, getCurrency()); } checkNumber(divisor); if (isOne(divisor)) { return this; } return new FastMoney(Math.round(this.number / divisor.doubleValue()), getCurrency()); } @Override public FastMoney[] divideAndRemainder(Number divisor) { if (NumberVerifier.isInfinityAndNotNaN(divisor)) { FastMoney zero = new FastMoney(0L, getCurrency()); return new FastMoney[]{zero, zero}; } checkNumber(divisor); BigDecimal div = MoneyUtils.getBigDecimal(divisor); BigDecimal[] res = getBigDecimal().divideAndRemainder(div); return new FastMoney[]{new FastMoney(res[0], getCurrency(), true), new FastMoney(res[1], getCurrency(), true)}; } @Override public FastMoney divideToIntegralValue(Number divisor) { if (NumberVerifier.isInfinityAndNotNaN(divisor)) { return new FastMoney(0L, getCurrency()); } checkNumber(divisor); if (isOne(divisor)) { return this; } BigDecimal div = MoneyUtils.getBigDecimal(divisor); return new FastMoney(getBigDecimal().divideToIntegralValue(div), getCurrency(), false); } @Override public FastMoney multiply(Number multiplicand) { NumberVerifier.checkNoInfinityOrNaN(multiplicand); checkNumber(multiplicand); if (isOne(multiplicand)) { return this; } return new FastMoney(Math.multiplyExact(this.number, getInternalNumber(multiplicand, false)) / 100000L, getCurrency()); } @Override public FastMoney negate() { return new FastMoney(Math.multiplyExact(this.number, -1), getCurrency()); } @Override public FastMoney plus() { return this; } @Override public FastMoney subtract(MonetaryAmount subtrahend) { checkAmountParameter(subtrahend); if (subtrahend.isZero()) { return this; } return new FastMoney(Math.subtractExact(this.number, getInternalNumber(subtrahend.getNumber(), false)), getCurrency()); } @Override public FastMoney remainder(Number divisor) { checkNumber(divisor); return new FastMoney(this.number % getInternalNumber(divisor, false), getCurrency()); } private boolean isOne(Number number) { BigDecimal bd = MoneyUtils.getBigDecimal(number); try { return bd.scale() == 0 && bd.longValueExact() == 1L; } catch (Exception e) { // The only way to end up here is that longValueExact throws an ArithmeticException, // so the amount is definitively not equal to 1. return false; } } @Override public FastMoney scaleByPowerOfTen(int power) { return new FastMoney(getNumber().numberValue(BigDecimal.class).scaleByPowerOfTen(power), getCurrency(), true); } @Override public boolean isZero() { return this.number == 0L; } @Override public boolean isPositive() { return this.number > 0L; } @Override public boolean isPositiveOrZero() { return this.number >= 0L; } @Override public boolean isNegative() { return this.number < 0L; } @Override public boolean isNegativeOrZero() { return this.number <= 0L; } public int getScale() { return FastMoney.SCALE; } public int getPrecision() { return getNumber().numberValue(BigDecimal.class).precision(); } @Override public int signum() { if (this.number < 0) { return -1; } if (this.number == 0) { return 0; } return 1; } @Override public boolean isLessThan(MonetaryAmount amount) { checkAmountParameter(amount); return getBigDecimal().compareTo(amount.getNumber().numberValue(BigDecimal.class)) < 0; } public boolean isLessThan(Number number) { checkNumber(number); return getBigDecimal().compareTo(MoneyUtils.getBigDecimal(number)) < 0; } @Override public boolean isLessThanOrEqualTo(MonetaryAmount amount) { checkAmountParameter(amount); return getBigDecimal().compareTo(amount.getNumber().numberValue(BigDecimal.class)) <= 0; } public boolean isLessThanOrEqualTo(Number number) { checkNumber(number); return getBigDecimal().compareTo(MoneyUtils.getBigDecimal(number)) <= 0; } @Override public boolean isGreaterThan(MonetaryAmount amount) { checkAmountParameter(amount); return getBigDecimal().compareTo(amount.getNumber().numberValue(BigDecimal.class)) > 0; } public boolean isGreaterThan(Number number) { checkNumber(number); return getBigDecimal().compareTo(MoneyUtils.getBigDecimal(number)) > 0; } @Override public boolean isGreaterThanOrEqualTo(MonetaryAmount amount) { checkAmountParameter(amount); return getBigDecimal().compareTo(amount.getNumber().numberValue(BigDecimal.class)) >= 0; } public boolean isGreaterThanOrEqualTo(Number number) { checkNumber(number); return getBigDecimal().compareTo(MoneyUtils.getBigDecimal(number)) >= 0; } @Override public boolean isEqualTo(MonetaryAmount amount) { checkAmountParameter(amount); return getBigDecimal().compareTo(amount.getNumber().numberValue(BigDecimal.class)) == 0; } public boolean hasSameNumberAs(Number number) { checkNumber(number); try { return this.number == getInternalNumber(number, false); } catch (ArithmeticException e) { return false; } } /** * Gets the number representation of the numeric value of this item. * * @return The {@link Number} representation matching best. */ @Override public NumberValue getNumber() { return new DefaultNumberValue(getBigDecimal()); } @Override public String toString() { return currency.toString() + ' ' + getBigDecimal(); } // Internal helper methods /** * Internal method to check for correct number parameter. * * @param number the number to be checked, including null.. * @throws NullPointerException If the number is null * @throws java.lang.ArithmeticException If the number exceeds the capabilities of this class. */ protected void checkNumber(Number number) { Objects.requireNonNull(number, "Number is required."); // numeric check for overflow... if (number.longValue() > MAX_BD.longValue()) { throw new ArithmeticException("Value exceeds maximal value: " + MAX_BD); } BigDecimal bd = MoneyUtils.getBigDecimal(number); if (bd.precision() > MAX_BD.precision()) { throw new ArithmeticException("Precision exceeds maximal precision: " + MAX_BD.precision()); } if (bd.scale() > SCALE) { if (Boolean.parseBoolean(MonetaryConfig.getConfig() .getOrDefault("org.javamoney.moneta.FastMoney.enforceScaleCompatibility", "false"))) { throw new ArithmeticException("Scale of " + bd + " exceeds maximal scale: " + SCALE); } else { if (LOG.isLoggable(Level.FINEST)) { LOG.finest("Scale exceeds maximal scale of FastMoney (" + SCALE + "), implicit rounding will be applied to " + number); } } } } @Override public FastMoney with(MonetaryOperator operator) { Objects.requireNonNull(operator); try { return FastMoney.class.cast(operator.apply(this)); } catch (ArithmeticException e) { throw e; } catch (Exception e) { throw new MonetaryException("Operator failed: " + operator, e); } } @Override public <R> R query(MonetaryQuery<R> query) { Objects.requireNonNull(query); try { return query.queryFrom(this); } catch (MonetaryException | ArithmeticException e) { throw e; } catch (Exception e) { throw new MonetaryException("Query failed: " + query, e); } } public static FastMoney from(MonetaryAmount amount) { if (FastMoney.class.isInstance(amount)) { return FastMoney.class.cast(amount); } return new FastMoney(amount.getNumber(), amount.getCurrency(), false); } /** * Obtains an instance of FastMoney from a text string such as 'EUR 25.25'. * * @param text the text to parse not null * @return FastMoney instance * @throws NullPointerException * @throws NumberFormatException * @throws UnknownCurrencyException */ public static FastMoney parse(CharSequence text) { return parse(text, DEFAULT_FORMATTER); } /** * Obtains an instance of FastMoney from a text using specific formatter. * * @param text the text to parse not null * @param formatter the formatter to use not null * @return FastMoney instance */ public static FastMoney parse(CharSequence text, MonetaryAmountFormat formatter) { return from(formatter.parse(text)); } private static final ToStringMonetaryAmountFormat DEFAULT_FORMATTER = ToStringMonetaryAmountFormat .of(ToStringMonetaryAmountFormatStyle.FAST_MONEY); private BigDecimal getBigDecimal() { return BigDecimal.valueOf(this.number).movePointLeft(SCALE); } @Override public FastMoney multiply(double multiplicand) { NumberVerifier.checkNoInfinityOrNaN(multiplicand); if (multiplicand == 1.0) { return this; } if (multiplicand == 0.0) { return new FastMoney(0, this.currency); } return new FastMoney(Math.round(this.number * multiplicand), this.currency); } @Override public FastMoney divide(long divisor) { if (divisor == 1L) { return this; } return new FastMoney(this.number / divisor, this.currency); } @Override public FastMoney divide(double divisor) { if (NumberVerifier.isInfinityAndNotNaN(divisor)) { return new FastMoney(0L, getCurrency()); } if (divisor == 1.0d) { return this; } return new FastMoney(Math.round(this.number / divisor), getCurrency()); } @Override public FastMoney remainder(long divisor) { return remainder(BigDecimal.valueOf(divisor)); } @Override public FastMoney remainder(double divisor) { if (NumberVerifier.isInfinityAndNotNaN(divisor)) { return new FastMoney(0L, getCurrency()); } return remainder(new BigDecimal(String.valueOf(divisor))); } @Override public FastMoney[] divideAndRemainder(long divisor) { return divideAndRemainder(BigDecimal.valueOf(divisor)); } @Override public FastMoney[] divideAndRemainder(double divisor) { if (NumberVerifier.isInfinityAndNotNaN(divisor)) { FastMoney zero = new FastMoney(0L, getCurrency()); return new FastMoney[]{zero, zero}; } else if (divisor == Double.NaN) { throw new ArithmeticException("Not a number: NaN."); } return divideAndRemainder(new BigDecimal(String.valueOf(divisor))); } @Override public FastMoney stripTrailingZeros() { return this; } @Override public FastMoney multiply(long multiplicand) { if (multiplicand == 1) { return this; } if (multiplicand == 0) { return new FastMoney(0L, this.currency); } return new FastMoney(Math.multiplyExact(multiplicand, this.number), this.currency); } @Override public FastMoney divideToIntegralValue(long divisor) { if (divisor == 1) { return this; } return divideToIntegralValue(MoneyUtils.getBigDecimal(divisor)); } @Override public FastMoney divideToIntegralValue(double divisor) { if (NumberVerifier.isInfinityAndNotNaN(divisor)) { return new FastMoney(0L, getCurrency()); } if (divisor == 1.0) { return this; } return divideToIntegralValue(MoneyUtils.getBigDecimal(divisor)); } @Override public MonetaryAmountFactory<FastMoney> getFactory() { return new FastMoneyAmountBuilder().setAmount(this); } }