CrackedRuby logo

CrackedRuby

Float Class and Precision

Overview

Ruby's Float class implements IEEE 754 double-precision floating-point numbers, providing 64-bit representation with approximately 15-17 decimal digits of precision. The class handles decimal numbers that cannot be exactly represented as integers, supporting mathematical operations while managing inherent precision limitations of binary floating-point arithmetic.

Float objects in Ruby automatically handle conversion from literals containing decimal points or scientific notation. The class provides comprehensive arithmetic operations, comparison methods, and utility functions for mathematical computations. Ruby's floating-point implementation follows standard conventions but introduces specific behaviors around precision, rounding, and edge cases that affect numeric calculations.

# Float creation from literals
price = 19.99
temperature = -40.5
scientific = 1.5e10

# Automatic conversion during arithmetic
result = 10 / 3.0  # => 3.3333333333333335
mixed = 5 + 2.7    # => 7.7

The Float class integrates with Ruby's numeric tower, automatically converting between Integer, Float, Rational, and BigDecimal as needed. Operations between different numeric types follow promotion rules that prioritize precision preservation while maintaining computational efficiency.

# Type promotion in mixed operations
integer_result = 10 + 5        # => 15 (Integer)
float_result = 10 + 5.0        # => 15.0 (Float)
rational_result = 10 + 1/3r    # => (31/3) (Rational)

Basic Usage

Float operations in Ruby handle standard arithmetic with automatic precision management. The class supports all basic mathematical operations while maintaining IEEE 754 compliance for special values and edge cases.

# Basic arithmetic operations
a = 10.5
b = 3.2

sum = a + b         # => 13.7
difference = a - b  # => 7.300000000000001
product = a * b     # => 33.60000000000001
quotient = a / b    # => 3.28125

Comparison operations work with other numeric types, but precision differences can affect equality testing. Ruby provides methods to handle approximate equality when dealing with floating-point calculations.

# Comparison and equality
x = 0.1 + 0.2
y = 0.3

# Direct comparison fails due to precision
x == y              # => false
x                   # => 0.30000000000000004

# Using approximate equality
(x - y).abs < Float::EPSILON  # => true

The Float class provides conversion methods for different numeric representations and string formatting with precision control.

# Conversion and formatting
value = 123.456789

# Rounding and precision
value.round         # => 123
value.round(2)      # => 123.46
value.round(-1)     # => 120.0

# String conversion with format control
"%.2f" % value      # => "123.46"
value.to_s          # => "123.456789"
sprintf("%.3e", value)  # => "1.235e+02"

Range operations with floats support step values and iteration, though precision concerns affect boundary conditions and step calculations.

# Float ranges and iteration
(1.0..3.0).step(0.5) { |f| puts f }
# Outputs: 1.0, 1.5, 2.0, 2.5, 3.0

# Range inclusion testing
range = 1.0...2.0
range.include?(1.5)     # => true
range.cover?(1.9999)    # => true

Mathematical operations include standard functions with domain-specific behavior for edge cases and special values.

# Mathematical functions
Math.sqrt(16.0)     # => 4.0
Math.sin(Math::PI)  # => 1.2246467991473532e-16 (approximately 0)
Math.log(Math::E)   # => 1.0

# Special values
Float::INFINITY     # => Infinity
-Float::INFINITY    # => -Infinity
Float::NAN          # => NaN

# Testing special values
(1.0 / 0.0).infinite?   # => 1
(0.0 / 0.0).nan?        # => true

Common Pitfalls

Floating-point precision creates subtle issues that affect program correctness. Binary representation cannot exactly store many decimal fractions, leading to accumulated errors in calculations.

# Classic precision problem
result = 0.0
10.times { result += 0.1 }
result == 1.0           # => false
result                  # => 0.9999999999999999

# Accumulation error in loops
sum = 0.0
1000.times { sum += 0.001 }
sum                     # => 0.9999999999999062
sum == 1.0              # => false

