Math #

Mathematical computation is a need that arises in almost every programming domain — from discount price calculations in e-commerce, audio signal processing, physics simulation in games, to machine learning algorithms. Ruby provides three layers of mathematical facilities: the Math module for scientific functions (trigonometry, logarithms, roots), built-in methods on the Numeric, Integer, and Float classes for everyday operations, and BigDecimal and Rational for high-precision needs. Understanding when to use each of these layers — and the floating-point precision traps lurking within — is the key to writing programs that produce correct numbers.

The Math Module and Constants #

Math is a built-in Ruby module providing scientific math functions. No require needed — available immediately when Ruby loads.

# Built-in constants
Math::PI    # => 3.141592653589793  (π)
Math::E     # => 2.718281828459045  (Euler's number)

# Using include for prefix-free access
include Math
PI    # => 3.141592653589793
sqrt(16)  # => 4.0

All Math methods return a Float, even when the input is an Integer. This is important to understand because it affects the precision of computation results.

Math.sqrt(9)      # => 3.0   (Float, not Integer!)
Math.sqrt(2)      # => 1.4142135623730951
Math.sqrt(-1)     # => NaN   (no error, but Not a Number)

# Check for invalid results
result = Math.sqrt(-5)
result.nan?        # => true
result.infinite?   # => nil

# Special Float constants
Float::INFINITY   # => Infinity
-Float::INFINITY  # => -Infinity
Float::NAN        # => NaN
Constant / MethodValueDescription
Math::PI3.14159265358979…π — the ratio of a circle’s circumference to its diameter
Math::E2.71828182845904…Euler’s number — the base of natural logarithms
Float::INFINITYPositive infinity
Float::NANNaNNot a Number — the result of an undefined operation
Float::EPSILON2.22e-16The smallest difference between two distinct Floats
Float::DIG15The number of decimal digits of Float precision

Roots and Powers #

Root and power operations are the foundation of many algorithms. Ruby provides them in several places with different trade-offs.

# Square root
Math.sqrt(25)      # => 5.0
Math.sqrt(2)       # => 1.4142135623730951

# Roots with powers (nth root)
# There's no Math.cbrt in Ruby, use the power formula
Math.cbrt = lambda { |x| x < 0 ? -((-x) ** (1.0/3)) : x ** (1.0/3) }
# Or directly:
27 ** (1.0/3)      # => 3.0  (cube root of 27)
8 ** (1.0/3)       # => 2.0
16 ** (1.0/4)      # => 2.0  (4th root)

# Powers with the ** operator
2 ** 10            # => 1024   (Integer result when base and exponent are Integers)
2 ** 0.5           # => 1.4142135623730951  (Float if the exponent is Float)
2.0 ** 10          # => 1024.0  (Float if the base is Float)

# Integer.pow with modulo — efficient for cryptography
# (a ** b) % m
2.pow(10, 1000)    # => 24  (equivalent to (2**10) % 1000, but more efficient)

# Math.hypot — hypotenuse length, avoids overflow
Math.hypot(3, 4)   # => 5.0  (sqrt(3² + 4²))
Math.hypot(5, 12)  # => 13.0
flowchart TD
    A[Need roots / powers] --> B{Result type?}
    B -- "Exact Integer" --> C{Is the result\ndefinitely an integer?}
    B -- "Float OK" --> D["x ** float_exponent\nor Math.sqrt(x)"]
    C -- Yes --> E["Integer ** Integer\ne.g. 2 ** 8 => 256"]
    C -- No --> F["Convert first:\nx.to_f ** (1.0/n)"]
    D --> G{Need high\nprecision?}
    G -- Yes --> H["BigDecimal + sqrt"]
    G -- No --> D
27 ** (1.0/3) should produce exactly 3.0, but because 1.0/3 is 0.3333... in floating-point, the result can be 2.9999999999999996 in some cases depending on the platform. If integer precision matters, verify with rounding: (x ** (1.0/n)).round.

