// A Developer's Field Guide

Ruby.

A dynamic, object-oriented language designed for programmer happiness. Everything is an object. Blocks are magic. The syntax reads like prose.

Created by Yukihiro "Matz" Matsumoto · 1995 | Current stable: Ruby 3.3+ | Philosophy: "The Principle of Least Surprise"

Philosophy & Key Differences

Ruby is a fully object-oriented, dynamically typed, interpreted language. If you're coming from Python, JavaScript, or Java, here's what sets Ruby apart:

ConceptRuby's Takevs. Others
Everything is an object1.classInteger
nil.classNilClass
Python primitives, Java int/float are not objects
No semicolons neededNewline ends a statementJavaScript/Java require ;
BlocksAnonymous code chunks passed to methodsCloser to JS callbacks but with cleaner syntax
Symbols:name — immutable identifiers, not stringsUnique to Ruby (Python has no direct equivalent)
Open classesYou can reopen and modify any class, including built-insMost languages lock built-in types
Expressive conditionalsreturn if condition (postfix)Most languages require wrapping in braces
TruthinessOnly false and nil are falsy — 0 and "" are truthy!Python/JS treat 0 and "" as falsy

Setup & Hello World

Installation

# macOS (via Homebrew)
brew install ruby

# Windows — use RubyInstaller: https://rubyinstaller.org

# Linux
sudo apt install ruby-full

# Recommended: use a version manager (rbenv or rvm)
rbenv install 3.3.0 && rbenv global 3.3.0

# Verify
ruby --version
irb --version       # interactive Ruby REPL

Hello World

helloworld.rb# Single-line output
puts "Hello, World!"

# With string interpolation
name = "Ruby"
puts "Hello from #{name}!"

# print does NOT add a newline; puts does
print "No newline here"
p "Debugging: shows quotes and type info"
# Run it:
ruby helloworld.rb

Core Syntax

Variables & Naming Conventions

Sigil / CaseTypeExample
lowercase / snake_caseLocal variableuser_name = "Alice"
@ prefixInstance variable@score = 0
@@ prefixClass variable@@count = 0
$ prefixGlobal variable$debug = false
UPPER_CASE or CamelCase startConstantMAX_SCORE = 100
CamelCaseClass / Module nameclass PlayerStats

Comments

# This is a single-line comment

=begin
  This is a multi-line comment block.
  Rarely used — most Rubyists prefer # on each line.
=end

String Literals

# Double quotes: interpolation + escape sequences
greeting = "Hello, #{name}!\n"

# Single quotes: literal — NO interpolation, NO \n
literal  = 'Hello, #{name}!\n'   # prints exactly as-is

# Heredoc: multi-line strings
poem = <<~TEXT
  Roses are red,
  Ruby is great.
TEXT

# Useful string methods
"hello".upcase          # => "HELLO"
"  hello  ".strip      # => "hello"
"hello world".split   # => ["hello", "world"]
"hello".include?("ell") # => true
"hello".gsub("l", "r") # => "herro"
"42".to_i              # => 42  (convert to Integer)
42.to_s               # => "42" (convert to String)

Operators

Arithmetic

10 + 3   # => 13
10 - 3   # => 7
10 * 3   # => 30
10 / 3   # => 3  (integer division!)
10.0 / 3 # => 3.333...
10 % 3   # => 1  (modulo)
2 ** 10  # => 1024 (exponent)

Comparison & Logic

==  !=  >  <  >=  <=
&&  ||  !
and or not  # lower precedence

# Spaceship operator — returns -1, 0, or 1
3 <=> 5    # => -1

# Safe navigation (no NoMethodError on nil)
user&.name  # => nil if user is nil

Assignment Shorthand

x += 1    # x = x + 1
x -= 1
x *= 2
x **= 2

# Conditional assignment (very idiomatic Ruby)
x ||= 10   # assign 10 only if x is nil or false
x &&= 20   # assign 20 only if x is already truthy

# Multiple assignment
a, b, c = 1, 2, 3
a, b = b, a        # swap! (no temp variable needed)
first, *rest = [1, 2, 3, 4]  # first=1, rest=[2,3,4]

Data Types