Equality comparisons with floats require careful handling. Direct equality often fails even for mathematically equivalent expressions due to representation differences.

# Misleading equality results
a = 0.15 + 0.15
b = 0.1 + 0.2
a == b                  # => false
a                       # => 0.3
b                       # => 0.30000000000000004

# Safe comparison using epsilon
def float_equal?(x, y, epsilon = Float::EPSILON)
  (x - y).abs < epsilon
end

float_equal?(a, b)      # => true

Division operations can produce unexpected special values that propagate through calculations and affect program logic.

# Division edge cases
positive_infinity = 1.0 / 0.0      # => Infinity
negative_infinity = -1.0 / 0.0     # => -Infinity
not_a_number = 0.0 / 0.0           # => NaN

# NaN contamination
result = 5.0 + not_a_number         # => NaN
result == result                    # => false (NaN != NaN)
result.nan?                         # => true

Rounding operations introduce bias and precision loss, especially when chaining multiple rounding operations or working near representational boundaries.

# Rounding bias and precision loss
values = [2.5, 3.5, 4.5, 5.5]
rounded = values.map { |v| v.round }    # => [2, 4, 4, 6] (banker's rounding)

# Precision loss in conversion chains
original = 1.23456789
rounded_down = original.round(2)        # => 1.23
back_to_float = rounded_down.to_f       # => 1.23 (precision lost)

# Chained operations amplify errors
x = 1.0
100.times { x = (x * 1.1).round(10) / 1.1 }
x                                       # => 0.9999999991

String conversion and parsing create additional precision challenges when serializing and deserializing float values.

# String round-trip precision issues
original = 0.1 + 0.2
string_rep = original.to_s              # => "0.30000000000000004"
parsed_back = string_rep.to_f           # => 0.30000000000000004

# Format-dependent precision
short_format = "%.2f" % original        # => "0.30"
parsed_short = short_format.to_f        # => 0.3 (different from original)

# JSON serialization concerns
require 'json'
data = { value: 0.1 + 0.2 }
json_string = JSON.generate(data)       # => '{"value":0.30000000000000004}'
parsed_data = JSON.parse(json_string)
parsed_data['value'] == data[:value]    # => true (preserved in JSON)

Range and iteration operations with floats create boundary condition problems and step accumulation errors.

# Step accumulation in ranges
steps = []
(0.0..1.0).step(0.1) { |x| steps << x }
steps.last == 1.0                      # => false
steps.last                             # => 0.9999999999999999

# Boundary inclusion problems
range = 0.0...1.0
range.include?(0.9999999999999999)     # => true
range.include?(1.0)                    # => false

# Floating-point step errors
current = 0.0
10.times { current += 0.1 }
current                                # => 0.9999999999999999
(0.0..1.0).step(0.1).to_a.size        # => 11 (includes accumulated error)

Performance & Memory

Float operations in Ruby execute at native machine speed for basic arithmetic, but performance characteristics vary significantly based on operation complexity and special value handling.

require 'benchmark'

# Basic arithmetic performance
Benchmark.bm(15) do |x|
  x.report("Float addition:") do
    sum = 0.0
    1_000_000.times { |i| sum += i * 1.1 }
  end
  
  x.report("Integer addition:") do
    sum = 0
    1_000_000.times { |i| sum += i }
  end
end

# Results show Float arithmetic ~2x slower than Integer
#                      user     system      total        real
# Float addition:   0.080000   0.000000   0.080000 (  0.083159)
# Integer addition: 0.040000   0.000000   0.040000 (  0.041833)

Mathematical functions carry significant performance overhead compared to basic arithmetic operations. Transcendental functions like trigonometric and logarithmic operations require careful optimization in performance-critical code.

# Mathematical function performance comparison
iterations = 100_000

Benchmark.bm(20) do |x|
  x.report("Basic multiplication:") do
    iterations.times { |i| i * 3.14159 }
  end
  
  x.report("Math.sin:") do
    iterations.times { |i| Math.sin(i) }
  end
  
  x.report("Math.sqrt:") do
    iterations.times { |i| Math.sqrt(i) }
  end
  
  x.report("Float#round:") do
    iterations.times { |i| (i * 1.1).round(2) }
  end
