Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
May 28, 2021 08:26 pm GMT

Ruby Money & BigDecimal

The problem

In my current job, we faced calculation errors when operating with float for Money.
After some investigation, we found this article

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:

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  endendclass Rate  attr_accessor :money  include BigDecimalCheck  enforce_big_decimal :moneyend

With that code in place a consumer code would work like this

r = Rate.newr.money = 33            # worksr.money = 33.0293       # worksr.money = "33"          # worksr.money = "33.0293"     # worksr.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:

class Foo  extend ClassMethod  # Now I'm able to call methods in ClassMethod form hereend

which will inject methods from ClassMethod into Foo object at class scope.

  • enforce_big_decimal
    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:

def unit_price=(value)  parsed_value = BigDecimal(value.to_s) # raise error if cannot parse  instance_variable_set(:@unit_price, parsed_value)enddef 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!


Original Link: https://dev.to/delbetu/ruby-money-bigdecimal-1chb

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To