# Ruby Practical Idioms Cheat Sheet

## Blocks

```ruby
[1, 2, 3].each { |n| puts n }        # one line: braces
[1, 2, 3].each do |n|                # multi-line: do...end
  puts n
end

# Braces bind tighter than do...end — this bites with paren-less calls:
puts [1, 2].map { |n| n }            # block goes to map    ✅
puts [1, 2].map do |n| n end         # block goes to puts   ❌

sum = 0
[1, 2, 3].each { |n| sum += n }      # blocks close over the enclosing scope
[1, 2].each { |n; tmp| tmp = n }     # `;` declares block-local vars

def with_timing                      # a method that takes a block
  start = Time.now
  result = yield                     # runs the block
  puts Time.now - start
  result
end
with_timing { slow_work }

def maybe
  return :no_block unless block_given?
  yield 1, 2                         # blocks can take several args
end

# &block turns the block into an object you can pass on
def each_row(&block)
  rows.each(&block)
end

[[1, 2], [3, 4]].each { |(a, b)| a + b }   # destructuring in the params
{ a: 1 }.each { |k, v| }                   # hash yields [key, value] pairs
[1, 2, 3].each { |_| :ignored }            # _ for "don't care"
```

## Enumerable — the workhorse

```ruby
nums = [1, 2, 3, 4, 5]

nums.map { |n| n * 2 }               # [2, 4, 6, 8, 10]
nums.select(&:even?)                 # [2, 4]        (filter is an alias)
nums.reject(&:even?)                 # [1, 3, 5]
nums.find { |n| n > 3 }              # 4             (detect is an alias)
nums.filter_map { |n| n * 2 if n.even? }   # map + compact in one pass
nums.flat_map { |n| [n, n] }         # map then flatten one level
nums.each_with_object([]) { |n, acc| acc << n * 2 }

nums.sum, nums.count, nums.count(&:even?)
nums.reduce(:+)                      # 15            (inject is an alias)
nums.reduce(0) { |acc, n| acc + n }
nums.min_by(&:abs), nums.max_by(&:abs)
nums.sum { |n| n * 2 }               # sum takes a block, no map needed

nums.partition(&:even?)              # [[2, 4], [1, 3, 5]]
nums.group_by { |n| n % 3 }          # {1=>[1, 4], 2=>[2, 5], 0=>[3]}
nums.tally                           # {1=>1, 2=>1, ...} — counts occurrences
nums.each_slice(2).to_a              # fixed-size chunks
nums.each_cons(2).to_a               # sliding pairs
nums.chunk_while { |a, b| b == a + 1 }.to_a    # runs of consecutive values
nums.zip(nums.map(&:to_s))

nums.take(2), nums.drop(2)
nums.take_while { |n| n < 3 }, nums.drop_while { |n| n < 3 }
nums.any?(&:even?), nums.all?(&:positive?), nums.none?(&:zero?), nums.one?(&:odd?)
nums.first(2), nums.each_entry, nums.to_a

# `each_with_index` gives (item, i); `with_index` chains onto any enumerator
nums.map.with_index { |n, i| "#{i}:#{n}" }
nums.each.with_index(1) { |n, i| ... }     # start counting at 1

# `lazy` for big or infinite sources: nothing runs until `first`/`to_a`
(1..Float::INFINITY).lazy.select(&:even?).first(5)
```

## Sorting & grouping

```ruby
users.sort_by(&:age)                       # by one key
users.sort_by { |u| [u.dept, -u.age] }     # multi-key, `-` for descending
users.sort_by { |u| [u.dept, u.name] }.reverse
users.max_by(3, &:age)                     # top 3 without a full sort
users.min_by(&:age)

words.sort_by(&:downcase)                  # case-insensitive
words.sort                                 # uses <=> — works on any Comparable
users.sort { |a, b| a.name <=> b.name }    # explicit comparator (slower)

users.group_by(&:dept)                            # {"eng" => [...], ...}
users.group_by(&:dept).transform_values(&:size)   # counts per group
users.each_with_object(Hash.new(0)) { |u, h| h[u.dept] += 1 }
users.sum(&:salary)
users.uniq(&:email)                        # uniq by a key
users.index_by(&:id)                       # Rails; plain Ruby: to_h { [_1.id, _1] }
users.to_h { |u| [u.id, u] }
```

## Procs, lambdas & method objects