Numbers

42          # Integer
3.14        # Float
1_000_000   # Underscores allowed for readability
0xFF        # Hex = 255
0b1010      # Binary = 10

42.even?    # => true
7.odd?     # => true
-5.abs     # => 5
3.7.ceil   # => 4
3.7.floor  # => 3
3.7.round  # => 4

Symbols

Symbols are immutable, unique identifiers. Unlike strings, the same symbol is always the same object in memory. Use them for keys, method names, and status flags.

:name       # A symbol
:status.to_s  # => "status"
"status".to_sym # => :status

# Symbols vs Strings in memory
:foo.object_id == :foo.object_id  # => true (same object!)
"foo".object_id == "foo".object_id # => false (different objects)

Nil, True, False

nil    # Ruby's null — it IS an object (NilClass)
true   # TrueClass
false  # FalseClass

# Only nil and false are falsy. 0 is TRUTHY in Ruby!
puts "truthy" if 0    # => prints "truthy"
puts "truthy" if ""   # => prints "truthy"

Control Flow

if / elsif / else / unless

if score >= 90
  puts "A"
elsif score >= 80
  puts "B"
else
  puts "Try again"
end

# unless = "if not"
unless logged_in
  redirect_to_login
end

# POSTFIX (one-liners — very idiomatic!)
puts "Adult"  if     age >= 18
puts "Minor"  unless age >= 18

# Ternary operator
label = score > 50 ? "Pass" : "Fail"

case / when (Switch)

grade = "B"

case grade
when "A"        then puts "Excellent"
when "B", "C"  then puts "Good"
when "D"        then puts "Passing"
else                 puts "Failed"
end

# case uses === (triple equals) — so ranges work!
case age
when 0..12    then puts "Child"
when 13..17   then puts "Teen"
when 18..     then puts "Adult"
end

Loops

# while
i = 0
while i < 5
  puts i
  i += 1
end

# until = "while not"
until i == 10
  i += 1
end

# loop (infinite, break manually)
loop do
  input = gets.chomp
  break if input == "quit"
end

# for..in (rarely used — prefer iterators)
for i in 1..5
  puts i
end

# times, upto, downto — very Ruby-like
5.times  { |i| puts i }       # 0,1,2,3,4
1.upto(5)  { |i| puts i }    # 1,2,3,4,5
5.downto(1) { |i| puts i }   # 5,4,3,2,1

# Loop control
next   # like continue — skip to next iteration
break  # exit the loop
redo   # restart current iteration without re-checking condition

Collections: Arrays & Hashes

Arrays

fruits = ["apple", "banana", "cherry"]
mixed  = [1, "hello", :ok, true, nil]  # any types
words  = %w[apple banana cherry]           # shorthand string array
syms   = %i[red green blue]               # shorthand symbol array

# Accessing
fruits[0]       # => "apple"
fruits[-1]      # => "cherry" (negative index from end)
fruits[0,2]     # => ["apple","banana"] (start, length)
fruits[1..2]    # => ["banana","cherry"] (range)
fruits.first    # => "apple"
fruits.last     # => "cherry"

# Modifying
fruits.push("date")    # add to end (also: fruits << "date")
fruits.pop             # remove from end, returns it
fruits.unshift("avocado") # add to front
fruits.shift           # remove from front
fruits.insert(1, "blueberry") # insert at index
fruits.delete("banana")  # remove by value

# Useful methods
fruits.length      # number of elements
fruits.sort         # sorted copy
fruits.sort!        # sort in-place (! = mutates)
fruits.reverse
fruits.flatten     # flatten nested arrays
fruits.uniq        # remove duplicates
fruits.compact     # remove nil values
fruits.include?("apple")  # => true
fruits.join(", ")  # => "apple, banana, cherry"
fruits.count       # same as length
fruits.sample      # random element
fruits.shuffle     # randomize order

# Set operations
[1,2,3] + [3,4]    # => [1,2,3,3,4] (concatenate)
[1,2,3] - [2]     # => [1,3]       (difference)
[1,2] & [2,3]     # => [2]         (intersection)
[1,2] | [2,3]     # => [1,2,3]     (union)

