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 |