Numbers
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Ruby 4.0 (4.0.7, checked on 2026-10-18); features newer than Ruby 3.0 name the version that introduced them.
Ruby has five kinds of built-in number, all subclasses of Numeric: Integer, Float, Rational, Complex, and (as
a bundled gem rather than core) BigDecimal. The two you use daily are Integer, which never overflows, and Float,
which is an IEEE 754 binary64 double and therefore cannot represent 0.1 exactly. Almost every number bug in a Ruby
program is one of two things: a Float used where an exact decimal was meant (money, percentages, counters that are
compared with ==), or an integer division that silently rounds toward negative infinity. This page explains the rules
behind both, shows the exact outputs, and then covers Rational, Complex, Math, Random and SecureRandom.
How number literals are written (1_000, 0b1010, 3r, 2i, 1e3) is in
Literals; the operators and their precedence (** binds tighter
than unary minus on a literal, % is modulo, not remainder) are in
Operators and Precedence. The conversion functions
Integer(), Float() and Rational() and the coerce protocol are introduced in
Conversions and Coercion, and the difference between ==,
eql? and equal? for numbers is in
Equality, Hashing and Comparison. The format
directives used below are covered in depth in Strings, ranges of numbers in
Ranges, and sum, minmax and friends in
Enumerable.
The numeric tower
All numbers inherit from Numeric, which mixes in Comparable. Numeric defines the behaviour that is the same for
every kind of number (abs, zero?, positive?, step, integer?, coerce, divmod, clamp through Comparable),
and each subclass supplies the arithmetic.
| Class | Literal | Exact? | Typical use |
|---|---|---|---|
|
|
Yes, any size. |
Counters, indexes, sizes, identifiers, money in the smallest unit (cents). |
|
|
No: binary64, about 15 to 17 significant digits. |
Measurements, ratios, anything where small relative error is fine. |
|
|
Yes. |
Exact fractions, money with arbitrary fractional parts, unit conversions. |
|
|
Depends on the parts (they can be Integer, Rational or Float). |
Signal processing, 2-D geometry, roots of negative numbers. |
|
|
Exact decimal up to a chosen precision. |
Decimal arithmetic with rounding modes, database |
Source: Ruby 4.0 documentation: Numeric
Integer
One class, any size
Since Ruby 3.2 there is exactly one integer class. Before that, small values were Fixnum and large ones Bignum,
both subclasses of Integer; the constants were deprecated in 2.4 and removed in 3.2, so Fixnum is now an
uninitialized constant error. Nothing in the language distinguishes the two representations any more: an Integer
that fits in a machine word is stored inline, one that does not is stored as a heap-allocated big number, and the
switch is invisible to the program. There is no overflow, no wrap-around and no Integer::MAX.
# frozen_string_literal: true
p 42.class # => Integer
p (2**64).class # => Integer
p 2**64 # => 18446744073709551616
p (2**200).bit_length # => 201
p 1_000_000 # => 1000000
p [0b1010, 0o17, 0xff] # => [10, 15, 255]
p Object.const_defined?(:Fixnum) # => false
begin
Object.const_get(:Fixnum)
rescue NameError => e
puts e.message # => uninitialized constant Fixnum
end
Source: Ruby 4.0 documentation: Integer
Arbitrary precision has a cost that fixed-width languages do not pay: an operation on huge numbers takes time and
memory proportional to their size, and nothing stops a program from asking for a number with billions of digits. Ruby
3.4 made one such case safer. Integer#** used to return Float::INFINITY when the result was very large;
since 3.4 it returns the exact Integer, and raises an exception when the result would be unreasonably large.
Rational#** changed in the same way. Treat an exponent that comes from user input as untrusted.
Division and modulo
Integer#/ is floor division: it rounds the quotient toward negative infinity, not toward zero as C and Java do.
% is the matching modulo, whose result has the sign of the divisor, so a == (a / b) * b + a % b always holds.
remainder is the truncating counterpart whose result has the sign of the dividend. divmod returns both the floor
quotient and the modulo in one call. Division by an Integer zero raises ZeroDivisionError; division of a Float
by zero does not (see Float).
# frozen_string_literal: true
p 7 / 2 # => 3
p(-7 / 2) # => -4
p(-7 % 3) # => 2
p(-7.remainder(3)) # => -1
p 7.divmod(-2) # => [-4, -1]
p 7.fdiv(2) # => 3.5
p 7.ceildiv(2) # => 4
p 7.quo(2) # => (7/2)
begin
7 / 0
rescue ZeroDivisionError => e
puts "#{e.class}: #{e.message}" # => ZeroDivisionError: divided by 0
end
Source: Integer#divmod
| Method | Meaning | -7 and 2 |
|---|---|---|
|
Floor division, result is an |
|
|
Floor modulo, sign of the divisor. |
|
|
Truncating remainder, sign of the dividend. |
|
|
|
|
|
Division that returns a |
|
|
Exact division: a |
|
|
Ceiling division, useful for page counts and batching. |
|
Writing a / b where you meant a fractional result is the most common integer bug: average = total / count is
0 when total is smaller than count. Use fdiv or convert one operand with to_f or to_r.
Exponentiation, roots and modular arithmetic
Integer#** with a negative exponent returns a Rational, not a Float. Integer#pow(exponent, modulus) computes
(self ** exponent) % modulus without ever building the huge intermediate number, which is what makes modular
exponentiation (RSA, Diffie-Hellman, hash functions) feasible. Integer.sqrt(n) is the exact floor of the square
root of an integer of any size; Math.sqrt converts to Float first and loses the low digits of a big number.
# frozen_string_literal: true
p 2**-1 # => (1/2)
p 10.pow(3) # => 1000
p 3.pow(200, 13) # => 9
p Integer.sqrt(99) # => 9
p Math.sqrt(10**40) # => 1.0e+20
p Integer.sqrt(10**40) # => 100000000000000000000
p 10.gcd(4) # => 2
p 10.lcm(4) # => 20
begin
Integer.sqrt(-1)
rescue Math::DomainError => e
puts e.message # => Numerical argument is out of domain - "isqrt"
end
Source: Integer#pow
Digits, bases and bits
digits(base = 10) returns the digits least-significant first, which is the order you want for most algorithms
(check digits, carries) and the reverse of how a number is written. to_s(base) and String#to_i(base) convert to and
from text in any base from 2 to 36. The bit operators &, |, ^, ~, <<, >> work on negative numbers as
infinite two’s complement, and n[i] is bit i. bit_length is the number of bits needed excluding the sign.
# frozen_string_literal: true
p 1234.digits # => [4, 3, 2, 1]
p 255.digits(16) # => [15, 15]
p 255.to_s(2) # => "11111111"
p "ff".to_i(16) # => 255
p 6.bit_length # => 3
p 5[0] # => 1
p [5 & 3, 5 | 3, 5 ^ 3, ~5] # => [1, 7, 6, -6]
p 1 << 70 # => 1180591620717411303424
p 97.chr # => "a"
p 255.chr # => "\xFF"
Source: Integer#digits
Integer#digits raises Math::DomainError for a negative receiver and ArgumentError for a base below 2. A related
method, Integer#bit_count, is on the master branch for Ruby 4.1 and is not available in 4.0.
Rounding integers
round, floor, ceil and truncate accept a negative number of digits to round to tens, hundreds and so on. For
integers round takes the half: keyword :up (the default), :even or :down, which only matters when the number is
exactly halfway.
# frozen_string_literal: true
p 15.round(-1) # => 20
p 25.round(-1, half: :even) # => 20
p 35.round(-1, half: :even) # => 40
p 25.round(-1, half: :down) # => 20
p 1234.floor(-2) # => 1200
p 1234.ceil(-2) # => 1300
p 5.clamp(1, 3) # => 3
p 5.clamp(..3) # => 3
Source: Integer#round
Predicates and comparison
zero?, positive?, negative?, even?, odd?, integer?, finite? and between?(min, max) are the readable
spellings of comparisons. succ and pred return the next and previous integer, and Integer === x is the test
used by case/when. Comparison across kinds works: 3 == 3.0 is true and 3 <=> 3.5 is -1; comparing with
something that is not a number returns nil from <=> and raises ArgumentError from <. A number and its
Float twin are == but not eql?, and they hash differently, so they are different Hash keys.
# frozen_string_literal: true
# rubocop:disable Lint/FloatComparison,Style/CaseEquality
p 3 == 3.0 # => true
p 3.eql?(3.0) # => false
p 3.hash == 3.0.hash # => false
p({ 3 => :a }[3.0]) # => nil
p 3 <=> "a" # => nil
p Integer === 2**70 # => true
p Integer.ancestors.take(4) # => [Integer, Numeric, Comparable, Object]
# rubocop:enable Lint/FloatComparison,Style/CaseEquality
Counting and iterating: times, upto, downto and step
The loop helpers on Integer are plain methods that take a block (or return an Enumerator without one), which is
how Ruby does counting loops instead of a C-style for. step is defined on Numeric, so it also works on floats and
rationals. Without a block, step and Range#step/Range#% return an Enumerator::ArithmeticSequence, a lazy
object that knows its first, last, size and sum without iterating.
# frozen_string_literal: true
3.times { |i| print i, " " } # prints: 0 1 2
puts
1.upto(4) { |i| print i, " " } # prints: 1 2 3 4
puts
4.downto(1) { |i| print i, " " } # prints: 4 3 2 1
puts
10.step(1, -4) { |i| print i, " " } # prints: 10 6 2
puts
p(3.times.map { it * 2 }) # => [0, 2, 4]
p 1.step(10, 4).to_a # => [1, 5, 9]
p 1.step(by: 2, to: 9).to_a # => [1, 3, 5, 7, 9]
p 1.step(by: 2).first(3) # => [1, 3, 5]
p 1.step(10, 4) # => (1.step(10, 4))
p 1.step(10, 4).class # => Enumerator::ArithmeticSequence
p (1..10) % 3 # => ((1..10).%(3))
p 1.0.step(2.0, 0.5).to_a # => [1.0, 1.5, 2.0]
p 0.1.step(0.5, 0.1).to_a # => [0.1, 0.2, 0.30000000000000004, 0.4, 0.5]
Source: Numeric#step
step on floats computes each value as start + n * step (not by repeated addition), so the error does not
accumulate and the number of elements is exact, but individual elements still carry representation noise
(0.30000000000000004 above). A step of 0 raises ArgumentError in the positional form. Counting with it
(Ruby 3.4) instead of a named block parameter is covered in
Blocks, Procs and Lambdas.
Float
What a Float is
A Float is an IEEE 754 binary64 value: 1 sign bit, 11 exponent bits and 53 significant bits (Float::MANT_DIG).
That gives 15 reliable decimal digits (Float::DIG) and a range up to Float::MAX (about 1.8e308) with a smallest
positive normal value Float::MIN (about 2.2e-308). Most decimal fractions have no finite binary expansion,
so the stored number is the nearest representable one: 0.1 is really 0.1000000000000000055511151231257827....
inspect and to_s print the shortest decimal string that reads back as the same double, which is why the
noise appears only after some arithmetic.
# frozen_string_literal: true
p 0.1 + 0.2 # => 0.30000000000000004
p 0.1 * 3 # => 0.30000000000000004
p 1.1 - 1.0 # => 0.10000000000000009
p Float::EPSILON # => 2.220446049250313e-16
p [Float::DIG, Float::MANT_DIG, Float::RADIX] # => [15, 53, 2]
p Float::MAX # => 1.7976931348623157e+308
p Float::MIN # => 2.2250738585072014e-308
p 0.1.next_float # => 0.10000000000000002
p 1.0.prev_float # => 0.9999999999999999
p 3.14.to_r # => (7070651414971679/2251799813685248)
p 1e3 # => 1000.0
p 1.0e-5 # => 1.0e-05
p 1e16 # => 1.0e+16
p 12_345_678_901_234_567.0 # => 1.2345678901234568e+16
Source: Ruby 4.0 documentation: Float
to_s switches to exponent notation for values of 1016 and above and for values below 0.0001. Float#to_r returns
the exact value of the double, while rationalize returns the simplest fraction within a tolerance.
Comparing floats
Never test floats for equality after arithmetic. Compare with a tolerance: an absolute epsilon for numbers near
zero, a relative one for large numbers. Float::EPSILON is the gap between 1.0 and the next double, so it is only
a sensible absolute tolerance for values of magnitude about one. The standard library’s Comparable and
Numeric do not provide an approx_equal?, so you write the helper (or use Minitest’s assert_in_delta, see
Testing with Minitest).
# frozen_string_literal: true
# rubocop:disable Lint/FloatComparison
def close?(left, right, rel: 1e-9, abs: 1e-12)
(left - right).abs <= [rel * [left.abs, right.abs].max, abs].max
end
p 0.1 + 0.2 == 0.3 # => false
p close?(0.1 + 0.2, 0.3) # => true
p close?(1e10 + 1, 1e10, rel: 1e-12) # => false
p close?(1e10 + 1, 1e10) # => true
p [0.1, 0.2, 0.3].sum # => 0.6
p [0.1, 0.2, 0.3].inject(:+) # => 0.6000000000000001
# rubocop:enable Lint/FloatComparison
Source: Float::EPSILON
Array#sum and Enumerable#sum use the Kahan-Babuska compensated summation algorithm when they meet floats, which
is why [0.1, 0.2, 0.3].sum is exactly 0.6 while inject(:+) is not. Prefer sum for totals of floats. It does not
make floating point exact: it only reduces the accumulated error.
NaN, infinity and division by zero
Float arithmetic never raises for the usual IEEE exceptions. Division by zero gives Infinity or -Infinity
(and 0.0 / 0 gives NaN), overflow gives Infinity, and NaN propagates through every operation and compares
unequal to everything, itself included. Use nan?, infinite? (returns 1, -1 or nil) and finite? to
test. Converting a non-finite float to an Integer raises FloatDomainError. Integer division by zero is the only case
that raises ZeroDivisionError.
# frozen_string_literal: true
# rubocop:disable Lint/BinaryOperatorWithIdenticalOperands
p 1 / 0.0 # => Infinity
p(-1 / 0.0) # => -Infinity
p 0.0 / 0 # => NaN
p Float::NAN == Float::NAN # => false
p Float::NAN.nan? # => true
p Float::INFINITY.infinite? # => 1
p 5.0.finite? # => true
p 1e308 * 10 # => Infinity
p 0.0 == -0.0 # => true
p(-0.0) # => -0.0
begin
Float::NAN.to_i
rescue FloatDomainError => e
puts "#{e.class}: #{e.message}" # => FloatDomainError: NaN
end
# rubocop:enable Lint/BinaryOperatorWithIdenticalOperands
Source: FloatDomainError
Rounding floats
round, floor, ceil and truncate return an Integer when called with no digits argument and a Float
when given ndigits (a negative ndigits rounds an integer part and returns an Integer). round rounds halves
away from zero by default. The half: keyword selects another rule: :up (away from zero, the default), :even
(banker’s rounding, which avoids a systematic upward bias when rounding many values) or :down (toward zero).
# frozen_string_literal: true
p [2.5.round, 3.5.round, -2.5.round] # => [3, 4, -3]
p [2.5.round(half: :even), 3.5.round(half: :even)] # => [2, 4]
p 2.5.round(half: :down) # => 2
p 1.2345.round(2) # => 1.23
p 1234.5678.round(-2) # => 1200
p [1.5.floor, -1.5.floor, -1.5.ceil, -1.5.truncate] # => [1, -2, -1, -1]
p 1.23456.floor(3) # => 1.234
p 1.23456.ceil(3) # => 1.235
p 2.675.round(2) # => 2.68
p 100.0.to_s # => "100.0"
Source: Float#round
Rounding a binary float to decimal places answers "what is the nearest decimal" for a number that is itself only
approximately that decimal, and Ruby’s round corrects for the representation error in the common cases (the
double nearest 2.675 is slightly below it, yet 2.675.round(2) is 2.68). Do not depend on it for money: store
amounts as integer cents or Rational, and round once at the end, with an explicit half: rule.
Division and modulo on floats
Float#%, divmod and modulo follow the same floor rules as for integers and return floats
(7.5.divmod(-2) is [-4, -0.5]), and fdiv and quo give a float and a rational. Conversion helpers: to_i
truncates, to_r is exact, rationalize(eps) is the simplest fraction within eps.
# frozen_string_literal: true
# rubocop:disable Lint/FloatComparison
p 7.5 % 2 # => 1.5
p 7.5.divmod(-2) # => [-4, -0.5]
p 3.14.rationalize(Rational(1, 100)) # => (22/7)
p 3.7.to_i # => 3
p 5.0.integer? # => false
p 1.0.eql?(1) # => false
# rubocop:enable Lint/FloatComparison
Source: Float#rationalize
Rational
A Rational is an exact fraction of two integers, kept in lowest terms with a positive denominator. The literal
suffix r (3r, 0.75r) builds one, and 0.1r is exactly one tenth, unlike the float 0.1. Rational(a, b)
and Rational("1/3") or Rational("0.75") parse; Rational(0.1) shows the exact binary value of the double.
Arithmetic between two rationals, or a rational and an integer, stays exact; mixing in a Float returns a Float.
Integer#quo gives an exact quotient. Rational supports floor, ceil, round (with half:), truncate,
numerator, denominator, to_f, and Rational(1, 0) raises ZeroDivisionError.
# frozen_string_literal: true
p 3r # => (3/1)
p 1/3r # => (1/3)
p Rational(1, 3) + Rational(1, 6) # => (1/2)
p Rational("0.75") # => (3/4)
p Rational(0.1) # => (3602879701896397/36028797018963968)
p "0.1".to_r # => (1/10)
p 0.1r + 0.2r == 0.3r # => true
p Rational(7, 2).floor # => 3
p Rational(7, 2).round # => 4
p Rational(5, 2).round(half: :even) # => 2
p Rational(22, 7).round(2) # => (157/50)
p Rational(22, 7).to_f # => 3.142857142857143
p Rational(1, 2) + 0.5 # => 1.0
p Rational(1, 2) + 1 # => (3/2)
p Rational(1, 3).to_s # => "1/3"
p Rational(1, 3).inspect # => "(1/3)"
begin
Rational(1, 0)
rescue ZeroDivisionError => e
puts "#{e.class}: #{e.message}" # => ZeroDivisionError: divided by 0
end
Source: Ruby 4.0 documentation: Rational
Exact money in the shelf
The shelf gem sells books and charges late fees, which is money. Two sound options exist: integers in the smallest
unit (cents), which is fast and what databases and payment APIs use, and Rational when fractions of a unit
(a 15% discount on a price in cents) must be exact until the final rounding. The Price class below keeps a
Rational, compares by value through Comparable, converts a plain number on the left of an operator through
coerce, and rounds only when it is printed.
# frozen_string_literal: true
module Shelf
# A price in a currency, kept as an exact Rational number of units.
class Price
include Comparable
attr_reader :amount
def initialize(amount)
@amount = Rational(amount)
freeze
end
def +(other) = self.class.new(amount + other.amount)
def *(other) = self.class.new(amount * other)
def <=>(other)
amount <=> other.amount if other.is_a?(self.class)
end
def coerce(number) = [self.class.new(number), self]
def to_s = format("%.2f", amount.round(2))
def inspect = "#<Shelf::Price #{self}>"
end
end
price = Shelf::Price.new("19.99")
p price # => #<Shelf::Price 19.99>
p price * 3 # => #<Shelf::Price 59.97>
p price * Rational(1, 3) # => #<Shelf::Price 6.66>
p price + Shelf::Price.new(5) # => #<Shelf::Price 24.99>
p 2 + price # => #<Shelf::Price 21.99>
p [Shelf::Price.new(3), Shelf::Price.new("2.5")].max # => #<Shelf::Price 3.00>
p Shelf::Price.new(1) < Shelf::Price.new(2) # => true
p Shelf::Price.new(0.1).amount # => (3602879701896397/36028797018963968)
p Shelf::Price.new("0.1").amount # => (1/10)
p ([Shelf::Price.new("0.10")] * 3).sum(Shelf::Price.new(0)) # => #<Shelf::Price 0.30>
Source: Numeric#coerce
Price.new(0.1) shows the trap: building a Rational from a Float captures the double’s binary error, so
parse amounts from strings ("0.1", 0.1r) or integers. coerce must return a pair [a, b] of objects the operator can
combine; here 2 + price calls price.coerce(2) and then Price.new(2) + price. The other direction of the
protocol, and the exact TypeError messages when coerce is missing, are in
Conversions and Coercion.
BigDecimal
BigDecimal is decimal floating point with arbitrary precision, in the style of SQL DECIMAL and Java’s
BigDecimal. It is not part of the core: it was a default gem until Ruby 3.3, became a bundled gem in Ruby 3.4 (installed
with Ruby, but no longer loadable under Bundler unless the Gemfile lists it) and is 4.0.1 in Ruby 4.0. In a
Bundler project add gem "bigdecimal"; without Bundler a plain require "bigdecimal" works. Rails and database
adapters depend on it already, so it is usually in the lockfile.
# frozen_string_literal: true
require "bigdecimal"
sum = BigDecimal("0.1") + BigDecimal("0.2")
p sum # => 0.3e0
p sum == BigDecimal("0.3") # => true
p sum.to_s("F") # => "0.3"
p BigDecimal("1") / 3 # => 0.33333333333333333333333333333333e0
p BigDecimal("10.25").round(1, half: :even).to_s("F") # => "10.2"
Source: BigDecimal
Always build a BigDecimal from a string or an integer: BigDecimal(0.1) needs an explicit precision argument and
captures the float’s error. Choose it when you need decimal rounding modes and a fixed number of decimal places
(accounting), and Rational when you need exact fractions without a decimal point (ratios, unit conversion).
The number of digits printed for BigDecimal("1") / 3 depends on the gem version and is not a stable contract.
Complex
Complex is a number with a real and an imaginary part, each of which may be an Integer, Rational or Float.
Literals: 2i is Complex(0, 2), and 3 + 4i builds (3+4i). Complex("1+2i") parses; Complex.polar(r, theta) builds from
magnitude and angle. abs is the magnitude, arg the angle, polar both, rectangular the pair of parts, conjugate
the mirror image. Complex numbers are not ordered: < is not defined and raises NoMethodError, though == works
and Complex(3, 0) == 3 is true. Raising a negative real to a fractional power returns a complex.
# frozen_string_literal: true
z = Complex(3, 4)
p z # => (3+4i)
p 3 + 4i # => (3+4i)
p [z.real, z.imaginary] # => [3, 4]
p z.abs # => 5.0
p z.conjugate # => (3-4i)
p z * Complex(1, 2) # => (-5+10i)
p z**2 # => (-7+24i)
p z.polar # => [5.0, 0.9272952180016122]
p Complex("1+2i") # => (1+2i)
p Complex(3, 0) == 3 # => true
p Complex(-1, 0)**0.5 # => (0.0+1.0i)
p(Complex.polar(1, Math::PI / 2).rectangular.map { it.round(10) }) # => [0.0, 1]
begin
z < Complex(2, 3)
rescue NoMethodError => e
puts e.message # => undefined method '<' for an instance of Complex
end
Source: Ruby 4.0 documentation: Complex
The error message uses the quote style '<' and the phrase "for an instance of" introduced in Ruby 3.4 and 3.3
respectively; older Rubies quoted the method name with a backtick and an apostrophe and named the receiver by its value.
Math
Math is a module of functions: call them as Math.sqrt(x) or include Math for bare names. The constants are
Math::PI and Math::E. Every function converts its arguments with to_f semantics (an Integer, Rational
or Float is fine, a String or nil raises TypeError) and returns a Float. A call outside the function’s domain raises
Math::DomainError (a subclass of ArgumentError), not NaN, except for a few functions that return -Infinity
at a pole such as Math.log(0).
| Group | Functions |
|---|---|
Roots and powers |
|
Logarithms |
|
Trigonometry |
|
Hyperbolic |
|
Special |
|
# frozen_string_literal: true
p Math.sqrt(16) # => 4.0
p Math.cbrt(27) # => 3.0
p Math.hypot(3, 4) # => 5.0
p Math.sin(Math::PI / 2) # => 1.0
p Math.atan2(1, 1) # => 0.7853981633974483
p Math.log(8, 2) # => 3.0
p Math.log10(1000) # => 3.0
p Math.log(0) # => -Infinity
p Math.gamma(5) # => 24.0
p Math.lgamma(5) # => [3.1780538303479453, 1]
begin
Math.sqrt(-1)
rescue Math::DomainError => e
puts "#{e.class}: #{e.message}" # => Math::DomainError: Numerical argument is out of domain - sqrt
end
begin
Math.sqrt("4")
rescue TypeError => e
puts "#{e.class}: #{e.message}" # => TypeError: can't convert String into Float
end
Source: Ruby 4.0 documentation: Math
log1p and expm1 (Ruby 4.0)
Math.log1p(x) is log(1 + x) and Math.expm1(x) is exp(x) - 1, computed so that they stay accurate when x is
close to zero. The naive forms first compute 1 + x, which throws away most of the digits of a tiny x. They are
used in finance (small interest rates), statistics and numerical code. They were added in Ruby 4.0 (Feature #21527).
# frozen_string_literal: true
x = 1e-10
p Math.log1p(x) # => 9.999999999500001e-11
p Math.log(1 + x) # => 1.000000082690371e-10
p Math.expm1(x) # => 1.00000000005e-10
p Math.exp(x) - 1 # => 1.000000082740371e-10
Source: Math.log1p
The log(1 + x) result is wrong from the eighth digit; log1p is right to full precision.
Random and SecureRandom
Random is a Mersenne Twister (MT19937) pseudo-random generator. It is fast and reproducible: two generators created
with the same seed produce the same sequence, which is what tests and simulations want. It is not cryptographically
secure, so never use it for tokens, passwords or anything an attacker must not guess.
| API | Behaviour |
|---|---|
|
A generator with its own state; |
|
A |
|
A binary |
|
The same on a default process-wide generator. |
|
Older global interface: |
|
A new random |
|
|
|
Take a |
# frozen_string_literal: true
rng = Random.new(42)
p rng.seed # => 42
p rng.rand # => 0.3745401188473625
p rng.rand(10) # => 7
p rng.rand(1..6) # => 5
p rng.bytes(4).bytesize # => 4
first = Random.new(42)
second = Random.new(42)
p first.rand(100) == second.rand(100) # => true
p [1, 2, 3, 4, 5].shuffle(random: Random.new(1)) # => [3, 2, 5, 1, 4]
p [1, 2, 3, 4, 5].sample(2, random: Random.new(1)) # => [4, 1]
begin
rng.rand(0)
rescue ArgumentError => e
puts "#{e.class}: #{e.message}" # => ArgumentError: invalid argument - 0
end
Source: Ruby 4.0 documentation: Random
The sequence for a given seed is stable across platforms for one major version of Ruby but is not guaranteed forever;
do not store expected random numbers in long-lived fixtures, store the properties you assert. Random::DEFAULT was
removed in Ruby 3.2: use Random.new or the Random.rand class methods.
SecureRandom
SecureRandom (the securerandom default gem, require "securerandom") draws from the operating system’s
cryptographically secure source (getrandom, /dev/urandom, CryptGenRandom). Use it for session tokens, API keys, password reset links,
salts and nonces. It also provides uuid, uuid_v4 and uuid_v7 (time-ordered, Ruby 3.3) and the formatters of Random::Formatter.
# frozen_string_literal: true
require "securerandom"
p SecureRandom.hex(8).size # => 16
p SecureRandom.uuid.size # => 36
p SecureRandom.uuid_v7.size # => 36
p SecureRandom.alphanumeric(12).size # => 12
p SecureRandom.base64(6).class # => String
p SecureRandom.urlsafe_base64(6).class # => String
p SecureRandom.random_bytes(4).bytesize # => 4
p SecureRandom.random_number(10).between?(0, 9) # => true
p SecureRandom.rand.class # => Float
Source: SecureRandom
SecureRandom is slower than Random and cannot be seeded, so it is the wrong tool for reproducible tests. The
Random::Formatter methods (hex, base64, alphanumeric, uuid) are also available on a Random instance after
require "random/formatter", with the same non-secure caveat. The security guidance for choosing between them
is in Security.
Parsing, converting and formatting numbers
Integer("42") and Float("1.5") are the strict conversions: they raise ArgumentError for text that is not entirely a
number and TypeError for nil, and accept exception: false to return nil instead. String#to_i and to_f are
lenient and stop at the first character that does not fit ("12abc".to_i is 12, "abc".to_i is 0). A base can
be passed as the second argument. Since Ruby 3.4 Float("1.") and Float("1.E-1") are accepted.
# frozen_string_literal: true
p Integer("42") # => 42
p Integer("0x1f", 16) # => 31
p Integer("12", 8) # => 10
p Integer("z", exception: false) # => nil
p Integer(3.99) # => 3
p Float("1.5") # => 1.5
p Float("abc", exception: false) # => nil
p "1_000".to_i # => 1000
begin
Integer("12abc")
rescue ArgumentError => e
puts "#{e.class}: #{e.message}" # => ArgumentError: invalid value for Integer(): "12abc"
end
begin
Integer(nil)
rescue TypeError => e
puts "#{e.class}: #{e.message}" # => TypeError: can't convert nil into Integer
end
Source: Kernel#Integer
Formatting is done with format (alias sprintf) and String#%: %d truncates a float to an integer, %.2f
rounds to two decimals, %05.1f pads with zeros, %+d forces a sign, %x, %o, %b print in a base, %e uses
exponent notation. There is no thousands-separator directive; group with gsub or reverse.scan.
# frozen_string_literal: true
ratio = 3.14159
fee = 0.75
puts format("%.2f", fee) # => 0.75
puts format("%<ratio>05.1f|%<n>+d|%<hex>x", ratio: ratio, n: 5, hex: 255) # => 003.1|+5|ff
puts format("%d", ratio) # => 3
big = 1e20
puts format("%s", big) # => 1.0e+20
puts 1_234_567.to_s.reverse.scan(/\d{1,3}/).join(",").reverse # => 1,234,567
puts 1_234_567.to_s.gsub(/(\d)(?=(\d{3})+$)/, '\1,') # => 1,234,567
A worked example: late fees and check digits
The shelf computes a late fee in whole cents with Integer arithmetic only, clamps it to a maximum with
clamp, and validates ISBN-13 numbers with digits. The average rating and reading speed use fdiv and one
rounding at the end.
# frozen_string_literal: true
LATE_FEE_CENTS_PER_DAY = 25
MAX_FEE_CENTS = 1_000
def late_fee_cents(days_late)
(days_late * LATE_FEE_CENTS_PER_DAY).clamp(0, MAX_FEE_CENTS)
end
def pages_per_day(pages, days) = pages.fdiv(days).ceil(1)
def isbn13_check_digit(first_twelve)
sum = first_twelve.digits.reverse.each_with_index.sum { |d, i| i.even? ? d : d * 3 }
(10 - (sum % 10)) % 10
end
def average_rating(ratings)
return nil if ratings.empty?
(ratings.sum.to_f / ratings.size).round(1)
end
p late_fee_cents(-2) # => 0
p late_fee_cents(3) # => 75
p late_fee_cents(90) # => 1000
p pages_per_day(352, 21) # => 16.8
p isbn13_check_digit(978_030_640_615) # => 7
p average_rating([5, 4, 4, 3]) # => 4.0
p average_rating([]) # => nil
puts format("%.2f", late_fee_cents(3) / 100.0) # => 0.75
Source: Comparable#clamp
Pitfalls
| Pitfall | What happens | Instead |
|---|---|---|
Float money |
|
Integer cents, |
|
Floor division: |
|
|
False after arithmetic. |
A tolerance helper, |
|
Captures the binary error. |
|
|
Binary input, ambiguous halves. |
Round a |
|
Predictable Mersenne Twister. |
|
|
Converts to |
|
|
|
|
Hash keys |
Different keys: |
Normalise the key type. |
|
Comparisons with |
Reject |
|
|
|
Where older material is outdated
Books and articles written before Ruby 3.2 describe two integer classes, Fixnum and Bignum, and show
1.class printing Fixnum. In Ruby 4.0 it prints Integer and the constants do not exist. Code that tests
x.is_a?(Fixnum) or dispatches on Bignum must use Integer. Older text may also show Integer#**
returning Infinity for huge results (changed in 3.4), Float("1.") raising (accepted since 3.4), require "bigdecimal"
working under Bundler without a Gemfile entry (not since 3.4, where it became a bundled gem), Random::DEFAULT (removed in 3.2),
and an error message of the form undefined method (2.5), <' for . Material that predates `Integer.sqrtInteger#digits (2.4),
Integer#ceildiv (3.2) and Numeric#step with keyword arguments (2.6 and later) implements them by hand with
Math.sqrt, to_s.chars and manual loops; the built-ins are exact and faster.
Version notes
| Version | Change |
|---|---|
2.4 |
|
2.5 |
|
3.2 |
|
3.3 |
|
3.4 |
|
4.0 |
|
4.1 (not released) |
|
References
-
Math, Math::DomainError, ZeroDivisionError and FloatDomainError
-
Enumerator::ArithmeticSequence, Comparable and Kernel#format
-
IEEE 754 (floating-point arithmetic), RuboCop Lint cops and The Ruby Style Guide