Ruby BigDecimal Precision: How Significant Digits Work
Precision in BigDecimal controls how many significant digits are tracked during calculations. It is worth understanding before you start writing financial code, because the defaults are generally safe but division requires explicit attention.
One thing to clarify upfront: precision is measured in significant digits, not decimal places. Those two concepts are related but distinct, and mixing them up leads to unexpected results.
Setting Precision at Creation
require 'bigdecimal'
# No precision limit — Ruby infers from the string
BigDecimal("1.23456789")
# => 0.123456789e1
# Limit to 4 significant digits
BigDecimal("1.23456789", 4)
# => 0.1235e1 (rounded to 4 sig figs)
The second argument is a minimum precision hint. Ruby may use more digits internally, but will not use fewer than you specify. For most string-based construction, you will not need to supply this argument at all — Ruby infers the appropriate precision from the string.
Precision During Arithmetic
Arithmetic operations propagate precision, which works well for most operations. Division, however, deserves special care:
a = BigDecimal("1.0")
b = BigDecimal("3.0")
# Division can produce repeating decimals — always specify precision
a.div(b, 10) # 10 significant digits
# => 0.3333333333e0
a / b # Uses default precision — may truncate unexpectedly
For division, always pass an explicit digit count to div to avoid truncation at unexpected places. This is the one area where BigDecimal’s defaults can surprise you.
Checking Precision
n = BigDecimal("3.14159265358979")
n.precision # => 15 (significant digits)
n.scale # => 14 (digits after decimal point)
precision— total significant digitsscale— digits to the right of the decimal point
You might wonder which one to reach for when validating input. In most financial contexts, scale is more useful because you typically want to enforce a specific number of decimal places rather than total significant figures.
Practical Guidance
For financial calculations, a precision of 20-40 significant digits is more than enough for any real-world monetary value. Using the default (inferred from the string literal) is safe for most applications:
# These are safe for financial work
price = BigDecimal("1999.99")
tax_rate = BigDecimal("0.0825")
total = price * (1 + tax_rate)
total.round(2).to_s("F")
# => "2164.989..." -> round(2) -> "2164.99"
There is one mistake worth avoiding, however. Calling BigDecimal() on a Float captures the binary floating-point representation, not the clean decimal value you intended:
# Wrong: captures the float's imprecision
BigDecimal(0.1)
# => 0.1000000000000000055511151231257827021181583404541015625e0
# Correct: always pass a string
BigDecimal("0.1")
# => 0.1e0
Always construct BigDecimal from a string. That single habit prevents an entire class of precision errors.