Hashes (Key-Value Maps)

# Symbol keys (preferred modern style)
person = { name: "Alice", age: 30, city: "Atlanta" }

# Old "hash rocket" style (any key type)
old_style = { "name" => "Alice", 42 => "answer" }

# Accessing
person[:name]          # => "Alice"
person[:missing]       # => nil (no error)
person.fetch(:name)    # => "Alice"
person.fetch(:missing, "N/A") # => "N/A" (default)

# Modifying
person[:email] = "alice@example.com"  # add/update
person.delete(:city)                   # remove

# Useful methods
person.keys          # => [:name, :age, :email]
person.values        # => ["Alice", 30, "alice@..."]
person.has_key?(:age) # => true
person.merge({ role: "admin" })  # returns new merged hash
person.each { |key, val| puts "#{key}: #{val}" }

Methods

# Basic definition — no 'function' keyword
def greet(name)
  "Hello, #{name}!"   # last expression is the return value
end

# Explicit return
def divide(a, b)
  return nil if b == 0
  a.to_f / b
end

# Default arguments
def power(base, exp = 2)
  base ** exp
end
power(3)     # => 9
power(3, 3) # => 27

# Keyword arguments (named params)
def create_user(name:, age:, role: "member")
  "#{name} (#{age}) - #{role}"
end
create_user(name: "Bob", age: 25)

# Splat — variable number of args
def sum(*numbers)
  numbers.sum   # numbers is an Array
end
sum(1, 2, 3, 4)  # => 10

# Double splat — keyword args as hash
def configure(**options)
  options.each { |k, v| puts "#{k} = #{v}" }
end

# Method name conventions
valid?    # ? suffix = returns boolean (predicate)
save!     # ! suffix = dangerous/mutates (bang method)

✦ Tip

In Ruby, parentheses on method calls are optional when unambiguous: puts "hello" and puts("hello") are identical. Most style guides omit parens for simple calls.

Blocks, Procs & Lambdas

Blocks are Ruby's most distinctive feature — anonymous chunks of code you pass to a method. Think of them like inline callbacks.

Blocks

# { } for single-line blocks
[1, 2, 3].each { |n| puts n }

# do..end for multi-line blocks
[1, 2, 3].each do |n|
  squared = n ** 2
  puts "#{n} squared = #{squared}"
end

# Yielding — calling a block from inside a method
def repeat(n)
  n.times { yield }
end
repeat(3) { puts "Hello!" }  # prints 3 times

# Pass arguments to block via yield
def transform(x)
  yield(x) if block_given?
end
transform(5) { |n| n * 2 }   # => 10

Key Enumerable Methods (powered by blocks)

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

nums.map    { |n| n * 2 }    # => [2,4,6,8,10]  (transform)
nums.select { |n| n.even? }  # => [2,4]          (filter)
nums.reject { |n| n.even? }  # => [1,3,5]        (opposite filter)
nums.find   { |n| n > 3 }    # => 4              (first match)
nums.all?  { |n| n > 0 }    # => true           (all pass?)
nums.any?  { |n| n > 4 }    # => true           (any pass?)
nums.none? { |n| n > 10 }   # => true
nums.count { |n| n.odd? }   # => 3
nums.sum                     # => 15
nums.min                     # => 1
nums.max                     # => 5
nums.sort_by { |n| -n }      # => [5,4,3,2,1] (descending)
nums.flat_map { |n| [n, n] } # => [1,1,2,2,3,3,4,4,5,5]

# reduce / inject — fold into a single value
nums.reduce(0) { |sum, n| sum + n }  # => 15
nums.reduce(:+)                      # same, using symbol shorthand

# each_with_object — build a result while iterating
result = nums.each_with_object({}) do |n, hash|
  hash[n] = n ** 2
end
# => {1=>1, 2=>4, 3=>9, 4=>16, 5=>25}

Procs & Lambdas (Stored Blocks)

# Proc — a saved block
double = Proc.new { |n| n * 2 }
double.call(5)   # => 10
double.(5)       # shorthand

# Lambda — stricter Proc (checks arg count, own return scope)
triple = lambda { |n| n * 3 }
triple = ->(n) { n * 3 }   # => stabby lambda syntax (preferred)
triple.call(5)   # => 15

