All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sisioh.baseunits.scala.money.Money.scala Maven / Gradle / Ivy

There is a newer version: 0.1.22
Show newest version
/*
 * Copyright 2011 Sisioh Project and the Others.
 * lastModified : 2011/04/22
 *
 * This file is part of Tricreo.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.sisioh.baseunits.scala.money

import java.util.{ Locale, Currency }
import org.sisioh.baseunits.scala.util.Ratio
import org.sisioh.baseunits.scala.time.Duration
import math.BigDecimal.RoundingMode

/**
 * 金額を表すクラス。
 *
 * ある一定の「量」と「通貨単位」から成るクラスである。
 *
 * @author j5ik2o
 * @param amount 量 [[scala.math.BigDecimal]]
 * @param currency 通貨単位 [[java.util.Currency]]
 */
class Money(val amount: BigDecimal, val currency: Currency)
    extends Ordered[Money] with Serializable {

  require(amount.scale == currency.getDefaultFractionDigits, "Scale of amount does not match currency")

  override def equals(obj: Any) = obj match {
    case that: Money => amount == that.amount && currency == that.currency
    //    case bd: BigDecimal => amount == bd
    //    case n: Int => amount == n
    //    case f: Float => amount == f
    //    case d: Double => amount == d
    case _           => false
  }

  override def hashCode = 31 * (amount.hashCode + currency.hashCode)

  /**
   * Returns a [[org.sisioh.baseunits.scala.money.Money]] whose amount is
   * the absolute amount of this [[org.sisioh.baseunits.scala.money.Money]], and whose scale is this.scale().
   *
   * @return 絶対金額
   */
  lazy val abs: Money = Money(amount.abs, currency)

  /**
   * 金額同士の比較を行う。
   *
   * 相対的に量が小さい方を「小さい」と判断する。通貨単位が異なる場合は [[java.lang.ClassCastException]] を
   * スローするが、どちらか片方の量が`0`である場合は例外をスローしない。
   *
   * 例えば`10 USD`と`0 JPY`は、後者の方が小さい。
   * また、`0 USD`と`0 JPY`は同じである。
   *
   * @param that 比較対象
   * @return `Comparable.compareTo(Object)`に準じる
   */
  override def compare(that: Money): Int = {
    require(currency == that.currency)
    amount compare that.amount
  }

  def /(divisor: Double): Money = dividedBy(divisor)

  def *(other: BigDecimal): Money = times(other)

  def +(other: Money): Money = {
    require(currency == other.currency)
    plus(other)
  }

  def -(other: Money): Money = {
    require(currency == other.currency)
    minus(other)
  }

  /**
   * この金額に対して、指定した`ratio`の割合の金額を返す。
   *
   * @param ratio 割合
   * @param scale スケール
   * @param roundingMode 丸めモード
   * @return 指定した割合の金額
   */
  def applying(ratio: Ratio, scale: Int, roundingMode: BigDecimal.RoundingMode.Value): Money = {
    val newAmount = ratio.times(amount).decimalValue(scale, roundingMode)
    Money.adjustBy(newAmount, currency)
  }

  /**
   * この金額に対して、指定した`ratio`の割合の金額を返す。
   *
   * @param ratio 割合
   * @param roundingMode 丸めモード
   * @return 指定した割合の金額
   */
  def applying(ratio: Ratio, roundingMode: BigDecimal.RoundingMode.Value): Money =
    applying(ratio, currency.getDefaultFractionDigits, roundingMode)

  /**
   * このオブジェクトの`amount`フィールド(量)を返す。
   *
   * CAUTION: このメソッドは、このオブジェクトがカプセル化する要素を外部に暴露する。取り扱いには充分注意のこと。
   *
   * How best to handle access to the internals? It is needed for
   * database mapping, UI presentation, and perhaps a few other
   * uses. Yet giving public access invites people to do the
   * real work of the Money object elsewhere.
   * Here is an experimental approach, giving access with a
   * warning label of sorts. Let us know how you like it.
   *
   * @return 量
   */
  val breachEncapsulationOfAmount = amount

  /**
   * このオブジェクトの`currency`フィールド(通貨単位)を返す。
   *
   * CAUTION: このメソッドは、このオブジェクトがカプセル化する要素を外部に暴露する。取り扱いには充分注意のこと。
   *
   * @return 通貨単位
   */
  val breachEncapsulationOfCurrency = currency

  /**
   * この金額を、`divisor`個に均等に分割した場合の金額を返す。
   *
   * 丸めモードは `RoundingMode#HALF_EVEN` を適用する。
   *
   * @param divisor 除数
   * @return 金額
   */
  def dividedBy(divisor: Double): Money =
    dividedBy(divisor, Money.DefaultRoundingMode)

  /**
   * この金額を、`divisor`個に均等に分割した場合の金額を返す。
   *
   * @param divisor 除数
   * @param roundingMode 丸めモード
   * @return 金額
   */
  def dividedBy(divisor: BigDecimal, roundingMode: BigDecimal.RoundingMode.Value): Money = {
    val newAmount = amount.bigDecimal.divide(divisor.bigDecimal, roundingMode.id)
    Money(BigDecimal(newAmount), currency)
  }

  /**
   * この金額を、`divisor`個に均等に分割した場合の金額を返す。
   *
   * @param divisor 除数
   * @param roundingMode 丸めモード
   * @return 金額
   */
  def dividedBy(divisor: Double, roundingMode: BigDecimal.RoundingMode.Value): Money =
    dividedBy(BigDecimal(divisor), roundingMode)

  /**
   * この金額の、`divisor`に対する割合を返す。
   *
   * @param divisor 除数
   * @return 割合
   * @throws ClassCastException 引数の通貨単位がこのインスタンスの通貨単位と異なる場合
   * @throws ArithmeticException 引数`divisor`の量が0だった場合
   */
  def dividedBy(divisor: Money): Ratio = {
    checkHasSameCurrencyAs(divisor)
    Ratio(amount, divisor.amount)
  }

  /**
   * このインスタンがあらわす金額が、`other`よりも大きいかどうか調べる。
   *
   * 等価の場合は`false`とする。
   *
   * @param other 基準金額
   * @return 大きい場合は`true`、そうでない場合は`false`
   * @throws ClassCastException 引数の通貨単位がこのインスタンスの通貨単位と異なる場合
   */
  def isGreaterThan(other: Money): Boolean =
    this > other

  /**
   * このインスタンがあらわす金額が、`other`よりも小さいかどうか調べる。
   *
   * 等価の場合は`false`とする。
   *
   * @param other 基準金額
   * @return 小さい場合は`true`、そうでない場合は`false`
   * @throws ClassCastException 引数の通貨単位がこのインスタンスの通貨単位と異なる場合
   */
  def isLessThan(other: Money): Boolean = this < other

  /**
   * このインスタンがあらわす金額が、負の金額かどうか調べる。
   *
   * ゼロの場合は`false`とする。
   *
   * @return 負の金額である場合は`true`、そうでない場合は`false`
   */
  lazy val isNegative: Boolean = amount < BigDecimal(0)

  /**
   * このインスタンがあらわす金額が、正の金額かどうか調べる。
   *
   * ゼロの場合は`false`とする。
   *
   * @return 正の金額である場合は`true`、そうでない場合は`false`
   */
  lazy val isPositive: Boolean = amount > BigDecimal(0)

  /**
   * このインスタンがあらわす金額が、ゼロかどうか調べる。
   *
   * @return ゼロである場合は`true`、そうでない場合は`false`
   */
  lazy val isZero: Boolean =
    equals(Money.adjustBy(0.0, currency))

  /**
   * この金額から`other`を差し引いた金額を返す。
   *
   * @param other 金額
   * @return 差し引き金額
   * @throws ClassCastException 引数の通貨単位がこのインスタンスの通貨単位と異なる場合
   */
  def minus(other: Money): Money =
    plus(other.negated)

  /**
   * Returns a `Money` whose amount is (-amount), and whose scale is this.scale().
   *
   * @return 金額
   */
  lazy val negated: Money =
    Money(BigDecimal(amount.bigDecimal.negate), currency)

  /**
   * 指定した時間量に対する、この金額の割合を返す。
   *
   * @param duration 時間量
   * @return 割合
   */
  def per(duration: Duration): MoneyTimeRate =
    new MoneyTimeRate(this, duration)

  /**
   * この金額に`other`を足した金額を返す。
   *
   * @param other 金額
   * @return 足した金額
   * @throws ClassCastException 引数の通貨単位がこのインスタンスの通貨単位と異なる場合
   */
  def plus(other: Money): Money = {
    checkHasSameCurrencyAs(other)
    Money.adjustBy(amount + other.amount, currency)
  }

  /**
   * この金額に`factor`を掛けた金額を返す。
   *
   * 丸めモードは `RoundingMode#HALF_EVEN` を適用する。
   *
   * TODO: Many apps require carrying extra precision in intermediate
   * calculations. The use of Ratio is a beginning, but need a comprehensive
   * solution. Currently, an invariant of Money is that the scale is the
   * currencies standard scale, but this will probably have to be suspended or
   * elaborated in intermediate calcs, or handled with defered calculations
   * like Ratio.
   *
   * @param factor 係数
   * @return 掛けた金額
   */
  def times(factor: BigDecimal): Money =
    times(factor, Money.DefaultRoundingMode)

  /**
   * この金額に`factor`を掛けた金額を返す。
   *
   * TODO: BigDecimal.multiply() scale is sum of scales of two multiplied
   * numbers. So what is scale of times?
   *
   * @param factor 係数
   * @param roundingMode 丸めモード
   * @return 掛けた金額
   */
  def times(factor: BigDecimal, roundingMode: BigDecimal.RoundingMode.Value): Money =
    Money.adjustBy(amount * factor, currency, roundingMode)

  /**
   * この金額に`amount`を掛けた金額を返す。
   *
   * 丸めモードは `RoundingMode#HALF_EVEN` を適用する。
   *
   * @param amount 係数
   * @return 掛けた金額
   */
  def times(amount: Double): Money =
    times(BigDecimal(amount))

  /**
   * この金額に`amount`を掛けた金額を返す。
   *
   * @param amount 係数
   * @param roundingMode 丸めモード
   * @return 掛けた金額
   */
  def times(amount: Double, roundingMode: BigDecimal.RoundingMode.Value): Money =
    times(BigDecimal(amount), roundingMode)

  /**
   * この金額に`amount`を掛けた金額を返す。
   *
   * 丸めモードは `RoundingMode#HALF_EVEN` を適用する。
   *
   * @param amount 係数
   * @return 掛けた金額
   */
  def times(amount: Int): Money =
    times(BigDecimal(amount))

  override def toString =
    currency.getSymbol + " " + amount

  /**
   * 指定したロケールにおける、単位つきの金額表現の文字列を返す。
   *
   * @param localeOption ロケールの`Option`。`None`の場合は `Locale#getDefault()` を利用する。
   * @return 金額の文字列表現
   */
  def toString(localeOption: Option[Locale]) = {
    def createStrng(_locale: Locale) = currency.getSymbol(_locale) + " " + amount
    localeOption match {
      case Some(locale) => createStrng(locale)
      case None         => createStrng(Locale.getDefault)
    }
  }

  private[money] def hasSameCurrencyAs(arg: Money): Boolean =
    currency.equals(arg.currency) || arg.amount.equals(BigDecimal(0)) || amount.equals(BigDecimal(0))

  /**
   * この金額に、最小の単位金額を足した金額、つまりこの金額よりも1ステップ分大きな金額を返す。
   *
   * @return この金額よりも1ステップ分大きな金額
   */
  private[money] lazy val incremented: Money = plus(minimumIncrement)

  /**
   * 最小の単位金額を返す。
   *
   * 例えば、日本円は1円であり、US$は1セント(つまり0.01ドル)である。
   *
   * This probably should be Currency responsibility. Even then, it may need
   * to be customized for specialty apps because there are other cases, where
   * the smallest increment is not the smallest unit.
   *
   * @return 最小の単位金額
   */
  private[money] lazy val minimumIncrement: Money = {
    val increment = BigDecimal(1).bigDecimal.movePointLeft(currency.getDefaultFractionDigits)
    Money(BigDecimal(increment), currency)
  }

  private def checkHasSameCurrencyAs(aMoney: Money): Unit = {
    if (hasSameCurrencyAs(aMoney) == false) {
      throw new ClassCastException(aMoney.toString() + " is not same currency as " + this.toString())
    }
  }

}

/**
 * `Money`コンパニオンオブジェクト。
 *
 * @author j5ik2o
 */
object Money {

  //implicit def bigDecimalToMoney(amount: Int) = apply(amount)

  val USD = Currency.getInstance("USD")

  val EUR = Currency.getInstance("EUR")

  val JPY = Currency.getInstance("JPY")

  val DefaultRoundingMode = BigDecimal.RoundingMode.HALF_EVEN

  def apply(amount: BigDecimal, currency: Currency): Money = new Money(amount, currency)

  def unapply(money: Money): Option[(BigDecimal, Currency)] = Some(money.amount, money.currency)

  /**
   * `amount`で表す量のドルを表すインスタンスを返す。
   *
   * This creation method is safe to use. It will adjust scale, but will not
   * round off the amount.
   *
   * @param amount 量
   * @return `amount`で表す量のドルを表すインスタンス
   */
  def dollars(amount: BigDecimal): Money = adjustBy(amount, USD)

  /**
   * `amount`で表す量のドルを表すインスタンスを返す。
   *
   * WARNING: Because of the indefinite precision of double, this method must
   * round off the value.
   *
   * @param amount 量
   * @return `amount`で表す量のドルを表すインスタンス
   */
  def dollars(amount: Double): Money = adjustBy(amount, USD)

