ruby 43 lines · 7 steps

Handling money safely with BigDecimal in Ruby

A module that converts, rounds, taxes, and splits money using exact decimal arithmetic instead of error-prone floats.

Explained by highlit
1require "bigdecimal"
2 
3module Money
4 CENTS = BigDecimal("0.01")
5 
6 module_function
7 
8 def to_decimal(amount)
9 case amount
10 when BigDecimal then amount
11 when Integer then BigDecimal(amount)
12 when Float then BigDecimal(amount.to_s)
13 when String then BigDecimal(amount)
14 else
15 raise ArgumentError, "cannot convert #{amount.class} to money"
16 end
17 end
18 
19 def round(amount)
20 to_decimal(amount).round(2, BigDecimal::ROUND_HALF_EVEN)
21 end
22 
23 def apply_tax(subtotal, rate)
24 tax = to_decimal(subtotal) * to_decimal(rate)
25 round(tax)
26 end
27 
28 def split(total, ways)
29 raise ArgumentError, "ways must be positive" unless ways.positive?
30 
31 cents = (to_decimal(total) / CENTS).round
32 base, remainder = cents.divmod(ways)
33 
34 Array.new(ways) do |i|
35 share = i < remainder ? base + 1 : base
36 (share * CENTS)
37 end
38 end
39 
40 def format(amount)
41 format("$%.2f", round(amount))
42 end
43end
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Representing money as BigDecimal avoids the rounding errors floats introduce, especially when parsing floats via their string form.
  2. 2Banker's rounding (ROUND_HALF_EVEN) reduces systematic bias when rounding many monetary values.
  3. 3Splitting money in integer cents and distributing the remainder guarantees the shares add back to the exact total.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Handling money safely with BigDecimal in Ruby — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code