/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.strata.basics.currency; import java.io.Serializable; import java.util.List; import java.util.function.DoubleUnaryOperator; import org.joda.beans.JodaBeanUtils; import org.joda.convert.FromString; import org.joda.convert.ToString; import com.google.common.base.Splitter; import com.google.common.collect.ComparisonChain; import com.google.common.math.DoubleMath; import com.opengamma.strata.collect.ArgChecker; /** * An amount of a currency. * <p> * This class represents a {@code double} amount associated with a currency. * It is specifically named "CurrencyAmount" and not "Money" to indicate that * it simply holds a currency and an amount. By contrast, naming it "Money" * would imply it was a suitable choice for accounting purposes, which it is not. * <p> * This design approach has been chosen primarily for performance reasons. * Using a {@code BigDecimal} is markedly slower. * <p> * A {@code double} is a 64 bit floating point value suitable for most calculations. * Floating point maths is * <a href="http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html">inexact</a> * due to the conflict between binary and decimal arithmetic. * As such, there is the potential for data loss at the margins. * For example, adding the {@code double} values {@code 0.1d} and {@code 0.2d} * results in {@code 0.30000000000000004} rather than {@code 0.3}. * As can be seen, the level of error is small, hence providing this class is * used appropriately, the use of {@code double} is acceptable. * For example, using this class to provide a meaningful result type after * calculations have completed would be an appropriate use. * <p> * This class is immutable and thread-safe. */ public final class CurrencyAmount implements FxConvertible<CurrencyAmount>, Comparable<CurrencyAmount>, Serializable { /** Serialization version. */ private static final long serialVersionUID = 1L; /** * The currency. * For example, in the value 'GBP 12.34' the currency is 'GBP'. */ private final Currency currency; /** * The amount of the currency. * For example, in the value 'GBP 12.34' the amount is 12.34. */ private final double amount; //------------------------------------------------------------------------- /** * Obtains a zero amount instance of {@code CurrencyAmount} for the specified currency. * * @param currency the currency the amount is in * @return the zero amount instance */ public static CurrencyAmount zero(Currency currency) { return of(currency, 0d); } /** * Obtains an instance of {@code CurrencyAmount} for the specified currency and amount. * * @param currency the currency the amount is in * @param amount the amount of the currency to represent * @return the currency amount */ public static CurrencyAmount of(Currency currency, double amount) { return new CurrencyAmount(currency, amount); } /** * Obtains an instance of {@code CurrencyAmount} for the specified ISO-4217 * three letter currency code and amount. * <p> * A currency is uniquely identified by ISO-4217 three letter code. * This method creates the currency if it is not known. * * @param currencyCode the three letter currency code, ASCII and upper case * @param amount the amount of the currency to represent * @return the currency amount * @throws IllegalArgumentException if the currency code is invalid */ public static CurrencyAmount of(String currencyCode, double amount) { return of(Currency.of(currencyCode), amount); } //------------------------------------------------------------------------- /** * Parses the string to produce a {@code CurrencyAmount}. * <p> * This parses the {@code toString} format of '${currency} ${amount}'. * * @param amountStr the amount string * @return the currency amount * @throws IllegalArgumentException if the amount cannot be parsed */ @FromString public static CurrencyAmount parse(String amountStr) { ArgChecker.notNull(amountStr, "amountStr"); List<String> split = Splitter.on(' ').splitToList(amountStr); if (split.size() != 2) { throw new IllegalArgumentException("Unable to parse amount, invalid format: " + amountStr); } try { Currency cur = Currency.parse(split.get(0)); double amount = Double.parseDouble(split.get(1)); return new CurrencyAmount(cur, amount); } catch (RuntimeException ex) { throw new IllegalArgumentException("Unable to parse amount: " + amountStr, ex); } } //------------------------------------------------------------------------- /** * Creates an instance. * * @param currency the currency * @param amount the amount */ private CurrencyAmount(Currency currency, double amount) { this.currency = ArgChecker.notNull(currency, "currency"); this.amount = amount; } //------------------------------------------------------------------------- /** * Gets the currency. * <p> * For example, in the value 'GBP 12.34' the currency is 'GBP'. * * @return the currency */ public Currency getCurrency() { return currency; } /** * Gets the amount of the currency. * <p> * For example, in the value 'GBP 12.34' the amount is 12.34. * * @return the amount */ public double getAmount() { return amount; } //------------------------------------------------------------------------- /** * Returns a copy of this {@code CurrencyAmount} with the specified amount added. * <p> * This adds the specified amount to this monetary amount, returning a new object. * The addition simply uses standard {@code double} arithmetic. * <p> * This instance is immutable and unaffected by this method. * * @param amountToAdd the amount to add, in the same currency * @return an amount based on this with the specified amount added * @throws IllegalArgumentException if the currencies are not equal */ public CurrencyAmount plus(CurrencyAmount amountToAdd) { ArgChecker.notNull(amountToAdd, "amountToAdd"); ArgChecker.isTrue(amountToAdd.getCurrency().equals(currency), "Unable to add amounts in different currencies"); return plus(amountToAdd.getAmount()); } /** * Returns a copy of this {@code CurrencyAmount} with the specified amount added. * <p> * This adds the specified amount to this monetary amount, returning a new object. * The addition simply uses standard {@code double} arithmetic. * <p> * This instance is immutable and unaffected by this method. * * @param amountToAdd the amount to add * @return an amount based on this with the specified amount added */ public CurrencyAmount plus(double amountToAdd) { return new CurrencyAmount(currency, amount + amountToAdd); } /** * Returns a copy of this {@code CurrencyAmount} with the specified amount subtracted. * <p> * This subtracts the specified amount to this monetary amount, returning a new object. * The addition simply uses standard {@code double} arithmetic. * <p> * This instance is immutable and unaffected by this method. * * @param amountToSubtract the amount to subtract, in the same currency * @return an amount based on this with the specified amount subtracted * @throws IllegalArgumentException if the currencies are not equal */ public CurrencyAmount minus(CurrencyAmount amountToSubtract) { ArgChecker.notNull(amountToSubtract, "amountToSubtract"); ArgChecker.isTrue(amountToSubtract.getCurrency().equals(currency), "Unable to subtract amounts in different currencies"); return minus(amountToSubtract.getAmount()); } /** * Returns a copy of this {@code CurrencyAmount} with the specified amount subtracted. * <p> * This subtracts the specified amount to this monetary amount, returning a new object. * The addition simply uses standard {@code double} arithmetic. * <p> * This instance is immutable and unaffected by this method. * * @param amountToSubtract the amount to subtract * @return an amount based on this with the specified amount subtracted */ public CurrencyAmount minus(double amountToSubtract) { return new CurrencyAmount(currency, amount - amountToSubtract); } //------------------------------------------------------------------------- /** * Returns a copy of this {@code CurrencyAmount} with the amount multiplied. * <p> * This takes this amount and multiplies it by the specified value. * The multiplication simply uses standard {@code double} arithmetic. * <p> * This instance is immutable and unaffected by this method. * * @param valueToMultiplyBy the scalar amount to multiply by * @return an amount based on this with the amount multiplied */ public CurrencyAmount multipliedBy(double valueToMultiplyBy) { return new CurrencyAmount(currency, amount * valueToMultiplyBy); } /** * Applies an operation to the amount. * <p> * This is generally used to apply a mathematical operation to the amount. * For example, the operator could multiply the amount by a constant, or take the inverse. * <pre> * multiplied = base.mapAmount(value -> (value < 0 ? 0 : value * 3)); * </pre> * * @param mapper the operator to be applied to the amount * @return a copy of this amount with the mapping applied to the original amount */ public CurrencyAmount mapAmount(DoubleUnaryOperator mapper) { ArgChecker.notNull(mapper, "mapper"); return new CurrencyAmount(currency, mapper.applyAsDouble(amount)); } //------------------------------------------------------------------------- /** * Returns a copy of this {@code CurrencyAmount} with the amount negated. * <p> * This takes this amount and negates it. If the amount is 0.0 or -0.0 the negated amount is 0.0. * <p> * This instance is immutable and unaffected by this method. * * @return an amount based on this with the amount negated */ public CurrencyAmount negated() { // Zero is treated as a special case to avoid creating -0.0 which produces surprising equality behaviour return new CurrencyAmount(currency, amount == 0d ? 0d : -amount); } /** * Returns a copy of this {@code CurrencyAmount} with a positive amount. * <p> * The result of this method will always be positive, where the amount is equal to {@code Math.abs(amount)}. * <p> * This instance is immutable and unaffected by this method. * * @return a currency amount based on this where the amount is positive */ public CurrencyAmount positive() { return amount < 0 ? negated() : this; } /** * Returns a copy of this {@code CurrencyAmount} with a negative amount. * <p> * The result of this method will always be negative, equal to {@code -Math.abs(amount)}. * <p> * This instance is immutable and unaffected by this method. * * @return a currency amount based on this where the amount is negative */ public CurrencyAmount negative() { return amount > 0 ? negated() : this; } //------------------------------------------------------------------------- /** * Converts this amount to an equivalent amount the specified currency. * <p> * The result will be expressed in terms of the given currency, converting * using the specified FX rate. * <p> * For example, if this represents 'GBP 100' and this method is called with * arguments {@code (USD, 1.6)} then the result will be 'USD 160'. * * @param resultCurrency the currency of the result * @param fxRate the FX rate from this currency to the result currency * @return the converted instance, which should be expressed in the specified currency * @throws IllegalArgumentException if the FX is not 1 when no conversion is required */ public CurrencyAmount convertedTo(Currency resultCurrency, double fxRate) { if (currency.equals(resultCurrency)) { if (DoubleMath.fuzzyEquals(fxRate, 1d, 1e-8)) { return this; } throw new IllegalArgumentException("FX rate must be 1 when no conversion required"); } return CurrencyAmount.of(resultCurrency, amount * fxRate); } /** * Converts this amount to an equivalent amount in the specified currency. * <p> * The result will be expressed in terms of the given currency. * If conversion is needed, the provider will be used to supply the FX rate. * * @param resultCurrency the currency of the result * @param rateProvider the provider of FX rates * @return the converted instance, in the specified currency * @throws RuntimeException if no FX rate could be found */ @Override public CurrencyAmount convertedTo(Currency resultCurrency, FxRateProvider rateProvider) { if (currency.equals(resultCurrency)) { return this; } double converted = rateProvider.convert(amount, currency, resultCurrency); return CurrencyAmount.of(resultCurrency, converted); } //------------------------------------------------------------------------- /** * Compares this currency amount to another. * <p> * This compares currencies alphabetically, then by amount. * * @param other the other amount * @return negative if less, zero if equal, positive if greater */ @Override public int compareTo(CurrencyAmount other) { return ComparisonChain.start() .compare(currency, other.currency) .compare(amount, other.amount) .result(); } /** * Checks if this currency amount equals another. * * @param obj the other amount, null returns false * @return true if equal */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj != null && obj.getClass() == this.getClass()) { CurrencyAmount other = (CurrencyAmount) obj; return currency.equals(other.currency) && JodaBeanUtils.equal(amount, other.amount); } return false; } /** * Returns a suitable hash code for the currency. * * @return the hash code */ @Override public int hashCode() { return currency.hashCode() * 31 + JodaBeanUtils.hashCode(amount); } //------------------------------------------------------------------------- /** * Gets the amount as a string. * <p> * The format is the currency code, followed by a space, followed by the * amount: '${currency} ${amount}'. * * @return the currency amount */ @Override @ToString public String toString() { return currency + " " + (DoubleMath.isMathematicalInteger(amount) ? Long.toString((long) amount) : Double.toString(amount)); } }