package com.console.utils.value; import com.console.core.exceptions.UnknownCurrencyCodeException; import java.io.Serializable; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.text.NumberFormat; import java.util.Currency; import java.util.logging.Level; import java.util.logging.Logger; import org.junit.Assert; import static java.math.RoundingMode.HALF_UP; /** * * @author farouka */ public class Money implements Serializable { /** * Why me */ private static final int[] cents = new int[]{1, 10, 100, 1000}; private BigDecimal amount; private Currency currency; //private MathContext DEFAULT_CONTEXT = new MathContext( 2, HALF_UP ); private MathContext DEFAULT_CONTEXT = new MathContext( 10, RoundingMode.HALF_DOWN ); public Money(long amount, Currency currency) { this.currency = currency; this.amount = BigDecimal.valueOf(amount, currency.getDefaultFractionDigits()); } /** * Creates a currency object from the long value provided assuming the long value * represents the base currency in the least monetary unit. For eg, new Money(500, "GBP") * is assumed to mean 5.00 great british pounds * @param amount in base monetary unit * @param currCode * @throws com.console.core.exceptions.UnknownCurrencyCodeException */ public Money(long amount, String currCode) throws UnknownCurrencyCodeException { this( amount, Currency.getInstance(currCode) ); } /** * Construct an IMMUTABLE money object from a double. It is assumed that * the whole part of the double is the Money with the fractional part representing * lowest denominator of the currency. For eg, new Money (50.99, "GBP") is assumed * to be 50 pounds and 99 pence. * PS. 89.788 will be truncated to 89.78 based on the defaultcurrencydigit of the currency * @param amount * @param curr */ public Money(double amount, Currency curr) { this.currency = curr; BigDecimal bd = BigDecimal.valueOf( amount ); this.amount = bd.setScale(centFactor(), HALF_UP); } private Money() { } /** * Construct an IMMUTABLE money object from a double. It is assumed that * the whole part of the double is the Money with the fractional part representing * lowest denominator of the currency. For eg, new Money (50.99, "GBP") is assumed * to be 50 pounds and 99 pence. * PS. 89.788 will be truncated to 89.78 based on the defaultcurrencydigit of the currency * code supplied * @param amount * @param currCode iso 4217 currency code * @throws com.console.core.exceptions.UnknownCurrencyCodeException */ public Money(double amount, String currCode) throws UnknownCurrencyCodeException { this.currency = Currency.getInstance(currCode); BigDecimal bd = BigDecimal.valueOf( amount ); this.amount = bd.setScale( currency.getDefaultFractionDigits(), HALF_UP); } /** * Constructs an IMMUTABLE money from a BigDecimal. the BigDecimal provided is only scaled * to used the default digits in currency object represented by the sting parameter * @param bigDecimal * @param currCode ISO 4217 cuurency code * @throws com.console.core.exceptions.UnknownCurrencyCodeException */ public Money(BigDecimal bigDecimal, String currCode ) throws UnknownCurrencyCodeException { this.currency = Currency.getInstance(currCode); this.amount = bigDecimal.setScale( currency.getDefaultFractionDigits(), HALF_UP); } /** * Constructs an IMMUTABLE money from a BigDecimal. the BigDecimal provided is only scaled * to used the default digits in currency object represented by the sting parameter * @param multiply * @param currency */ public Money(BigDecimal bigDecimal, Currency currency) { this.currency = currency; this.amount = bigDecimal.setScale( currency.getDefaultFractionDigits(), HALF_UP); } // public boolean assertSameCurrencyAs(Money arg) { // return this.currency.getCurrencyCode().equals(arg.currency.getCurrencyCode()); // } // public boolean assertSameCurrencyAs(Money money) throws IncompatibleCurrencyException{ if ( this.currency == null ) { throw new IncompatibleCurrencyException( "currency.invalid" ); } if ( money == null ) { throw new IncompatibleCurrencyException( "currency.invalid" ); } Assert.assertEquals("money math mismatch", currency, money.currency); return true; } private int centFactor() { return cents[ getCurrency().getDefaultFractionDigits() ]; } public BigDecimal amount() { return amount; } public long amountAsLong(){ return amount.unscaledValue().longValue(); } public Currency getCurrency() { return currency; } // common currencies public static Money dollars(double amount) { Money result = null; try { result = new Money(amount, "USD"); } catch (UnknownCurrencyCodeException ex) { Logger.getLogger(Money.class.getName()).log(Level.SEVERE, null, ex); } return result; } public static Money dollars(long amount) { Money result = null; try { result = new Money(amount, "USD"); } catch (UnknownCurrencyCodeException ex) { Logger.getLogger(Money.class.getName()).log(Level.SEVERE, null, ex); } return result; } public static Money pounds(double amount) { Money result = null; try { result = new Money(amount, "GBP"); } catch (UnknownCurrencyCodeException ex) { Logger.getLogger(Money.class.getName()).log(Level.SEVERE, null, ex); } return result; } public static Money pounds(long amount) { Money result = null; try { result = new Money(amount, "GBP"); } catch (UnknownCurrencyCodeException ex) { Logger.getLogger(Money.class.getName()).log(Level.SEVERE, null, ex); } return result; } public static Money pounds(BigDecimal amount) { Money result = null; try { result = new Money(amount, "GBP"); } catch (UnknownCurrencyCodeException ex) { Logger.getLogger(Money.class.getName()).log(Level.SEVERE, null, ex); } return result; } @Override public int hashCode() { int hash = (int) ( amount.hashCode() ^ (amount.hashCode() >>> 32) ); return hash; } @Override public boolean equals(Object other) { return (other instanceof Money && equals((Money) other)); } public boolean equals(Money other) { return ( currency.equals(other.currency) && (amount.equals(other.amount)) ); } public Money add(Money other) throws Exception{ assertSameCurrencyAs( other ); return newMoney(amount.add(other.amount, DEFAULT_CONTEXT)); } private int compareTo(Money money) throws Exception { assertSameCurrencyAs( money ); return amount.compareTo( money.amount ); } public Money multiply(BigDecimal amount) { return new Money( this.amount().multiply(amount, DEFAULT_CONTEXT), currency); } public Money multiply( BigDecimal amount, RoundingMode roundingMode ) { MathContext ct = new MathContext( currency.getDefaultFractionDigits(), roundingMode ); return new Money( amount().multiply(amount, ct), currency); } private Money newMoney(BigDecimal amount) { return new Money( amount, this.currency ); } public Money multiply(double amount) { return multiply( new BigDecimal( amount ) ); } public Money subtract(Money other) throws Exception { assertSameCurrencyAs(other); return newMoney( amount.subtract(other.amount, DEFAULT_CONTEXT) ); } public int compareTo(Object other) throws Exception { return compareTo((Money) other); } public boolean greaterThan(Money other)throws Exception { return (compareTo(other) > 0); } // public Money[] allocate(int n){ // Money lowResult = newMoney( amount.unscaledValue().longValue()/n ); // Money highResult = newMoney(lowResult.amount + 1); // Money[] results = new Money[n]; // int remainder = (int) amount % n; // // for(int i = 0; i < remainder; i++)results[i] = highResult; // for(int i = 0; i < n; i++) results[i] = lowResult; // // return results; // } // // public Money[]allocate(long[] ratios){ // long total = 0; // for (int i = 0; i < ratios.length; i++) { // total += ratios[i]; // } // long remainder = amount; // Money[] results = new Money[ratios.length]; // for (int i = 0; i < results.length; i++) { // results[i] = newMoney(amount * ratios[i]/total); // remainder -= results[i].amount; // } // for (int i = 0; i < remainder; i++) { // results[i].amount++; // } // return results; // // } public Money divideByNumber( double divisor){ BigDecimal div = BigDecimal.valueOf( divisor ); BigDecimal ans = this.amount.divide(div, DEFAULT_CONTEXT); return new Money(ans, this.currency); } public int getQuotient( Money divisor ){ BigDecimal ans = this.amount.divide(divisor.amount, RoundingMode.DOWN); return ans.intValue(); } /** * divides toe moneys and return the quotient and Remainder this method has been customised, * for my money transfer needs...sorry * @param divisor * @return */ public int[] getQuotientandRemainder(Money divisor){ int[] ans = new int[2]; BigDecimal[] bdArr = this.amount.divideAndRemainder(divisor.amount, DEFAULT_CONTEXT); BigDecimal quo = bdArr[0]; BigDecimal rem = bdArr[1]; ans[0] = quo.intValue(); if( rem.compareTo(BigDecimal.ZERO) == 0 ){ ans[1] =0; }else{ ans[1] = 1; } return ans; } public String toFormattedString() { NumberFormat nf = NumberFormat.getCurrencyInstance(); nf.setCurrency( currency ); nf.setGroupingUsed( true ); nf.setMaximumFractionDigits( currency.getDefaultFractionDigits() ); return nf.format( this.amount.doubleValue() ); } /** * Returns the ISO-4217 currency code of the currency * attached to this money. * * @return The ISO-4217 currency code. */ public String getCurrencyCode() { return currency.getCurrencyCode(); } @Override public String toString() { return amount.toString(); } /** * Returns the precision for this money. The precision is the total number * of digits that the value can represent. This includes the integer part. * So, 18 would be able to represent: * *1234567890.12345678
* *1234567890123456.78
* *123456789012345678
* *0.123456789012345678
* * @return The precision. */ public int precision() { return amount.precision(); } /** * Returns the 'scale' for this money. The scale is the number of * digits that are moved to the fractional part, assuming that all * digits are represented by a single integer value. For example: * *If: 123456789012345678
has scaling 2, it would be : * *1234567890123456.78
* * @return The scale value. */ public int scale() { return amount.scale(); } /** * Returns the sign for the money (negative or positive). * -1 if negative, 0 if 0.00 (zero), 1 if positive. * * @return The sign of the money. */ public int signum() { return amount.signum(); } }
And here is the UnknownCurrencyCodeException class
package com.console.lib.utils; /** * An exception which is raised when an unrecognised currency code is passed to the * Currency class. * * @author Farouk Alhassan * @see Currency */ public class UnknownCurrencyCodeException extends Exception { // Reason for exception private String reason = null; /** * Create a new unknown currency code exception. * * @param reason for the exception */ public UnknownCurrencyCodeException(String reason) { this.reason = reason; } /** * Return the reason this exception was raised. * * @return the reason why the string isn't a valid currency code */ public String getReason() { return reason; } /** * Convert the exception to a string * * @return string version of the exception */ public String toString() { return getReason(); } }
3 comments:
The use of MathContext is completely wrong here, If you are looking to preserve 2 digits after decimal then use BigDecimal.setScale(precision, RoundingMode), Using MathContext only take cares of Significant digits.
So in case where you will have amount 20.2 * 3 = 61.6
your implementation will result
62 (2 significant digits)
I am not sure what you mean as this class has been used extensively. here is an example quick app i just run to see if you are right.
public class Main {
public static void main(String[] args) {
Money cash = Money.pounds(20.2);
Money result = cash.multiply(3);
System.out.println("result="+result.toFormattedString());
}
}
And here is the output.
result=£60.60
Sorry I couldn't format the code:
Ohh I just realised I have changed that part since I posted this.
The code now looks like
private MathContext DEFAULT_CONTEXT = new MathContext( 10, RoundingMode.HALF_DOWN );
Post a Comment