Trigonometric Functions #

All Math trigonometric functions work in radians, not degrees. This is a very common source of errors for beginners and experienced developers alike who come from environments defaulting to degrees.

# Basic functions — all arguments in RADIANS
Math.sin(0)             # => 0.0
Math.sin(Math::PI / 2)  # => 1.0         (sin 90°)
Math.cos(0)             # => 1.0
Math.cos(Math::PI)      # => -1.0        (cos 180°)
Math.tan(Math::PI / 4)  # => 0.9999...   (tan 45° ≈ 1.0)

# Inverse functions (arc)
Math.asin(1.0)          # => 1.5707963...  (π/2 = 90°)
Math.acos(1.0)          # => 0.0           (0°)
Math.atan(1.0)          # => 0.7853...     (π/4 = 45°)
Math.atan2(1, 1)        # => 0.7853...     (π/4)
Math.atan2(-1, -1)      # => -2.3561...    (225° or -135°)

# Degrees ↔ radians conversion
def degrees_to_radians(degrees)
  degrees * Math::PI / 180.0
end

def radians_to_degrees(radians)
  radians * 180.0 / Math::PI
end

Math.sin(degrees_to_radians(30))   # => 0.5  (sin 30°)
Math.cos(degrees_to_radians(60))   # => 0.5  (cos 60°)

radians_to_degrees(Math::PI)       # => 180.0
radians_to_degrees(Math::PI / 2)   # => 90.0
# ANTI-PATTERN: passing degrees directly to trig functions
angle = 45
Math.sin(angle)   # => 0.8509...  WRONG! sin(45 radians), not sin(45°)

# CORRECT: convert to radians first
Math.sin(angle * Math::PI / 180)  # => 0.7071...  (the correct sin 45°)

Math.atan2(y, x) deserves special attention — it’s a superior version of atan because it handles all quadrants correctly and never divides by zero:

# atan2 returns an angle in the range (-π, π]
Math.atan2(1, 0)    # => π/2   (90°)   — pointing up
Math.atan2(-1, 0)   # => -π/2  (-90°)  — pointing down
Math.atan2(0, -1)   # => π     (180°)  — pointing left
Math.atan2(0, 1)    # => 0     (0°)    — pointing right

# Application: compute the angle between two points
def angle_between(x1, y1, x2, y2)
  radians_to_degrees(Math.atan2(y2 - y1, x2 - x1))
end

angle_between(0, 0, 1, 1)   # => 45.0°
angle_between(0, 0, 0, 1)   # => 90.0°

Hyperbolic Functions #

Hyperbolic functions are useful in scientific computing, signal processing, and artificial neural networks (activation functions like tanh).

Math.sinh(0)   # => 0.0    (hyperbolic sine)
Math.cosh(0)   # => 1.0    (hyperbolic cosine)
Math.tanh(0)   # => 0.0    (hyperbolic tangent)
Math.tanh(1)   # => 0.7615941559557649

# Identity: cosh²(x) - sinh²(x) = 1
x = 2.5
(Math.cosh(x) ** 2 - Math.sinh(x) ** 2).round(10)  # => 1.0

# Inverse hyperbolic functions
Math.asinh(0)   # => 0.0
Math.acosh(1)   # => 0.0
Math.atanh(0)   # => 0.0

Logarithms and Exponentials #

Logarithms and exponentials are core operations in algorithm complexity analysis, statistics, and finance (compound interest, exponential growth).

# Exponentials — e^x
Math.exp(0)     # => 1.0
Math.exp(1)     # => 2.718281828459045  (the value of e)
Math.exp(2)     # => 7.38905609893065

# Natural logarithm (base e)
Math.log(1)           # => 0.0
Math.log(Math::E)     # => 1.0
Math.log(Math::E**2)  # => 2.0

