Operators and Precedence
|
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), the baseline of the whole section; features newer than
Ruby 3.0 name the version that introduced them. Most of what looks like an operator in Ruby is an ordinary method call
with nicer syntax, and the rest is a small set of keyword operators that the parser handles itself. This page explains
that split, gives the complete precedence table and the associativity rules, and then walks through every operator
family: logical operators and truthiness, arithmetic, comparison and equality, pattern and match operators, bitwise and
shift operators, the operators that Array, String and Set give a collection meaning to, safe navigation, the ternary
operator, ranges and the flip-flop, defined?, the modifier forms, and the way blocks bind. It ends with the
compiler warnings and runtime errors that operators produce and with a table of what changed between Ruby 3.0 and 4.0.
It builds on Literals (numbers, strings, ranges and symbols as values) and
Assignment and Multiple Assignment (=, the
abbreviated forms such as ||=, and attribute assignment). It is continued by
Control Expressions (if, case and loops, which consume the
values these operators produce), Equality, Hashing and Comparison
(==, eql?, equal?, <=> and Comparable in depth), Conversions and Coercion
(coerce and implicit conversion) and Numbers (the numeric tower). For how other
languages treat the same ideas, see Rust (operators are trait methods there,
a close cousin of Ruby’s operator methods) and Go (no operator overloading).
Every example was checked with ruby -wc, run with Ruby 4.0.7 with warnings enabled (ruby -w), and linted with
RuboCop 1.92 using the configuration of the running example gem (Ruby 4.0 target, double-quoted strings); the output
shown is the real output. A few examples deliberately trigger RuboCop or interpreter warnings because the warning is the
subject; those are shown together with the warning text.
Operators are methods, keywords are not
In Ruby, 7 + 3 is a method call: the receiver is 7, the method is + and the argument is 3. The parser only
makes the call look like arithmetic. The three spellings below reach the same method, and the owner of the method is
the class of the left operand, which is why 3 * "ab" and "ab" * 3 are different calls.
# frozen_string_literal: true
# Operator syntax, an explicit method call and send all reach the same method.
p 7 + 3 # => 10
p 7.+(3) # => 10
p 7.send(:+, 3) # => 10
p 7.public_send(:-, 3) # => 4
p [1, 2, 3].<<(4) # => [1, 2, 3, 4]
p 5.method(:+).owner # => Integer
p 5.method(:+).arity # => 1
# The left operand decides which method runs.
p "ab" * 3 # => "ababab"
begin
3 * "ab"
rescue TypeError => e
puts "#{e.class}: #{e.message}"
end
begin
nil + 1
rescue NoMethodError => e
puts "#{e.class}: #{e.message}"
end
Source: Ruby 4.0 syntax: Operators and Integer#+
Two consequences follow. First, how an operator behaves is decided by the left operand, so operators are not
symmetric unless the class makes them so (coerce, covered in
Conversions and Coercion, is the protocol numbers use to
make 5 + fine work). Second, a class can define any of these operators for its own instances, and that is how
Set, Rational, Time and Pathname offer +, -, << and /.
Which operators are methods
| Group | Operators | Notes |
|---|---|---|
Unary |
|
|
Arithmetic |
|
|
Comparison |
|
|
Matching |
|
|
Bitwise and shift |
|
|
Element access |
|
|
Call |
|
|
Source: BasicObject#!, BasicObject#!= and Comparable
Defining them is done with a normal def. The method names for the unary operators carry an @ (-@, +@) to
tell them from the binary ones. A small Fine value object from the running example shows the common set:
# frozen_string_literal: true
module Shelf
# A late fee in cents, with the operators a small money type needs.
Fine = Data.define(:cents) do
include Comparable
def +(other) = with(cents: cents + other.cents)
def -(other) = with(cents: cents - other.cents)
def *(other) = with(cents: cents * other)
def -@ = with(cents: -cents)
def +@ = self
def <=>(other) = other.is_a?(Fine) ? cents <=> other.cents : nil
def !
cents.zero?
end
def coerce(number) = [Fine.new(number), self]
def to_s = format("$%.2f", cents / 100.0)
end
end
late = Shelf::Fine.new(150)
lost = Shelf::Fine.new(275)
puts late + lost # prints $4.25
puts lost - late # prints $1.25
puts late * 3 # prints $4.50
puts(-late) # prints $-1.50
puts(+late) # prints $1.50
puts 100 + late # prints $2.50
p late < lost # => true
p late.between?(late, lost) # => true
p [lost, late].min # => #<data Shelf::Fine cents=150>
p(!late) # => false
p(!Shelf::Fine.new(0)) # => true
p late == Shelf::Fine.new(150) # => true
begin
late < 5
rescue ArgumentError => e
puts "#{e.class}: #{e.message}"
end
Source: Comparable and Numeric#coerce
A few details are worth noting in that listing. 100 + late works only because Integer#+ does not know Fine, calls
late.coerce(100) and then retries the addition with the two values that coerce returned. late < 5 fails with an
ArgumentError because Comparable raises when <=> returns nil. And def ! is legal, but redefining ! to
mean something other than "negation" is almost always a mistake: not and ! then change meaning for every
conditional that receives the object.
Collection-like classes use the indexing, append, call and match operators. The Tray class below defines nine of them
and shows each call form, including tray.(2), &tray (through to_proc) and the === that case and when use.
# frozen_string_literal: true
module Shelf
Book = Data.define(:title, :year)
# A tray of books that overloads the collection-style operators.
class Tray
def initialize(*books) = @books = books
def <<(book)
@books << book
self
end
def [](index) = @books[index]
def []=(index, book)
@books[index] = book
end
def +(other) = Tray.new(*@books, *other.to_a)
def ~ = Tray.new(*@books.reverse)
def ===(other) = @books.include?(other)
def =~(pattern) = @books.index { |book| pattern.match?(book.title) }
def call(index) = self[index]
def to_a = @books.dup
def to_proc = method(:[]).to_proc
def size = @books.size
end
end
dune = Shelf::Book.new("Dune", 1965)
emma = Shelf::Book.new("Emma", 1815)
tray = Shelf::Tray.new(dune)
tray << emma << Shelf::Book.new("Ulysses", 1922)
p tray[1].title # => "Emma"
tray[0] = Shelf::Book.new("Dune Messiah", 1969)
p tray[0].title # => "Dune Messiah"
both = tray + Shelf::Tray.new(dune)
p both.size # => 4
p((~tray)[0].title) # => "Ulysses"
p(tray =~ /Ulysses/) # => 2
p tray.call(2).title # => "Ulysses"
p [0, 2].map(&tray).map(&:title) # => ["Dune Messiah", "Ulysses"]
case emma
when tray then puts "emma is on the tray"
end
empty = Shelf::Tray.new
p tray != empty # => true
p !tray # => false
Source: Ruby 4.0 syntax: Methods and Object#===
Which operators are keywords
Everything else is syntax and cannot be redefined. The Ruby documentation says it plainly: logical operators are not methods, they are tokenized at a lower level. Trying to define one is a syntax error, not a method that never gets called:
ruby -e 'def &&(o) = o'
-e: -e:1: syntax errors found (SyntaxError)
> 1 | def &&(o) = o
| ^~~ expected an `end` to close the `def` statement
| ^ expected a delimiter to close the parameters
| ^~ unexpected '&&', ignoring it
| ^~ unexpected '&&'; expected a method name
| ^ unexpected '='; target cannot be written
| ^ unexpected local variable or method, expecting end-of-input
The keyword operators are:
-
&&,||,and,orandnot(short-circuit logic;notis a keyword that calls!on its operand, so!can be redefined but the keyword itself cannot). -
? :(the ternary conditional),..and...(range construction; the range operators build aRangeand a literal range in a condition becomes a flip-flop, see below), anddefined?. -
=and the abbreviated assignments+=,-=,*=,||=,&&=and so on. An abbreviated assignment is rewritten toa = a op b, soa += bcalls+(a method you can define), whereas||=and&&=are keyword logic and are never methods. The exact expansions are in Assignment and Multiple Assignment. -
.,&.and::(method call, safe method call and scope resolution), which are punctuation, not operators with methods of their own. -
The modifier keywords
if,unless,while,untilandrescue.
!= and !~ sit in between: they are methods, but the default implementations are built from == and =~, so you
normally define == (or <=> with Comparable) and get != for free. Defining != yourself is rare. Since Ruby 3.2
Object no longer defines =~ at all (Kernel#=~ was removed), so Object.new =~ /x/ is a
NoMethodError, as the match-operator example below shows.
The precedence table
Precedence decides how an expression with several operators is grouped; associativity decides how operators of the same level group. The official table runs from highest to lowest:
| Level | Operators | Associativity | Notes |
|---|---|---|---|
0 (not in the table) |
|
left |
Method calls, safe calls, scope lookups and indexing bind tighter than every operator below: |
1 |
|
right |
|
2 |
|
right |
|
3 |
unary |
right |
Lower than |
4 |
|
left |
|
5 |
|
left |
|
6 |
|
left |
Lower than |
7 |
|
left |
|
8 |
|
left |
|
9 |
|
left |
Chaining is allowed by the grammar but useless ( |
10 |
|
none |
Non-associative: |
11 |
|
left |
|
12 |
|
left |
|
13 |
|
none |
|
14 |
|
right |
|
15 |
modifier |
left |
|
16 |
|
right |
|
17 |
|
Lower than assignment: |
|
18 |
|
right |
Lower than |
19 |
|
left |
Same level for both, unlike |
20 |
modifier |
left |
The lowest of the statement-level forms. |
21 |
|
The official table ends with brace blocks and adds that |
Source: Ruby 4.0 syntax: Precedence
The official page lists levels 1 to 21 only; the "level 0" row is the usual reading of the grammar and is verified by
the examples below (-negative.abs is -4 for negative = -4, so the call happened before the minus). The
documentation also notes that (a if b rescue c) parses as ((a if b) rescue c) for reasons unrelated to precedence,
and that all the words in the table that are not symbols are keywords. The grouping of each line below is explained by
the rows of the table:
# frozen_string_literal: true
stock = 5
# unary minus on a variable is lower than **, a minus sign glued to a literal is not
p(-2**2) # => -4
p((-2)**2) # => 4
base = 2
p(-base**2) # => -4
# a minus glued to a numeric literal is part of the literal, a minus before a name is an operator
negative = -4
p(-negative.abs) # => -4
p(-4.abs) # => 4
# ** is right associative; the other binary operators are left associative
p(2**3**2) # => 512
p((2**3)**2) # => 64
p(10 - 4 - 3) # => 3
p(2 + (3 * 4)) # => 14
# + and - bind tighter than <<, and & tighter than ==
p(1 << (2 + 1)) # => 8
p(4 & 5 == 4) # => true
p((4 & 5) == 4) # => true
p(1 | 2 ^ 3 & 4) # => 3
# ! is higher than &&, ranges are lower than +, ?: is lower than ranges
closed = true
p(!closed && false) # => false
p(1..stock + 1) # => 1..6
label = stock > 3 ? "big" : "small"
p label # => "big"
p(defined?(stock) && stock) # => 5
total = subtotal = 3
p [total, subtotal] # => [3, 3]
Source: Ruby 4.0 syntax: Precedence
Because the table is long and a few levels are surprising (<< lower than +, & higher than ==, .. lower than
||), the practical rules are:
-
Add parentheses whenever two operators from different families meet and the result is not obvious. RuboCop’s
Lint/AmbiguousOperatorPrecedenceflags exactly these places, andLint/AmbiguousRangeflags complex range ends. -
Never rely on the relative order of
&&/||andand/or; use one family per expression. -
Use
==for comparison and=for assignment; mixing them in a condition produces a warning (see the warnings section).
The figure reads from the tightest binding at the top to the loosest at the bottom: an operator lower in the figure is applied after the ones above it.
Truthiness
Every Ruby value is either falsy or truthy in a condition, and exactly two objects are falsy: nil and false.
Everything else is truthy, including 0, 0.0, the empty string, the empty array and the empty hash. This differs from
C, JavaScript and Python, where zero and empty containers are false; Ruby code that needs "is it empty" must ask
empty? or zero? explicitly.
# frozen_string_literal: true
[nil, false, true, 0, "", [], {}, 0.0, :a].each do |value|
puts "#{value.inspect.ljust(5)} is #{value ? 'truthy' : 'falsy'}"
end
value = 0
p !!value # => true
p !value # => false
p nil.to_a # => []
p nil & true # => false
p nil | 1 # => true
p nil.to_s # => ""
p NilClass.instance_method(:&).owner # => NilClass
Source: NilClass and Ruby 4.0 syntax: Control Expressions
The first block prints the truth table; the rest shows what the operators do with the two falsy objects. !!value (a
double negation) converts any value to true or false and is the usual way to coerce for a predicate that must
return a real boolean; RuboCop’s Style/DoubleNegation discourages it in favour of an explicit nil?/empty? test or a
comparison, which is why the example is the one line RuboCop reports in that file. nil also defines &, | and ^
(nil & true is false, nil | 1 is true), to_a ([]), to_s ("") and inspect ("nil"), which is why
nil often flows through code without an error until a method it does not define is called.
true, false and nil are the only instances of TrueClass, FalseClass and NilClass. Because they are
singletons, nil.equal?(nil) is always true, and comparisons against them are cheap.
Logical operators: &&, || and !
&& and || do not return true or false; they return one of their operands. a && b evaluates a, returns it
if it is falsy, and otherwise evaluates and returns b. a || b returns a if it is truthy and otherwise evaluates
and returns b. The right operand is evaluated only when the left one does not decide the result, which is called
short-circuit evaluation. The documentation puts it as "the last evaluated expression defines the result".
# frozen_string_literal: true
title = "Dune"
subtitle = nil
p(title && title.length) # => 4
p(subtitle && subtitle.length) # => nil
p(subtitle || "no subtitle") # => "no subtitle"
p(subtitle || false) # => false
# the result is the last operand evaluated, not true or false
total = 1 + 2
p(total || :never) # => 3
# the right side is not evaluated once the left side decides
calls = []
note = lambda do |tag, value|
calls << tag
value
end
first = note.call(:first, nil)
second = first && note.call(:second, 1)
p [first, second, calls] # => [nil, nil, [:first]]
used = note.call(:third, 5) || note.call(:fourth, 6)
p [used, calls] # => [5, [:first, :third]]
# & and | on booleans are ordinary methods: both operands are always evaluated
no = false
both = no & note.call(:fifth, true)
p [both, calls.last] # => [false, :fifth]
The same behaviour as a picture, for both operators:
This is what makes the common idioms work: title = cached || "Untitled" (default), user && user.name (guard, though
&. below is usually better), and File.exist?(path) or raise ... (control flow, below). Two warnings come with it:
-
Never rely on
&&returning a boolean.late.fine_centsin theLoanexample below returnsfalsefor an on-time loan and anIntegerfor a late one, which is surprising for a method that reads like a number. A predicate method (name ending in?) should returntrueorfalse; a method that computes a value should always return a value of the same kind. -
&and|on booleans are ordinary methods, so they always evaluate both operands. Use them only when the side effect of the right operand is wanted, as the last lines of the example show.
The ! operator is a method too (BasicObject#!), so !x is x.!. It is higher than every binary operator, so
!a == b is (!a) == b; write a != b or !(a == b) when you mean the other reading. not is its low-precedence
twin and is covered next.
and, or and not
and, or and not mean the same as &&, || and ! but sit at the bottom of the precedence table, below
assignment. They are not stylistic variants: they exist to chain statements, in the style of Perl’s open(...) or die,
and they change how an assignment is grouped.
# frozen_string_literal: true
cached = nil
yes = true
no = false
# && and || bind tighter than =, so the assignment receives the whole expression.
title = cached || "Untitled"
p title # => "Untitled"
# `or` is lower than =, so the assignment happens first and the `or` is discarded.
other = cached or "Untitled"
p other # => nil
found = yes && no
p found # => false
found = yes and no
p found # => true
# `not` is lower than ==, ! is higher.
one = 1
p(not one == 2) # => true
p((not one) == 2) # => false
p(!one == 2) # => false
Read the output line by line:
-
title = cached || "Untitled"assigns"Untitled", because||is above=. -
other = cached or "Untitled"is(other = cached) or "Untitled":othergetsnil, and theorbranch is evaluated and thrown away. This is the classic bug, and the reason most style guides forbidand/orin conditions and assignments. -
found = yes and nostorestruefor the same reason. -
not one == 2isnot (one == 2), so it istrue;(not one) == 2isfalse == 2;!one == 2is(!one) == 2.
The grouping of the first assignment pair is easier to see as a tree:
The legitimate use of and, or and not is control flow, where the left side is a whole statement and the right side
is another statement that should run only when the left one succeeded (and) or failed (or):
# frozen_string_literal: true
require "tmpdir"
Dir.mktmpdir do |dir|
path = File.join(dir, "title.txt")
File.write(path, "Dune\n")
# the idiomatic use of the low-precedence operators: control flow
title = File.read(path).strip or raise "no title"
puts title
File.exist?(File.join(dir, "missing")) or puts "no such file, carrying on"
File.exist?(path) and puts "file found"
puts "not found" unless File.exist?(path)
end
Even there, an explicit if/unless or a guard clause (raise ... unless title) is usually clearer, and RuboCop’s
Style/AndOr flags and and or inside conditions by default. A workable rule: use &&, || and ! everywhere;
accept or raise/or return as an idiom only in code that already uses it; and never mix the two families in one
expression.
Predicates and explaining variables
Long boolean expressions read better when the pieces have names. A predicate method (a method whose name ends in ?
and that returns true or false) names a condition and can be reused and tested; an explaining variable names a
sub-condition inside one method. Both let the operators read like the rule they implement:
# frozen_string_literal: true
module Shelf
# Predicates built from named conditions instead of one long expression.
Loan = Data.define(:days_late, :renewals, :reserved) do
def overdue? = days_late.positive?
def renewable?
under_limit = renewals < 2
under_limit && !overdue? && !reserved
end
# && returns its last evaluated operand, so this is false or an Integer
def fine_cents = overdue? && (days_late * 25)
def status = (overdue? && "overdue") || (renewable? && "renewable") || "on loan"
end
end
fresh = Shelf::Loan.new(days_late: 0, renewals: 0, reserved: false)
late = Shelf::Loan.new(days_late: 4, renewals: 1, reserved: false)
held = Shelf::Loan.new(days_late: 0, renewals: 2, reserved: true)
p fresh.renewable? # => true
p late.renewable? # => false
p fresh.fine_cents # => false
p late.fine_cents # => 100
p [fresh, late, held].map(&:status) # => ["renewable", "overdue", "on loan"]
Source: Data and Ruby 4.0 syntax: Methods
status chains || over && terms to pick the first matching label, which works only because each term is either a
string or false. When a chain has more than two or three terms, a case expression (see
Control Expressions) is clearer. The parentheses around
(days_late * 25) and around each && term are not required by the grammar; they are there so that neither the reader nor
Lint/AmbiguousOperatorPrecedence has to remember the table.
|| and ||= for defaults
value || default is the idiom for "use the default when the value is missing", and value ||= default is
value || (value = default). Both treat false as missing, which is wrong when false is a legitimate stored value:
# frozen_string_literal: true
options = { archived: false }
# || cannot tell false from "missing", so a stored false is replaced by the default
p(options[:archived] || true) # => true
# fetch distinguishes a missing key from a false value
p options.fetch(:archived, true) # => false
p options.fetch(:limit, 10) # => 10
# ||= has the same trap, because it is a || b = c in disguise
options[:archived] ||= true
p options # => {archived: true}
Source: Hash#fetch and Ruby 4.0 syntax: Abbreviated Assignment
Use Hash#fetch (or key?) whenever a hash value can legitimately be false or nil; the same applies to
ENV.fetch. Pattern matching with in (see Pattern Matching) is
the other tool for "take the value if the key is present".
Arithmetic operators
The arithmetic operators are +, -, *, /, % and **, plus unary -. Integers behave exactly: they
have arbitrary size (2**200 is an exact Integer), / is floor division, and the result of an operation between two
integers is an integer. Mixing in a Float makes the result a Float; mixing in a Rational or Complex keeps the
more exact type.
# frozen_string_literal: true
pages = 7
# integer division floors toward negative infinity
p pages / 2 # => 3
p(-pages / 2) # => -4
# the sign of % follows the divisor, the sign of remainder follows the receiver
p pages % 3 # => 1
p(-pages % 3) # => 2
p pages % -3 # => -2
p((-pages).remainder(3)) # => -1
p pages.divmod(2) # => [3, 1]
p((-pages).divmod(2)) # => [-4, 1]
p pages.fdiv(2) # => 3.5
p pages / 2.0 # => 3.5
p pages.ceildiv(2) # => 4
p pages.div(2) # => 3
p 7.0.div(2) # => 3
p 2**10 # => 1024
# a negative exponent gives a Rational
p 2**-2 # => (1/4)
p 2**0.5 # => 1.4142135623730951
# modular exponentiation without building the huge intermediate number
p 10.pow(3, 7) # => 6
p Integer.sqrt(17) # => 4
p 2**200 # => 1606938044258990275541962092341162602522202993782792835301376
p Rational(1, 3) + Rational(1, 6) # => (1/2)
Source: Integer#/, Integer#%, Integer#**, Integer#pow and Integer#fdiv
The points to remember:
-
Integer division floors.
7 / 2is3and-7 / 2is-4, not-3.Integer#divis the same,fdivalways returns aFloat,divmodreturns quotient and remainder together, andceildiv(added in Ruby 3.2) rounds up, which is what pagination needs:total_pages = book_count.ceildiv(page_size). -
The sign of
%follows the divisor, so-7 % 3is2and7 % -3is-2.remainderfollows the dividend instead (-7.remainder(3)is-1). Code ported from C or Java, where%takes the dividend’s sign, needs attention here.%andmoduloare the same method. -
**is right associative and binds tighter than unary minus.2**3**2is 512 and-base**2is-(base**2). A negative integer exponent returns aRational(2**-2is(1/4)) and a fractional one aFloat.pow(exponent, modulus)does modular exponentiation without building the huge intermediate number. -
Since Ruby 3.4,
Integer#**andRational#**no longer returnInfinityorNaNfor results that do not fit: they return the exact value, and raiseArgumentError(exponent is too large) when the result would be absurdly large. -
Integer.sqrt(n)returns the exact integer square root;Math.sqrtreturns aFloatand loses precision for large integers.
Zero, infinity and floating-point surprises
# frozen_string_literal: true
def attempt
yield
rescue ZeroDivisionError, ArgumentError => e
puts "#{e.class}: #{e.message}"
end
zero = 0
attempt { 1 / zero }
attempt { 1 % zero }
attempt { 1.0 % 0.0 }
attempt { 10**10_000_000_000 }
p 1 / 0.0 # => Infinity
p(-1 / 0.0) # => -Infinity
p (0 / 0.0).nan? # => true
p 1.fdiv(zero) # => Infinity
sum = 0.1 + 0.2
p sum # => 0.30000000000000004
p sum.eql?(0.3) # => false
p((sum - 0.3).abs < Float::EPSILON) # => true
p 0.1r + 0.2r == 0.3r # => true
Source: ZeroDivisionError, Float and Rational
Integer division and modulo by zero raise ZeroDivisionError (divided by 0). Float division by zero does not: 1 / 0.0 is
Infinity, -1 / 0.0 is -Infinity and 0 / 0.0 is NaN. Note, however, that in the Ruby version used here the modulo
operator raises ZeroDivisionError even for floats (1.0 % 0.0), so do not assume NaN there. Floating-point numbers are binary
approximations, so 0.1 + 0.2 is 0.30000000000000004 and == on computed floats is almost never what you want;
compare with a tolerance (Float::EPSILON for numbers near 1, a relative tolerance otherwise), round first, or use
Rational (the r suffix makes a literal) or BigDecimal when exactness matters, for example for money. The
Numbers page covers the numeric tower, Integer/Float/Rational/Complex
conversions and rounding in depth.
Operators on strings and arrays reuse the same symbols with different meanings, which is why the left operand matters:
"ab" * 3 repeats, [1, 2] * "," joins and [1, 2] * 2 repeats; "%d" % 5 formats; Array#- removes elements and
Array#& intersects. The next sections list them.
Comparison and equality operators
==, !=, <, <=, >, >= and <=> compare values. They return true or false, except <=>, the spaceship
operator, which returns -1, 0 or 1 for "less", "equal" and "greater", and nil when the two values cannot be compared.
<=> is the foundation: sort, min, max and Comparable all use it.
# frozen_string_literal: true
one = 1
two = 2
p one <=> two # => -1
p two <=> 2 # => 0
p two <=> one # => 1
p one <=> "a" # => nil
p "apple" <=> "banana" # => -1
p [1, 2] <=> [1, 3] # => -1
p [1, 2] <=> [1] # => 1
p :a <=> :b # => -1
p nil <=> 1 # => nil
p one == 1.0 # => true
p one.eql?(1.0) # => false
p one.equal?(1) # => true
p one != two # => true
p 5.clamp(1, 3) # => 3
p 5.between?(1, 10) # => true
Source: Comparable, Integer#<⇒ and Array#<⇒
Things to keep in mind:
-
<=>returnsnilfor incomparable operands (1 <=> "a"), and then<,>andsortraiseArgumentError: comparison of Integer with String failedwhile==quietly saysfalse. -
Strings compare byte-wise by default, so
"Zebra" <=> "apple"is-1; a case-insensitive or locale-aware order needscasecmp,casecmp?or an explicitsort_by. Arrays compare element by element, and a shorter array that is a prefix of a longer one sorts first. -
1 == 1.0istrue(numeric value equality), but1.eql?(1.0)isfalse(same class and value, the test hash keys use) andequal?tests object identity. Never overrideequal?. -
Defining
<=>and includingComparablegives<,<=,>,>=,==,between?andclamp, asFinedid above.Comparable#==is derived from<=>, so a<=>that returnsnilor0for different objects also changes equality. -
A class that defines
==should defineeql?andhashtogether if its instances are used as hash keys or in sets. The full contract, and the special case ofFloat::NAN(which is not equal to itself), is in Equality, Hashing and Comparison.
Case equality: ===
=== is the operator behind case/when. Despite its name it is not "stricter equality": it means "does this pattern
cover that value?". when pattern calls pattern === value, so the pattern is always the receiver. Each class defines the
sense that makes sense for it: Module#=== is is_a?, Range#=== is membership, Regexp#=== is a match, Proc#===
calls the proc, and for most other objects it is ==.
# frozen_string_literal: true
big = ->(n) { n > 2 }
# === is what `case`/`when` calls; read it as "does this pattern cover that value?"
p Integer.===(3) # => true
p (1..5).===(3) # => true
p(/ab/.match?("cab")) # => true
p(/ab/.===("cab")) # => true
sym = :a
p sym.===(:a) # => true
p big.===(3) # => true
p String.===(String) # => false
p Class.===(String) # => true
count = 3
case count
when big then puts "matched the lambda"
end
word = "cab"
case word
when /ab/ then puts "matched the regexp"
end
Source: Object#===, Module#===, Range#===, Regexp#=== and Proc#===
Calling === by hand is rare and RuboCop’s Style/CaseEquality discourages it; prefer is_a?, match? and cover?
outside case. Note the asymmetry: String === String is false (the class String is not itself a string) whereas
Class === String is true. case/when and its use of === with splat lists and lambdas are covered in
Control Expressions.
Match operators: =~ and !~
string =~ regexp returns the index of the first match or nil, and sets the special variables $~, $1, $2 and so on;
!~ returns true when there is no match. The regexp may be on either side. When a regexp literal with named captures
is on the left, the captures become local variables, which is why the example below can use first and second
without declaring them. Regexp.last_match is the readable spelling of $~.
# frozen_string_literal: true
title = "Dune Messiah"
p title =~ /Mes/ # => 5
p title !~ /xyz/ # => true
p title.match?(/dune/i) # => true
# a regexp literal on the left creates local variables for named captures
if /(?<first>\w+) (?<second>\w+)/ =~ title
p first
p second
end
p Regexp.last_match(0) # => "Dune Messiah"
p nil =~ /x/ # => nil
p :Dune =~ /un/ # => 1
begin
Object.new =~ /x/
rescue NoMethodError => e
puts "#{e.class}: #{e.message}"
end
Source: Regexp#=, String#= and NilClass#=
When the position is not needed, prefer match?: it returns a boolean, does not allocate a MatchData and does not
touch $~. Object#=~ was removed in Ruby 3.2; nil =~ /x/ still works (it returns nil) because NilClass
defines it, but an arbitrary object raises NoMethodError. Regular expressions themselves are the subject of
Regular Expressions.
Bitwise and shift operators
&, |, ^ and ~ operate on the bits of integers; << and >> shift. Integers are conceptually infinite two’s
complement numbers, so ~5 is -6 and there is no overflow: 1 << 70 is an exact large integer. Integer#[] reads a bit
or a bit range, and anybits?, allbits? and nobits? are the readable forms of the usual masks (RuboCop’s
Style/BitwisePredicate suggests them).
# frozen_string_literal: true
flags = 0b1100
mask = 0b1010
p flags & mask # => 8
p flags | mask # => 14
p flags ^ mask # => 6
p ~flags # => -13
p 1 << 4 # => 16
p(-16 >> 2) # => -4
p 1 << 70 # => 1180591620717411303424
p flags[2] # => 1
p flags[0, 3] # => 4
p flags.anybits?(mask) # => true
p flags.allbits?(0b0100) # => true
p flags.nobits?(0b0011) # => true
p flags.bit_length # => 4
Source: Integer#&, Integer#<<, Integer#[] and Integer#anybits?
A right shift of a negative number keeps the sign (-16 >> 2 is -4), and a negative shift count shifts the other way.
Precedence trap for readers coming from C and Java: there flags & mask == mask groups as flags & (mask == mask), while in
Ruby & binds tighter than ==, so it is (flags & mask) == mask. Write the parentheses anyway; the code then means the
same in every language.
Collection and string operators
The same symbols are reused by the core collections. They are ordinary methods, so the left operand decides.
# frozen_string_literal: true
a = [1, 2]
b = [2, 3]
p a & b # => [2]
p a | b # => [1, 2, 3]
p a - b # => [1]
p a + b # => [1, 2, 2, 3]
p a * "," # => "1,2"
p a * 2 # => [1, 2, 1, 2]
p a <=> b # => -1
p Set[1, 2] & Set[2, 3] # => Set[2]
p Set[1, 2] ^ Set[2, 3] # => Set[1, 3]
p Set[1, 2] <= Set[1, 2, 3] # => true
p({ a: 1 } <= { a: 1, b: 2 }) # => true
p [1] << 2 << 3 # => [1, 2, 3]
yes = true
no = false
p yes & no # => false
p yes ^ yes # => false
p no | yes # => true
-
Array#&,Array#|andArray#-compare elements withhashandeql?and remove duplicates from the result;Array#+concatenates and keeps them.Array#<=>is lexicographic. -
Setis available withoutrequire "set"since Ruby 3.2 and uses&,|,-,^and<=/>=for the set operations;Hash#<=andHash#>=test hash containment (Ruby 2.3 and later). -
<<on an array or set returns the receiver, so appends can be chained; on a string it appends a string or an integer code point and mutates the receiver, which makes it the main source ofFrozenErrorbelow. -
<<is also the start of a here-document. After a method name,puts <<TEXTstarts a heredoc, whereas after a local variablebuffer <<"x"is an append; keep a space on both sides of the append operator so that the reader (and the parser after a method call) cannot confuse the two.
Strings
# frozen_string_literal: true
title = "Dune"
begin
title << " Messiah"
rescue FrozenError => e
puts "#{e.class}: #{e.message}"
end
# += builds a new string, << changes the receiver
title += " Messiah"
p title # => "Dune Messiah"
buffer = +"Title: "
buffer << title << "!"
p buffer # => "Title: Dune Messiah!"
p buffer.frozen? # => false
p (-buffer).frozen? # => true
p "ab" "cd" # => "abcd"
p((+"ab") << 99) # => "abc"
price = 3.14159
p format("%.2f", price) # => "3.14"
p "%05.1f|%-5s|%+d" % [3.14159, "ab", 5] # => "003.1|ab |+5"
p "%<name>s has %<count>03d copies" % { name: "Dune", count: 2 } # => "Dune has 002 copies"
p "-" * 3 # => "---"
-
With
# frozen_string_literal: true(the default in the examples of this section) every literal is frozen, andtitle << " Messiah"raisesFrozenError: can't modify frozen String: "Dune". Without the magic comment, Ruby 3.4 treats literals as chilled: they can still be modified, butruby -w(or enabling the:deprecatedwarning category) printsliteral string will be frozen in the futureat the first modification. The default is expected to become frozen in a future major release. -
+=creates a new string (title += xistitle = title + x), so it works on frozen strings.<<mutates, so use it on a mutable buffer created with unary plus,+""orString.new. -
Unary
+stringreturns a mutable copy when the receiver is frozen (otherwise itself), and unary-stringreturns a frozen, deduplicated string; the example shows both flags. -
"ab" "cd"(adjacent literals) is a compile-time concatenation, not an operator; RuboCop flags it asLint/ImplicitStringConcatenationwhen it is probably a missing comma. -
%formats with the same directives asKernel#format: positional arrays,%<name>sand%{name}for hashes, and%05.1f-style flags. RuboCop prefersformat/sprintfoverString#%and annotated tokens over%swhen more than one placeholder is used.
The full list of String methods and encodings is in Strings.
Safe navigation: &.
receiver&.method(args) calls the method unless the receiver is nil, in which case the whole call (including the
evaluation of its arguments) is skipped and the result is nil. It replaces the user && user.address && user.address.city
ladder. Only nil is special: false&.to_s calls to_s.
# frozen_string_literal: true
Owner = Struct.new(:name, :address)
Address = Struct.new(:city)
owner = Owner.new("Ana", nil)
p owner.address&.city # => nil
owner.address = Address.new("Lyon")
p owner.address&.city # => "Lyon"
nothing = nil
p nothing&.length # => nil
p false&.to_s # => "false"
# the arguments are not evaluated when the receiver is nil
calls = []
nothing&.push(calls << :argument_evaluated)
p calls # => []
# it works with operator-assignment and with operators written as methods
count = nil
p(count&.+(1)) # => nil
text = nil
text &&= text.upcase
p text # => nil
owner.address&.city = "Paris"
p owner.address.city # => "Paris"
owner.address&.city ||= "Rome"
p owner.address.city # => "Paris"
# the safe call guards only one link of the chain
begin
nothing&.foo.bar
rescue NoMethodError => e
puts "#{e.class}: #{e.message}"
end
p nothing&.foo&.bar # => nil
names = [nil, "x"]
p(names.map { it&.upcase }) # => [nil, "X"]
p names.compact.map(&:upcase) # => ["X"]
Details that matter in practice:
-
The arguments are not evaluated for a
nilreceiver (callsstays empty), which is useful but also a side-effect trap. -
Operator calls are legal in method-call form (
count&.+(1)), and attribute and abbreviated assignment work:owner.address&.city = "Paris"assigns only whenaddressis notnil, andowner.address&.city ||= "Rome"assigns only when the receiver exists andcityis falsy. -
The guard covers one link. In
nothing&.foo.bar,baris still sent to the result ofnothing&.foo, which isnil, so you get aNoMethodErrorfornilunless every link uses&.. RuboCop’sLint/SafeNavigationChaindetects this, andLint/RedundantSafeNavigationflags&.on receivers that cannot benil. -
&.hidesnilrather than handling it. A chain of four&.calls often means a missing value object or a missingfetch; it is a good fit at the boundary of optional data and a poor one for values that should never be absent. -
With
map { it&.upcase }the safe call keepsnilelements in the result ([nil, "X"]);compact.map(&:upcase)drops them. Theitimplicit parameter is new in Ruby 3.4.
calling-methods-and-argument-forwarding covers receivers and argument syntax in full:
Calling Methods and Argument Forwarding.
The ternary operator
condition ? then_value : else_value is an expression with the same meaning as the if expression, with lower
precedence than ranges and higher than assignment and the modifiers. It is right associative.
# frozen_string_literal: true
stock = 0
p(stock.zero? ? "sold out" : "in stock") # => "sold out"
label = stock > 5 ? "plenty" : (stock.positive? ? "few" : "none")
p label # => "none"
# for more than two outcomes, an if-expression reads better than a nested ternary
level =
if stock > 5 then "plenty"
elsif stock.positive? then "few"
else "none"
end
p level # => "none"
kind = stock.zero? ? :empty : :stocked
p kind # => :empty
flag = stock.zero?
p(flag ? [1] : [2]) # => [1]
# spacing matters next to symbols and ? methods
p(flag ?"yes":"no") # => "yes"
p(flag ? "yes": "no") # => "yes"
Source: Ruby 4.0 syntax: Ternary if
-
Use it for a single, short choice between two values (
stock.zero? ? "sold out" : "in stock"). Nested ternaries are legal but RuboCop’sStyle/NestedTernaryOperatorflags them even when parenthesized; anifexpression assigned to a variable (shown above) is clearer. -
ifis also an expression in Ruby: the last expression of the chosen branch is the value, solevel = if ... endneeds no ternary at all. See Control Expressions. -
Spacing matters next to symbols and predicate methods.
flag ?"yes":"no"andflag ? "yes": "no"happen to parse, but they are fragile and RuboCop reports the missing spaces (Layout/SpaceAroundOperators);x ? :a : :bneeds the spaces. -
Both branches are evaluated lazily: only the chosen branch runs.
Ranges in conditions and the flip-flop
.. builds an inclusive range and ... an exclusive one. Both are operators in the precedence table and are lower
than arithmetic, so 1..n + 1 is 1..(n + 1). Beginless and endless ranges ((..5), (1..)) are first-class values and
work in case/when and with cover?. The details of Range are in
Ranges.
# frozen_string_literal: true
n = 3
# .. has lower precedence than +, so the end is n + 1
p(1..n + 1) # => 1..4
p (1..n).sum # => 6
p (1..).first(2) # => [1, 2]
p (..5).cover?(3) # => true
p 1.step(10, 4).to_a # => [1, 5, 9]
p ("a".."e").to_a # => ["a", "b", "c", "d", "e"]
p (1...4).size # => 3
age = 30
p age.between?(18, 65) # => true
p (18..65).cover?(age) # => true
case age
when ..17 then puts "minor"
when 18.. then puts "adult"
end
Source: Range and Ruby 4.0 syntax: Ranges
Range#cover? and Range#=== test the bounds only, which is what a "between" test needs. include? and member? are
equivalent for numeric ranges, but for other element types (a range of strings) they walk the elements, so prefer
cover? when the range is already at hand; Comparable#between? is the choice when it is not.
The flip-flop
A range of two conditions inside a condition (if, unless, ?:, while, a modifier) is not a Range at all. It is the
flip-flop operator, borrowed from awk and Perl: it becomes true when the first condition is true, stays true on later
evaluations, and turns false after the second condition is true. It keeps state between evaluations, so it is only
meaningful inside a loop or iterator.
# frozen_string_literal: true
lines = %w[intro BEGIN one two END outro BEGIN three END]
# a range of two conditions inside a condition is a flip-flop: it stays "on" between them
inside = lines.select { |line| true if (line == "BEGIN")..(line == "END") }
p inside # => ["BEGIN", "one", "two", "END", "BEGIN", "three", "END"]
body = lines.select do |line|
next false unless (line == "BEGIN")..(line == "END")
!%w[BEGIN END].include?(line)
end
p body # => ["one", "two", "three"]
# the same thing without a flip-flop, with an explicit state variable
collecting = false
explicit = lines.select do |line|
collecting = true if line == "BEGIN"
keep = collecting
collecting = false if line == "END"
keep
end
p explicit # => ["BEGIN", "one", "two", "END", "BEGIN", "three", "END"]
# .. checks the end condition on the line that turned it on, ... does not
numbers = [1, 2, 3, 4, 5, 6]
p(numbers.select { |i| true if (i == 2)..i.even? }) # => [2]
p(numbers.select { |i| true if (i == 2)...i.even? }) # => [2, 3, 4]
Source: Ruby 4.0 syntax: Flip-Flop
-
..checks the second condition on the same evaluation that turned the flip-flop on;...does not. With[1, 2, 3, 4, 5, 6]and the pair(i == 2)andi.even?,..yields only[2]while...yields[2, 3, 4]. -
A range of two integer literals in a condition (
if 1..2) is also a flip-flop, compared against the line number$.; it is almost always a mistake and Ruby warns (see the warnings section). -
Flip-flops are rare, state hidden in the operator is easy to misread, and RuboCop’s
Lint/FlipFlopflags every use. An explicit state variable, as in the last block of the example, is the maintainable alternative.
defined?
defined? is a keyword operator that tests whether an expression could be evaluated, without evaluating the expression
itself; it returns a description string or nil. It has very low precedence, below assignment, so defined? a = 1 asks about the
assignment and defined?(a) && a needs the parentheses.
# frozen_string_literal: true
stock = 3
p defined?(stock) # => "local-variable"
p defined?(@missing) # => nil
p defined?(puts) # => "method"
p defined?(String) # => "constant"
p defined?(1 + 1) # => "method"
p defined?(yield) # => nil
p defined?(@loans = 2) # => "assignment"
p defined?(@missing || 1) # => "expression"
p defined?(Shelf::Missing) # => nil
p defined?(super) # => nil
p defined?(zzz) # => nil
p(defined?(zzz) ? 1 : 2) # => 2
Source: Ruby 4.0 syntax: defined?
The strings include "local-variable", "method", "constant", "instance-variable", "global-variable", "assignment",
"expression", "yield", "super", "self", "nil", "true" and "false". Idiomatic uses are defined?(@memo)
(telling a variable that was never set from one set to nil, which @memo.nil? cannot do), defined?(yield) and defined?(super) inside
methods, and defined?(SomeConstant) to test for an optional dependency without raising NameError.
block_given? and respond_to? are usually clearer for the first and last of those cases.
Modifiers and block binding
The statement modifiers if, unless, while, until and rescue are the loosest operators of all. A modifier applies
to the whole statement on its left, including an assignment:
# frozen_string_literal: true
def parse(text) = Integer(text)
# a rescue modifier is lower than =, so it guards the whole right-hand side
count = begin
parse("x")
rescue ArgumentError
0
end
p count # => 0
value = parse("x") rescue 0
p value # => 0
p((raise "boom" rescue "rescued")) # => "rescued"
items = [3, 1, 2]
# a brace block binds to the nearest call, a do block to the whole command
p(items.reject { it > 1 }) # => [1]
rejected = items.reject do |n|
n > 1
end
p rejected # => [1]
puts items.map do |n| # prints #<Enumerator:0x000000012493d3f0>
n * 2
end
puts(items.map { |n| n * 2 })
-
value = parse("x") rescue 0isvalue = (parse("x") rescue 0); the rescue modifier rescues onlyStandardError, hides which error happened and is flagged byStyle/RescueModifier. Abegin/rescueblock (first assignment of the example) orrescue ArgumentErrorin a method body is the precise alternative; see Exceptions and Error Handling. -
x = 5 if condassigns only whencondis truthy; ifxwas not assigned before, it isnilafterwards, because the parser still creates the variable. -
A
{ }block binds to the nearest call, ado ... endblock to the whole command.puts items.map do |n| ... endtherefore passes the block toputs, which ignores it, andmapwithout a block returns an enumerator (the output#<Enumerator:0x...>has an address that differs on every run). Use braces for chained calls whose result is used anddo ... endfor blocks that perform actions, or parenthesize (puts(items.map { ... })). RuboCop reports the mistake asLint/AmbiguousBlockAssociation. Blocks and procs are covered in Blocks, Procs and Lambdas.
Line continuation and leading logical operators
A newline ends a statement unless the expression is obviously incomplete: after a binary operator, a comma or an opening
bracket, or when the next line starts with a method-call dot. Starting with Ruby 4.0, the logical operators &&,
||, and and or at the beginning of a line also continue the previous line, like a leading dot:
# frozen_string_literal: true
stock = 2
overdue = false
available = stock.positive?
&& !overdue
p available # => true
cached = nil
label = cached
|| "fallback"
p label # => "fallback"
ready = available
and stock > 5
p ready # => true
if stock.positive?
&& stock < 5
puts "few left"
end
Source: Ruby 4.0 NEWS: Language changes
Ruby 3.4 and older do not accept such a line, so code that has to run on them must keep the operator at the end of the
previous line. The feature allows layouts such as an if whose
conditions each begin with &&. Note that ready = available followed by and stock > 5 still assigns first: the
low precedence of and is unchanged, and ready is true even though the whole expression was falsy.
Warnings and errors
The interpreter warns about operator mistakes when run with -w; these are the ones you are most likely to meet, with the
text produced by Ruby 4.0.7.
# frozen_string_literal: true
count = 3
if count = 4
puts "assigned"
end
warn_assign.rb:4: warning: found '= literal' in conditional, should be ==
assigned
Source: Ruby 4.0 syntax: if Expression
A literal on the right of = in a condition is almost certainly a typo for ==. A non-literal assignment such as if (m =
re.match(s)) is idiomatic and silent when it is parenthesized.
# frozen_string_literal: true
def show(*args) = args
p show -1 # => [-1]
n = 5
p n -1 # => 4
p n - 1 # => 4
p show *[1, 2] # => [1, 2]
warn_ambiguous.rb:5: warning: ambiguous first argument; put parentheses or a space even after `-` operator
warn_ambiguous.rb:7: warning: '-' after local variable or literal is interpreted as binary operator even though it seems like unary operator
warn_ambiguous.rb:9: warning: ambiguous `*` has been interpreted as an argument prefix
[-1]
4
4
[1, 2]
Source: Ruby 4.0 syntax: Calling Methods
Whitespace changes the meaning of -, *, & and [ after a method name: show -1 passes -1, n -1 (with n a local
variable) subtracts. Put parentheses on calls, or spaces on both sides of a binary operator; RuboCop’s
Lint/AmbiguousOperator reports the first and the third call.
# frozen_string_literal: true
value = 5
puts value +1 # prints 6
puts (1 + 2) * 3 # prints 9
puts((1 + 2) * 3) # prints 9
warn_spacing.rb:4: warning: '+' after local variable or literal is interpreted as binary operator even though it seems like unary operator
6
9
9
Source: Ruby 4.0 syntax: Calling Methods
puts (1 + 2) * 3 prints 9 because the parenthesized group is only the first part of the argument, which is why the
parenthesized call puts((1 + 2) * 3) is the form to write; puts value +1 is parsed as a subtraction (6) with a warning.
# frozen_string_literal: true
x = 1
p x == 1 # => true
x == 2
x + 1
puts "ok" # prints ok
warn_void.rb:5: warning: possibly useless use of == in void context
warn_void.rb:6: warning: possibly useless use of + in void context
true
ok
Source: Ruby 4.0 syntax: Precedence
A comparison or arithmetic result that is thrown away is almost always a bug (x == 2 was meant to be x = 2).
# frozen_string_literal: true
if 1..2
puts "range literal in condition"
end
puts "done" if "text" # prints done
puts "x" if /re/
warn_literal.rb:3: warning: integer literal in flip-flop
warn_literal.rb:3: warning: integer literal in flip-flop
warn_literal.rb:6: warning: string literal in condition
warn_literal.rb:7: warning: regex literal in condition
done
Source: Ruby 4.0 syntax: Flip-Flop
A literal in a condition is constant, and an integer range literal is silently turned into a flip-flop on $..
The other errors this page’s operators raise are summarized below.
| Message | Raised by | Typical cause |
|---|---|---|
|
|
The left operand is a number and the right one cannot be coerced; |
|
|
|
|
any operator |
The left operand is |
|
|
|
|
|
Integer or float modulo by zero; guard or rescue it. |
|
|
Mutating a frozen literal; build a |
|
|
An exponent so large that the exact result cannot be built (Ruby 3.4 and later). |
Source: Exception hierarchy and TypeError
What changed between Ruby 3.0 and 4.0
| Version | Change affecting operators |
|---|---|
3.2 |
|
3.4 |
|
4.0 |
|
Source: Ruby 4.0 NEWS, Ruby 3.4 NEWS and Ruby 3.2 NEWS
Where older books and tutorials are outdated
Much written about operators in older Ruby books and blog posts still holds, because the operator set has barely changed in 25 years. A few points have moved, and a reader of an older book should check them:
-
Fixnum/Bignumare gone (unified intoIntegerin 2.4, constants removed in 3.2), so "overflow into Bignum" discussions are only history. -
Object#=~used to exist and returnednil; it was removed in 3.2. -
Integer#**with a huge exponent used to giveInfinityand now gives the exact integer or an error (3.4). -
Leading
&&/||on the next line used to be a syntax error and is now accepted (4.0). Style guides that say "always put the operator at the end of the line" describe the older constraint rather than a rule of the language today. -
and/orguidance has not changed: the common advice to avoid them in conditions and assignments is still correct, and the control-flow use (or raise) is still idiomatic. -
Setno longer needsrequire "set", and error messages in 3.4 and later use'instead of the backtick-quote pair (undefined method '+' for nil), so an exact message copied from an older book will not match what you see. -
itin blocks (3.4) and endless methods (3.0) make operator-heavy one-liners shorter than older texts show. -
The books mostly show
putsoutput; modern documentation and this section use the 3.4+inspectformat ({name: "Dune"}instead of{:name=>"Dune"}).
Summary
-
Operators are methods unless they are keywords; define
+,-,<=>,==,[]and friends on your own classes, andinclude Comparableafter defining<=>. -
Only
nilandfalseare falsy;&&and||return an operand, not a boolean, and short-circuit. -
Use
&&,||and!in expressions and conditions; keepand,orandnotfor control flow, if at all. -
Parenthesize mixed operator families; remember that
&is above==,<<is below+,..is below||, and**is above unary minus. -
/floors for integers,%follows the divisor,fdivandRationalavoid surprises, and floats are never compared with==after arithmetic. -
&.guards one link only,||and||=confusefalsewith missing,rescuemodifiers hide errors, anddo ... endversus{ }changes which call receives the block.
References
-
Ruby 4.0 documentation: Control Expressions (ternary, modifiers, flip-flop)
-
Ruby 4.0 documentation: Calling Methods (safe navigation)
-
Ruby 4.0 documentation: Miscellaneous Syntax (
defined?) -
Comparable, Integer, Float, NilClass and BasicObject
-
Ruby 4.0 NEWS and the 3.2 and 3.4 release notes
-
The Ruby Style Guide and RuboCop Lint cops (
AmbiguousOperatorPrecedence,AmbiguousBlockAssociation,FlipFlop,SafeNavigationChain)