end

# Math functions are 10-50x slower than basic arithmetic

Memory usage patterns for Float objects depend on creation context and garbage collection behavior. Ruby optimizes Float storage but object allocation overhead affects performance in numeric-intensive applications.

# Memory allocation patterns
require 'objspace'

# Measure Float object creation overhead
before_objects = ObjectSpace.count_objects[:T_FLOAT]

# Create many Float objects
floats = Array.new(100_000) { |i| i * 3.14159 }

after_objects = ObjectSpace.count_objects[:T_FLOAT]
created_objects = after_objects - before_objects

puts "Float objects created: #{created_objects}"
puts "Memory per Float: #{ObjectSpace.memsize_of(3.14159)} bytes"

# Garbage collection impact on Float-heavy operations
GC.disable
start_time = Time.now
1_000_000.times { |i| Math.sqrt(i.to_f) }
gc_disabled_time = Time.now - start_time

GC.enable
start_time = Time.now
1_000_000.times { |i| Math.sqrt(i.to_f) }
gc_enabled_time = Time.now - start_time

puts "GC disabled: #{gc_disabled_time}s"
puts "GC enabled: #{gc_enabled_time}s"

Precision operations like rounding and formatting create performance bottlenecks in high-throughput scenarios. String conversion operations are particularly expensive for large-scale numeric processing.

# Precision operation performance impact
large_dataset = Array.new(100_000) { rand * 1000.0 }

Benchmark.bm(20) do |x|
  x.report("Raw calculations:") do
    large_dataset.each { |n| n * 2.0 + 1.0 }
  end
  
  x.report("With rounding:") do
    large_dataset.each { |n| (n * 2.0 + 1.0).round(2) }
  end
  
  x.report("With string format:") do
    large_dataset.each { |n| "%.2f" % (n * 2.0 + 1.0) }
  end
  
  x.report("With to_s:") do
    large_dataset.each { |n| (n * 2.0 + 1.0).to_s }
  end
end

# String operations are 5-10x slower than numeric operations

Optimization strategies for Float-intensive code include minimizing object allocation, batching operations, and using appropriate data structures for numeric computation.

# Optimization techniques for Float operations
class FloatProcessor
  def self.sum_optimized(array)
    # Use inject/reduce for better performance than manual accumulation
    array.inject(0.0, :+)
  end
  
  def self.sum_unoptimized(array)
    sum = 0.0
    array.each { |value| sum += value }
    sum
  end
  
  def self.batch_round(array, precision)
    # Batch operations reduce method call overhead
    multiplier = 10.0 ** precision
    array.map { |value| (value * multiplier).round / multiplier }
  end
  
  def self.memory_efficient_range_sum(start, finish, step)
    # Avoid creating intermediate arrays for large ranges
    sum = 0.0
    current = start
    while current <= finish
      sum += current
      current += step
    end
    sum
  end
end

# Performance comparison
data = Array.new(50_000) { rand * 100.0 }

Benchmark.bm(25) do |x|
  x.report("Optimized sum:") do
    FloatProcessor.sum_optimized(data)
  end
  
  x.report("Unoptimized sum:") do
    FloatProcessor.sum_unoptimized(data)
  end
  
  x.report("Batch rounding:") do
    FloatProcessor.batch_round(data, 2)
  end
end

Error Handling & Debugging

Float precision errors manifest as subtle calculation discrepancies that compound through complex operations. Effective debugging requires systematic approaches to identify precision boundaries and accumulation patterns.

# Debugging precision accumulation errors
class PrecisionTracker
  def initialize
    @operations = []
    @precision_loss = 0.0
  end
  
  def track_operation(description, expected, actual)
    error = (expected - actual).abs
    @operations << {
      description: description,
      expected: expected,
      actual: actual,
      error: error,
      relative_error: error / expected.abs
    }
    @precision_loss += error
  end
  
  def report
    puts "Precision Loss Report:"
    puts "Total accumulated error: #{@precision_loss}"
    @operations.each do |op|
      puts "#{op[:description]}: expected=#{op[:expected]}, actual=#{op[:actual]}, error=#{op[:error]}"
    end
  end