```ruby
square = ->(n) { n * n }              # lambda literal
square.call(3); square.(3); square[3] # three ways to call

add = lambda { |a, b| a + b }
blk = proc { |a, b| a.to_i + b.to_i } # proc: lenient about arity, returns from the caller

[1, 2].map(&square)                   # & converts a proc to a block
[1, 2].map(&:to_s)                    # &:sym → { |x| x.to_s }
[1, 2].map(&method(:puts))            # & on a Method object

# lambda vs proc, the two differences that matter:
#   arity   — a lambda raises on the wrong number of args, a proc pads with nil
#   return  — `return` in a lambda returns from the lambda, in a proc from the method

adder = ->(a) { ->(b) { a + b } }     # closures compose
add_two = adder.call(2)
add_two.call(3)                       # 5

double = ->(n) { n * 2 }
inc    = ->(n) { n + 1 }
(double >> inc).call(3)               # 7  — double, then inc
(double << inc).call(3)               # 8  — inc, then double

[1, 2, 3].map { _1 * 2 }              # _1.._9 numbered params (2.7+)
[1, 2, 3].map { it * 2 }              # `it` for a single param (3.4+)
```

## nil, safe navigation & defaults

```ruby
user&.address&.city         # nil instead of NoMethodError
user&.save!

name = user&.name || "anon"
list = maybe_nil.to_a       # nil.to_a == [], nil.to_s == "", nil.to_i == 0
h.fetch(:key, "default")
h.fetch(:key) { expensive } # block form: only evaluated when missing

value.nil?                  # the only reliable nil test — 0 and "" are truthy
Array(nil), Array([1]), Array(1)      # [], [1], [1] — normalise to an array

@cache ||= expensive        # memoise (careful: re-runs if the result is nil/false)
@cache = defined?(@cache) ? @cache : expensive   # memoise nil/false correctly

x = h.dig(:a, :b, :c)       # nil-safe nested lookup
items.compact               # drop nils
items.compact_blank         # Rails: drops "", [], {} too

def name = @name.presence || "anon"   # Rails: presence → nil if blank
```

## Pattern matching (case/in)

```ruby
config = { db: { host: "localhost", port: 5432 } }

case config
in { db: { host: String => host, port: Integer => port } }
  "#{host}:#{port}"
in { db: { url: String => url } }
  url
else
  raise ArgumentError
end

case response
in { status: 200..299, body: }        # binds `body`, checks a range
  parse(body)
in { status: 404 }
  nil
in { status: Integer => code } if code >= 500
  retry_later
end

case point
in [x, y]           then "2D"
in [x, y, z]        then "3D"
in []               then "empty"
in [Integer => first, *rest] then rest
end

# One-line forms
config => { db: { host: } }           # destructure or raise (rightward assignment)
if config in { db: { host: String } } # boolean test
  ...
end
```

## String formatting

```ruby
format("%05.2f", 3.14159)             # "03.14"   (sprintf is the same method)
format("%-10s|", "left")              # "left      |"
format("%s scored %d%%", name, 90)
"%s is %d" % ["Ada", 36]              # % operator form

12345.to_s.reverse.scan(/\d{1,3}/).join(",").reverse   # "12,345"
"%.2f" % 1234.5                       # "1234.50"

"a,b".split(",")                      # ["a", "b"]
["a", "b"].join(", ")
"hello".center(11, "*")               # "***hello***"
"7".rjust(3, "0")                     # "007"
"abc".ljust(5)

s.each_char, s.scan(/\w+/)            # tokenise
"CamelCase".gsub(/([a-z])([A-Z])/, '\1_\2').downcase   # camel → snake

<<~TEXT                               # squiggly heredoc strips indentation
  Dear #{name},
    indented line
TEXT
```

## Struct & Data

```ruby
Point = Struct.new(:x, :y) do         # mutable value object, free ==, to_a, each
  def distance = Math.sqrt(x**2 + y**2)
end
p1 = Point.new(3, 4)
p1.x; p1.x = 5; p1.to_a; p1 == Point.new(5, 4)

Named = Struct.new(:x, :y, keyword_init: true)
Named.new(x: 1, y: 2)

Coord = Data.define(:lat, :lng)       # 3.2+: immutable, keyword or positional
c = Coord.new(lat: 48.8, lng: 2.3)
c.with(lat: 45.0)                     # a new copy, one field changed
c.to_h                                # {lat: 48.8, lng: 2.3}

# Reach for Data over a bare Hash the moment a shape has a name.
```

## Mutation, dup & freeze

```ruby
# Convention: `!` marks the mutating/dangerous twin of a safe method.
a.sort   # new array          a.sort!   # sorts in place, nil if nothing changed
s.upcase # new string         s.upcase! # in place
h.merge  # new hash           h.merge!  # in place

b = a.dup                  # shallow copy — nested objects are shared
b = a.clone                # dup + keeps frozen state and singleton methods
deep = Marshal.load(Marshal.dump(a))   # crude deep copy

CONFIG = { host: "x" }.freeze          # freeze constants: mutation now raises
CONFIG.frozen?
s = +"mutable"                         # unary + gives an unfrozen string
s = -"interned"                        # unary - gives a frozen, deduped one

# `each` over a collection you are mutating is a classic bug — build a new one:
kept = items.reject { |i| i.expired? }
```

