# Ruby Basics Cheat Sheet

## Strings

```ruby
s = "hello"
s.length                    # 5
s.upcase, s.downcase, s.capitalize, s.swapcase
s.strip, s.lstrip, s.rstrip # trim whitespace
s.chomp                     # drop a trailing newline only
s.chars                     # ["h", "e", "l", "l", "o"]
s.bytes, s.lines            # bytes / lines as arrays

"a-b-c".split("-")          # ["a", "b", "c"]
%w[a b c].join("-")         # "a-b-c"
"ab" * 3                    # "ababab"
"ab" + "cd"                 # "abcd"  (both sides must be strings)

s[0], s[-1], s[1..3], s[1, 2]   # index, from the end, range, offset+length
s.start_with?("he"), s.end_with?("lo")
s.include?("ell")
s.index("l")                # 2  (nil if absent)

s.sub("l", "L")             # first match
s.gsub("l", "L")            # every match
s.tr("el", "ip")            # char-by-char translation
s.delete("l"), s.squeeze    # "heo", "helo"

# Interpolation only works in double quotes
name = "world"
"hello #{name}"             # "hello world"
'hello #{name}'             # literal, no interpolation

# Multi-line: heredoc. `<<~` strips the common indentation.
text = <<~SQL
  select *
  from users
SQL

s.to_sym, :hello.to_s
"42".to_i, "3.14".to_f, 42.to_s, 255.to_s(2)   # "11111111"
"abc".frozen?               # true — string literals are frozen in 3.x with the magic comment
```

## Symbols

```ruby
:name                       # an interned, immutable name — cheap to compare
:name.to_s                  # "name"
"name".to_sym               # :name

# Symbols are the default for hash keys, method names, and enum-ish values
{ status: :active }         # => {status: :active}
[1, 2, 3].map(&:to_s)       # symbol-to-proc: same as { |n| n.to_s }

user.respond_to?(:save)
user.send(:save)            # call by name (public_send skips private methods)
```

## Numbers

```ruby
7 / 2                       # 3   — integer division
7.0 / 2                     # 3.5
7.fdiv(2)                   # 3.5
7 % 3, -7 % 3               # 1, 2  (sign follows the divisor)
7.divmod(3)                 # [2, 1]
2 ** 10                     # 1024

3.7.round, 3.7.floor, 3.7.ceil        # 4, 3, 4
3.14159.round(2)            # 3.14
1234.5678.truncate(2)       # 1234.56

10.times { |i| ... }        # 0..9
1.upto(5) { |i| ... }
5.downto(1) { |i| ... }
1.step(10, 3).to_a          # [1, 4, 7, 10]

rand(100)                   # 0..99
rand(1.0..2.0)
Integer("42"), Float("1.5") # strict: raises on garbage, unlike to_i

1_000_000                   # underscores are ignored
0xff, 0b1010, 0o755         # 255, 10, 493

require "bigdecimal/util"
"0.1".to_d + "0.2".to_d     # exact money maths — never use Float for money
```

## Arrays

```ruby
a = [3, 1, 2]
a = Array.new(3, 0)         # [0, 0, 0]
a = Array.new(3) { |i| i * i }  # [0, 1, 4]
%w[a b c]                   # ["a", "b", "c"]   — %i[a b c] gives symbols

a.first, a.last, a[-2]
a.first(2), a.last(2)       # slices, not just one element
a[1..], a[0, 2], a.dig(0, 1)

a << 4                      # push (returns the array, so it chains)
a.push(4, 5); a.pop         # end
a.unshift(0); a.shift       # front
a.insert(1, :x)
a.delete(3)                 # by value
a.delete_at(0)              # by index
a.delete_if { |x| x.odd? }  # in place

a.sort, a.sort!             # ! mutates and returns self
a.reverse, a.uniq, a.flatten, a.compact   # compact drops nils
a.sum, a.min, a.max, a.minmax
a.include?(2), a.index(2), a.count(2)
a.sample, a.shuffle
a.each_slice(2).to_a        # [[3, 1], [2]]
a.each_cons(2).to_a         # sliding window pairs

[1, 2] + [3]                # concat
[1, 2, 3] - [2]             # difference     => [1, 3]
[1, 2] & [2, 3]             # intersection   => [2]
[1, 2] | [2, 3]             # union, uniq    => [1, 2, 3]
[1, 2].product([3, 4])      # [[1, 3], [1, 4], [2, 3], [2, 4]]
[[1, 2], [3, 4]].transpose  # [[1, 3], [2, 4]]
[1, 2].zip([3, 4])          # [[1, 3], [2, 4]]

a.empty?, a.any?, a.all?, a.none?, a.one?
```

