Overview
BigDecimal provides arbitrary precision decimal arithmetic in Ruby, addressing floating-point precision limitations inherent in binary representation. Ruby implements BigDecimal as a wrapper around the GNU Multiple Precision Arithmetic Library, storing numbers as character strings internally and performing calculations with specified precision levels.
The BigDecimal class handles decimal numbers with user-defined precision, preventing the accumulation errors common with Float arithmetic. Ruby's implementation maintains exact decimal representation for financial calculations, scientific computing, and any domain requiring precise decimal arithmetic.
require 'bigdecimal'
# Float precision issues
0.1 + 0.2
# => 0.30000000000000004
# BigDecimal exact representation
BigDecimal('0.1') + BigDecimal('0.2')
# => 0.3e0
BigDecimal objects store numbers as strings of digits with an associated exponent, allowing representation of extremely large or small numbers without precision loss. The class provides standard arithmetic operations, comparison methods, and conversion utilities while maintaining exact decimal semantics.
# Large number representation
large = BigDecimal('1234567890.123456789012345678901234567890')
large.precision
# => 40
# Scientific notation handling
scientific = BigDecimal('1.23e-100')
scientific.to_s
# => "0.123e-99"
Ruby's BigDecimal integrates with the numeric tower, supporting coercion with Integer and Rational types. The implementation handles special values including positive and negative infinity, NaN (Not a Number), and properly signed zeros.
Basic Usage
Creating BigDecimal instances requires the BigDecimal constructor with string arguments to maintain precision. Passing numeric values directly converts through Float, potentially introducing precision errors that BigDecimal aims to prevent.
require 'bigdecimal'
# Correct instantiation
precise = BigDecimal('123.456789')
precise.precision
# => 9
# Avoid numeric arguments
imprecise = BigDecimal(123.456789) # Float conversion occurs first
precise == imprecise
# => false
Arithmetic operations between BigDecimal instances maintain precision according to Ruby's decimal arithmetic rules. Addition and subtraction preserve the maximum precision of operands, while multiplication and division may require explicit precision specification.
a = BigDecimal('12.34')
b = BigDecimal('56.789')
# Addition preserves precision
sum = a + b
sum.to_s
# => "0.69129e2"
# Multiplication may need precision control
product = a * b
product.precision
# => 7
Setting global precision affects subsequent BigDecimal operations through the BigDecimal.precision
method. This controls the number of significant digits maintained during calculations and determines the default behavior for operations requiring precision specification.
# Set global precision
BigDecimal.precision = 50
# Operations use global precision
result = BigDecimal('1') / BigDecimal('3')
result.to_s
# => "0.33333333333333333333333333333333333333333333333333e0"
# Check current precision
BigDecimal.precision
# => 50
Conversion methods transform BigDecimal instances to other numeric types or string representations. The to_f
method converts to Float with potential precision loss, while to_s
provides various formatting options including scientific notation and fixed-point representation.
decimal = BigDecimal('123.456')
# Convert to different types
decimal.to_i
# => 123
decimal.to_f
# => 123.456
decimal.to_r
# => (15432/125)
# String formatting options
decimal.to_s('F') # Fixed-point notation
# => "123.456"
decimal.to_s('E') # Scientific notation
# => "0.123456E3"
Error Handling & Debugging
BigDecimal operations can raise several exception types, particularly when dealing with invalid inputs, division by zero, or operations resulting in infinity or NaN values. Understanding these exceptions enables proper error handling in decimal arithmetic code.
require 'bigdecimal'
# Invalid string format
begin
BigDecimal('invalid_number')
rescue ArgumentError => e
puts e.message
# => "invalid value for BigDecimal(): \"invalid_number\""
end
# Division by zero behavior
numerator = BigDecimal('10')
denominator = BigDecimal('0')
result = numerator / denominator
result.infinite?
# => 1 (positive infinity)
result.to_s
# => "Infinity"
The BigDecimal class provides inspection methods for detecting special values and debugging precision-related issues. These methods help identify when calculations produce infinity, NaN, or unexpected precision results.
# Create special values for testing
infinity = BigDecimal('1') / BigDecimal('0')
nan = BigDecimal('0') / BigDecimal('0')
normal = BigDecimal('123.45')
# Check value types
infinity.infinite? # => 1 (positive), -1 (negative), nil (finite)
nan.nan? # => true
normal.finite? # => true
# Debug precision issues
decimal = BigDecimal('123.456789012345')
decimal.precision # => 15
decimal.scale # => 12 (digits after decimal point)
Rounding modes control behavior when operations exceed the current precision setting. BigDecimal supports multiple IEEE 754 rounding modes, and improper rounding mode selection can cause unexpected results or exceptions.
# Set specific rounding mode
BigDecimal.mode(BigDecimal::ROUND_MODE, BigDecimal::ROUND_HALF_UP)
# Division with controlled precision
result = BigDecimal('10') / BigDecimal('3')
result.round(4, BigDecimal::ROUND_HALF_EVEN)
# => 0.3333e1
# Exception handling for invalid modes
begin
BigDecimal.mode(BigDecimal::ROUND_MODE, 999)
rescue ArgumentError => e
puts "Invalid rounding mode: #{e.message}"
end
Debugging BigDecimal calculations requires attention to precision propagation through complex expressions. Intermediate results may accumulate precision differently than expected, affecting final calculation accuracy.
# Track precision through calculations
a = BigDecimal('1.23', 10)
b = BigDecimal('4.56', 15)
c = BigDecimal('7.89', 8)
intermediate = (a * b)
puts "Intermediate precision: #{intermediate.precision}"
# Intermediate precision: 7
final = intermediate + c
puts "Final precision: #{final.precision}"
# Final precision: 11
puts "Final value: #{final}"
# Final value: 0.13377e2
Performance & Memory
BigDecimal operations consume significantly more memory and CPU time compared to native Float arithmetic due to arbitrary precision storage and calculation overhead. Each BigDecimal instance stores digits as character strings, requiring memory proportional to the number of significant digits.
require 'bigdecimal'
require 'benchmark'
# Memory usage comparison
float_number = 123.456789
bigdec_number = BigDecimal('123.456789')
# BigDecimal uses more memory per instance
# Float: 8 bytes (64-bit)
# BigDecimal: ~40+ bytes base overhead plus digit storage
Arithmetic performance varies significantly based on operand precision and operation type. Addition and subtraction performance scales with the maximum precision of operands, while multiplication and division exhibit quadratic scaling behavior for very high precision calculations.
# Benchmark different precision levels
Benchmark.measure do
1000.times do
a = BigDecimal('1.23456789', 10)
b = BigDecimal('9.87654321', 10)
result = a * b
end
end
# => #<Benchmark::Tms:0x... @real=0.025...>
Benchmark.measure do
1000.times do
a = BigDecimal('1.23456789012345678901234567890', 50)
b = BigDecimal('9.87654321098765432109876543210', 50)
result = a * b
end
end
# => #<Benchmark::Tms:0x... @real=0.180...>
Memory allocation patterns in BigDecimal calculations can cause performance bottlenecks in tight loops or high-frequency operations. Reusing BigDecimal instances where possible reduces garbage collection pressure and improves overall performance.
# Inefficient: creates new instances in loop
sum = BigDecimal('0')
1000.times do |i|
sum += BigDecimal(i.to_s) # New BigDecimal each iteration
end
# More efficient: minimize object creation
sum = BigDecimal('0')
1000.times do |i|
sum += i # Integer coercion more efficient
end
Precision management directly affects performance characteristics. Higher precision settings increase memory usage and calculation time exponentially for complex operations. Setting precision appropriately for the required accuracy prevents unnecessary performance degradation.
# Performance vs precision trade-off
BigDecimal.precision = 10
fast_calculation = BigDecimal('1') / BigDecimal('3')
BigDecimal.precision = 100
slow_calculation = BigDecimal('1') / BigDecimal('3')
# Higher precision calculations take significantly longer
# but provide more accurate results
Common Pitfalls
String construction represents the most frequent source of BigDecimal precision errors. Creating BigDecimal instances from Float values introduces the same precision limitations that BigDecimal aims to solve, defeating the purpose of using arbitrary precision arithmetic.
# Wrong: Float conversion loses precision
bad_decimal = BigDecimal(0.1 + 0.2)
bad_decimal.to_s
# => "0.30000000000000004e0"
# Correct: String construction preserves precision
good_decimal = BigDecimal('0.1') + BigDecimal('0.2')
good_decimal.to_s
# => "0.3e0"
Comparison operations between BigDecimal and Float types can produce unexpected results due to precision differences and conversion behavior. Ruby's coercion rules may not always produce the intuitive comparison result when mixing numeric types.
decimal = BigDecimal('0.1')
float = 0.1
# Comparison may not work as expected
decimal == float
# => false (precision difference)
# Safe comparison requires type consistency
decimal == BigDecimal('0.1')
# => true
# Or explicit conversion
decimal == BigDecimal(float.to_s)
# => false (still precision issues from Float)
Precision loss occurs when BigDecimal results exceed the current precision setting without explicit handling. Operations silently truncate results to fit the precision limit, potentially causing subtle calculation errors in long computation chains.
BigDecimal.precision = 5
# Precision loss in calculation
a = BigDecimal('1.23456789')
b = BigDecimal('9.87654321')
result = a * b
result.precision
# => 5 (truncated from potential higher precision)
# Explicit precision control prevents loss
result_full = a.mult(b, 20) # Specify desired precision
result_full.precision
# => 11
Rounding behavior differences between BigDecimal modes and other numeric types cause inconsistencies in financial calculations. Default rounding modes may not match business requirements or mathematical expectations, leading to incorrect results in critical applications.
# Default rounding may not match expectations
value = BigDecimal('2.5')
value.round
# => 0.2e1 (rounds to 2, not 3)
# Banker's rounding vs arithmetic rounding
BigDecimal('2.5').round(0, BigDecimal::ROUND_HALF_EVEN)
# => 0.2e1
BigDecimal('2.5').round(0, BigDecimal::ROUND_HALF_UP)
# => 0.3e1
# Different results for the same input
Reference
Core Methods
Method | Parameters | Returns | Description |
---|---|---|---|
BigDecimal(str) |
str (String) |
BigDecimal |
Creates new BigDecimal from string |
BigDecimal(num, digits) |
num (Numeric), digits (Integer) |
BigDecimal |
Creates BigDecimal with specified precision |
#+ |
other (Numeric) |
BigDecimal |
Addition operator |
#- |
other (Numeric) |
BigDecimal |
Subtraction operator |
#* |
other (Numeric) |
BigDecimal |
Multiplication operator |
#/ |
other (Numeric) |
BigDecimal |
Division operator |
#** |
exp (Integer) |
BigDecimal |
Power operation |
#% |
other (Numeric) |
BigDecimal |
Modulo operation |
Precision and Rounding
Method | Parameters | Returns | Description |
---|---|---|---|
#precision |
None | Integer |
Number of significant digits |
#scale |
None | Integer |
Number of digits after decimal point |
#round |
n (Integer), mode (Integer) |
BigDecimal |
Rounds to n decimal places |
#truncate |
n (Integer) |
BigDecimal |
Truncates to n decimal places |
#ceil |
n (Integer) |
BigDecimal |
Ceiling to n decimal places |
#floor |
n (Integer) |
BigDecimal |
Floor to n decimal places |
Conversion Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#to_f |
None | Float |
Converts to Float (may lose precision) |
#to_i |
None | Integer |
Converts to Integer (truncates) |
#to_r |
None | Rational |
Converts to Rational |
#to_s |
format (String) |
String |
String representation with optional formatting |
Special Value Detection
Method | Parameters | Returns | Description |
---|---|---|---|
#finite? |
None | Boolean |
True if finite number |
#infinite? |
None | Integer or nil |
1 for +∞, -1 for -∞, nil for finite |
#nan? |
None | Boolean |
True if Not a Number |
#zero? |
None | Boolean |
True if zero |
Global Configuration
Method | Parameters | Returns | Description |
---|---|---|---|
BigDecimal.precision |
None | Integer |
Current global precision |
BigDecimal.precision= |
digits (Integer) |
Integer |
Sets global precision |
BigDecimal.mode |
mode , value |
Integer |
Gets/sets calculation mode |
Rounding Modes
Constant | Value | Description |
---|---|---|
ROUND_UP |
1 | Round away from zero |
ROUND_DOWN |
2 | Round toward zero |
ROUND_HALF_UP |
3 | Round to nearest, ties away from zero |
ROUND_HALF_DOWN |
4 | Round to nearest, ties toward zero |
ROUND_HALF_EVEN |
5 | Round to nearest, ties to even |
ROUND_CEILING |
6 | Round toward positive infinity |
ROUND_FLOOR |
7 | Round toward negative infinity |
Exception Types
Exception | Description |
---|---|
ArgumentError |
Invalid string format or parameters |
FloatDomainError |
Mathematical domain errors |
ZeroDivisionError |
Division by zero (when configured) |
Mode Constants
Constant | Description |
---|---|
EXCEPTION_INFINITY |
Control infinity exception behavior |
EXCEPTION_NaN |
Control NaN exception behavior |
EXCEPTION_UNDERFLOW |
Control underflow exception behavior |
EXCEPTION_OVERFLOW |
Control overflow exception behavior |
ROUND_MODE |
Rounding mode configuration |