## Time & dates

```ruby
require "time"
require "date"

Time.now                              # local
Time.now.utc                          # always store and compare in UTC
Time.now.to_i                         # unix epoch
Time.at(1_700_000_000)

Time.now.strftime("%Y-%m-%d %H:%M:%S")    # format
Time.parse("2026-08-26 14:00")            # lenient parse
Time.iso8601("2026-08-26T14:00:00Z")      # strict
Time.now.iso8601                          # serialise

Date.today, Date.today - 7, (Date.today >> 1)   # a week ago, a month ahead
Date.new(2026, 8, 26).strftime("%d/%m/%Y")
(Date.new(2026, 1, 1)..Date.new(2026, 1, 5)).to_a

elapsed = Time.now - start            # a Float, in seconds
Process.clock_gettime(Process::CLOCK_MONOTONIC)   # for measuring durations
```

## Regex quickies

```ruby
"a1b2" =~ /\d/                        # 1 — index of the first match, else nil
"a1b2".match?(/\d/)                   # true — fastest, allocates nothing
"a1b2"[/\d+/]                         # "1" — the matched text, or nil
"a1b2".scan(/\d/)                     # ["1", "2"] — every match
"a1b2".gsub(/\d/) { |d| d.to_i + 1 }  # block gets each match

m = "2026-08-26".match(/(?<y>\d{4})-(?<m>\d{2})/)
m[:y], m[:m], m[0], m.pre_match, m.post_match

if "id=42" =~ /id=(\d+)/
  $1                                  # "42" — also $~, $` and $'
end

"a-b".split(/[-_]/)
text.gsub(/\s+/, " ").strip           # squeeze whitespace

/foo/i     # case-insensitive
/^x/       # start of *line*   — \A is start of string
/x$/       # end of *line*     — \z is end of string (\Z allows a trailing \n)
/a.+?b/    # lazy quantifier
Regexp.escape(user_input)             # never interpolate raw input into a regex
```

## Misc that saves time

```ruby
x.tap { |v| puts v }                  # peek inside a chain, returns x
x.then { |v| v * 2 }                  # pipe x into a block, returns the result

require "pp"; pp object               # pretty-print a nested structure
p object                              # inspect + newline, returns the object
puts object                           # to_s, returns nil

defined?(foo)                         # "local-variable", "method", nil...
__method__                            # the current method's name
caller                                # the backtrace, as strings
binding.irb                           # drop into a REPL right here

ObjectSpace.count_objects             # allocation counts
require "benchmark"
Benchmark.realtime { work }

Comparable, Enumerable                # include these + <=> or each and get the rest free
obj.frozen?, obj.object_id, obj.hash
obj.instance_variables
User.instance_methods(false)          # methods defined on User itself

ENV.fetch("DATABASE_URL")             # fetch, not [] — fail loudly on a missing var
ARGV                                  # command-line args
$stdout.sync = true                   # unbuffered output in a container
abort "message"                       # print to stderr and exit 1
at_exit { cleanup }
```

## Gems, bundler & tooling

```bash
ruby -v
ruby -e 'puts 1 + 1'              # one-liner
ruby -rjson -e 'puts JSON.parse(STDIN.read)["a"]'   # -r requires a lib
irb                               # REPL

bundle init                       # create a Gemfile
bundle add rspec --group development
bundle install
bundle update rails               # one gem only
bundle exec rspec                 # run inside the bundle
bundle outdated

gem install rubocop
rubocop -a                        # autocorrect the safe cops
rubocop -A                        # ...and the unsafe ones

rspec                             # run the suite
rspec spec/models/user_spec.rb:42 # one example, by line
ruby -Ilib -e 'require "mygem"'
```

## Naming & style conventions

| Convention | Means |
| --- | --- |
| `snake_case` | methods, variables, files |
| `CamelCase` | classes and modules |
| `SCREAMING_SNAKE` | constants (freeze them) |
| `name?` | returns a boolean |
| `name!` | the dangerous twin: mutates, raises, or both |
| `name=` | a setter, called as `obj.name = x` |
| `@ivar` / `@@cvar` / `$global` | instance / class / global variable |
| `_unused` | a parameter you must accept but won't use |

- Two-space indent, no tabs. `do...end` for multi-line blocks, `{ }` for one-liners.
- Prefer `each`/`map` over `for`; prefer guard clauses over nesting.
- `unless` for a simple negative, never with `else` and never with `&&`/`||`.
- Methods return their last expression: an explicit `return` is for early exits only.
- Bang methods often return `nil` when nothing changed — never chain off `sort!`.