## Hashes

```ruby
h = { name: "Ada", age: 36 }            # symbol keys (the usual)
h = { "name" => "Ada" }                 # any object can be a key
h = Hash.new(0)                         # default value for missing keys
h = Hash.new { |hash, k| hash[k] = [] } # default *and* store it

h[:name]                    # "Ada"      (nil if missing)
h.fetch(:name)              # raises KeyError if missing
h.fetch(:city, "n/a")       # with a default
h.dig(:user, :address, :city)           # nil-safe nesting

h[:city] = "London"
h.store(:city, "London")
h.delete(:age)
h.key?(:name), h.value?(36)
h.keys, h.values, h.size
h.to_a                      # [[:name, "Ada"], ...]

h.each { |key, value| ... }
h.map { |k, v| [k, v.to_s] }.to_h
h.transform_values(&:to_s)
h.transform_keys(&:to_s)
h.select { |k, v| v.is_a?(String) }     # filter is an alias
h.reject { |k, _| k == :age }
h.sum { |_, v| v.to_i }
h.min_by { |_, v| v }

h.merge(other)              # new hash, other wins on conflict
h.merge(other) { |key, mine, theirs| mine + theirs }
h.merge!(other)             # in place
h.slice(:name, :age)        # keep only these
h.except(:age)              # drop these
h.any? { |k, v| v.nil? }
h.group_by { |k, v| v.class }
```

## Ranges

```ruby
(1..5).to_a                 # [1, 2, 3, 4, 5]   — inclusive
(1...5).to_a                # [1, 2, 3, 4]      — exclusive end
("a".."e").to_a             # ["a", "b", "c", "d", "e"]

(1..10).include?(5)         # cover? is faster for numeric ranges
(1..10).step(2).to_a        # [1, 3, 5, 7, 9]
(1..Float::INFINITY).lazy.map { |n| n * 2 }.first(3)   # [2, 4, 6]

(1..)                       # beginless/endless ranges: (..10), (1..)
array[2..]                  # from index 2 to the end

case age
when 0..12  then :child
when 13..17 then :teen
else             :adult
end
```

## Conditionals

```ruby
if x > 0
  :positive
elsif x < 0
  :negative
else
  :zero
end

puts "ok" if valid?         # modifier form — reads best on one short line
puts "no" unless valid?     # unless == if not (never use unless/else)

# Everything except nil and false is truthy: 0, "" and [] are all true.
value = maybe_nil || "default"
value ||= "default"         # assign only if nil/false
count = h[:n] ||= 0

x = condition ? "yes" : "no"

case status
when :active, :trial then charge!
when String          then puts "got a string"
when /^adm/          then :admin
else                      :unknown
end

# case with no subject reads like a cond
label =
  case
  when score > 90 then "A"
  when score > 80 then "B"
  else                 "C"
  end
```

## Loops

```ruby
5.times { |i| puts i }
[1, 2, 3].each { |n| puts n }
[1, 2, 3].each_with_index { |n, i| puts "#{i}: #{n}" }
h.each_pair { |k, v| puts "#{k}=#{v}" }

# for/while exist but blocks are the idiom; reach for while only for
# genuinely unbounded loops.
while queue.any?
  item = queue.shift
  next if item.nil?         # skip
  break if item == :stop    # exit
  redo                      # rerun this iteration (rare)
end

until done? do work end
loop do                     # infinite; StopIteration breaks it cleanly
  break unless more?
end

3.times.map { |i| i * 2 }   # [0, 2, 4] — times returns an enumerator
```

## Methods & arguments