# Logarithm with a specific base
Math.log(100, 10)     # => 2.0     (log₁₀)
Math.log(8, 2)        # => 3.0     (log₂)
Math.log(27, 3)       # => 3.0     (log₃)

# Base-2 and base-10 logarithms — shortcuts
Math.log2(1024)       # => 10.0
Math.log10(1000)      # => 3.0
Math.log10(0.001)     # => -3.0
# Application: continuous growth calculation
# Formula: A = P * e^(r*t)
def continuous_growth(principal, annual_rate, years)
  principal * Math.exp(annual_rate * years)
end

# Rp 10 million at 5% per year for 10 years
result = continuous_growth(10_000_000, 0.05, 10)
puts "Result: Rp #{result.round.to_s.reverse.scan(/.{1,3}/).join('.').reverse}"

# Converting between logarithm bases
# log_b(x) = ln(x) / ln(b)
def log_base(x, b)
  Math.log(x) / Math.log(b)
end

log_base(32, 2)    # => 5.0   (2^5 = 32)
log_base(243, 3)   # => 5.0   (3^5 = 243)
flowchart LR
    A[Logarithm operation] --> B{Base?}
    B -- "e (natural)" --> C["Math.log(x)"]
    B -- "2" --> D["Math.log2(x)"]
    B -- "10" --> E["Math.log10(x)"]
    B -- "Other b" --> F["Math.log(x, b)\nor\nMath.log(x)/Math.log(b)"]
    C --> G[Float result]
    D --> G
    E --> G
    F --> G
Math.log(0) produces -Infinity, and Math.log(-1) produces NaN. Neither raises an exception — the program keeps running with invalid values. Always validate input before calling logarithm functions if zero or negative values are possible.

Rounding and Truncation #

Converting Floats to Integers with various rounding strategies is a very common operation, especially in financial calculations and data display.

number = 3.7

# round — round to the nearest (default: 0 decimals)
number.round        # => 4
3.5.round           # => 4    (Ruby: halves round up for positives)
(-3.5).round        # => -4   (down for negatives — "round half away from zero")
3.567.round(2)      # => 3.57   (2 decimals)
3.567.round(1)      # => 3.6
1234.5.round(-2)    # => 1200   (round to the nearest hundred)

# ceil — round up (toward infinity)
3.1.ceil            # => 4
-3.7.ceil           # => -3    (up toward 0)
3.123.ceil(2)       # => 3.13

# floor — round down (toward -infinity)
3.9.floor           # => 3
-3.1.floor          # => -4    (down away from 0)
3.987.floor(2)      # => 3.98

# truncate — cut off the decimals (toward 0)
3.9.truncate        # => 3     (same as floor for positives)
-3.9.truncate       # => -3    (different from floor for negatives!)

# divmod — divide and remainder at once
17.divmod(5)        # => [3, 2]   ([quotient, remainder])
(-17).divmod(5)     # => [-4, 3]  (floor division)

The difference between truncate and floor for negative numbers is a frequent source of bugs:

# truncate — cut toward zero
-3.9.truncate   # => -3   (toward 0)

# floor — round down (toward -infinity)
-3.9.floor      # => -4   (away from 0)

# ANTI-PATTERN: assuming truncate == floor
def page(offset, per_page)
  (offset / per_page.to_f).truncate  # wrong for negative offsets
end

# CORRECT: use Integer division or explicit floor
def page(offset, per_page)
  offset / per_page   # Integer division — floors by default in Ruby
end
Method3.7-3.7Principle
round4-4To nearest, halves away from zero
ceil4-3Always up (→ +∞)
floor3-4Always down (→ -∞)
truncate3-3Always toward zero

Integer Operations #

Ruby’s Integer class stores many useful math methods beyond ordinary arithmetic operators.

# Absolute value
(-5).abs     # => 5
(-3.7).abs   # => 3.7