# Convert method to block with &method(:name)
["1", "2", "3"].map(&method(:puts))

# & converts a symbol to a block — very idiomatic!
["hello", "world"].map(&:upcase)  # => ["HELLO", "WORLD"]
[1, nil, 2, nil].select(&:itself) # => [1, 2]

Object-Oriented Programming

class Animal
  # Class-level constant
  KINGDOM = "Animalia"

  # attr_accessor creates getter + setter methods
  # attr_reader = read only, attr_writer = write only
  attr_accessor :name, :age

  # Constructor
  def initialize(name, age)
    @name = name
    @age  = age
  end

  # Instance method
  def speak
    "..."
  end

  # to_s — used when object is printed
  def to_s
    "#{@name} (age #{@age})"
  end

  # Class method (self. prefix)
  def self.kingdom
    KINGDOM
  end
end

# Inheritance
class Dog < Animal
  attr_accessor :breed

  def initialize(name, age, breed)
    super(name, age)   # call parent constructor
    @breed = breed
  end

  def speak
    "Woof!"
  end

  def to_s
    "#{super} [#{@breed}]"   # call parent to_s
  end
end

rex = Dog.new("Rex", 3, "Labrador")
puts rex             # => "Rex (age 3) [Labrador]"
puts rex.speak        # => "Woof!"
puts rex.is_a?(Animal) # => true
puts rex.class        # => Dog
puts Dog.kingdom      # => "Animalia"

Comparable & Enumerable (Mixin Modules)

class Box
  include Comparable
  attr_accessor :volume

  def initialize(v) = @volume = v

  # Define <=> and you get <, >, ==, between?, clamp, sort for free
  def <=>(other)
    @volume <=> other.volume
  end
end

boxes = [Box.new(5), Box.new(2), Box.new(8)]
boxes.sort.map(&:volume)  # => [2, 5, 8]
boxes.max.volume          # => 8

Modules & Mixins

Modules serve two purposes: namespacing (grouping related code) and mixins (sharing behavior across classes without inheritance).

# Module as a mixin
module Greetable
  def greet
    "Hi, I'm #{name}"
  end
end

module Farewell
  def bye
    "Goodbye from #{name}!"
  end
end

class Person
  include Greetable    # adds instance methods
  include Farewell
  extend  SomeModule   # adds class methods (not shown)

  attr_reader :name
  def initialize(name) = @name = name
end

alice = Person.new("Alice")
alice.greet  # => "Hi, I'm Alice"
alice.bye    # => "Goodbye from Alice!"

# Module as namespace
module Payments
  class Invoice
    def generate; ...; end
  end

  class Receipt
    def print; ...; end
  end
end

inv = Payments::Invoice.new

Exception Handling

begin
  result = 10 / 0                    # raises ZeroDivisionError
rescue ZeroDivisionError => e
  puts "Division error: #{e.message}"
rescue TypeError, ArgumentError => e
  puts "Type/Arg error: #{e.message}"
rescue => e                            # catch-all StandardError
  puts "Unexpected: #{e.message}"
else
  puts "Success! Result = #{result}"  # runs only if no exception
ensure
  puts "Always runs (like finally)"
end

# raise your own exceptions
raise ArgumentError, "Age must be positive" if age < 0

# Custom exception class
class InsufficientFundsError < StandardError
  def initialize(amount)
    super("Need $#{amount} more")
  end
end

# retry — inside rescue, attempt the block again
attempts = 0
begin
  attempts += 1
  risky_operation
rescue NetworkError
  retry if attempts < 3
end

File I/O & User Input

# User input from terminal
print "Enter your name: "
name = gets.chomp   # gets reads a line; chomp strips the \n

# Reading files
content = File.read("data.txt")          # whole file as string
lines   = File.readlines("data.txt")     # array of lines

File.foreach("data.txt") do |line|      # memory-efficient
  puts line.chomp
end

# Writing files
File.write("output.txt", "Hello!\n")   # write (overwrites)

File.open("output.txt", "a") do |f|   # "a" = append mode
  f.puts "Another line"
