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 / Method | Value | Description |
|---|---|---|
Math::PI | 3.14159265358979… | π — the ratio of a circle’s circumference to its diameter |
Math::E | 2.71828182845904… | Euler’s number — the base of natural logarithms |
Float::INFINITY | ∞ | Positive infinity |
Float::NAN | NaN | Not a Number — the result of an undefined operation |
Float::EPSILON | 2.22e-16 | The smallest difference between two distinct Floats |
Float::DIG | 15 | The 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 --> D27 ** (1.0/3)should produce exactly3.0, but because1.0/3is0.3333...in floating-point, the result can be2.9999999999999996in 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 --> GMath.log(0)produces-Infinity, andMath.log(-1)producesNaN. 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
| Method | 3.7 | -3.7 | Principle |
|---|---|---|---|
round | 4 | -4 | To nearest, halves away from zero |
ceil | 4 | -3 | Always up (→ +∞) |
floor | 3 | -4 | Always down (→ -∞) |
truncate | 3 | -3 | Always 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 #
Mathneeds norequire— immediately available, all methods returnFloat, all trig arguments are in radians.- Converting degrees to radians is always required before
sin/cos/tan— useangle * Math::PI / 180; skipping this conversion is the most common geometry computing error.atan2(y, x)is better thanatan(y/x)— handles all quadrants correctly and never divides by zero.- Floats aren’t suitable for money —
0.1 + 0.2 != 0.3isn’t a Ruby bug but IEEE 754’s nature; useBigDecimalor an integer representation (cents) for financial calculations.BigDecimalmust be initialized from a String —BigDecimal("0.1")is correct,BigDecimal(0.1)is wrong because the Float is already imprecise by the time it reaches the constructor.SecureRandomfor tokens,Randomfor simulation — don’t userandfor cryptographic or security needs.Integer#gcdandInteger#lcmare built-in — no manual implementation needed; available directly and efficient.Math.sqrt(-n)producesNaN, not an exception — always check for negative input before callingsqrt, or check the result with.nan?.- Rounding has four different modes —
round,ceil,floor, andtruncatebehave 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.