end

# Example usage in debugging
tracker = PrecisionTracker.new
result = 0.0

10.times do |i|
  expected = (i + 1) * 0.1
  result += 0.1
  tracker.track_operation("Step #{i + 1}", expected, result)
end

tracker.report

Exception handling around Float operations focuses on domain errors, overflow conditions, and special value detection. Ruby's Float class raises specific exceptions for mathematical domain violations.

# Exception handling for mathematical domain errors
def safe_mathematical_operation(value, operation)
  begin
    case operation
    when :sqrt
      raise Math::DomainError, "Square root of negative number" if value < 0
      Math.sqrt(value)
    when :log
      raise Math::DomainError, "Logarithm of non-positive number" if value <= 0
      Math.log(value)
    when :acos
      raise Math::DomainError, "Arccosine domain error" if value < -1 || value > 1
      Math.acos(value)
    else
      raise ArgumentError, "Unknown operation: #{operation}"
    end
  rescue Math::DomainError => e
    puts "Mathematical domain error: #{e.message}"
    Float::NAN
  rescue ZeroDivisionError => e
    puts "Division by zero: #{e.message}"
    value > 0 ? Float::INFINITY : -Float::INFINITY
  end
end

# Usage examples
result1 = safe_mathematical_operation(-4.0, :sqrt)    # => NaN
result2 = safe_mathematical_operation(0.0, :log)      # => NaN  
result3 = safe_mathematical_operation(1.5, :acos)     # => NaN

Validation strategies for Float inputs prevent cascading precision errors and ensure data integrity in calculations. Input sanitization and range checking catch problematic values before they affect computation.

# Validation framework for Float operations
class FloatValidator
  EPSILON = 1e-10
  
  def self.validate_finite(value, name = "value")
    raise ArgumentError, "#{name} must be finite, got #{value}" unless value.finite?
    value
  end
  
  def self.validate_range(value, min, max, name = "value")
    unless min <= value && value <= max
      raise ArgumentError, "#{name} must be between #{min} and #{max}, got #{value}"
    end
    value
  end
  
  def self.validate_precision(value, max_precision, name = "value")
    # Check if value has more precision than expected
    rounded = value.round(max_precision)
    if (value - rounded).abs > EPSILON
      raise ArgumentError, "#{name} exceeds maximum precision of #{max_precision} decimal places"
    end
    value
  end
  
  def self.validate_not_near_zero(value, name = "value")
    if value.abs < EPSILON
      raise ArgumentError, "#{name} is too close to zero for safe division"
    end
    value
  end
end

# Example validation usage
begin
  price = 19.999999999
  FloatValidator.validate_precision(price, 2, "price")
rescue ArgumentError => e
  puts "Validation error: #{e.message}"
  price = price.round(2)  # Fix the precision issue
end

Debugging techniques for complex Float calculations include step-by-step precision tracking, alternative calculation methods for verification, and specialized testing approaches for edge cases.