# GCD and LCM — important for fractions and algorithms
12.gcd(8)    # => 4    (Greatest Common Divisor)
12.lcm(8)    # => 24   (Least Common Multiple)
12.gcd(0)    # => 12
12.gcdlcm(8) # => [4, 24]  (both at once)

# Prime checks — no built-in method in pure Ruby
# Requires 'prime'
require 'prime'
Prime.prime?(7)    # => true
Prime.prime?(10)   # => false
Prime.prime?(2)    # => true

# Generate prime numbers
Prime.first(10)    # => [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Prime.each(20) { |p| print "#{p} " }
# => 2 3 5 7 11 13 17 19

# Prime factorization
Prime.prime_division(12)   # => [[2, 2], [3, 1]]  (2² × 3¹)
Prime.prime_division(60)   # => [[2, 2], [3, 1], [5, 1]]

# Digits — split an integer into an array of digits
255.digits       # => [5, 5, 2]  (from units to the largest!)
255.digits(16)   # => [15, 15]   (in base 16: 0xFF)
1234.digits      # => [4, 3, 2, 1]

# Bits
5.to_s(2)        # => "101"  (binary representation)
255.to_s(16)     # => "ff"   (hex representation)
255.to_s(8)      # => "377"  (octal representation)

# Bitwise operations
5 & 3    # => 1   (AND: 101 & 011 = 001)
5 | 3    # => 7   (OR:  101 | 011 = 111)
5 ^ 3    # => 6   (XOR: 101 ^ 011 = 110)
~5       # => -6  (NOT)
5 << 1   # => 10  (left shift: multiply by 2)
5 >> 1   # => 2   (right shift: divide by 2)

Random Numbers with Random #

Random number generation is needed for simulation, data shuffling, games, security tokens, and testing. Ruby provides Random and Kernel#rand with different characteristics.

# rand — the simplest way
rand        # => Float between 0.0 and 1.0 (exclusive)
rand(10)    # => Integer between 0 and 9 (0 inclusive, 10 exclusive)
rand(1..6)  # => Integer between 1 and 6 (both inclusive)
rand(1.0..2.0)  # => Float between 1.0 and 2.0

# Random object — more control, can be seeded
rng = Random.new(42)  # fixed seed — reproducible results
rng.rand(100)         # => always the same for the same seed
rng.rand(100)         # => different from the previous call, but deterministic

# Default seed
Random.new_seed   # => random integer from the OS

# Array#sample and Array#shuffle — use the built-in RNG
[1, 2, 3, 4, 5].sample       # => one random element
[1, 2, 3, 4, 5].sample(3)    # => 3 random elements, no duplicates
[1, 2, 3, 4, 5].shuffle      # => array with shuffled order

# With a specific seed for reproducibility
[1, 2, 3, 4, 5].shuffle(random: Random.new(42))
# => always produces the same order
# ANTI-PATTERN: using rand for security tokens
def create_token
  rand(36**16).to_s(36)   # DON'T! PRNG isn't cryptographic
end

# CORRECT: use SecureRandom for security needs
require 'securerandom'

SecureRandom.hex(16)        # => "a3f2b1c4d5e6f7a8b9c0d1e2f3a4b5c6"
SecureRandom.urlsafe_base64 # => URL-safe base64 string
SecureRandom.uuid           # => "550e8400-e29b-41d4-a716-446655440000"
SecureRandom.random_number(100)  # => cryptographic Integer, 0..99
flowchart TD
    A[Need random numbers] --> B[For security?]
    B -- Yes --> C["SecureRandom\n(cryptographic)"]
    B -- No --> D{Need reproducible?}
    D -- Yes --> E["Random.new(seed)\nrng.rand(...)"]
    D -- No --> F["Kernel#rand\nor Array#sample"]
    C --> G["SecureRandom.hex\nSecureRandom.uuid\nSecureRandom.random_number"]
    E --> H[Deterministic results\nsuitable for testing]
    F --> I[Fast, non-cryptographic]

