Ruby Float Point Arithmetic and Truncation

How to keep precision on float point arithmetic?

1
2
3
4
5
190000 * ( 783.0 / 10000 )
# => 14876.999999999998

( 190000 * 783.0 ) / 10000
# => 14877.0

How to make a 2 point truncation instead of rounding?

1
2
3
4
5
195555 * 0.0783
# => 15311.956499999998

( 195555 * 0.0783 ).round(2)
# => 15311.96

Plain Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# Public: A calculator aims handling Float operation precision and
# saving the result with truncated 2 point Float.
#
# Examples
#
#   190000 * 0.0783
#   # => 14876.999999999998
#   190000 * 783 / 10000
#   # => 14877
#
#   cal = RateCalculator.new(190000, 0.0783)
#   cal.run
#   # => 14877.0
#
#
#   195555 * 0.0783
#   # => 15311.956499999998
#
#   cal = RateCalculator.new(195555, 0.0783)
#   # => 15311.95
#
# Returns a Float
class RateCalculator
  attr_reader :base, :rate

  # Internal: Handles 6 point rate.
  MAGNIFIER = 1000000

  # Public: Initialization
  #
  # base - Integer
  # rate - Numeric
  def initialize(base, rate)
    raise "#initialize: <base> needs to be Integer" unless base.is_a? Integer

    @base = base
    @rate = rate
  end

  def run
    truncate_2_point MAGNIFIER*rate*base/MAGNIFIER
  end

  private

    def truncate_2_point(float)
      (float * 100).to_i / 100.0
    end
end

It works, but with so many worries about the unknown conditions.

BigDecimal

First, what the hell happens on the precision of float point arithmetic?

1
2
0.1 + 0.2
# => 0.30000000000000004

According to What Every Programmer Should Know About Floating-Point Arithmetic, the answer is the binary fraction issue.

Binary Fraction

Specifically, binary can only represent those numbers as a finite fraction where the denominator is a power of 2. Unfortunately, this does not include most of the numbers that can be represented as finite fraction in base 10, like 0.1.

To get through the precision problem, Ruby provides the Arbitrary-Precision Decimal shipped by BigDecimal. And so sweet, BigDecimal supports several rounding modes, including :truncate.

Here is the final solution.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
require 'bigdecimal'

# Public: A calculator aims handling arithmatic precision and
# saving the result with 2 points truncated decimal.
#
# Examples
#
#   190000 * 0.0783
#   # => 14876.999999999998
#   190000 * 783 / 10000
#   # => 14877
#
#   cal = RateCalculator.new(190000, 0.0783).run
#   # => 14877.0
#
#
#   195555 * 0.0783
#   # => 15311.956499999998
#
#   cal = RateCalculator.new(195555, 0.0783).run
#   # => 15311.95
#
# Returns a BigDecimal
class RateCalculator
  attr_reader :base, :rate

  def initialize(base, rate)
    @base = BigDecimal(base.to_s)
    @rate = BigDecimal(rate.to_s)
  end

  def run
    BigDecimal.save_rounding_mode do
      BigDecimal.mode(BigDecimal::ROUND_MODE, :truncate)
      (base*rate).round(2)
    end
  end
end

Reference