# Comprehensive debugging toolkit for Float calculations
module FloatDebugger
  def self.compare_calculations(name, method1, method2, inputs)
    puts "Comparing calculation methods for #{name}:"
    
    inputs.each do |input|
      result1 = method1.call(input)
      result2 = method2.call(input)
      difference = (result1 - result2).abs
      
      puts "Input: #{input}"
      puts "  Method 1: #{result1}"
      puts "  Method 2: #{result2}"
      puts "  Difference: #{difference}"
      puts "  Relative error: #{difference / [result1.abs, result2.abs].max}"
      puts
    end
  end
  
  def self.precision_boundary_test(operation, test_values)
    puts "Testing precision boundaries for #{operation}:"
    
    test_values.each do |value|
      begin
        result = yield(value)
        puts "#{value} -> #{result} (#{result.class})"
        
        # Check for special values
        if result.nan?
          puts "  WARNING: Result is NaN"
        elsif result.infinite?
          puts "  WARNING: Result is infinite"
        elsif result.zero? && value != 0.0
          puts "  WARNING: Underflow to zero"
        end
      rescue => e
        puts "#{value} -> ERROR: #{e.message}"
      end
    end
  end
  
  def self.step_by_step_calculation(initial_value, operations)
    current = initial_value
    puts "Step-by-step calculation starting with #{current}:"
    
    operations.each_with_index do |operation, index|
      previous = current
      current = operation.call(current)
      error_from_previous = current - previous
      
      puts "Step #{index + 1}: #{previous} -> #{current}"
      puts "  Change: #{error_from_previous}"
      puts "  Absolute value: #{current.abs}"
      puts "  Special value checks: finite=#{current.finite?}, nan=#{current.nan?}, infinite=#{current.infinite?}"
      puts
    end
    
    current
  end
end

# Example debugging usage
method1 = ->(x) { Math.sqrt(x * x + 1) - 1 }
method2 = ->(x) { (x * x) / (Math.sqrt(x * x + 1) + 1) }

FloatDebugger.compare_calculations(
  "sqrt(x^2 + 1) - 1 vs x^2/(sqrt(x^2 + 1) + 1)",
  method1,
  method2,
  [1e-8, 1e-10, 1e-12, 1e-15]
)

Error recovery strategies provide fallback mechanisms when Float calculations produce unexpected results or encounter edge cases that affect program stability.

# Error recovery framework for Float operations
class FloatCalculationRecovery
  def self.safe_divide(numerator, denominator, fallback: 0.0)
    return Float::INFINITY if denominator == 0.0 && numerator > 0.0
    return -Float::INFINITY if denominator == 0.0 && numerator < 0.0
    return Float::NAN if denominator == 0.0 && numerator == 0.0
    
    result = numerator / denominator
    return fallback unless result.finite?
    result
  end
  
  def self.safe_percentage(part, total, fallback: 0.0)
    return fallback if total == 0.0
    percentage = (part / total) * 100.0
    return fallback unless percentage.finite?
    percentage.clamp(0.0, 100.0)
  end
  
  def self.robust_average(values)
    return 0.0 if values.empty?
    
    # Filter out non-finite values
    finite_values = values.select(&:finite?)
    return 0.0 if finite_values.empty?
    
    sum = finite_values.sum
    return 0.0 unless sum.finite?
    
    sum / finite_values.length
  end
  
  def self.interpolate_with_fallback(x1, y1, x2, y2, target_x, fallback: 0.0)
    return fallback if x1 == x2  # Avoid division by zero
    
    # Linear interpolation formula: y = y1 + (y2-y1) * (x-x1)/(x2-x1)
    slope = (y2 - y1) / (x2 - x1)
    return fallback unless slope.finite?
    
    result = y1 + slope * (target_x - x1)
    return fallback unless result.finite?
    result
  end
end

Reference

Core Float Methods

Method Parameters Returns Description
Float(value) value (Numeric, String) Float Converts value to Float, raises exception on invalid input
#+(other) other (Numeric) Numeric Addition with type promotion
#-(other) other (Numeric) Numeric Subtraction with type promotion
#*(other) other (Numeric) Numeric Multiplication with type promotion
#/(other) other (Numeric) Numeric Division with type promotion
#%(other) other (Numeric) Numeric Modulo operation
#**(other) other (Numeric) Numeric Exponentiation
#<=>(other) other (Numeric) Integer, nil Comparison operator (-1, 0, 1)
#==(other) other (Numeric) Boolean Equality comparison
#<(other) other (Numeric) Boolean Less than comparison
#>(other) other (Numeric) Boolean Greater than comparison
#<=(other) other (Numeric) Boolean Less than or equal comparison
#>=(other) other (Numeric) Boolean Greater than or equal comparison