```ruby
def greet(name)
  "hello #{name}"           # the last expression is the return value
end

def greet(name = "world")   # default
def sum(*nums)              # splat: any number of positional args
def config(**opts)          # double splat: keyword args as a hash
def each_row(&block)        # capture the block as a proc

def create(name:, age: 0)   # required and optional keywords
create(name: "Ada")

args = ["a", "b"]
opts = { age: 3 }
greet(*args)                # splat on the way in
create(**opts)

def find(id) = Record.all[id]     # endless method (3.0+), for one-liners

def valid?(x) = !x.nil?     # `?` → returns a boolean
def save!                   # `!` → the dangerous/mutating variant
def name=(value)            # `=` → assignment: obj.name = "x"
  @name = value
end

# Multiple return values are just an array
def min_max(a) = [a.min, a.max]
lo, hi = min_max([3, 1, 2])
```

## Classes

```ruby
class User
  attr_reader :name         # defines name
  attr_writer :email        # defines email=
  attr_accessor :age        # both

  ADMIN_ROLES = %i[owner admin].freeze   # constant

  def initialize(name, age: 0)
    @name = name            # instance variable
    @age = age
  end

  def self.build(attrs)     # class method
    new(attrs[:name])
  end

  def to_s = "#{@name} (#{@age})"        # used by string interpolation

  def <=>(other) = age <=> other.age     # gives you sort, min, max...
  include Comparable                     # ...and < > == between?

  private

  def secret = "hidden"     # everything below `private` is private
end

user = User.new("Ada", age: 36)
user.name
user.instance_variable_get(:@name)       # escape hatch, avoid in real code

class Admin < User          # inheritance
  def initialize(name)
    super(name, age: 0)     # bare `super` forwards the same args
  end
end

user.is_a?(User), user.instance_of?(User), User.ancestors
user.class, user.respond_to?(:name)
```

## Modules & mixins

```ruby
module Greetable
  def greet = "hi, #{name}"       # instance methods for the includer
end

module Countable
  def self.included(base)         # hook
    base.extend(ClassMethods)
  end

  module ClassMethods
    def count = all.size
  end
end

class User
  include Greetable               # adds instance methods
  extend  Countable               # adds class methods
  prepend Auditing                # inserts *before* the class in the chain
end

# Modules are also namespaces
module Billing
  class Invoice; end
  RATE = 0.2
end
Billing::Invoice.new
Billing::RATE

module_function                   # in a module: make the methods callable as Billing.foo
```

## Exceptions

```ruby
begin
  risky!
rescue ArgumentError, TypeError => e
  warn e.message
rescue StandardError => e         # never rescue Exception: it catches ^C and NoMemoryError
  report(e)
  raise                           # re-raise the same error, backtrace intact
else
  puts "no error"
ensure
  file&.close                     # always runs
end

# Method-level rescue — no begin needed
def fetch
  http_get
rescue Timeout::Error
  nil
end

raise ArgumentError, "id required"
raise MyError.new("boom")

class MyError < StandardError
  def initialize(msg = "something broke") = super
end

value = Integer(input) rescue 0   # inline rescue: terse, but swallows everything

# Retries
attempts = 0
begin
  call_api
rescue Net::OpenTimeout
  retry if (attempts += 1) < 3
  raise
end
```

## Files & paths

```ruby
require "pathname"

File.read("a.txt")                   # whole file as a string
File.readlines("a.txt", chomp: true) # array of lines, no newlines
File.write("a.txt", "content")       # truncates; mode: "a" to append

File.open("a.txt", "w") do |f|       # block form closes the file for you
  f.puts "line"
end

File.foreach("big.log") { |line| ... }   # streams, constant memory

File.exist?("a.txt"), File.directory?("dir"), File.size("a.txt")
File.basename("/a/b.txt", ".txt")    # "b"
File.extname("/a/b.txt")             # ".txt"
File.dirname("/a/b.txt")             # "/a"
File.join("a", "b", "c.txt")         # "a/b/c.txt"
File.expand_path("../c", __dir__)

Dir.glob("**/*.rb")                  # recursive
Dir.children("dir")                  # names, without . and ..

require "fileutils"
FileUtils.mkdir_p("a/b/c")
FileUtils.cp("a", "b"); FileUtils.mv("a", "b"); FileUtils.rm_f("a")

path = Pathname("config") / "app.yml"    # Pathname composes with /
path.exist?, path.read, path.each_line

require "json"
data = JSON.parse(File.read("a.json"), symbolize_names: true)
File.write("a.json", JSON.pretty_generate(data))

require "csv"
CSV.foreach("a.csv", headers: true) { |row| row["name"] }
```
