Saving Money with Ruby




- Accurate decimal numbers by definition, not by chance -

Wolfgang Teuber, Leipzig on Rails, updated Nov 14th, 2015

Saving Money with Ruby

When dealing with money, the question is...


savings     = 50000  # amount invested
interest    = 0.02   # annual interest rate: 2%
withholding = 0.25   # flat rate withholding tax: 25%
soli        = 0.055  # solidarity supplement: 5.5%
church      = 0.04   # church tax: 4%
exempt      = 801    # tax exemption: 801

PerfectClass.perfect_method(savings, interest, withholding, soli, church, exempt)

Would you ship this code?

Saving Money with Ruby

...probably not


savings     = 50000  # amount invested
interest    = 0.02   # annual interest rate: 2%
withholding = 0.25   # flat rate withholding tax: 25%
soli        = 0.055  # solidarity supplement: 5.5%
church      = 0.04   # church tax: 4%
exempt      = 801    # tax exemption: 801

PerfectClass.perfect_method(savings, interest, withholding, soli, church, exempt)

Integer? Wait a minute...

Saving Money with Ruby

Dividing an Integer by an Integer results in an Integer.


a = 10
a = a / 3
a = a * 3
a == 10    # false, a == 9

Solution:

Don't use Integer*,
try duck typing (.0) or typecasting (.to_f) instead.

*for financial calculations

Saving Money with Ruby

...here we go


savings     = 50000.0  # amount invested
interest    = 0.02     # annual interest rate: 2%
withholding = 0.25     # flat rate withholding tax: 25%
soli        = 0.055    # solidarity supplement: 5.5%
church      = 0.04     # church tax: 4%
exempt      = 801.0    # tax exemption: 801

PerfectClass.perfect_method(savings, interest, withholding, soli, church, exempt)

Would you ship this code?

Saving Money with Ruby

...probably not


savings     = 50000.0  # amount invested
interest    = 0.02     # annual interest rate: 2%
withholding = 0.25     # flat rate withholding tax: 25%
soli        = 0.055    # solidarity supplement: 5.5%
church      = 0.04     # church tax: 4%
exempt      = 801.0    # tax exemption: 801

PerfectClass.perfect_method(savings, interest, withholding, soli, church, exempt)

Float? Looks good at first sight...

Saving Money with Ruby

...right, that's the issue. Float is inaccurate by definition.


Float::DIG           # number of significant digits, default: 15
0.25.to_r            # (1/4)
123456789.987654321  # 123456789.98765433
0.02.to_r            # (5764607523034235/288230376151711744)
4.55 % 0.05          # 0.04999999999999957

Solution:

Don't use Float*,
try BigDecimal instead.

*for financial calculations

Saving Money with Ruby

...here we go again


require 'bigdecimal'
require 'bigdecimal/util'

savings     = 50000.0.to_d  # amount invested
interest    = 0.02.to_d     # annual interest rate: 2%
withholding = 0.25.to_d     # flat rate withholding tax: 25%
soli        = 0.055.to_d    # solidarity supplement: 5.5%
church      = 0.04.to_d     # church tax: 4%
exempt      = 801.0.to_d    # tax exemption: 801

PerfectClass.perfect_method(savings, interest, withholding, soli, church, exempt)

Would you ship this code?

Saving Money with Ruby

...maybe, maybe not


require 'bigdecimal'
require 'bigdecimal/util'

savings     = 50000.0.to_d  # amount invested
interest    = 0.02.to_d     # annual interest rate: 2%
withholding = 0.25.to_d     # flat rate withholding tax: 25%
soli        = 0.055.to_d    # solidarity supplement: 5.5%
church      = 0.04.to_d     # church tax: 4%
exempt      = 801.0.to_d    # tax exemption: 801

PerfectClass.perfect_method(savings, interest, withholding, soli, church, exempt)
        

Still Float? Hm, will it work this time?

Saving Money with Ruby

...let's see


# util.rb
...
class Float < Numeric
  # Convert +flt+ to a BigDecimal and return it.
  #
  #     require 'bigdecimal'
  #     require 'bigdecimal/util'
  #
  #     0.5.to_d
  #     # => #<BigDecimal:1dc69e0,'0.5E0',9(18)>
  #
  def to_d(precision=nil)
    BigDecimal(self, precision || Float::DIG)
  end
end
...
ext/bigdecimal/lib/bigdecimal/util.rb#L26-L41

.to_d even uses maximum precision by default...

Saving Money with Ruby

...I don't want to be picky, but check this out.


interest = 0.07
interest.to_r          # (1261007895663739/18014398509481984)
interest.to_d.to_r     # (7000000000000001/100000000000000000)

What now? Use Rational? Since it's sooo accurate?
Isn't there a Money gem? Maybe MRI just can't do it...

Saving Money with Ruby

...well, there are tools

Rational and Money
  • are great for new or small projects
  • require massive refactoring in existing projects
  • need to work with SQL-DB, noSQL-DB, APIs, JS, gems ?


interest = 7.to_r / 100
interest.to_s               # "7/100"
'%.2f' % interest           # "0.07"

require 'money'
Money.new(7, 'EUR').to_s    # "0,07"
Money.new(100, 'USD').to_s  # "1.00"

...integrating either of them in legacy code will be a huge effort.
Is there a better option?

Saving Money with Ruby