end   # file auto-closes after block

# Check file/directory existence
File.exist?("data.txt")
Dir.exist?("./output")
Dir.mkdir("./output")
Dir.glob("*.rb")    # list all .rb files

The Ruby Adventure: A Complete Beginner Program

📚 Library Management System

A terminal app that combines classes, modules, file I/O, exceptions, iterators, hashes, symbols, and blocks — covering nearly every core Ruby concept in one cohesive, real-world program.

This program is a mini library system. It deliberately touches: classes with inheritance, modules/mixins, attr_accessor, symbols, hashes, arrays, iterators (map/select/sort_by), blocks, exception handling, file persistence, user input, constants, class methods, predicate methods, and string interpolation. Each section is annotated.

library.rb#!/usr/bin/env ruby
# ==============================================
# RUBY LIBRARY MANAGEMENT SYSTEM
# A comprehensive beginner's Ruby program
# ==============================================

# ── MODULES ──────────────────────────────────
# Modules as mixins: shared behavior across classes
module Displayable
  def display_header(title)
    width = 50
    puts "─" * width
    puts " #{title}"
    puts "─" * width
  end
end

module Saveable
  DATA_FILE = "library_data.txt"   # module-level constant

  def save_to_file(data)
    File.open(DATA_FILE, "w") do |f|
      data.each { |line| f.puts line }
    end
    puts "💾 Saved #{data.length} records to #{DATA_FILE}"
  end

  def load_from_file
    return [] unless File.exist?(DATA_FILE)
    File.readlines(DATA_FILE).map(&:chomp)
  end
end

# ── BASE CLASS ────────────────────────────────
class LibraryItem
  include Comparable       # mixin: gives us <, >, sort
  include Displayable      # our custom mixin

  attr_accessor :title, :year, :status
  attr_reader   :id        # read-only

  @@item_count = 0         # class variable: shared across all instances

  def initialize(title, year)
    @title  = title
    @year   = year
    @status = :available   # Symbols as state flags!
    @@item_count += 1
    @id = @@item_count
  end

  # Defines sort order via Comparable mixin
  def <=>(other)
    @title <=> other.title
  end

  # Predicate methods (? suffix = returns boolean)
  def available? = @status == :available
  def checked_out? = @status == :checked_out

  # Class method (called on LibraryItem, not instances)
  def self.total_items
    @@item_count
  end

  def status_icon
    @status == :available ? "✅" : "❌"
  end

  def to_s
    "[#{@id}] #{@title} (#{@year}) #{status_icon}"
  end
end

# ── SUBCLASSES (Inheritance) ──────────────────
class Book < LibraryItem
  attr_accessor :author, :genre, :pages

  def initialize(title, author, year, genre: :fiction, pages: 0)
    super(title, year)   # call parent initialize
    @author = author
    @genre  = genre      # keyword arg with default symbol
    @pages  = pages
  end

  def long_read? = @pages > 400

  def to_s
    "#{super} | Book by #{@author} | #{@genre} | #{@pages}pp"
  end

  # Serialise to a pipe-delimited string for file storage
  def serialize
    ["Book", @title, @author, @year, @genre, @pages, @status].join("|")
  end
end

class DVD < LibraryItem
  attr_accessor :director, :runtime_mins

  def initialize(title, director, year, runtime_mins:)
    super(title, year)
    @director    = director
    @runtime_mins = runtime_mins
  end

  def runtime_formatted
    h, m = @runtime_mins.divmod(60)   # multiple assignment!
    "#{h}h #{m}m"
  end

  def to_s
    "#{super} | DVD by #{@director} | #{runtime_formatted}"
  end

  def serialize
    ["DVD", @title, @director, @year, @runtime_mins, @status].join("|")
  end
end

