CrackedRuby logo

CrackedRuby

Numeric Base Class

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