Floats and Precision: Traps You Must Understand #

Floating-point is the number representation system used by all modern programming languages, and it has fundamental limitations that can cause very subtle bugs.

# The famous floating-point precision problem
0.1 + 0.2           # => 0.30000000000000004   (not 0.3!)
0.1 + 0.2 == 0.3    # => false  !

# Why? Floats can't represent 0.1 and 0.2 exactly in binary —
# just like 1/3 can't be written exactly in decimal

# Safe Float comparison — use an epsilon
def floats_equal?(a, b, epsilon = 1e-10)
  (a - b).abs < epsilon
end

floats_equal?(0.1 + 0.2, 0.3)   # => true

# For financial calculations — DON'T use Float!
price = 0.1 + 0.2
puts price   # => 0.30000000000000004

# ANTI-PATTERN: Float for money
total = 0.1 + 0.2
puts "Total: Rp #{(total * 100).round / 100.0}"

# CORRECT: use Integer (cents/points) or BigDecimal
total_cents = 10 + 20   # in cents: 10 cents + 20 cents
puts "Total: Rp #{total_cents / 100.0}"  # => "Total: Rp 0.3"

BigDecimal for High Precision #

When exact numerical precision is an absolute requirement — finance, taxation, accounting — BigDecimal is the solution. It represents decimal numbers exactly with controllable precision.

require 'bigdecimal'
require 'bigdecimal/util'  # for the .to_d method on literals

# Creating BigDecimal
a = BigDecimal("0.1")
b = BigDecimal("0.2")
(a + b).to_s    # => "0.3E0"
(a + b) == BigDecimal("0.3")   # => true  (exact precision!)

# Conversion methods — IMPORTANT: always use Strings, not Floats!
BigDecimal("0.1")         # CORRECT: from a String
BigDecimal(0.1)           # WRONG: from a Float it's already imprecise!
BigDecimal(0.1.to_s)      # OK: convert the Float to a String first

# With .to_d (needs bigdecimal/util)
"0.1".to_d + "0.2".to_d   # => 0.3e0

# Operation precision
BigDecimal("1") / BigDecimal("3")
# => 0.3333333333333333333333333333e0  (default precision)

(BigDecimal("1") / BigDecimal("3")).round(10).to_s
# => "0.3333333333e0"

# Rounding modes
require 'bigdecimal/math'
value = BigDecimal("2.5")
value.round(0, BigDecimal::ROUND_HALF_UP)    # => 3
value.round(0, BigDecimal::ROUND_HALF_DOWN)  # => 2
value.round(0, BigDecimal::ROUND_HALF_EVEN)  # => 2  (banker's rounding)
# A correct tax calculation example
def calculate_tax(price_str, tax_percent_str)
  price = BigDecimal(price_str)
  tax   = BigDecimal(tax_percent_str) / 100
  tax_total = (price * tax).round(2, BigDecimal::ROUND_HALF_UP)
  {
    price: price,
    tax:   tax_total,
    total: price + tax_total
  }
end

result = calculate_tax("150000.00", "11")
puts "Price:  Rp #{result[:price]}"    # Rp 0.15e6
puts "VAT:    Rp #{result[:tax]}"      # Rp 0.165e5
puts "Total:  Rp #{result[:total]}"    # Rp 0.16515e6

Rational — Exact Fraction Representation #

Rational represents numbers as exact numerator/denominator fractions, without losing precision. Useful for symbolic mathematics and algorithms involving fractions.

# Creating Rationals
r = Rational(1, 3)     # => (1/3)
r.to_f                 # => 0.3333333333333333

Rational(2, 4)         # => (1/2)  (automatically simplified)
Rational(3)            # => (3/1)

# Operations with Rationals
Rational(1, 3) + Rational(1, 6)    # => (1/2)  (exact!)
Rational(1, 3) * Rational(3, 4)    # => (1/4)
Rational(2, 3) ** 2                # => (4/9)