# ── LIBRARY CLASS ─────────────────────────────
class Library
  include Displayable
  include Saveable

  attr_reader :name

  def initialize(name)
    @name  = name
    @items = []   # Array of LibraryItem objects
  end

  def add_item(item)
    @items << item     # << is the shovel/append operator
    puts "✅ Added: #{item.title}"
  end

  def checkout(id)
    item = find_by_id(id)
    raise ArgumentError, "Item ##{id} not found" unless item
    raise RuntimeError,  "'#{item.title}' is already checked out" if item.checked_out?

    item.status = :checked_out
    puts "📤 Checked out: #{item.title}"
  end

  def return_item(id)
    item = find_by_id(id)
    raise ArgumentError, "Item ##{id} not found" unless item
    raise RuntimeError,  "'#{item.title}' is not checked out" if item.available?

    item.status = :available
    puts "📥 Returned: #{item.title}"
  end

  # search using select (filter) and any? — block-powered
  def search(query)
    query = query.downcase
    @items.select do |item|
      item.title.downcase.include?(query) ||
        (item.is_a?(Book) && item.author.downcase.include?(query))
    end
  end

  def available_items
    @items.select(&:available?)  # & converts symbol to block
  end

  def books    = @items.select { |i| i.is_a?(Book) }
  def dvds     = @items.select { |i| i.is_a?(DVD) }
  def sorted   = @items.sort        # uses <=> from Comparable
  def by_year  = @items.sort_by(&:year)

  # Statistics using reduce and map
  def stats
    {
      total:        @items.count,
      available:    @items.count(&:available?),
      checked_out:  @items.count(&:checked_out?),
      books:        books.count,
      dvds:         dvds.count,
      avg_book_pages: (books.map(&:pages).sum.to_f / [books.count, 1].max).round(1)
    }
  end

  def display_all
    display_header("📚 #{@name} — All Items")
    if @items.empty?
      puts "  No items in library."
    else
      sorted.each_with_index do |item, idx|
        puts "  #{idx + 1}. #{item}"
      end
    end
    puts
  end

  def display_stats
    s = stats
    display_header("📊 Library Statistics")
    s.each do |key, value|
      puts "  #{key.to_s.ljust(20)} #{value}"
    end
    puts
  end

  def save
    save_to_file(@items.map(&:serialize))
  end

  private   # methods below are private

  def find_by_id(id)
    @items.find { |item| item.id == id }
  end
end

# ── MAIN PROGRAM ──────────────────────────────
def main
  lib = Library.new("Ruby City Public Library")

  # Populate with seed data
  lib.add_item Book.new("The Pragmatic Programmer", "Hunt & Thomas", 1999,
                        genre: :tech, pages: 352)
  lib.add_item Book.new("Eloquent Ruby", "Russ Olsen", 2011,
                        genre: :tech, pages: 448)
  lib.add_item Book.new("Dune", "Frank Herbert", 1965,
                        genre: :scifi, pages: 688)
  lib.add_item Book.new("1984", "George Orwell", 1949,
                        genre: :dystopia, pages: 328)
  lib.add_item DVD.new("Blade Runner 2049", "Denis Villeneuve", 2017,
                       runtime_mins: 163)
  lib.add_item DVD.new("The Matrix", "The Wachowskis", 1999,
                       runtime_mins: 136)

  puts
  lib.display_all
  lib.display_stats

  # Checkout and return flow with exception handling
  puts "─" * 50
  puts "CHECKOUT DEMO"
  puts "─" * 50
  lib.checkout(1)
  lib.checkout(3)

  begin
    lib.checkout(1)   # already checked out → raises RuntimeError
  rescue RuntimeError => e
    puts "⚠️  #{e.message}"
  end

  begin
    lib.checkout(99)  # doesn't exist → raises ArgumentError
  rescue ArgumentError => e
    puts "⚠️  #{e.message}"
  end

  lib.return_item(1)
  puts

  # Search
  puts "─" * 50
  puts "SEARCH RESULTS FOR 'ruby'"
  puts "─" * 50
  results = lib.search("ruby")
  results.each { |item| puts "  → #{item}" }
  puts

  # Functional-style reporting with blocks and iterators
  puts "─" * 50
  puts "LONG READS (books over 400 pages)"
  puts "─" * 50
  lib.books
     .select(&:long_read?)
     .sort_by(&:pages)
     .map { |b| "  #{b.title} — #{b.pages} pages" }
     .each(&method(:puts))
  puts

  # Group by genre using each_with_object (hash building)
  puts "─" * 50
  puts "BOOKS BY GENRE"
  puts "─" * 50
  genres = lib.books.each_with_object(Hash.new { |h, k| h[k] = [] }) do |book, hash|
    hash[book.genre] << book.title
  end
  genres.each do |genre, titles|
    puts "  :#{genre} → #{titles.join(', ')}"
  end
  puts

  # Save to file
  lib.save

  # Final updated display
  lib.display_all
  lib.display_stats
