Overview
The Numeric class serves as the abstract base class for all numeric types in Ruby, providing common functionality across Integer, Float, Rational, and Complex classes. Ruby implements a sophisticated coercion system through Numeric that automatically handles type conversions during arithmetic operations, ensuring mathematical operations work seamlessly between different numeric types.
Numeric defines the interface for arithmetic operators, comparison methods, and conversion functions that all numeric classes inherit. The class implements a coercion protocol that allows mixed-type arithmetic operations to work correctly by converting operands to compatible types before performing calculations.
# All numeric classes inherit from Numeric
42.class.superclass # => Integer < Numeric
3.14.class.superclass # => Float < Numeric
Rational(1, 3).class.superclass # => Rational < Numeric
Complex(1, 2).class.superclass # => Complex < Numeric
The coercion mechanism activates automatically when Ruby encounters operations between different numeric types. When an operation like 2 + 3.5
occurs, Ruby calls the coercion protocol to convert both operands to a common type before performing the addition.
# Coercion happens automatically
2 + 3.5 # => 5.5 (Integer coerced to Float)
1 + 0.5r # => (3/2) (Integer coerced to Rational)
2 * (1+2i) # => (2+4i) (Integer coerced to Complex)
Numeric classes form a hierarchy where more precise types can coerce to less precise types during operations. Complex numbers can represent any other numeric type, Rational numbers can represent integers, and Float provides approximate representation for most numeric values.
Basic Usage
Numeric provides core arithmetic operations that work consistently across all numeric types. The binary operators +
, -
, *
, /
, %
, and **
handle mixed-type arithmetic through the coercion protocol.
# Mixed-type arithmetic operations
integer = 42
float = 3.14
rational = Rational(22, 7)
complex = Complex(1, 2)
# Addition automatically handles type coercion
integer + float # => 45.14
integer + rational # => (316/7)
float + complex # => (4.14+2.0i)
The comparison operators work similarly, providing consistent behavior across types. Ruby defines comparison methods <
, <=
, >
, >=
, and <=>
that handle coercion appropriately.
# Comparisons work across numeric types
42 > 41.9 # => true
Rational(1,2) < 0.6 # => true
3.0 == 3 # => true
# The spaceship operator returns -1, 0, or 1
42 <=> 41 # => 1
3.14 <=> 3.14 # => 0
2 <=> 5 # => -1
Numeric provides several conversion methods that transform numbers between different representations. The to_i
, to_f
, to_r
, and to_c
methods convert to Integer, Float, Rational, and Complex respectively.
# Converting between numeric types
value = 3.14159
value.to_i # => 3
value.to_r # => (7074029114692207/2251799813685248)
value.to_c # => (3.14159+0i)
# Rational provides exact conversion
Rational(22, 7).to_f # => 3.142857142857143
Rational(22, 7).to_i # => 3
The round
, floor
, ceil
, and truncate
methods provide different rounding behaviors. These methods accept an optional digits parameter for controlling precision.
value = 3.14159
value.round # => 3
value.round(2) # => 3.14
value.floor # => 3
value.ceil # => 4
value.truncate # => 3
value.truncate(3) # => 3.141
Advanced Usage
The coercion protocol implements a sophisticated type promotion system through the coerce
method. When Ruby encounters a binary operation between different numeric types, it calls coerce
on the right operand with the left operand as an argument.
# Understanding the coercion protocol
class CustomNumeric < Numeric
def initialize(value)
@value = value
end
def coerce(other)
[Float(other), @value.to_f]
end
def +(other)
if other.is_a?(CustomNumeric)
CustomNumeric.new(@value + other.instance_variable_get(:@value))
else
# Trigger coercion for other types
coerced = coerce(other)
coerced[0] + coerced[1]
end
end
def to_f
@value.to_f
end
end
custom = CustomNumeric.new(5)
result = 3.14 + custom # => 8.14
Numeric supports step iteration through the step
method, which generates sequences of numbers with specified increments. This method works with all numeric types and handles coercion appropriately.
# Step iteration with different numeric types
1.step(10, 2).to_a # => [1, 3, 5, 7, 9]
0.step(1, 0.25).to_a # => [0.0, 0.25, 0.5, 0.75, 1.0]
Rational(1,4).step(1, Rational(1,4)) { |x| puts x }
# => 1/4, 1/2, 3/4, 1/1
# Step with blocks for memory-efficient processing
sum = 0
1.step(1000000, 7) { |n| sum += n }
puts sum # => 7142857142857
The modulo operation %
and divmod method provide division with remainder calculations that respect numeric type hierarchies. These operations maintain precision according to the operand types.
# Modulo operations across types
17 % 5 # => 2
17.5 % 5 # => 2.5
Rational(17,2) % 5 # => (3/2)
# divmod returns quotient and remainder
17.divmod(5) # => [3, 2]
17.5.divmod(5) # => [3, 2.5]
Rational(35,2).divmod(5) # => [3, (3/2)]
# Works with negative numbers
-17.divmod(5) # => [-4, 3]
17.divmod(-5) # => [-4, -3]
Ruby implements numeric type hierarchy through singleton methods that define conversion behavior. The Integer
, Float
, Rational
, and Complex
methods create instances while handling edge cases appropriately.
# Type conversion methods handle edge cases
Integer("42") # => 42
Integer("42.7") # raises ArgumentError
Integer("42.7", 10) # raises ArgumentError
Integer(42.7) # => 42 (truncates)
Float("3.14") # => 3.14
Float("inf") # => Infinity
Float("-inf") # => -Infinity
Float("nan") # => NaN
Rational("22/7") # => (22/7)
Rational(0.1) # => (3602879701896397/36028797018963968)
Rational("0.1") # => (1/10)
Complex("1+2i") # => (1+2i)
Complex(1, 2) # => (1+2i)
Complex("1@2") # => (-0.4161468365471424+0.9092974268256817i) # polar
Common Pitfalls
Floating point precision issues create unexpected behavior when comparing float values or converting between numeric types. Ruby's Float class uses IEEE 754 double precision, which cannot exactly represent many decimal fractions.
# Float precision pitfalls
0.1 + 0.2 == 0.3 # => false
0.1 + 0.2 # => 0.30000000000000004
# Use rational arithmetic for exact decimal calculations
Rational("0.1") + Rational("0.2") == Rational("0.3") # => true
# Or compare with tolerance
(0.1 + 0.2 - 0.3).abs < Float::EPSILON # => true
# Converting floats to rationals can produce large fractions
0.1.to_r # => (3602879701896397/36028797018963968)
Rational("0.1") # => (1/10)
Type coercion behaves differently with numeric and non-numeric objects. When Ruby cannot coerce types, it raises a TypeError, but the error location might be unexpected.
# Coercion failures
begin
"5" + 3
rescue TypeError => e
puts e.message # => no implicit conversion of Integer into String
end
begin
5 + "3"
rescue TypeError => e
puts e.message # => String can't be coerced into Integer
end
# Define coerce method for custom classes
class Temperature
def initialize(celsius)
@celsius = celsius
end
def coerce(other)
[other, @celsius]
end
def +(other)
Temperature.new(@celsius + other)
end
end
temp = Temperature.new(20)
# This fails because Integer#+ doesn't know about Temperature
begin
25 + temp
rescue TypeError => e
puts e.message # => Temperature can't be coerced into Integer
end
Division operations produce different result types depending on the operands, leading to unexpected behavior in calculations that expect consistent types.
# Division type behavior changes
10 / 3 # => 3 (Integer division)
10.0 / 3 # => 3.3333333333333335 (Float division)
10 / 3.0 # => 3.3333333333333335 (Float division)
Rational(10, 3) # => (10/3) (Exact rational)
# Use fdiv for consistent float division
10.fdiv(3) # => 3.3333333333333335
(-5).fdiv(2) # => -2.5
# Integer division rounds toward negative infinity
-7 / 3 # => -3 (not -2)
-7 % 3 # => 2
-7.divmod(3) # => [-3, 2]
Comparison operations between different numeric types can produce counterintuitive results, especially with complex numbers and special float values.
# Complex numbers are not ordered
begin
Complex(1, 2) > Complex(2, 1)
rescue ArgumentError => e
puts e.message # => comparison of Complex with Complex failed
end
# But equality works
Complex(2, 0) == 2 # => true
Complex(2, 1) == 2 # => false
# Special float values have unique comparison behavior
Float::INFINITY > 1000000 # => true
Float::NAN == Float::NAN # => false
Float::NAN.equal?(Float::NAN) # => true
# Range behavior with floats
(1.0..3.0).include?(2.5) # => true
(1.0...3.0).include?(3.0) # => false
Error Handling & Debugging
Numeric operations generate specific exception types that indicate the nature of calculation problems. Ruby raises ZeroDivisionError for division by zero, ArgumentError for invalid conversions, and TypeError for unsupported operations.
# Handling division by zero
def safe_divide(numerator, denominator)
begin
result = numerator / denominator
return result
rescue ZeroDivisionError => e
puts "Cannot divide by zero: #{e.message}"
return Float::INFINITY if numerator > 0
return -Float::INFINITY if numerator < 0
return Float::NAN if numerator == 0
end
end
safe_divide(10, 0) # => Infinity
safe_divide(-10, 0) # => -Infinity
safe_divide(0, 0) # => NaN
# Float division by zero returns Infinity, not an exception
10.0 / 0 # => Infinity
-10.0 / 0 # => -Infinity
0.0 / 0.0 # => NaN
Conversion errors occur when Ruby cannot parse string representations or convert between incompatible types. These operations raise ArgumentError with descriptive messages.
# String conversion error handling
def parse_number(string)
begin
# Try integer first
return Integer(string)
rescue ArgumentError
begin
# Try float
return Float(string)
rescue ArgumentError
begin
# Try rational
return Rational(string)
rescue ArgumentError
begin
# Try complex
return Complex(string)
rescue ArgumentError => e
puts "Cannot parse '#{string}' as a number: #{e.message}"
return nil
end
end
end
end
end
parse_number("42") # => 42
parse_number("3.14") # => 3.14
parse_number("22/7") # => (22/7)
parse_number("1+2i") # => (1+2i)
parse_number("invalid") # => Cannot parse 'invalid' as a number: invalid value for Complex(): "invalid"
Range and boundary conditions require careful handling, especially when working with different numeric precisions and types that have different representable ranges.
# Handling numeric boundary conditions
def validate_numeric_range(value, min, max)
unless value.is_a?(Numeric)
raise TypeError, "Expected Numeric, got #{value.class}"
end
if value.respond_to?(:nan?) && value.nan?
raise ArgumentError, "NaN values are not allowed"
end
if value.respond_to?(:infinite?) && value.infinite?
raise ArgumentError, "Infinite values are not allowed"
end
unless value >= min && value <= max
raise ArgumentError, "Value #{value} outside range #{min}..#{max}"
end
value
end
validate_numeric_range(5, 0, 10) # => 5
validate_numeric_range(Float::NAN, 0, 10) # raises ArgumentError
validate_numeric_range(Float::INFINITY, 0, 10) # raises ArgumentError
Reference
Core Arithmetic Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#+@ |
none | self |
Unary plus operator |
#-@ |
none | Numeric |
Unary minus operator |
#+(other) |
other (Numeric) |
Numeric |
Addition with coercion |
#-(other) |
other (Numeric) |
Numeric |
Subtraction with coercion |
#*(other) |
other (Numeric) |
Numeric |
Multiplication with coercion |
#/(other) |
other (Numeric) |
Numeric |
Division with coercion |
#%(other) |
other (Numeric) |
Numeric |
Modulo operation |
#**(other) |
other (Numeric) |
Numeric |
Exponentiation |
#divmod(other) |
other (Numeric) |
[Numeric, Numeric] |
Division with quotient and remainder |
Comparison Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#<=>(other) |
other (Numeric) |
Integer , nil |
Three-way comparison |
#<(other) |
other (Numeric) |
Boolean |
Less than comparison |
#<=(other) |
other (Numeric) |
Boolean |
Less than or equal comparison |
#>(other) |
other (Numeric) |
Boolean |
Greater than comparison |
#>=(other) |
other (Numeric) |
Boolean |
Greater than or equal comparison |
#==(other) |
other (Object) |
Boolean |
Equality comparison |
#eql?(other) |
other (Object) |
Boolean |
Type-strict equality |
Conversion Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#to_i |
none | Integer |
Convert to integer (truncate) |
#to_f |
none | Float |
Convert to float |
#to_r |
none | Rational |
Convert to rational |
#to_c |
none | Complex |
Convert to complex |
#to_int |
none | Integer |
Convert to integer (strict) |
#round(digits=0) |
digits (Integer) |
Numeric |
Round to specified precision |
#floor(digits=0) |
digits (Integer) |
Numeric |
Round down to specified precision |
#ceil(digits=0) |
digits (Integer) |
Numeric |
Round up to specified precision |
#truncate(digits=0) |
digits (Integer) |
Numeric |
Truncate to specified precision |
Iteration Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#step(limit, step=1) |
limit (Numeric), step (Numeric) |
Enumerator |
Generate numeric sequence |
#step(limit, step=1) { block } |
limit (Numeric), step (Numeric), block |
self |
Iterate numeric sequence |
Type Checking Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#integer? |
none | Boolean |
Check if value is integer |
#real? |
none | Boolean |
Check if value is real |
#finite? |
none | Boolean |
Check if value is finite |
#infinite? |
none | Integer , nil |
Check if value is infinite |
#zero? |
none | Boolean |
Check if value equals zero |
#nonzero? |
none | self , nil |
Return self if non-zero |
#positive? |
none | Boolean |
Check if value is positive |
#negative? |
none | Boolean |
Check if value is negative |
Coercion Protocol
Method | Parameters | Returns | Description |
---|---|---|---|
#coerce(other) |
other (Numeric) |
[Numeric, Numeric] |
Convert operands for binary operations |
Constants
Constant | Type | Value | Description |
---|---|---|---|
Float::INFINITY |
Float | Infinity | Positive infinity |
Float::NAN |
Float | NaN | Not a number |
Float::EPSILON |
Float | 2.220446049250313e-16 | Machine epsilon |
Float::MAX |
Float | 1.7976931348623157e+308 | Maximum finite value |
Float::MIN |
Float | 2.2250738585072014e-308 | Minimum positive value |
Exception Hierarchy
StandardError
├── ArgumentError # Invalid conversion arguments
├── TypeError # Type coercion failures
├── ZeroDivisionError # Division by zero (integers)
├── FloatDomainError # Invalid float operations
└── RangeError # Numeric values out of range