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.

classDiagram Comparable <|.. Numeric Numeric <|-- Integer Numeric <|-- Float Numeric <|-- Rational Numeric <|-- Complex Numeric <|-- BigDecimal class Numeric { +coerce(other) +step(limit, step) +integer?() } class Integer { arbitrary precision +pow(exp, mod) +digits(base) } class Float { IEEE 754 binary64 +round(ndigits, half) +nan?() } class Rational { exact numerator/denominator } class Complex { real + imaginary } class BigDecimal { bundled gem decimal floating point }
Class Literal Exact? Typical use

Integer

42, 1_000_000, 0b1010, 0o17, 0xff

Yes, any size.

Counters, indexes, sizes, identifiers, money in the smallest unit (cents).

Float

3.14, 1e3, 1.0e-5

No: binary64, about 15 to 17 significant digits.

Measurements, ratios, anything where small relative error is fine.

Rational

3r, 0.75r, 1/3r

Yes.

Exact fractions, money with arbitrary fractional parts, unit conversions.

Complex

2i, 3 + 4i, Complex(3, 4)

Depends on the parts (they can be Integer, Rational or Float).

Signal processing, 2-D geometry, roots of negative numbers.

BigDecimal

BigDecimal("0.1") (no literal)

Exact decimal up to a chosen precision.

Decimal arithmetic with rounding modes, database DECIMAL columns.

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

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

/, div

Floor division, result is an Integer.

-4

%, modulo

Floor modulo, sign of the divisor.

1

remainder

Truncating remainder, sign of the dividend.

-1

divmod

[div, modulo].

[-4, 1]

fdiv

Division that returns a Float, correct even for huge integers.

-3.5

quo

Exact division: a Rational for two integers.

(-7/2)

ceildiv (3.2)

Ceiling division, useful for page counts and batching.

-3

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

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

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

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

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

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

sqrt, cbrt, hypot(a, b), exp

Logarithms

log(x), log(x, base), log2, log10, and new in 4.0 log1p, expm1

Trigonometry

sin, cos, tan, asin, acos, atan, atan2(y, x)

Hyperbolic

sinh, cosh, tanh, asinh, acosh, atanh

Special

erf, erfc, gamma, lgamma

# 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

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

Random.new(seed)

A generator with its own state; seed reads the seed back. Without an argument the seed is random.

rng.rand, rand(n), rand(a..b)

A Float in [0, 1); an Integer in [0, n); a value in the range (an Integer for integer bounds, a Float for float bounds). rand(0) on a Random instance raises ArgumentError.

rng.bytes(n)

A binary String of n pseudo-random bytes.

Random.rand, Random.bytes, Random.seed

The same on a default process-wide generator.

Kernel#rand, Kernel#srand

Older global interface: srand(n) seeds the default generator, rand(0) returns a float.

Random.new_seed

A new random Integer seed from the system’s entropy source.

Random.urandom(n)

n bytes from the operating system’s secure source.

Array#shuffle(random:), Array#sample(random:)

Take a Random instance to make a shuffle reproducible.

# 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

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

Pitfalls

Pitfall What happens Instead

Float money

0.1 + 0.2 != 0.3; totals drift by a cent.

Integer cents, Rational or BigDecimal; round once at the end.

a / b on integers

Floor division: 1 / 2 is 0, -7 / 2 is -4.

fdiv, quo, to_r, or divmod when both parts matter.

== on floats

False after arithmetic.

A tolerance helper, assert_in_delta in tests.

Rational(0.1)

Captures the binary error.

Rational("0.1") or 0.1r.

Float#round for money

Binary input, ambiguous halves.

Round a Rational or BigDecimal with an explicit half:.

rand for tokens

Predictable Mersenne Twister.

SecureRandom.

Math.sqrt on big integers

Converts to Float, loses digits, Infinity above 10308.

Integer.sqrt.

Integer(string) vs to_i

to_i hides bad input as 0.

Integer(string, exception: false) and check for nil.

Hash keys 1 and 1.0

Different keys: eql? and hash differ.

Normalise the key type.

NaN in sorts

Comparisons with NaN return nil or false; sort raises ArgumentError.

Reject nan? values first.

Fixnum, Bignum

NameError since 3.2.

Integer.

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 <' for . Material that predates `Integer.sqrt (2.5), Integer#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

Fixnum and Bignum unified under Integer and deprecated; Integer#digits.

2.5

Integer.sqrt.

3.2

Fixnum and Bignum constants removed; Integer#ceildiv; Random::DEFAULT removed.

3.3

SecureRandom.uuid_v7; error messages say "for an instance of".

3.4

Integer#** and Rational#** return exact results (or raise) instead of Infinity; Float("1.") accepted; bigdecimal becomes a bundled gem; it block parameter.

4.0

Math.log1p and Math.expm1; bigdecimal 4.0.1.

4.1 (not released)

Integer#bit_count on the development branch, mentioned here only as upcoming.