Operators #
Operators are the heart of expressions — without them, code is just a collection of standalone values with no relationships between them. Ruby has all the standard operators you’ll find in other languages, plus a few unique ones that exist nowhere else: the spaceship operator (<=>), the safe navigation operator (&.), and conditional assignment (||=, &&=). Another thing that makes Ruby operators interesting is that almost all of them are actually methods — which means you can redefine or add operators to your own classes. This article covers every Ruby operator from the most basic to the most idiomatic, along with the common pitfalls that trip up new developers.
Arithmetic Operators #
Arithmetic operators are the most familiar — but Ruby has a few behaviors to watch out for, especially integer division and modulo with negative numbers.
a = 17
b = 5
puts a + b # => 22 (addition)
puts a - b # => 12 (subtraction)
puts a * b # => 85 (multiplication)
puts a / b # => 3 (integer division — decimal discarded!)
puts a % b # => 2 (modulo — remainder)
puts a ** b # => 1419857 (power: 17⁵)
puts -a # => -17 (unary negation)
Integer vs Float Division #
This is the most common source of confusion for Ruby beginners:
# ANTI-PATTERN: forgetting that dividing two Integers produces an Integer
def average(total, count)
total / count # if both are Integers, the result is an Integer!
end
puts average(10, 3) # => 3 (not 3.333!)
# CORRECT — three ways to get a Float result:
puts 10.fdiv(3) # => 3.3333... — the most idiomatic Ruby
puts 10.to_f / 3 # => 3.3333... — explicit conversion
puts 10 / 3.0 # => 3.3333... — force one operand to Float
puts Rational(10, 3).to_f # => 3.3333... — via Rational
Modulo vs Remainder #
Ruby uses % as modulo (not remainder), which behaves differently for negative numbers:
# Modulo in Ruby — the result always has the same sign as the DIVISOR
puts 7 % 3 # => 1
puts -7 % 3 # => 2 (not -1!)
puts 7 % -3 # => -2 (not 1!)
puts -7 % -3 # => -1
# Ruby also has .remainder, which follows the sign of the DIVIDEND
puts 7.remainder(3) # => 1
puts (-7).remainder(3) # => -1 (follows the sign of -7)
puts 7.remainder(-3) # => 1 (follows the sign of 7)
# Practical use of modulo — array rotation / clocks
current_hour = 23
later = (current_hour + 3) % 24 # => 2 (not 26)
days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
puts days[6 % 7] # => "Sun"
puts days[7 % 7] # => "Mon" (wraps back to the start)
Comparison Operators #
Comparison operators return true or false, except for the spaceship operator (<=>) which returns -1, 0, or 1.
a = 10
b = 20
puts a == b # => false (equal — by value)
puts a != b # => true (not equal)
puts a > b # => false (greater than)
puts a < b # => true (less than)
puts a >= b # => false (greater than or equal)
puts a <= b # => true (less than or equal)
== vs equal? vs eql? #
Ruby has three operators/methods for comparing equality, and they all mean different things:
# == : VALUE equality (can be overridden in a class)
puts 1 == 1.0 # => true (same value even though types differ)
puts "a" == "a" # => true (same string content)
# equal? : OBJECT IDENTITY equality (same object_id)
puts 1.equal?(1) # => true (small Integers are cached by Ruby)
puts "a".equal?("a") # => false (two different String objects in memory)
puts :sym.equal?(:sym) # => true (Symbols are singletons)
# eql? : VALUE and TYPE equality (used by Hash to compare keys)
puts 1.eql?(1) # => true
puts 1.eql?(1.0) # => false (different types: Integer vs Float)
puts "a".eql?("a") # => true
Guide to choosing an equality operator:
== → almost always what you want (value equality)
equal? → check whether two variables point to the EXACT same object
eql? → rarely used directly, but important for Hash keys
The Spaceship Operator <=> #
The spaceship operator is a three-way comparison that returns -1, 0, or 1. It’s the foundation of all sorting in Ruby.
puts 1 <=> 2 # => -1 (left is smaller)
puts 2 <=> 2 # => 0 (equal)
puts 3 <=> 2 # => 1 (left is greater)
puts "a" <=> "b" # => -1
puts "b" <=> "a" # => 1
# How Array#sort uses <=>
numbers = [5, 2, 8, 1, 9, 3]
numbers.sort # => [1, 2, 3, 5, 8, 9] (ascending, default)
# sort_by — more idiomatic for sorting by an object attribute
people = [
{ name: "Citra", age: 28 },
{ name: "Andi", age: 35 },
{ name: "Bagas", age: 22 }
]
people.sort_by { |p| p[:age] }
# => [{name: "Bagas", age: 22}, {name: "Citra", age: 28}, {name: "Andi", age: 35}]
people.sort_by { |p| [-p[:age], p[:name]] } # age desc, name asc on ties
# Implementing Comparable — include the module to get all comparison operators
class Product
include Comparable
attr_reader :name, :price
def initialize(name, price)
@name = name
@price = price
end
def <=>(other)
@price <=> other.price # define the comparison basis
end
end
laptop = Product.new("Laptop", 15_000_000)
mouse = Product.new("Mouse", 350_000)
monitor = Product.new("Monitor", 3_500_000)
products = [laptop, mouse, monitor]
products.sort.map(&:name) # => ["Mouse", "Monitor", "Laptop"] — sortable for free!
puts laptop > mouse # => true
puts mouse.between?(mouse, laptop) # => true — free from Comparable
Logical Operators #
Ruby has two sets of logical operators: symbols (&&, ||, !) and words (and, or, not). They’re logically equivalent, but have very different precedence.
a = true
b = false
puts a && b # => false (AND)
puts a || b # => true (OR)
puts !a # => false (NOT)
puts !b # => true
Short-Circuit Evaluation #
The && and || operators use short-circuit evaluation — the right operand is only evaluated if needed. This isn’t just a performance optimization; it’s often used deliberately in idiomatic Ruby.
# && stops at the FIRST falsy operand
puts false && expensive_operation() # expensive_operation is never called!
puts nil && raise("Never reached") # raise is not executed
# || stops at the FIRST truthy operand
puts true || expensive_operation() # expensive_operation is never called!
puts "yes" || raise("Never reached") # raise is not executed
# Practical pattern: guard clause with &&
def process(user)
user && user.active? && user.has_permission?(:process)
end
# Practical pattern: default value with ||
name = user_input || "Guest"
config = ENV["TIMEOUT"] || "30"
&& vs and, || vs or #
This is one of the subtlest sources of bugs in Ruby — the precedence difference between symbols and words:
# && has HIGHER precedence than =
# || has HIGHER precedence than =
value = true && false # => value = (true && false) = false
value = true || false # => value = (true || false) = true
# and has LOWER precedence than =
# or has LOWER precedence than =
value = true and false # => (value = true) and false → value = true !
value = true or false # => (value = true) or false → value = true !
# ANTI-PATTERN: using and/or for conditional assignment
x = some_value and do_something # surprising because of precedence
# CORRECT: use && and || for conditional expressions
x = some_value && do_something
# and/or are only suitable for simple flow control — not expressions
File.exist?(path) or raise "File not found" # ok but better with raise unless
raise "File not found" unless File.exist?(path) # more idiomatic
Assignment Operators #
Assignment operators in Ruby are far richer than just =. There’s a compound assignment for every arithmetic and bitwise operator, plus two very idiomatic conditional assignment operators.
Basic and Compound Assignment #
a = 10 # basic assignment
a += 5 # a = a + 5 => 15
a -= 3 # a = a - 3 => 12
a *= 2 # a = a * 2 => 24
a /= 4 # a = a / 4 => 6
a %= 4 # a = a % 4 => 2
a **= 3 # a = a ** 3 => 8
# Compound assignment for bitwise
b = 0b1010
b &= 0b1100 # => 0b1000 (AND)
b |= 0b0011 # => 0b1011 (OR)
b ^= 0b0101 # => 0b1110 (XOR)
b <<= 2 # => shift left 2 bits
b >>= 1 # => shift right 1 bit
Conditional Assignment — ||= and &&= #
These two operators are among the most idiomatic Ruby features, making code extremely concise:
# ||= : assign only if the variable is currently falsy (nil or false)
@cache ||= {} # initialize only if not already set
name ||= "Guest" # use "Guest" if name is nil or false
config ||= load_config() # call load_config() only if config isn't set yet
# How to read it: "assign if it has no value yet"
# Equivalent to:
@cache = @cache || {}
name = name || "Guest"
# &&= : assign only if the variable is currently truthy
# "assign only if it already has a value"
user.name &&= user.name.strip # strip only if user.name isn't nil
data &&= process(data) # process only if data exists
# The most common ||= use case: memoization
class MonthlyReport
def total_sales
@total_sales ||= calculate_from_database # calculate once, cache forever
end
private
def calculate_from_database
puts "Calculating from DB..." # only appears once
# expensive query...
150_000_000
end
end
report = MonthlyReport.new
puts report.total_sales # => "Calculating from DB..." => 150000000
puts report.total_sales # => 150000000 (straight from cache, no query)
Memoization with||=has one weakness: if your method can returnfalseornilas a valid value,||=will recalculate every time it’s called becausefalseandnilare falsy. In that case, use a pattern withdefined?or a sentinel variable:@result = calculate() unless defined?(@result).
The Ternary Operator #
The ternary operator (?:) is a compact form of if-else for one-line expressions that return a value.
# Syntax: condition ? value_if_true : value_if_false
age = 20
status = age >= 18 ? "adult" : "minor"
puts status # => "adult"
# Common use: concise conditional assignment
discount = member? ? 0.15 : 0.0
label = balance > 0 ? "Credit" : "Debit"
color = error? ? :red : :green
# ANTI-PATTERN: nested ternary — very hard to read
result = a > 0 ? (b > 0 ? "both positive" : "only a positive") : "a negative or zero"
# CORRECT: use if-elsif for nested conditions
result = if a > 0 && b > 0
"both positive"
elsif a > 0
"only a positive"
else
"a negative or zero"
end
# ANTI-PATTERN: ternary for side effects (not returning a value)
condition ? puts("yes") : puts("no") # confusing
# CORRECT: if-else for side effects
if condition
puts "yes"
else
puts "no"
end
Range Operators #
The range operators (.. and ...) create a Range object representing a sequence of consecutive values.
inclusive = 1..10 # 1, 2, 3, ..., 10 (includes 10)
exclusive = 1...10 # 1, 2, 3, ..., 9 (excludes 10)
# Iteration
(1..5).each { |n| print "#{n} " } # => 1 2 3 4 5
# Membership checks
puts (1..10).include?(5) # => true
puts (1..10).include?(10) # => true
puts (1...10).include?(10) # => false
# Usage in case/when — very idiomatic Ruby
def bmi_classification(bmi)
case bmi
when ...18.5 then "Underweight"
when 18.5..24.9 then "Normal"
when 25.0..29.9 then "Overweight"
when 30.0.. then "Obese"
end
end
puts bmi_classification(22.5) # => "Normal"
puts bmi_classification(31.0) # => "Obese"
# Endless ranges (Ruby 2.6+) — no upper bound
(18..) # means "18 and above, no limit"
(..17) # means "up to and including 17" (beginless range, Ruby 2.7+)
age = 25
puts (18..).include?(age) # => true
# Step on ranges
(0..20).step(5).to_a # => [0, 5, 10, 15, 20]
(0.0..1.0).step(0.25).to_a # => [0.0, 0.25, 0.5, 0.75, 1.0]
Bitwise Operators #
Bitwise operators work directly on the binary representation of Integers. They’re most often used for flag manipulation, masking, and low-level operations.
a = 0b1010_1010 # 170 in decimal
b = 0b1100_1100 # 204 in decimal
puts (a & b).to_s(2) # => "10001000" — bitwise AND
puts (a | b).to_s(2) # => "11101110" — bitwise OR
puts (a ^ b).to_s(2) # => "1100110" — bitwise XOR (different bits)
puts (~a & 0xFF).to_s(2) # => "1010101" — bitwise NOT (masked to 8 bits)
puts (a << 2).to_s(2) # => "1010101000" — shift left 2 bits (multiply by 4)
puts (a >> 2).to_s(2) # => "101010" — shift right 2 bits (divide by 4)
# Practical use: bit flags for permissions
READ = 0b001 # 1
WRITE = 0b010 # 2
DELETE = 0b100 # 4
# Setting permissions
editor_perms = READ | WRITE # => 0b011 = 3
admin_perms = READ | WRITE | DELETE # => 0b111 = 7
# Checking permissions
def can_read?(perms)
perms & READ != 0
end
def can_delete?(perms)
perms & DELETE != 0
end
puts can_read?(editor_perms) # => true
puts can_delete?(editor_perms) # => false
puts can_delete?(admin_perms) # => true
# Revoking permissions
editor_without_write = editor_perms & ~WRITE
puts can_write?(editor_without_write) # => false
Ruby’s Unique Operators #
Besides the standard operators, Ruby has a few that don’t exist (or rarely exist) in other languages.
The Safe Navigation Operator (&.) #
The &. operator (called the lonely operator or safe navigation) calls a method only if the object isn’t nil, preventing NoMethodError.
# ANTI-PATTERN: verbose nil checks
if user && user.profile && user.profile.address
puts user.profile.address.city
end
# CORRECT: safe navigation operator
puts user&.profile&.address&.city
# Very useful for method chaining on possibly-nil values
city_name = order&.shipping&.address&.city || "Unknown"
uppercase_email = user&.email&.upcase
The defined? Operator #
defined? is an operator that checks whether an expression is defined, returning a descriptive String or nil.
x = 10
puts defined?(x) # => "local-variable"
puts defined?(y) # => nil (y hasn't been defined)
puts defined?(String) # => "constant"
puts defined?(puts) # => "method"
puts defined?(@ivar) # => nil (if not yet set)
puts defined?(1 + 1) # => "expression"
# Use case: truly safe initialization (not ||=)
@result = calculate() unless defined?(@result)
# Safe even if calculate() returns nil or false
The Pattern Matching Operator (=>) #
Since Ruby 3.0, the => operator is used for pattern matching — an expressive way to destructure data.
# Rightward assignment (Ruby 3.0+)
calculate_total(order) => total
# Deconstruct a hash
data = { name: "Rina", age: 28, city: "Bandung" }
data => { name:, age: } # extract name and age into local variables
puts name # => "Rina"
puts age # => 28
# Pattern matching with case/in (Ruby 3.x)
response = { status: 200, body: { user: { name: "Budi" } } }
case response
in { status: 200, body: { user: { name: String => name } } }
puts "Login successful: #{name}"
in { status: 401 }
puts "Unauthorized"
in { status: 500 }
puts "Server error"
end
Precedence (Operator Priority Order) #
When an expression contains multiple operators, Ruby evaluates them by precedence order. Operators with higher precedence are evaluated first.
| Precedence | Operator | Description |
|---|---|---|
| Highest | !, ~, unary + | Negation and complement |
** | Power | |
unary - | Numeric negation | |
*, /, % | Multiplication, division, modulo | |
+, - | Addition, subtraction | |
<<, >> | Bitwise shift | |
& | Bitwise AND | |
^, | | Bitwise XOR and OR | |
<=, <, >, >= | Comparison | |
<=>, ==, ===, !=, =~, !~ | Equality and match | |
&& | Logical AND | |
|| | Logical OR | |
.., ... | Range | |
? : | Ternary | |
=, +=, ` | ||
not | Word negation | |
| Lowest | and, or | Word AND/OR |
# Precedence examples in practice
puts 2 + 3 * 4 # => 14 (not 20 — * binds tighter than +)
puts (2 + 3) * 4 # => 20 (parentheses force the order)
puts !false || true # => true (!false first, then ||)
puts !(false || true) # => false (parentheses first)
# Common precedence traps
x = 1 + 2 == 3 # => x = (1 + 2 == 3) = true (not x = 1 + (2 == 3))
y = true || false && false # => y = true || (false && false) = true
Custom Operators in Your Own Classes #
Because operators in Ruby are fundamentally methods, you can define operator behavior for your own classes. This is one of Ruby’s most powerful features.
class Vector
attr_reader :x, :y
def initialize(x, y)
@x = x
@y = y
end
def +(other)
Vector.new(@x + other.x, @y + other.y)
end
def -(other)
Vector.new(@x - other.x, @y - other.y)
end
def *(scalar)
Vector.new(@x * scalar, @y * scalar)
end
def ==(other)
@x == other.x && @y == other.y
end
def <=>(other)
magnitude <=> other.magnitude
end
def [](index)
index == 0 ? @x : @y # array-like access: v[0], v[1]
end
def magnitude
Math.sqrt(@x**2 + @y**2)
end
def to_s
"(#{@x}, #{@y})"
end
end
v1 = Vector.new(3, 4)
v2 = Vector.new(1, 2)
puts v1 + v2 # => (4, 6)
puts v1 - v2 # => (2, 2)
puts v1 * 3 # => (9, 12)
puts v1 == Vector.new(3, 4) # => true
puts v1[0] # => 3
puts v1[1] # => 4
puts v1.magnitude # => 5.0
vectors = [v2, v1, Vector.new(0, 1)]
vectors.sort.map(&:to_s) # => ["(0, 1)", "(1, 2)", "(3, 4)"]
# Operators you CAN define in your own class:
# + - * / % ** (arithmetic)
# == <=> < > <= >= (comparison)
# & | ^ ~ << >> (bitwise)
# [] []= (element access)
# ! (negation)
# Operators you CANNOT redefine:
# && || and or not (logical — not methods)
# = += ||= etc. (assignment — not methods)
# .. ... (range — not methods)
# ? : (ternary — not methods)
Summary #
- Dividing two Integers produces an Integer — use
fdiv,to_f, or a Float operand if you need a decimal result.- Modulo
%follows the divisor’s sign — unlike.remainder, which follows the dividend’s sign. Important for negative numbers.==for value,equal?for identity,eql?for value+type — almost always use==in day-to-day code.- The spaceship
<=>is the foundation of sorting — implement it in your own class and includeComparableto get all comparison operators for free.&&and||for expressions,and/oronly for flow control — the precedence difference makesand/ordangerous in assignment expressions.||=for memoization and default values — but beware if your method can returnnilorfalseas valid values.- Safe navigation
&.prevents NoMethodError — use it for method chaining on possibly-nil values.- Ternary for one-line expressions — don’t nest them and don’t use them for side effects without a return value.
- Operators in Ruby are methods — you can define
+,-,[],==,<=>, and more in your own classes for an expressive, natural API.and/orhave lower precedence than=—x = a and bmeans(x = a) and b, notx = (a and b).