如何在Java中建模一定数量的Money

时间:2010-11-10 14:42:57

标签: java design-patterns model modeling

任何人都可以推荐一个类库设计模式 模拟金额吗?

我想它应该支持:

  • 多种货币
  • 表示小数位数的机制
  • 数学(尊重某些舍入约定(cfr ie。Banker's Rounding))
  • 与字符串表示进行序列化
  • ......?

即。 19.99美元可以序列化为“ USD-2-00000001999 ”          (而2表示小数位数)

7 个答案:

答案 0 :(得分:9)

我会检查monetary项目中的JScience模块(Jean Marie Dautelle)。

根据您的具体需求,Stephen Colebourne不久前启动了Joda-Money“一个更集中的项目” [比JScience])。但目前还没有完整版本(一年前发布了0.5版本)。

答案 1 :(得分:4)

Joda-money 看起来也很有希望:http://www.joda.org/joda-money/ (虽然不确定生产准备如何)

答案 2 :(得分:4)

除了Paul所说的,还有Martin Fowler的Money Pattern

答案 3 :(得分:4)

JSR 354 JavaMoney应该成为Java 9的一部分。 请查看presentation了解更多详情。

这个JSR应该是Joda Money的替代品,但目前只有Joda Money在生产中稳定并经过测试。

您也可以查看这些库:

答案 4 :(得分:1)

这是带有Martin Fowler分析模式的java金钱类的完整示例:

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();
    }
}

感谢farouka在http://cameotutorials.blogspot.com/2009/06/money-class-for-use-in-currency.html

答案 5 :(得分:0)

这看起来可能有所帮助,但我对此没有经验:http://quantlib.org/index.shtml

答案 6 :(得分:0)

由于Java本身没有金钱之门(AFAIK),因此一种可靠的策略将遵循现有的最佳实践,而在金钱方面,Martin Fowler在他的著作《企业应用程序架构的模式》中提供了一个很好的方法。 。 A brief can be found here

但是不幸的是,我找不到在Java There is a good one for PHP, FYR

上实现它的Github的任何解决方案。

此外,还有一整页的说明以及Java的部分实现(您可以复制/粘贴原始版本)。如果您有兴趣咨询它,这里就是(如果仍然如此):

http://www.codecodex.com/wiki/Money_Utility_class