# Literal conversion with the r suffix (Ruby 2.1+)
r = 1/3r               # => (1/3)   shortcut for Rational(1, 3)
3/4r                   # => (3/4)

# Comparison
Rational(1, 3) == Rational(2, 6)   # => true  (both are the same)

# Conversion
Rational(22, 7).to_f   # => 3.142857142857143
Rational(22, 7).to_i   # => 3   (truncate)

Basic Statistics with Enumerable #

Ruby doesn’t have a built-in statistics module as complete as Python’s, but combining Enumerable with math operations can compute basic statistics very elegantly.

data = [4, 8, 15, 16, 23, 42]

# Sum
data.sum              # => 108

# Mean
mean = data.sum.to_f / data.size   # => 18.0

# Minimum and maximum
data.min              # => 4
data.max              # => 42
data.minmax           # => [4, 42]

# Median
def median(arr)
  sorted = arr.sort
  mid = sorted.size / 2
  sorted.size.odd? ? sorted[mid] : (sorted[mid-1] + sorted[mid]) / 2.0
end

median(data)          # => 15.5

# Variance and standard deviation
def standard_deviation(arr)
  mean = arr.sum.to_f / arr.size
  variance = arr.sum { |x| (x - mean) ** 2 } / arr.size
  Math.sqrt(variance)
end

standard_deviation(data).round(4)   # => 12.2985

# Percentiles
def percentile(arr, p)
  sorted = arr.sort
  index = (p / 100.0) * (sorted.size - 1)
  lower = sorted[index.floor]
  upper = sorted[index.ceil]
  lower + (upper - lower) * (index - index.floor)
end

percentile(data, 75).round(2)   # => 23.75  (the 75th percentile)

When to Switch to a Different Approach #

Keep using built-in Math / Numeric when:
  ✓ Standard trigonometry, logarithms, roots
  ✓ Common integer and float arithmetic
  ✓ Rounding and numeric type conversion
  ✓ Non-cryptographic random numbers
  ✓ Basic statistics (mean, median, standard deviation)

Consider other approaches when:
  ✗ Financial/tax calculations — use BigDecimal (always!)
  ✗ Random numbers for security — use SecureRandom
  ✗ Linear algebra / matrices — use the numo-narray gem or the matrix stdlib
  ✗ Complex statistics — use the distribution or statsample gems
  ✗ Symbolic computation — use the symengine gem or manual Rational
  ✗ Large-scale numerical computation — consider integrating with Python/SciPy

Summary #

  • Math needs no require — immediately available, all methods return Float, all trig arguments are in radians.
  • Converting degrees to radians is always required before sin/cos/tan — use angle * Math::PI / 180; skipping this conversion is the most common geometry computing error.
  • atan2(y, x) is better than atan(y/x) — handles all quadrants correctly and never divides by zero.
  • Floats aren’t suitable for money0.1 + 0.2 != 0.3 isn’t a Ruby bug but IEEE 754’s nature; use BigDecimal or an integer representation (cents) for financial calculations.
  • BigDecimal must be initialized from a StringBigDecimal("0.1") is correct, BigDecimal(0.1) is wrong because the Float is already imprecise by the time it reaches the constructor.
  • SecureRandom for tokens, Random for simulation — don’t use rand for cryptographic or security needs.
  • Integer#gcd and Integer#lcm are built-in — no manual implementation needed; available directly and efficient.
  • Math.sqrt(-n) produces NaN, not an exception — always check for negative input before calling sqrt, or check the result with .nan?.
  • Rounding has four different modesround, ceil, floor, and truncate behave differently for negative numbers; choose per business rules, not by default.
  • require 'prime' for prime numbers — available in Ruby’s stdlib, no external gem needed for basic prime operations.

← Previous: IO   Next: Enumerable →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact