Skip to content

Commit

Permalink
Merge pull request #375 from commercetools/optimize_Money
Browse files Browse the repository at this point in the history
optimize Money
  • Loading branch information
yanns authored Feb 3, 2022
2 parents d9d831a + 9b1f502 commit 49fa8b9
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 38 deletions.
102 changes: 67 additions & 35 deletions util/src/main/scala/Money.scala
Original file line number Diff line number Diff line change
Expand Up @@ -83,31 +83,21 @@ object BaseMoney {
* @param currency
* The currency of the amount.
*/
case class Money private (amount: BigDecimal, currency: Currency)
case class Money private (centAmount: Long, currency: Currency)
extends BaseMoney
with Ordered[Money] {
import Money._

require(
amount.scale == currency.getDefaultFractionDigits,
"The scale of the given amount does not match the scale of the provided currency." +
" - " + amount.scale + " <-> " + currency.getDefaultFractionDigits
)

private val centFactor: Double = 1 / pow(10, currency.getDefaultFractionDigits)

lazy val centAmount: Long = (amount / centFactor).toLong

private val backwardsCompatibleRoundingModeForOperations = BigDecimal.RoundingMode.HALF_EVEN

val `type`: String = TypeName

lazy val fractionDigits: Int = currency.getDefaultFractionDigits
override def fractionDigits: Int = currency.getDefaultFractionDigits
override lazy val amount: BigDecimal = BigDecimal(centAmount) * cachedCentFactor(fractionDigits)

def withCentAmount(centAmount: Long): Money = {
val newAmount = BigDecimal(centAmount) * centFactor
copy(amount = newAmount.setScale(currency.getDefaultFractionDigits))
}
def withCentAmount(centAmount: Long): Money =
copy(centAmount = centAmount)

def toHighPrecisionMoney(fractionDigits: Int): HighPrecisionMoney =
HighPrecisionMoney.fromMoney(this, fractionDigits)
Expand Down Expand Up @@ -200,7 +190,7 @@ case class Money private (amount: BigDecimal, currency: Currency)
*/
def partition(ratios: Int*): Seq[Money] = {
val total = ratios.sum
val amountInCents = (this.amount / centFactor).toBigInt
val amountInCents = BigInt(this.centAmount)
val amounts = ratios.map(amountInCents * _ / total)
var remainder = amounts.foldLeft(amountInCents)(_ - _)
amounts.map { amount =>
Expand All @@ -215,11 +205,29 @@ case class Money private (amount: BigDecimal, currency: Currency)

def compare(that: Money): Int = {
BaseMoney.requireSameCurrency(this, that)
this.amount.compare(that.amount)
}
this.centAmount.compare(that.centAmount)
}

override def toString: String = {
val centAmountString = this.centAmount.toString
val formattedString = new StringBuilder()
val centAmountLength = centAmountString.length
val decimals = this.fractionDigits
val missing = decimals - centAmountLength
if (missing >= 0) {
formattedString.append("0.")
for (_ <- 1 to missing)
formattedString.append('0')
}

override def toString: String =
this.amount.bigDecimal.toPlainString + " " + this.currency.getCurrencyCode
var i = 0
centAmountString.iterator.foreach { c =>
formattedString.append(c)
i += 1
if (centAmountLength - i == decimals) formattedString.append(".")
}
formattedString.result() + " " + this.currency.getCurrencyCode
}

def toString(nf: NumberFormat, locale: Locale): String = {
require(nf.getCurrency eq this.currency)
Expand Down Expand Up @@ -253,17 +261,46 @@ object Money {
def GBP(amount: BigDecimal): Money = decimalAmountWithCurrencyAndHalfEvenRounding(amount, "GBP")
def JPY(amount: BigDecimal): Money = decimalAmountWithCurrencyAndHalfEvenRounding(amount, "JPY")

val CurrencyCodeField: String = "currencyCode"
val CentAmountField: String = "centAmount"
val FractionDigitsField: String = "fractionDigits"
val TypeName: String = "centPrecision"
final val CurrencyCodeField: String = "currencyCode"
final val CentAmountField: String = "centAmount"
final val FractionDigitsField: String = "fractionDigits"
final val TypeName: String = "centPrecision"

def fromDecimalAmount(amount: BigDecimal, currency: Currency)(implicit
mode: RoundingMode): Money =
Money(amount.setScale(currency.getDefaultFractionDigits, mode), currency)
mode: RoundingMode): Money = {
val fractionDigits = currency.getDefaultFractionDigits
val centAmountBigDecimal = amount * cachedCentPower(fractionDigits)
val centAmount = centAmountBigDecimal.setScale(0, mode).longValue
Money(centAmount, currency)
}

def apply(amount: BigDecimal, currency: Currency): Money = {
require(
amount.scale == currency.getDefaultFractionDigits,
"The scale of the given amount does not match the scale of the provided currency." +
" - " + amount.scale + " <-> " + currency.getDefaultFractionDigits
)
fromDecimalAmount(amount, currency)(BigDecimal.RoundingMode.UNNECESSARY)
}

private final val bdOne: BigDecimal = BigDecimal(1)
final val bdTen: BigDecimal = BigDecimal(10)

val bdOne: BigDecimal = BigDecimal(1)
val bdTen: BigDecimal = BigDecimal(10)
private final val centPowerZeroFractionDigit = bdOne
private final val centPowerOneFractionDigit = bdTen
private final val centPowerTwoFractionDigit = bdTen.pow(2)
private final val centPowerThreeFractionDigit = bdTen.pow(3)
private final val centPowerFourFractionDigit = bdTen.pow(4)

private[util] def cachedCentPower(currencyFractionDigits: Int): BigDecimal =
currencyFractionDigits match {
case 0 => centPowerZeroFractionDigit
case 1 => centPowerOneFractionDigit
case 2 => centPowerTwoFractionDigit
case 3 => centPowerThreeFractionDigit
case 4 => centPowerFourFractionDigit
case other => bdTen.pow(other)
}

private val centFactorZeroFractionDigit = bdOne / bdTen.pow(0)
private val centFactorOneFractionDigit = bdOne / bdTen.pow(1)
Expand All @@ -281,13 +318,8 @@ object Money {
case other => bdOne / bdTen.pow(other)
}

def fromCentAmount(centAmount: Long, currency: Currency): Money = {
val currencyFractionDigits = currency.getDefaultFractionDigits
val centFactor = cachedCentFactor(currencyFractionDigits)
val amount = BigDecimal(centAmount) * centFactor

fromDecimalAmount(amount, currency)(BigDecimal.RoundingMode.UNNECESSARY)
}
def fromCentAmount(centAmount: Long, currency: Currency): Money =
new Money(centAmount, currency)

private val cachedZeroEUR = fromCentAmount(0L, Currency.getInstance("EUR"))
private val cachedZeroUSD = fromCentAmount(0L, Currency.getInstance("USD"))
Expand Down
4 changes: 1 addition & 3 deletions util/src/test/scala/DomainObjectsGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ object DomainObjectsGen {

val money: Gen[Money] = for {
currency <- currency
amount <- Gen
.chooseNum[Int](Int.MinValue, Int.MaxValue)
.map(i => BigDecimal(i, currency.getDefaultFractionDigits))
amount <- Gen.chooseNum[Long](Long.MinValue, Long.MaxValue)
} yield Money(amount, currency)

val highPrecisionMoney: Gen[HighPrecisionMoney] = for {
Expand Down
4 changes: 4 additions & 0 deletions util/src/test/scala/MoneySpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ class MoneySpec extends AnyFunSpec with Matchers with ScalaCheckDrivenPropertyCh

it("should provide convenient toString") {
(1.00 EUR).toString must be("1.00 EUR")
(0.10 EUR).toString must be("0.10 EUR")
(0.01 EUR).toString must be("0.01 EUR")
(0.00 EUR).toString must be("0.00 EUR")
(94.5 EUR).toString must be("94.50 EUR")
}

it("should not fail on toString") {
Expand Down

0 comments on commit 49fa8b9

Please sign in to comment.