How to model an amount of Money in Java [closed]
We don’t allow questions seeking recommendations for books, tools, software libraries, and more. You can edit the question so it can be answered with facts and citations.
Closed 3 years ago.
Improve this questionCan anybody recommend a class library or a design pattern that models amounts of Money?
I guess it should support:
- multiple currencies
- a mechanism to indicate the number of decimals
- math (respecting certain开发者_高级运维 rounding conventions (cfr ie. Banker's Rounding))
- serialisation to and from a String representation
- ... ?
I.e. 19.99 USD could be serialized into "USD-2-00000001999"
(Whereas 2 indicates the number of decimals)I would check the monetary module from the JScience project (by Jean Marie Dautelle).
Depending on your exact needs, Stephen Colebourne started Joda-Money ("a more focussed project" [than JScience]) some time ago. But there is no full release available yet (version 0.5 was released one year ago).
Adding to what Paul said, there's Martin Fowler's Money Pattern.
Joda-money looks promising as well: http://www.joda.org/joda-money/ (Not sure how production ready it is though)
There is JSR 354 JavaMoney that should become a part of Java 9. take look into presentation for more details.
This JSR should be a replacement for Joda Money, but currently only Joda Money is stable and tested in production.
Also you can take a look on this libraries:
JScience - with monetary unit
Time and Money ("need alpha users")
- Scala Currency class example
This is a full example of the java money class with Martin Fowler's Analysis pattern:
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();
}
}
Thanks to farouka at http://cameotutorials.blogspot.com/2009/06/money-class-for-use-in-currency.html
this looks like it might help, but I have no experience with it: http://quantlib.org/index.shtml
As Java itself doesn't have a class for money(AFAIK), one reliable strategy would be following existing best practices & when it comes to money, Martin Fowler has provided a well one on his Book: Patterns of Enterprise Application Architecture. A brief can be found here.
But unfortunately I couldn't found any solution on Github implementing it in Java There is a good one for PHP, FYR
Also, there is a full page of explanation as well as partly implementation in Java(which you can copy/paste the raw version). If you are interested in consulting it, here it is(if still the case):
http://www.codecodex.com/wiki/Money_Utility_class
精彩评论