Precision and Rounding Methods

Method Parameters Returns Description
#round(ndigits=0, half: :up) ndigits (Integer), half (Symbol) Integer/Float Round to specified decimal places
#ceil(ndigits=0) ndigits (Integer) Integer/Float Round up to nearest integer or decimal
#floor(ndigits=0) ndigits (Integer) Integer/Float Round down to nearest integer or decimal
#truncate(ndigits=0) ndigits (Integer) Integer/Float Truncate toward zero
#abs None Float Absolute value
#magnitude None Float Absolute value (alias for abs)

String Conversion and Formatting

Method Parameters Returns Description
#to_s None String Convert to string representation
#to_i None Integer Convert to integer (truncate)
#to_r None Rational Convert to rational number
#to_c None Complex Convert to complex number
#inspect None String Developer-friendly string representation

Special Value Testing Methods

Method Parameters Returns Description
#finite? None Boolean Test if number is finite (not infinite or NaN)
#infinite? None Integer, nil Returns 1 for +∞, -1 for -∞, nil otherwise
#nan? None Boolean Test if value is Not a Number (NaN)
#zero? None Boolean Test if value equals zero
#positive? None Boolean Test if value is greater than zero
#negative? None Boolean Test if value is less than zero

Mathematical Functions (Math module)

Method Parameters Returns Description
Math.sqrt(x) x (Numeric) Float Square root
Math.sin(x) x (Numeric) Float Sine (radians)
Math.cos(x) x (Numeric) Float Cosine (radians)
Math.tan(x) x (Numeric) Float Tangent (radians)
Math.log(x, base=E) x, base (Numeric) Float Logarithm
Math.exp(x) x (Numeric) Float Exponential function (e^x)
Math.hypot(x, y) x, y (Numeric) Float Euclidean distance

Float Constants

Constant Value Description
Float::INFINITY Infinity Positive infinity
Float::NAN NaN Not a Number
Float::EPSILON 2.220446049250313e-16 Smallest distinguishable difference
Float::MANT_DIG 53 Number of mantissa digits
Float::MAX_EXP 1024 Maximum exponent
Float::MIN_EXP -1021 Minimum exponent
Float::MAX 1.7976931348623157e+308 Maximum finite value
Float::MIN 2.2250738585072014e-308 Minimum positive normal value
Float::DIG 15 Decimal digits of precision
Float::ROUNDS 1 Rounding mode (1 = round to nearest)

Rounding Mode Options

Mode Symbol Description
Round half up :up Round 0.5 up to 1 (default)
Round half even :even Round to nearest even (banker's rounding)
Round half down :down Round 0.5 down to 0

String Format Specifiers

Specifier Description Example
%f Fixed-point notation "%.2f" % 3.14159"3.14"
%e Scientific notation "%.2e" % 3141.59"3.14e+03"
%g Shorter of %f or %e "%.3g" % 3.14159"3.14"
%+f Force sign display "%+.2f" % 3.14"+3.14"
%8.2f Width and precision "%8.2f" % 3.14" 3.14"

Precision Comparison Patterns

# Epsilon-based equality
def float_equal?(a, b, epsilon = Float::EPSILON)
  (a - b).abs < epsilon
end

# Relative tolerance comparison  
def relative_equal?(a, b, tolerance = 1e-10)
  return true if a == b
  return false if a.zero? || b.zero?
  ((a - b).abs / [a.abs, b.abs].max) < tolerance
end

# ULP (Units in Last Place) comparison
def ulp_equal?(a, b, ulps = 1)
  return true if a == b
  return false unless a.finite? && b.finite?
  # Implementation requires bit manipulation
end

Common Precision Patterns

Pattern Use Case Implementation
Currency calculation Financial precision Use BigDecimal or scale integers
Scientific computation High precision needed Track significant figures
User interface Display formatting Round for presentation only
Tolerance checking Approximate equality Use relative or absolute epsilon
Range operations Boundary conditions Account for step accumulation