typelevel/squants

Do not require implicit MoneyNumeric in context if calculations are in same currency

pathikrit opened this issue · 1 comments

Related to #231

Why do we need a money context if we are in the same currency?

Couple of ways to solve this:

  1. Type safe approach: We can make this possible by Money having a dependent type of currency and providing a default MoneyNumeric where each argument has the same dependent currency type
  2. Unsafe approach: We provide a default implicit instance which requires an implicit currency converter in scope. We also provide a default low priority currency converter instance that throws run time exceptions when converting different currencies or returns 1 if same currency when converting.

So I'm happy to see the Squants team be so active lately. So I thought I would offer a solution to this problem.

What we really want in this situation is to give a default value to the implicit variable moneyContext. But the apply methods in the Money object are overloaded and you can't give default values to multiple versions of an overloaded method in Scala. So the only way I found to solve this issue is to create a series of classes that represent each overload of the apply method. Then those classes have implicit conversions to Money and some overrides to make them work with the testing code.

The pseudo classes:

  case class PseudoMoney1(value: BigDecimal) {
    def convert(implicit context: MoneyContext = defaultMoneyContext): Money =
      new Money(value, context.defaultCurrency)
    lazy val del = convert
    override def toString: String = del.toString
    override def equals(o: Any): Boolean = o match {
      case pm: PseudoMoney1 => pm.del.equals(del)
      case pm: PseudoMoney2 => pm.del.equals(del)
      case pm: PseudoMoneyStr => pm.del.equals(del)
      case m: Money => del.equals(m)
      case _ => false
    }
    override def hashCode: Int = del.hashCode
  }

  case class PseudoMoney2(value: BigDecimal, currency: Currency) {
    def convert(implicit context: MoneyContext = defaultMoneyContext): Money =
      new Money(value, currency)
    lazy val del = convert
    override def toString: String = del.toString
    override def equals(o: Any): Boolean = {
      o match {
        case pm: PseudoMoney1 => pm.del.equals(del)
        case pm: PseudoMoney2 => pm.del.equals(del)
        case pm: PseudoMoneyStr => pm.del.equals(del)
        case m: Money => del.equals(m)
        case _ => false
      }}
    override def hashCode: Int = del.hashCode
  }

  case class PseudoMoneyStr(s: String) {
    def convert(
      implicit context: MoneyContext = defaultMoneyContext): Try[Money] =
      context.dimension(s)
    lazy val del = convert.get
    override def toString: String = del.toString
    override def equals(o: Any): Boolean = o match {
      case pm: PseudoMoney1 => pm.del.equals(del)
      case pm: PseudoMoney2 => pm.del.equals(del)
      case pm: PseudoMoneyStr => pm.del.equals(del)
      case m: Money => del.equals(m)
      case _ => false
    }
    override def hashCode: Int = del.hashCode
  }

Then to the Money class change the equals method to:

  /**
   * Override for Quantity.equal to only match Moneys of like Currency
   * @param that Money must be of matching value and unit
   * @return
   */
  override def equals(that: Any): Boolean = {
    that match {
      case m: Money  {
        if (currency == m.currency) m.value == value
        else if (!m.context.rates.isEmpty) m.in(currency).value == value
        else false
      }
      case pm: PseudoMoney1  equals(pm.convert)
      case pm: PseudoMoney2  equals(pm.convert)
      case pm: PseudoMoneyStr  equals(pm.convert)
      case _         false
    }
  }

and finally the implicit conversions:

  implicit def pseudoMoney1ToMoney(pseudo: PseudoMoney1)(
    implicit context: MoneyContext = defaultMoneyContext): Money =
    pseudo.convert
  implicit def pseudoMoney2ToMoney(pseudo: PseudoMoney2)(
    implicit context: MoneyContext = defaultMoneyContext): Money =
    pseudo.convert
  implicit def pseudoMoneyStrToMoney(pseudo: PseudoMoneyStr)(
    implicit context: MoneyContext = defaultMoneyContext): Try[Money] =
    pseudo.convert

And that should solve this issue nicely. Maybe someone else knows of a better way but this seems like the best way to solve this problem here. This way a user always has a currency converter and the user doesn't need to provide one unless they want to override its behavior.

It should be noted that the lazy val del (short for delegate) makes this a bit weird. Del exists to prevent unnecessary object creation in some use cases. However, del could use the default money context instead of one declared in the user's scope. To avoid this, just declare the type of the variable to which you assign the return from the apply method. Alternatively, we could just remove del altogether and ignore the issue of unnecessary calls to the implicit conversion. Perhaps the compiler will remove them for us.