CrackedRuby logo

CrackedRuby

Rational Numbers

Overview

Ruby's Rational class provides exact fractional arithmetic without the precision loss inherent in floating-point numbers. The class represents rational numbers as a pair of integers: a numerator and denominator. Ruby automatically reduces fractions to their lowest terms and handles arithmetic operations between rational numbers, integers, and floats.

The Rational class sits within Ruby's numeric hierarchy alongside Integer and Float, implementing standard arithmetic operations while maintaining mathematical precision. Ruby creates rational numbers through several mechanisms: the Rational() constructor method, string parsing, and automatic promotion during certain mathematical operations.

# Creating rational numbers
Rational(3, 4)        # => (3/4)
Rational('0.25')      # => (1/4)
Rational(22, 7)       # => (22/7)

Ruby integrates rational numbers seamlessly with other numeric types. When performing arithmetic between rationals and integers, Ruby promotes the integer to a rational number. Operations between rationals and floats typically convert the rational to a float, potentially losing precision.

Rational(1, 3) + 2    # => (7/3) 
Rational(1, 3) + 2.0  # => 2.3333333333333335

The class automatically handles common fraction operations including addition, subtraction, multiplication, division, and exponentiation. Ruby reduces all results to lowest terms, ensuring consistent representation across calculations.

Rational(2, 4) + Rational(3, 6)  # => (1/1) - automatically reduced
Rational(5, 6) * Rational(2, 3)  # => (5/9)

Basic Usage

Creating rational numbers begins with the Rational() constructor, which accepts various input formats. The method takes two integer arguments representing numerator and denominator, automatically reducing the fraction to lowest terms.

r1 = Rational(4, 8)      # => (1/2)
r2 = Rational(-3, 9)     # => (-1/3)
r3 = Rational(0, 5)      # => (0/1)

String inputs provide another creation method, accepting decimal strings, fraction strings, and scientific notation. Ruby parses these formats and converts them to exact rational representations.

Rational('0.125')        # => (1/8)
Rational('22/7')         # => (22/7)
Rational('-3.14159')     # => (-314159/100000)
Rational('1.5e-2')       # => (3/200)

Arithmetic operations between rational numbers maintain exact precision. Addition and subtraction find common denominators automatically, while multiplication and division follow standard fractional arithmetic rules.

a = Rational(1, 4)
b = Rational(1, 6)

a + b                    # => (5/12)
a - b                    # => (1/12)  
a * b                    # => (1/24)
a / b                    # => (3/2)

Comparison operations work naturally with rational numbers, comparing mathematical values rather than internal representations. Ruby handles mixed comparisons between rationals, integers, and floats.

Rational(1, 2) == 0.5           # => true
Rational(2, 3) > Rational(3, 5) # => true
Rational(4, 5) < 1              # => true
[Rational(1,3), Rational(1,2), Rational(2,3)].sort
# => [(1/3), (1/2), (2/3)]

Converting rational numbers to other numeric types uses standard Ruby conversion methods. The #to_f method converts to float, potentially losing precision, while #to_i truncates toward zero.

Rational(22, 7).to_f     # => 3.142857142857143
Rational(22, 7).to_i     # => 3
Rational(-5, 2).to_i     # => -2
Rational(8, 4).to_i      # => 2

Advanced Usage

Complex rational arithmetic involves chaining operations while maintaining precision throughout multi-step calculations. Ruby preserves exact values during intermediate calculations, providing accurate final results for financial and scientific computations.

# Compound interest calculation with exact precision
principal = Rational(1000)
rate = Rational(5, 100)        # 5% as exact fraction
time = 10

compound_interest = principal * (1 + rate) ** time
simple_interest = principal * (1 + rate * time)

difference = compound_interest - simple_interest
# => (9765625000000000000000000001/15625000000000000000000000)
difference.to_f  # => 628.8946267

# Compare with floating-point calculation
float_compound = 1000.0 * (1.05 ** 10)  # => 1628.8946267249757
float_simple = 1000.0 * (1 + 0.05 * 10) # => 1500.0
float_difference = float_compound - float_simple  # => 128.89462672497572

Advanced fraction manipulation includes accessing numerator and denominator components directly. The #numerator and #denominator methods return the reduced fraction's components as integers.

r = Rational(144, 60)    # Automatically reduced to (12/5)
r.numerator              # => 12
r.denominator            # => 5

# Reconstructing equivalent fractions
original_num = r.numerator * 5    # => 60
original_den = r.denominator * 5  # => 25
Rational(original_num, original_den) == r  # => true

Mathematical functions work with rational inputs, though they typically return float values when exact rational results are impossible. Ruby provides rational-specific methods for operations that can maintain precision.

# Power operations with rational exponents
Rational(8, 27) ** Rational(1, 3)  # => 0.6666666666666666 (cube root)
Rational(4, 9) ** Rational(1, 2)   # => 0.6666666666666666 (square root)

# Absolute value maintains rational type
Rational(-22, 7).abs               # => (22/7)