...yes there is String.to_d


require 'bigdecimal'
require 'bigdecimal/util'

savings     = '50000'.to_d  # amount invested
interest    = '0.02'.to_d   # annual interest rate: 2%
withholding = '0.25'.to_d   # flat rate withholding tax: 25%
soli        = '0.055'.to_d  # solidarity supplement: 5.5%
church      = '0.04'.to_d   # church tax: 4%
exempt      = '801'.to_d    # tax exemption: 801

PerfectClass.perfect_method(savings, interest, withholding, soli, church, exempt)

String? How disappointing...

Saving Money with Ruby

...like always, it's a trade-off.


# util.rb
class String
  # Convert +string+ to a BigDecimal and return it.
  #
  #     require 'bigdecimal'
  #     require 'bigdecimal/util'
  #
  #     "0.5".to_d
  #     # => #<BigDecimal:1dc69e0,'0.5E0',9(18)>
  #
  def to_d
    BigDecimal(self)
  end
end

ext/bigdecimal/lib/bigdecimal/util.rb#L47-L62

What about the precision parameter?

Saving Money with Ruby

...it's not needed, precision is arbitrary.


/* bigdecimal.c */
BigDecimal_new(int argc, VALUE *argv)
{
   ...
   case T_STRING:
      /* fall through */
      default:
	break;
    }
    StringValueCStr(iniValue);
    return VpAlloc(mf, RSTRING_PTR(iniValue));
}

ext/bigdecimal/bigdecimal.c#L2509-L2555

CRuby uses char* for internal representation.

Saving Money with Ruby

...it's accurate and it inherits from Numeric.


a = '0.027'.to_d
b = '0.011'.to_d
a.to_r                     # (27/1000)
b.to_r                     # (11/1000)
a * b                      # 0.000297
a + b                      # 0.038
a - 2 * b                  # 0.005
a.round(2)                 # 0.03
'4.55'.to_d % '0.05'.to_d  # 0.0

I'll check out the BigDecimal doc to see what else I can do with it.
Are we done now?

Saving Money with Ruby

...we havn't even started. Will you fix the code before shipping?


savings     = 50000  # amount invested
interest    = 0.02   # annual interest rate: 2%
withholding = 0.25   # flat rate withholding tax: 25%
soli        = 0.055  # solidarity supplement: 5.5%
church      = 0.04   # church tax: 4%
exempt      = 801    # tax exemption: 801

PerfectClass.perfect_method(savings, interest, withholding, soli, church, exempt)

Hm, it worked alright so far. I have my doubts it needs fixing.

Saving Money with Ruby

...let me give you a warning. Really, I mean it!



A Ruby warning.



Ok, what would that look like?

Saving Money with Ruby

...like this.


/* bigdecimal.c */
BigDecimal_new(int argc, VALUE *argv)
{
...
    case T_FLOAT:
    rb_warn("initializing BigDecimal with an instance of Float.");
    if (mf > DBL_DIG+1) {
        rb_raise(rb_eArgError, "precision too large.");
    }
    /* fall through */
...
    StringValueCStr(iniValue);
    return VpAlloc(mf, RSTRING_PTR(iniValue));
}

01-bigdecimal_float_warning.patch#L9

That's only BigDecimal.new though, what about .to_d?

Saving Money with Ruby

Well spotted! Here we go...


# util.rb
class Float < Numeric
  # Convert +flt+ to a BigDecimal and return it.
  #     0.5.to_d
  #     # => #
  #
  def to_d(precision=nil)
    location = caller[0].gsub(/:in `.*'$/, '')
    warn("#{location}: warning: calling .to_d on an instance of Float.")
    old_verbose, $VERBOSE = $VERBOSE, nil
    BigDecimal(self, precision || Float::DIG)
  ensure
    $VERBOSE = old_verbose
  end
end

01-bigdecimal_float_warning.patch#L22

Could you show me?

Saving Money with Ruby

...sure.


$> curl -sSL https://get.rvm.io | bash
$> rvm get head
$> rvm install 2.2.3 --patch float_warnings -n float_warnings
$> rvm use ruby-2.2.3-float_warnings
$> irb
2.2.3-float_warnings :001 >

DEMO

Saving Money with Ruby

DEMO output


$> curl -sSL https://get.rvm.io | bash
$> rvm get head
$> rvm install 2.2.3 --patch float_warnings -n float_warnings
$> rvm use ruby-2.2.3-float_warnings
$> irb
2.2.3-float_warnings :001 > require 'bigdecimal'
 => true 
2.2.3-float_warnings :002 > BigDecimal.new(1.0, 2)
            (irb):2: warning: initializing BigDecimal with an instance of Float.
 => #<BigDecimal:1c8e370,'0.1E1',9(27)>
2.2.3-float_warnings :003 > require 'bigdecimal/util'
 => true 
2.2.3-float_warnings :004 > 1.2.to_d
            (irb):4: warning: calling .to_d on an instance of Float.
 => #<BigDecimal:1c58ab8,'0.12E1',18(36)>

Saving Money with Ruby





   Q & A

Knowing all that, you could even be





Making Money with Ruby




I could start with throwing out some Floats ;-)

Saving Money with Ruby





    Thank You.





https://slides.com/wolfgangteuber/saving-money-with-ruby https://github.com/knugie/rvm-patchsets