- Accurate decimal numbers by definition, not by chance -

Wolfgang Teuber, Leipzig on Rails, updated Nov 14th, 2015When 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?

...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...

Dividing an `Integer`

by an `Integer`

results in an `Integer`

.

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

Don't use
`Integer`

*,

try duck typing (.0) or typecasting (.to_f) instead.

...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?

...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...

...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
```

Don't use `Float`

*,

try `BigDecimal`

instead.

...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?

...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?

...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...

...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...

...well, there are tools

- 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?

...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...

...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?

...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.

...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?

...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.

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

Ok, what would that look like?

...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?

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?

...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

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)>
```

Knowing all that, you could even be

I could start with throwing out some `Float`

s ;-)

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