# Reciprocal operation
Rational(3, 5).reciprocal          # => (5/3)
1 / Rational(3, 5)                 # => (5/3) - equivalent

Working with arrays and collections of rational numbers requires understanding how Ruby handles numeric type coercion during operations. Rational numbers integrate with enumerable operations while maintaining precision.

fractions = [Rational(1,2), Rational(1,3), Rational(1,6)]
sum = fractions.reduce(:+)         # => (1/1)
average = sum / fractions.length   # => (1/3)

# Complex reduction with rational preservation
data = (1..10).map { |i| Rational(1, i) }
harmonic_mean = data.length / data.map(&:reciprocal).reduce(:+)
# => (3628800/9237383900) ≈ 0.000387

Rational numbers support range operations and step iterations, maintaining exact precision throughout sequences. This capability proves valuable for generating precise numeric sequences.

# Rational range iteration
(Rational(1,4)..Rational(3,4)).step(Rational(1,8)) do |r|
  puts "#{r} = #{r.to_f}"
end
# (1/4) = 0.25
# (3/8) = 0.375  
# (1/2) = 0.5
# (5/8) = 0.625
# (3/4) = 0.75

Performance & Memory

Rational arithmetic operations require more computational overhead than floating-point calculations due to fraction reduction and integer arithmetic on potentially large numerators and denominators. Performance differences become significant in computation-intensive scenarios.

require 'benchmark'

n = 1_000_000

Benchmark.bm do |x|
  x.report("Float arithmetic:") do
    result = 0.0
    n.times { |i| result += 1.0 / (i + 1) }
  end
  
  x.report("Rational arithmetic:") do  
    result = Rational(0)
    n.times { |i| result += Rational(1, i + 1) }
  end
end

# Typical results:
#                      user     system      total        real
# Float arithmetic:    0.312000   0.000000   0.312000 (  0.312445)
# Rational arithmetic: 8.765000   0.012000   8.777000 (  8.798234)

Memory consumption for rational numbers varies based on the size of numerator and denominator values. Small integers consume minimal memory, but operations that produce large fractions can create substantial memory overhead.

# Memory-efficient rational numbers
small_rational = Rational(3, 4)        # Minimal memory usage
ObjectSpace.memsize_of(small_rational) # Typically 40 bytes

# Memory-intensive rational numbers  
large_rational = Rational(10**50, 10**60 + 1)
ObjectSpace.memsize_of(large_rational) # Significantly larger

# Demonstrating memory growth during calculations
initial = Rational(1, 3)
10.times do |i|
  initial = initial ** 2 + Rational(1, 10**i)
  puts "Iteration #{i}: #{ObjectSpace.memsize_of(initial)} bytes"
end

Optimization strategies for rational-heavy calculations include converting to floats when exact precision becomes unnecessary, using rational numbers only for critical calculations, and periodically checking fraction complexity.

# Hybrid approach: rational for precision, float for speed
def calculate_compound_interest(principal, rate, periods, precision_threshold = 1000)
  rational_rate = Rational(rate)
  
  if periods < precision_threshold
    # Use rational for exact calculation
    principal * (1 + rational_rate) ** periods
  else
    # Switch to float for performance
    principal.to_f * (1 + rate) ** periods
  end
end

# Fraction complexity monitoring
def check_fraction_complexity(rational)
  num_digits = rational.numerator.to_s.length
  den_digits = rational.denominator.to_s.length
  
  if num_digits + den_digits > 20
    puts "Warning: Complex fraction detected (#{num_digits}+#{den_digits} digits)"
    puts "Consider approximation: #{rational.to_f}"
  end
end

Caching and memoization techniques can improve performance for repeated rational calculations, especially when working with common fractions or mathematical constants.

class RationalCache
  def initialize
    @cache = {}
  end
  
  def cached_power(base, exponent)
    key = [base.numerator, base.denominator, exponent]
    @cache[key] ||= base ** exponent
  end
  
  def cached_factorial_reciprocal(n)
    @cache["factorial_#{n}"] ||= Rational(1, (1..n).reduce(:*))
  end
end

# Usage example
cache = RationalCache.new
result = (1..10).map { |i| cache.cached_factorial_reciprocal(i) }
# Subsequent calls use cached values

Common Pitfalls

Precision loss occurs when mixing rational numbers with floating-point arithmetic, often producing unexpected results. Ruby automatically converts rationals to floats during mixed operations, destroying exact precision.

# Precision loss through float mixing
exact = Rational(1, 3)            # => (1/3)
imprecise = exact + 0.1           # => 0.43333333333333335
back_to_rational = Rational(imprecise) # => (7818749721369205/18014398509481984)

# The rational representation of the float is not (1/3) + (1/10)
Rational(1, 3) + Rational(1, 10)  # => (13/30) - the exact answer

String parsing edge cases can produce unexpected rational values, particularly with decimal strings containing repeating patterns or very long decimal expansions.

# Problematic string parsing
Rational('0.333333333333333')     # => (333333333333333/1000000000000000)
Rational('1.000000000000001')     # => (1000000000000001/1000000000000000)

