CrackedRuby logo

CrackedRuby

BigDecimal for Arbitrary Precision

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