  /**
   * This creation method is safe to use. It will adjust scale, but will not
   * round off the amount.
   * @param amount 量
   * @return `amount`で表す量のユーロを表すインスタンス
   */
  def euros(amount: BigDecimal): Money = adjustBy(amount, EUR)

  /**
   * WARNING: Because of the indefinite precision of double, this method must
   * round off the value.
   * @param amount 量
   * @return `amount`で表す量のユーロを表すインスタンス
   */
  def euros(amount: Double): Money = adjustBy(amount, EUR)

  /**
   * [[scala.Iterable]]に含む全ての金額の合計金額を返す。
   *
   * 合計金額の通貨単位は、 `monies`の要素の(共通した)通貨単位となるが、
   * `Collection`が空の場合は、現在のデフォルトロケールにおける通貨単位で、量が0のインスタンスを返す。
   *
   * @param monies 金額の集合
   * @return 合計金額
   * @throws ClassCastException 引数の通貨単位の中に通貨単位が異なるものを含む場合。
   * 				ただし、量が0の金額については通貨単位を考慮しないので例外は発生しない。
   */
  def sum(monies: Iterable[Money]): Money = {
    if (monies.isEmpty) {
      Money.zero(Currency.getInstance(Locale.getDefault))
    } else {
      monies.reduceLeft(_ + _)
    }
  }