# These are not equivalent to common fractions
Rational('0.333333333333333') == Rational(1, 3)  # => false

# Better: explicit fraction creation
Rational(1, 3)                    # => (1/3) - exact representation

Division by zero errors occur differently with rational numbers compared to floats. Ruby raises ZeroDivisionError for rational division by zero, while float division returns infinity.

# Different division by zero behavior
begin
  Rational(1, 0)                  # Raises ZeroDivisionError immediately
rescue ZeroDivisionError => e
  puts "Rational division by zero: #{e.message}"
end

# Float division returns infinity/NaN
1.0 / 0.0                         # => Infinity
0.0 / 0.0                         # => NaN

# Runtime division by zero
begin
  Rational(5, 3) / Rational(0, 1) # Raises ZeroDivisionError
rescue ZeroDivisionError => e
  puts "Runtime division by zero: #{e.message}"  
end

Automatic reduction can mask intended precision, making it difficult to recover original fraction representations. Ruby always stores rationals in lowest terms, discarding information about the original input format.

# Loss of original representation
original = Rational(100, 200)     # Input as 100/200
stored = original                 # Actually stored as (1/2)
stored.numerator                  # => 1 (not 100)
stored.denominator                # => 2 (not 200)

# Cannot recover original 100/200 representation
# This affects applications needing specific denominator values

# Workaround: store original values separately
class PreservingRational
  attr_reader :rational, :original_num, :original_den
  
  def initialize(num, den)
    @original_num = num
    @original_den = den  
    @rational = Rational(num, den)
  end
  
  def reduced?
    @rational.numerator != @original_num || 
    @rational.denominator != @original_den
  end
end

Performance degradation accumulates through repeated operations, especially when fraction complexity grows unbounded. Long calculation chains can produce rationals with very large numerators and denominators.

# Fraction complexity explosion
result = Rational(1, 2)
100.times do |i|
  result = result + Rational(1, i + 3)
  
  # Check complexity every 10 iterations
  if (i + 1) % 10 == 0
    digits = result.numerator.to_s.length + result.denominator.to_s.length
    puts "Iteration #{i + 1}: #{digits} total digits"
  end
end

# Final result may have hundreds of digits
puts "Final: #{result.numerator.to_s.length}+#{result.denominator.to_s.length} digits"

Comparison pitfalls emerge when comparing rationals with floats that cannot be exactly represented. Floating-point imprecision can cause unexpected inequality results.

# Surprising comparison results
r = Rational(1, 10)               # => (1/10) - exact
f = 0.1                           # Approximate float representation

r == f                            # => true (Ruby handles this well)
r.to_f == f                       # => true  

# But with complex calculations:
calc_r = Rational(1, 3) * 3       # => (1/1) - exact  
calc_f = (1.0 / 3.0) * 3          # => 0.9999999999999999 - imprecise

calc_r == 1                       # => true
calc_f == 1                       # => false
calc_r == calc_f                  # => false

Reference

Core Methods

Method Parameters Returns Description
Rational(numerator, denominator=1) Integer, Integer Rational Creates rational number from integers
Rational(string) String Rational Parses string representation
#numerator None Integer Returns reduced numerator
#denominator None Integer Returns reduced denominator
#+, #-, #*, #/ Numeric Rational/Float Standard arithmetic operations
#** Numeric Rational/Float Exponentiation
#abs None Rational Absolute value
#reciprocal None Rational Returns 1/self

Conversion Methods

Method Returns Description
#to_f Float Convert to floating-point
#to_i Integer Truncate to integer
#to_r Rational Return self
#to_s String String representation as "numerator/denominator"
#inspect String Same as to_s with parentheses

Comparison Methods

Method Parameters Returns Description
#==, #!= Numeric Boolean Equality comparison
#<, #<=, #>, #>= Numeric Boolean Magnitude comparison
#<=> Numeric Integer Three-way comparison (-1, 0, 1)
#between? Numeric, Numeric Boolean Range inclusion test

String Format Examples

Input String Resulting Rational Notes
"3/4" (3/4) Standard fraction notation
"0.25" (1/4) Decimal converted to fraction
"-1.5" (-3/2) Negative decimal
"1.5e-2" (3/200) Scientific notation
"22/7" (22/7) Improper fraction
" 3 / 4 " (3/4) Whitespace ignored

Error Conditions

Condition Exception Example
Zero denominator ZeroDivisionError Rational(1, 0)
Invalid string format ArgumentError Rational("abc")
Division by zero ZeroDivisionError Rational(1,2) / 0

Performance Characteristics

Operation Time Complexity Notes
Creation O(log(min(a,b))) GCD calculation for reduction
Addition/Subtraction O(log(max(a,b,c,d))) LCM and GCD operations
Multiplication O(log(max(a,b,c,d))) GCD for reduction
Division O(log(max(a,b,c,d))) Same as multiplication
Comparison O(log(max(a,b,c,d))) Cross multiplication

Constants and Limits

Constant Value Description
Rational(0) (0/1) Rational zero
Rational(1) (1/1) Rational one
Maximum precision Platform dependent Limited by Integer size