end

# Guard clause: only run main if this file is executed directly
# (not required/imported by another file)
main if __FILE__ == $PROGRAM_NAME

Expected Output

✅ Added: The Pragmatic Programmer
✅ Added: Eloquent Ruby
✅ Added: Dune
✅ Added: 1984
✅ Added: Blade Runner 2049
✅ Added: The Matrix

──────────────────────────────────────────────────
 📚 Ruby City Public Library — All Items
──────────────────────────────────────────────────
  1. [4] 1984 (1949) ✅ | Book by George Orwell | dystopia | 328pp
  2. [5] Blade Runner 2049 (2017) ✅ | DVD by Denis Villeneuve | 2h 43m
  3. [3] Dune (1965) ✅ | Book by Frank Herbert | scifi | 688pp
  4. [2] Eloquent Ruby (2011) ✅ | Book by Russ Olsen | tech | 448pp
  5. [6] The Matrix (1999) ✅ | DVD by The Wachowskis | 2h 16m
  6. [1] The Pragmatic Programmer (1999) ✅ | Book by Hunt & Thomas | tech | 352pp

CHECKOUT DEMO
──────────────────────────────────────────────────
📤 Checked out: The Pragmatic Programmer
📤 Checked out: Dune
⚠️  'The Pragmatic Programmer' is already checked out
⚠️  Item #99 not found
📥 Returned: The Pragmatic Programmer

SEARCH RESULTS FOR 'ruby'
  → [2] Eloquent Ruby (2011) ✅ | Book by Russ Olsen | tech | 448pp

LONG READS (books over 400 pages)
  Eloquent Ruby — 448 pages
  Dune — 688 pages

BOOKS BY GENRE
  :tech     → The Pragmatic Programmer, Eloquent Ruby
  :scifi    → Dune
  :dystopia → 1984

💾 Saved 6 records to library_data.txt

📌 What This Program Covers

Classes & Inheritance (LibraryItem → Book, DVD) · Modules/Mixins (Displayable, Saveable, Comparable) · attr_accessor / attr_reader · Symbols as state (:available, :checked_out) · Class variables (@@item_count) · Class methods (self.total_items) · Predicate methods (available?, long_read?) · Keyword arguments (genre:, pages:) · Splat/multi-assign (h, m = divmod) · Blocks & iterators (each, map, select, reject, find, sort_by, each_with_object, each_with_index) · Symbol-to-proc (&:available?) · Exception handling (begin/rescue/raise) · File I/O (File.open, readlines, write) · String interpolation · Guard clause (__FILE__ == $PROGRAM_NAME)

Ranges & Other Essentials

Ranges

(1..10)   # inclusive (1 to 10)
(1...10)  # exclusive (1 to 9)
("a".."z") # chars!
r = (1..100)
r.include?(50)  # => true
r.min           # => 1
r.to_a          # to array
r.step(5) { |n| p n } 

Useful Stdlib

require 'date'
Date.today
Date.new(2024, 1, 1)

require 'json'
JSON.parse('{"a":1}')
{ a: 1 }.to_json

require 'set'
s = Set.new([1,2,2,3])
# => {1, 2, 3}

Gems (Ruby Packages)

# Install a gem
gem install rails
gem install colorize

# In your code
require 'colorize'
puts "Hello!".colorize(:red)

# Bundler — manage project dependencies
bundle init          # creates Gemfile
bundle install       # installs gems from Gemfile
bundle exec ruby app.rb

⚡ Next Steps

After mastering these fundamentals: explore Bundler + Gemfile for dependency management, RSpec for testing, Rake for task automation, and ultimately Ruby on Rails or Sinatra for web development. The Ruby community's gem ecosystem is one of the most mature in any language.