Pull to refresh

Класс Money

Reading time 7 min
Views 19K
Множество приложений оперирует деньгами, но в большинстве языков программирования, в том числе и в Java, нет стандартного класса Money. Многие просто хранят денежные величины в переменный с плавающей точкой, но это плохое решение, так как всегда возникают проблемы с округлением, которые приходится каждый раз решать по-разному. Кроме того, деньги могут измеряться в разных величинах (рублях, долларах, евро и других). Нельзя складывать или сравнивать евро с рублями — это неминуемо вызовет ошибку в результате.

При реализации класса денег нужно первым делом выбрать тип данных для хранения величины денежной суммы. Первым делом в голову приходит, что нужно использовать тип с плавающей точкой. Это не будет хорошим решением. Дело в том, что вычисление с плавающей точкой не всегда приводят к желательному результату. Попробуйте выполнить следующий пример:
double val = 0.00;
for (int i = 0; i < 10; i++)
  val += 0.10;
System.out.println( val == 1.00 );
* This source code was highlighted with Source Code Highlighter.

Поэтому лучше выбрать целочисленный тип и выражать его в наименьших единицах для выбранной валюты: в копейках или центах. Для валюты лучше использовать стандартный для Java тип данных Currency.

Класс, который получился у меня:
package money;

import java.util.Currency;
import java.util.Locale;
import org.junit.Assert;

/**
* Created by IntelliJ IDEA.
* User: Anthony
* Date: 14.09.2008
* Time: 15:25:26
* To change this template use File | Settings | File Templates.
*/
public class Money {
  private long amount;
  private Currency currency;

  /**
   * return Числовое значение денег.
   */
  public double getAmount() {
    return (double)amount / centFactor();
  }

  /**
   * return Текущую валюту
   */
  public Currency getCurrency() {
    return currency;
  }

  public Money(long amount, Currency currency )
  {
    this.currency = currency;
    this.amount = amount * centFactor();
  }

  public Money(double amount, Currency currency)
  {
    this.currency = currency;
    this.amount = Math.round( amount * centFactor() );
  }

  private Money()
  {
  }

  public static Money dollars(double amount)
  {
    return new Money(amount, Currency.getInstance( Locale.US ) );
  }

  public static Money locale(double amount)
  {
    return new Money(amount, Currency.getInstance( Locale.getDefault() ) );
  }

  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    Money money = (Money) o;

    if (amount != money.amount) return false;
    if (currency != null? !currency.equals(money.currency): money.currency != null) return false;

    return true;
  }

  public int hashCode() {
    int result;
    result = (int) (amount ^ (amount >>> 32));
    result = 31 * result + (currency != null? currency.hashCode(): 0);
    return result;
  }

  /**
   * Функция складывает деньги.
   * param other с чем сложить.
   * return Результат сложения.
   */
  public Money add(Money other)
  {
    assertSameCurrencyAs(other);
    return newMoney(amount + other.amount);
  }

  /**
   * Функция вычитает деньги.
   * param other вычистаемое.
   * return Результат вычитания.
   */
  public Money subtract(Money other)
  {
    assertSameCurrencyAs(other);
    return newMoney(amount — other.amount);
  }

  /**
   * Функция умнажает деньги на коэффициент.
   * param arg вычистаемое.
   * return Результат умножения.
   */
  public Money multiply(double arg)
  {
    return new Money(getAmount() * arg, currency);
  }

  public int compareTo(Object other)
  {
    return compareTo((Money)other);
  }

  /**
   * Фунция сравнения денег.
   * param other с чем сравнить
   * return
   * -1 меньше<br>
   * 0 равно<br>
   * 1 больше<br>
   */
  public int compareTo(Money other)
  {
    assertSameCurrencyAs(other);
    if(amount < other.amount)
      return -1;
    if(amount == other.amount)
      return 0;

    return 1;
  }

  /**
   * Больше ли деньги.
   * param other с чем сравнивать.
   * return True, если больше.
   */
  public boolean greaterThan(Money other)
  {
    return (compareTo(other) > 0);
  }

  /**
   * Разделить деньги на несколько частей.
   * param n количество частей.
   * return Массив разделенных денег.
   */
  public Money[] allocate(int n)
  {
    Money lowResult = newMoney(amount/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 = remainder; i < n; i++)
      results[i] = lowResult;
    return results;
  }

  /**
   * Разделяет деньги на неравный части.
   * param ratios пропорция для разделения.
   * return Массив разделенных денег.
   */
  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;
  }

  private static final int [ ] cents = new int[] { 1, 10, 100, 1000 };
  private int centFactor()
  {
    return cents[currency.getDefaultFractionDigits()];
  }

  private void assertSameCurrencyAs(Money arg)
  {
    Assert.assertEquals(«money math mismatch», currency, arg.currency);
  }

  private Money newMoney(long amount)
  {
    Money money = new Money();
    money.currency = this.currency;
    money.amount = amount;
    return money;
  }
}
* This source code was highlighted with Source Code Highlighter.


И тест JUnit к нему:
package test.money;

import junit.framework.Test;
import junit.framework.TestSuite;
import junit.framework.TestCase;
import junit.framework.Assert;
import money.Money;

/**
* Money Tester.
*
* author <Authors name>
* since <pre>09/14/2008</pre>
* version 1.0
*/
public class MoneyTest extends TestCase {
  public MoneyTest(String name) {
    super(name);
  }

  public void setUp() throws Exception {
    super.setUp();
  }

  public void tearDown() throws Exception {
    super.tearDown();
  }

  /**
   *
   * Method: add(Money other)
   *
   */
  public void testAdd() throws Exception {
    Money m1 = Money.dollars( 1.316 );
    Money m2 = Money.dollars( 1.291 );
    Assert.assertEquals( m1.add( m2 ), Money.dollars(2.61) );
  }

  /**
   *
   * Method: subtract(Money other)
   *
   */
  public void testSubtract() throws Exception {
    Money m1 = Money.dollars( 1.316 );
    Money m2 = Money.dollars( 1.291 );
    Assert.assertEquals( m1.subtract( m2 ), Money.dollars(0.03) );
  }

  /**
   *
   * Method: compareTo(Object other)
   *
   */
  public void testCompareTo() throws Exception {
    Money m1 = Money.dollars( 1.316 );
    Money m2 = Money.dollars( 1.313 );
    Assert.assertEquals( m1.compareTo( m2 ), 1 );
  }

  /**
   *
   * Method: multiply(double arg)
   *
   */
  public void testMultiply() throws Exception {
    Money m1 = Money.dollars( 1.316 );
    Assert.assertEquals( m1.multiply( 1.333 ), Money.dollars(1.76) );
  }

  /**
   *
   * Method: allocate(int n)
   *
   */
  public void testAllocateN() throws Exception {

    Money m[] = Money.dollars( 1.35 ).allocate( 3 );
    Assert.assertEquals( m[0].add( m[1] ).add( m[2] ), Money.dollars(1.35) );
  }

  /**
   *
   * Method: allocate(long[] ratios)
   *
   */
  public void testAllocateRatios() throws Exception {
    long[] allocation = {3,7};
    Money[] result = Money.dollars(0.05).allocate(allocation);
    assertEquals(Money.dollars(0.02), result[0]);
    assertEquals(Money.dollars(0.03), result[1]);
  }

  public static Test suite() {
    return new TestSuite(MoneyTest.class);
  }
}
* This source code was highlighted with Source Code Highlighter.


Статья написана по мотивам книги Мартина Фаулера «Архитектура корпоративных приложений».
Tags:
Hubs:
+14
Comments 50
Comments Comments 50

Articles