Ruby Money & BigDecimal
Tags: ruby, metaprogramming, float, bigdecimal
Published: 2021-05-28 20:26:54

## The problem

In my current job, we faced calculation errors when operating with `float` for Money.
After some investigation, we found this [article](https://husobee.github.io/money/float/2016/09/23/never-use-floats-for-currency.html)

Our first approach was to find every usage of money attributes and parse them with `BigDecimal`.  
This solution has some drawbacks. First, we would need to replace it in many places. Second, it doesn't prevent future developers to use float.  

In order to overcome those issues, I wanted to enforce a validation over every money attribute. 
Then if a future execution accidentally does `money_attr = 233.0` (float) we could detect that error and report it.
After thinking for a moment I thought that would be preferable to do a conversion (`float->BigDecimal`) rather than raising an error.

So I'd like to write Ruby code to say: "hey if someone tries to assign a `float` to a money attribute then convert it to `BigDecimal`"

## The Solution

In order to do that I came up with this solution:

```ruby
module BigDecimalCheck
  def self.included(klass)
    klass.extend(ClassMethods)
  end

  module ClassMethods
    def enforce_big_decimal(*attrs)
      attrs.each do |attr|
        define_method :"#{attr}=" do |value|
          # try to convert argument to BigDecimal
          instance_variable_set(:"@#{attr}", BigDecimal(value.to_s))
        end
      end
    end
  end
end

class Rate
  attr_accessor :money

  include BigDecimalCheck
  enforce_big_decimal :money
end
```

With that code in place a consumer code would work like this
```ruby
r = Rate.new
r.money = 33            # works
r.money = 33.0293       # works
r.money = "33"          # works
r.money = "33.0293"     # works
r.money = "no numeric"  # Argument Error

```

## How this solution work?

* `self.included` it is a hook that Ruby modules provide. It is called when the module is included and receives the class that included it.  

* `klass.extend(ClassMethod)` Let's say that klass = Foo, then this would be the same as doing:
```ruby
class Foo
  extend ClassMethod
  # Now I'm able to call methods in ClassMethod form here
end
```
which will inject methods from ClassMethod into Foo object at class scope. 

* `enforce_big_decimal`

```ruby
    def enforce_big_decimal(*attrs)
      attrs.each do |attr|
        define_method :"#{attr}=" do |value|
          # try to convert argument to BigDecimal
          instance_variable_set(:"@#{attr}", BigDecimal(value.to_s))
        end
      end
    end
```

If I call `enforce_big_decimal :unit_price, total_price`
It will define two methods:  
```ruby
def unit_price=(value)
  parsed_value = BigDecimal(value.to_s) # raise error if cannot parse
  instance_variable_set(:@unit_price, parsed_value)
end
def total_price=(value)
  parsed_value = BigDecimal(value.to_s) # raise error if cannot parse
  instance_variable_set(:@total_price, parsed_value)
end
```

## Conslusion

I've shown an example of how to generalize the solution of a problem by using ruby meta-programming techniques.  
I hope it can help you solve similar problems.  
Feel free to ask questions or suggest improvements.  
Thanks for reading!