  /**
   * This creation method is safe to use. It will adjust scale, but will not
   * round off the amount.
   *
   * @param amount 量
   * @param currency 通貨単位
   * @return 金額
   */
  def adjustBy(amount: BigDecimal, currency: Currency): Money =
    adjustBy(amount, currency, BigDecimal.RoundingMode.UNNECESSARY)

  /**
   * For convenience, an amount can be rounded to create a Money.
   *
   * @param rawAmount 量
   * @param currency 通貨単位
   * @param roundingMode 丸めモード
   * @return 金額
   */
  def adjustBy(rawAmount: BigDecimal, currency: Currency, roundingMode: BigDecimal.RoundingMode.Value): Money = {
    val amount = rawAmount.setScale(currency.getDefaultFractionDigits, roundingMode)
    new Money(amount, currency)
  }

  /**
   * WARNING: Because of the indefinite precision of double, this method must
   * round off the value.
   *
   * @param dblAmount 量
   * @param currency 通貨単位
   * @return 金額
   */
  def adjustBy(dblAmount: Double, currency: Currency): Money =
    adjustBy(dblAmount, currency, DefaultRoundingMode)

  /**
   * Because of the indefinite precision of double, this method must round off
   * the value. This method gives the client control of the rounding mode.
   *
   * @param dblAmount 量
   * @param currency 通貨単位
   * @param roundingMode 丸めモード
   * @return 金額
   */
  def adjustRound(dblAmount: Double, currency: Currency, roundingMode: BigDecimal.RoundingMode.Value): Money = {
    val rawAmount = BigDecimal(dblAmount)
    adjustBy(rawAmount, currency, roundingMode)
  }

  /**
   * This creation method is safe to use. It will adjust scale, but will not
   * round off the amount.
   *
   * @param amount 量
   * @return `amount`で表す量の円を表すインスタンス
   */
  def yens(amount: BigDecimal): Money = adjustBy(amount, JPY)

  /**
   * WARNING: Because of the indefinite precision of double, this method must
   * round off the value.
   *
   * @param amount 量
   * @return `amount`で表す量の円を表すインスタンス
   */
  def yens(amount: Double): Money = adjustBy(amount, JPY)

  /**
   * 指定した通貨単位を持つ、量が0の金額を返す。
   *
   * @param currency 通貨単位
   * @return 金額
   */
  def zero(currency: Currency): Money = adjustBy(0.0, currency)

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy