Ruby Notes

2020/07/19

Ruby Notes

This is the notes I’ve taken while reading the book “The Well-Grounded Rubyist”, I tried to make it useful as a reference but do not expect this to be a well-written tutorial for beginners.

Basics
Objects, methods and local variables
Classes
Modules
Default object, scope and visibility
Control Flow
Built-in basics
Strings, symbols and other scalar objects
Collection and container objects
Enumerable and Enumerator
Regular Expression
File and IO

Basics

Basic operations

Basic I/O:

Special Objects

true
false
nil
self

For conditional expression, both false and nil is evaluated as false.

Comments are ignored by the interpreter:

# a line of comment
x = 3 # inline comment

Convention

Note: I’ve seen coding conventions that are different from below, so either agree to a convention from the beginning, or use whatever convention that’s already been adopted in existing code.

Objects

If you know what OO is, the idea of Objects in Ruby is very straightforward:

e.g.

x = "100".to_i
y = "120".to_i(9)
puts "Hi" # bareword-style invocation

Installation, Package, Version

Sometimes it’s useful to have multiple Ruby version installed, to check current configuration, load a library package called rbconfig:

irb --simple-prompt -r rbconfig
>> RbConfig::CONFIG[term]

Directories lookup for term:

Term Directory contents
rubylibdir Ruby standard library
bindir Ruby command-line tools
archdir Architecture-specific extensions and libraries
sitedir Your own or third-party extensions and libraries
vendordir Third-party extensions and libraries (in Ruby)
sitelibdir Your own Ruby language extensions (in Ruby)
sitearchdir Your own Ruby language extensions (in C)

Check default load paths:

ruby -e 'puts $:'

-e means inline script to the interpreter

Ruby tools and applications

Interpreter command-line arguments

Switch Description
-c checks the syntax of a program file without executing it, usually used in conjunction with -w
-w gives warning messages during program execution
-e executes the code provided in quotation marks on the command line, for multiple lines use literal line breaks (ie. Enter) or semicolons
-l line mode: prints a newline after every line of output
rname requires the named feature, ruby -rscanf
-v shows Ruby version and executes the program in verbose mode
--version shows Ruby version information
-h shows information about all command-line switches for the interpreter

rake

rake provides functionalities similar to make, it reads and executes tasks defined in file, Rakefile.

namespace :project do
  desc "Clean files temporarily generated during building"
  task :clean do
    # cleaning
  end
end

gem

gem install library

Objects, methods and local variables

Generic Object

robot = Object.new

def robot.talk
  puts "I am a robot"
end

Methods that take arguments

def robot.c2f(c)
  c * 9.0 / 5 + 32
end

Another Example: Product

product = Object.new

def product.name
  "A Robot"
end

def product.full_description
  "A Robot that can Talk"
end

def product.is_complete?
  false
end

Built-in Methods of an Object

To see a list of innate methods of object:

p Object.new.methods.sort

Method Arguments

def mixed_args(a,b,*c,d)
  puts "Arguments:"
  p a,b,c,d
end

mixed_args(1,2,3,4,5)
# Arguments:
# 1
# 2
# [3, 4]
# 5

mixed_args(1,2,3)
# 1
# 2
# []
# 3

def args_unleashed(a,b=1,*c,d,e)
  p a,b,c,d,e
end

args_unleashed(1,2,3,4,5)
# 1
# 2
# [3]
# 4
# 5

args_unleashed(1,2,3)
# 1
# 1
# []
# 2
# 3
def broken_args(x,*y,z=1)
end

Local variables and assignment

Classes

class Robot
  def name
    "A Robot"
  end
  def name
    "B Robot"
  end
end

robot = Robot.new
puts robot.name

# reopen class
class Robot
  def age
    return 10
  end
end

Instance Variables

class Robot
  def initialize(name,age)
    @name = name
    @age = age
  end
  def name
    @name
  end
  def age
    @age
  end
  def get_older
    @age = @age + 1
  end
  def cost=(amount)
    @cost = amount
  end
  def cost
    @cost
  end
end

robot = Robot.new("A Robot", 11)
robot.cost=(100.00)
# syntactic sugar
robot.cost = 100.00
class Robot
  attr_reader :name, :age, :cost
  attr_writer :cost
  # same as:
  attr_reader :name, :age
  attr_accessor :cost
end

Inheritance

class SmartRobot < Robot
  attr_accessor :ai
end
c = Class.new do
  def say_hi
    puts "Hi!"
  end
end

c.new.say_hi

Singleton Method, Class Method

def Robot.most_expensive(*robots)
  robots.max_by(&:cost)
end

Constants

Modules

module MyModule
  def ruby_version
    system("ruby -v")
  end
end

class MyClass
  include MyModule
end
obj = MyClass.new
obj.ruby_version

e.g. a simple FILO stack module

module Stacklike
  def stack
    @stack ||= []
  end
  def add(obj)
    stack.push(obj)
  end
  def take
    stack.pop
  end
end
require_relative "stacklike"
class Stack
  include Stacklike
end

Method Lookup

When an object is sent a message:

module M
  def report
    puts "'report' method in module M"
  end
end
class C
  include M
  def report
    puts "'report' method in class C"
    super
  end
end

How super handles arguments:

Module can be used to organize code:

module Tools
  class Hammer
  end
end

h = Tools::Hammer.new

Default object, scope and visibility

Alternative ways of defining class methods:

class C
  def self.x
  end
end

class C
  class << self
    def x
    end
  end
end

Scope and Variables

Global

Local

Constants

module M
  class C
    class D
      module N
        X = 1
      end
    end
    puts D::N::X # 1
  end
end

## refer to X
M::C::D::N::X

Class variables

class Camera
  @@models = []
  @@cameras = {}
  @@total_count = 0
  attr_reader :model
  def self.total_count
    @total_count ||= 0
  end
  def self.total_count=(n)
    @total_count = n
  end
  def self.add_model(model)
    unless @@models.include?(model)
      @@models << model
      @@cameras[model] = 0
    end
  end
  def initialize(model)
    if @@models.include?(model)
      puts "Creating a new #{model}!"
      @model = model
      @@cameras[model] += 1
      self.class.total_count += 1
    else
      raise "No such model: #{model}."
    end
  end
  def model_count
    @@cameras[self.model]
  end
end

class Leica < Camera
end

Camera.add_model("M9")
Camera.add_model("M10")
Camera.add_model("X-Pro3")
f1 = Camera.new("X-Pro3")
l1 = Leica.new("M9")
l2 = Leica.new("M10")
puts "There are #{Camera.total_count} Cameras!"
puts "There are #{Leica.total_count} Leicas!"
# Leica would have its own total count
# Output:
# Creating a new X-Pro3!
# Creating a new M9!
# Creating a new M10!
# There are 1 Cameras!
# There are 2 Leicas!

Method-access rules

Control Flow

Conditional

The if, elsif and else together evaluate to an object, the value is whatever is represented by the code in the successful branch, if the if statement doesn’t succeed anywhere its value is nil.

if condition
  # code executed if condition is true
end

# in one line:
if condition then code end

# semicolons as line breaks
if condition; code; end

if condition
  # code executed if condition is true
else
  # code executed if condition is false
end

if condition1
  # code
elsif condition2
  # code
else condition3
  # code
end

Negate a codition using not, ! or unless:

# negate a condition usually have parentheses
if not condition
if !condition

# alternative way for negating a condition
unless condition

unless x > 100
  puts "Small number!"
else
  puts "Big number!"
end

Conditional modifiers are usually used for simple condition, without else or elsif branching.

puts "Big numbers!" if x > 100
puts "Pass!" unless x < 50

When Ruby parser sees sequence identifier, equal-sign, and value, it allocates space for local variable without assignment of a value:

if false
  x = 1
end
# the value of x is nil since parser allocates space for x without a value

Assignment in a conditional test evaluates to the value of the assignment, some times it’s needed:

if name = /ha/.match(name)
  puts "Found a match!"
  puts m
else
  puts "No match"
end

case

A case statement starts with an expression and goes through a list of possible matches, each possible match is contained in a when statement consisting of one or more possible matching objects and a segment of code.

answer = gets.chomp
case answer
when "yes"
  puts "Good-bye!"
  exit
when "no"
  puts "Good!"
else
  puts "Unknown answer"
end

It’s possible to have more than one possible match in a single when:

case answer
when "y", "yes"
  puts "Bye!"
  exit

It’s also possible to start a case statement without test expression, this is an alternative way of if statement:

case
when camera.model == "Leica"
  puts "You got a Leica!"
when camera.manufactured_year < 2000
  puts "Your camera is old!"
end
# equivalent
when "yes"
if "yes" === answer
if "yes".===(answer)

# override ===
class Camera
  def ===(other_camera)
    self.model == other_camera.model
  end
end

Loops

Ruby allows looping repeatedly through code with conditional logic:

loop { code }
loop do
  code
end

Terminates the loop:

n = 1
loop do
  n += 1
  break if n > 9
end

Skip to the next iteration without finishing current iteration:

n = 1
loop do
  n += 1
  next unless n == 10
  break
end

Conditional looping with while and until:

while condition
  # code executed when condition is true
end

while can also work with begin / end:

begin
  # code executed
end while condition

Like if and unless, while has also a pair until:

until condition
  # code executed if condition is false
end

begin
  # code executed
end until condition

There’s also a shorter way to use while and until:

n = n + 1 until n == 10
n = n + 1 while n < 10

Looping through a list of values using for:

scores = [30, 50, 70, 38, 48, 41]
PASS = 50
for score in scores
  if score >= PASS
    puts "Pass!"
  else
    puts "Fail!"
  end
end

Iterators and code blocks

String#split for example, splits its receiver on the delimiter argument and returns an array of splitted components, with a block, split also yields the split components to the block, one at a time.

array = [1, 2, 3]
array.map { |n| n * 10 }
array.map do |n| n * 10 end

Implementing different types of loops using yield:

def my_loop
  while true
    yield
  end
end

def my_simpler_loop
  yield while true
end
class Integer
  def my_times
    c = 0
    until c == self
      yield c
      c += 1
    end
    self # return the value itself
  end
end

rtn = 3.my_times { |i| puts "I'm on iteration #{i}" }
class Array
  def my_each
    c = 0
    until c == size
      yield self[c]
      c += 1
    end
    self
  end
end

array = [1, 2, 3, 4, 5]
array.my_each { |e| puts "I got #{e}" }
class Array
  def my_map
    c = 0
    acc = []
    until c == size
      acc << yield self[c]
      c += 1
    end
    acc
  end
end

names = ["Tom", "Bill"]
names.my_map { |name| name.upcase }

A simpler my_map using my_each:

class Array
  def my_map
    acc = []
    my_each { |e| acc << yield e }
    acc
  end
end

Block parameters and variable scopes

Error handling and exceptions

Common exceptions:

Use rescue to catch and handle exception:

n = gets.to_i
begin
  result = 100 / n
rescue
  puts "Invalid number for division 100/#{n}"
  exit
end
puts "100/#{n} is #{result}."

It’s a good practice to specify what exceptions you want to handle, to catch specific exception:

rescue ZeroDivisionError

The beginning of a method or code block provides an implicit begin/end context, as a result, using rescue inside a method or code block won’t need begin:

def load_camera_profile
  filename = gets.chomp
  file = File.open(filename)
  yield file
  file.close
  rescue
    puts "Cannot open camera profile #{filename}!"
end
def load_camera_profile
  filename = gets.chomp
  begin
    file = File.open(filename)
  rescue
    puts "Cannot open camera profile #{filename}!"
    return
  end
  yield file
  file.close
end

For NoMethodError when calling a method on nil, save navigation is allowed using &. instead of .:

if collection.cameras&.first&.model == "Leica"
  puts "Your first camera is a Leica!"
end

binding.irb

Raising exceptions

Re-raise catched exception:

begin
  file = File.open(filename)
rescue => e
  puts "Cannot open file #{filename}"
  # Ruby is smart enough to figure out it's the same exception to raise
  raise
end
def line_from_file(filename, substring)
  file = File.open(filename)
  begin
    line = file.gets
    raise ArgumentError unless line.include?(substring)
  rescue ArgumentError
    puts "Invalid line!"
    raise
  ensure
    file.close
  end
  return line
end

Built-in basics

Literal constructors

Class Literal constructor e.g.
String Quotation marks “new string”, new string
Symbol Leading colon :symbol, :"symbol with spaces"
Array Square brackets [1, 2, 3, 4, 5]
Hash Curly braces {“Name”: “Tom”}
Range Two or three dots 0..9, 0…12
Regexp Forward slashes /([a-z]+)/
Proc lambda Dash, arrow, parentheses, braces ->(x, y) { x*y}

Syntactic sugar

class Meter
  attr_accessor :exposure
  def initialize(start_exposure=0)
    self.exposure = start_exposure
  end
  def +(e)
    self.exposure += e
  end
  def -(x)
    self.exposure -= e
  end
  def to_s
    exposure.to_s
  end
end

meter = Meter.new(1)
meter += 1
meter -= 2

Overriding unary operators

class Saturation
  def initialize(s=0)
    @sat = s
  end
  def to_s
    @sat.to_s
  end
  def +@
    @sat + 1
  end
  def -@
    @sat - 1
  end
  def !
    -@sat
  end
end

saturation = Saturation.new
puts +saturation
puts !saturation
puts (not saturation) # not saturation

Bang (!) methods

Built-in and custom to_* methods

Ruby offers a number of built-in methods whose name consist of to_ plus an indicator of a class to which method method converts an object.

Array conversion

Number conversion

Boolean

Some expressions’ values:

The special object nil

Objects comparison

To make an object Comparable:

class Camera
  include Comparable
  attr_accessor :rating
  def <=>(other_camera)
    # the following can also be simplified as:
    # self.rating <=> other_camera.rating
    if self.rating < other_camera.rating
      -1
    elsif self.rating > other_camera.rating
      1
    else
      0
    end
  end
end

Inspecting objects

Strings, symbols and other scalar objects

text = << EOM
First line
Second line
EOM

another_text = <<-EOM
The EOM doesn't have to be flush left!
  EOM

single_quoted = <<-`EOM`
this is a
single quoted line
EOM

a = <<EOM.to_i * 10
5
EOM
puts a # output is 50

Basic string manipulation

Use [] operator/method to retrieve the nth character in a string, negative numbers index from the end of the string:

"Ruby is cool"[5] # "i"
"Ruby is cool"[-1] # "l"

A second integer argument m gets a substring of m characters:

"Ruby is cool"[1, 2] # "ub"

A single range object can be provided as argument, two dots are inclusive, three dots means exclusive, the second index must always be closer to the end of the string than the first index:

'Ruby is cool'[1..3] # "uby"
'Ruby is cool'[1...3] # "ub"

Get substring return nil if substring isn’t found, regular expressions can be used as well for matching.

"ruby is cool"["is cool"] # "is cool"
"ruby is cool"["something"] # nil

slice removes the characters from the string; slice! removes permanently

string = "ruby is cool"
string.slice!("is ")
string # "ruby cool"

Use []= to set part of a string to a new value, integers, ranges, strings and regular expressions all work, if the part isn’t found, or nothing matches, IndexError is raised:

string = "A quick brown fox jumps over a lazy dog."
string["quick"] = "slow"
string[-1] = "!"
string # "A slow brown fox jumps over a lazy dog!"

Use + for string concatenation, use << to append second string permanently to an existing string, use #{} for string interpolation.

"Hi" + "there" # "Hi there"
string = "Hi "
string << "there" # "Hi there"
"#{string}there." # "Hi there"

Querying strings

string = "A quick brown fox jumps over a lazy dog."
string.include?("quick") # true
string.include?("slow") # false
string.start_with?("A") # true
string.end_with?("B") # false
string.start_with?(/[A-Z]/) # true
string.empty? # false
"".empty? # true
string.size # 40
string.count("a") # 2
string.count("g-m") # 6
string.count("aey. ") # 13
string.count("^a-z") # 9 for 8 spaces and one fullstop
string.index("o") # 10
string.rindex("o") # 37
string.ord # 97
"a".ord # 97
97.chr # a

String comparison and transformation

Symbols

Symbols are instances of the built-in class Symbol, the literal constructor is the leading colon.

"a".to_sym # :a
"hello".intern # :"hello"
:a.to_s # "a"
:xyz.object_id # same as below
:xyz.object_id # same as above
Symbol.all_symbols # return all the symbols

# use grep instead of include?
# include? would first put the symbol to the table
# which makes the result be always true
Symbol.all_symbols.grep(/abc/)

Most common uses of symbols are method arguments and hash keys.

atr_accessor :name

# the "send" method also takes symbol as argument
"abc".send(:upcase)

# this also works
some_object.send(method_name.to_sym)

person = { :name => "Tom", :age => 18 }
person[:name] # "Tom"

# simplified hash keys:
person = { name: "Tom", age: 36 }

Numericals

Times and dates

Three classes exist for times and dates: Time, Date and DateTime.

Creating date objects

Date.today

# send year, month and day
# without parameter, month and day default to 1, year defaults to -4712
Date.new(2020, 8, 14)

# parse a date string
# assumes order of year/month/day
Date.parse("2020/8/14")

Creating time objects

Time.new # a.k.a now
Time.at(100000000) # number of seconds since the epoch (midnight on 1st Jan, 1970, GMT)
Time.mktime(2020,8,14,6,31,3) # year, month, day, hour, minute and seconds, with reasonable defaults
require 'time'
Time.parse("August 14, 2020, 6:32 am")

Creating date/time objects

Date/time query methods

The time and date objects have methods that represents themselves.

dt = DateTime.now
dt.year
dt.hour
dt.minute
dt.second # DateTime objects have second and sec
t = Time.now
t.month
t.sec # Time objects have only sec
d = Date.today
d.day

t.sunday?
d.saturday?
dt.friday?

d.leap?
dt.leap?
t.dst? # daylight saving time available to Time only

Date/time formatting methods

t = Time.now # 2020-08-14 06:49:19 +1000
t.strftime("%m-%d-%y") # "08-14-20"
Date.today.rfc2822
DateTime.now.httpdate
Specifier Description
%Y Year (four digits)
%y Year (last two digits)
%b, %B Short month, full month
%m Day of month (left-padded with zeros)
%e Day of month (left-padded with blanks)
%a, %A Short day name, full day name
%H, %I Hour (24-hour clock), hour (12-hour clock)
%M Minute
%S Second
%c “%a %b %d %H:%M:%S %Y”
%x “%m/%d/%y”

Date/time arithmetic

dt = DateTime.now # 2020-08-14T07:01:01+10:00
puts dt + 10 # add 10 days: 2020-08-24T07:01:01+10:00
puts dt >> 3 # 3 months after: 2020-11-14T07:01:01+10:00
puts dt.next # 2020-08-15T07:01:01+10:00
puts dt.next_year # 2021-08-14T07:01:01+10:00
puts dt.next_month(2) # 2020-10-14T07:01:01+10:00
puts dt.prev_day(10) # 2020-08-04T07:01:01+10:00

d = Date.today # 2020-08-14
next_week = d + 7 # 2020-08-21
d.upto(next_week) { |date| puts "#{date} is a #{date.strftime("%A")}" }
# 2020-08-14 is a Friday
# 2020-08-15 is a Saturday
# 2020-08-16 is a Sunday
# 2020-08-17 is a Monday
# 2020-08-18 is a Tuesday
# 2020-08-19 is a Wednesday
# 2020-08-20 is a Thursday
# 2020-08-21 is a Friday

Collection and container objects

Array

a = Array.new
Array.new(3) # [nil, nil, nil]
Array.new(2, "ab") # ["ab", "ab"]
Array.new(3) { |i| 10 * (i + 1) } # [10, 20, 30]

Array.new(3, "ef") # uses the same object "ef"
a[0] << "g" # so this would cause each of the three "ef" become "efg"

Array.new(3) { "abc" } # different object "abc"

# literal constructor
a = [1, 2, "three", 4, []]

string = "text"
Array(string) # ["text"]

def string.to_a
  split(//)
end

Array(string) # ["t", "e", "s", "t"]
# parsing as single quoted string
%w(Tom Jack) # ["Tom", "Jack"]

# parsing as double quoted string
%W{Tom is #{10-2} years old} # ["Tom", "is", "8", "years", "old"]

# space neeed to be escaped
%w(Black\ Smith is a nice person) # ["Black Smith", "is", "a", "nice", "person"]

%i(a b c) # [:a, :b, :c]

Array manipulation

Simple getter and setter: [] and []=

a = []
a[0] = "first" # a.[]=(0, "first")
a = [1,2,3,4,5]
a[2] # 3

The getter and setter take a second argument as length:

a = %w(a quick brown fox jumps over a lazy dog)
a[3,2] # ["fox", "jump"]
a[3, 2] = ["chicken", "run"]
# a is now ["a", "quick", "brown", "chicken", "run", "over", "a", "lazy", "dog"]
a.values_at(3, 8) # ["chicken", "dog"]
a = [1, 2, 3, 4]
a.unshift(0) # [0,1,2,3,4]
a.push(5) # [0,1,2,3,4,5]
a << 6 # [0,1,2,3,4,5,6]
a.push(7, 8) # [0,1,2,3,4,5,6,7,8]

Array querying

Method name Meaning
`a.size(synonym: length, count) | Number of elemnents in the array |
a.empty? whether the array is empty
a.include?(item) true if the array includes item, false otherwise
a.count(item) number of occurrences of item in array
a.first(n=1) First n elements of array
a.last(n=1) Last n elements of array
a.sample(n=1) n random elements from array

Hashes

Hash is a colection of objects consisting of key/value pairs, the => operator connects a key on the left with the value corresponding to it on the right.

A hash can be created via:

  1. literal constructor (curly braces)
    • h = {}
  2. Hash.new method
    • argument to Hash.new is treated as default value for nonexistent hash keys
  3. Hash.[] method
    • takes a comma separated list of items
    • even number of arguments are treated as alternating keys and values
    • odd number of argument will raise a fatal error
    • also can pass an array of arrays, each subarray has two elements
    • Hash[ [[1, 2], [3,4]] ] # {1=>2, 3=>4}
  4. top-level method Hash
    • empty hash created when called with empty array or nil
    • otherwise to_hash is called on its single argument
weekdays = { "Monday" => "Mon",
             "Tusday" => "Tue",
             "Wednesday" => "Wed",
             "Thursday" => "Thur",
             "Friday" => "Fri",
             "Saturday" => "Sat",
             "Sunday" => "Sun"}
weekdays["Monday"] # "Mon"

Adding a key/value pair to a hash:

country_names["US"] = "United States"
country_names.[]=("US", "United States")
country_names.store("US", "United States")

Retrieving values from a hash:

country = country_names["US"] # "United States"
country = country_names.fetch("US")

When looking up a nonexistent key:

h = Hash.new {|hash,key| hash[key] = 0}

Combining hashes with other hashes

Selecting and rejecting elements from a hash

h = Hash[1,2,3,4,5,6] # {1=>2, 3=>4, 5=>6}
h.select {|k,v| k > 1} # {3=>4, 5=>6}
h.reject {|k,v| k > 1} # {1=>2}
{name: "Tom", address: nil}.compact # {name: "Tom"}

Hash querying

method meaning
h.has_key?(1) true if h has the key 1
h.include?(1) synonym for has_key?
h.key?(1) synonym for has_key?
h.member?(1) synonym for has_key?
h.has_value?("three") true if any value in h is "three"
h.value?("three") synonym for has_value?
h.empty? true if h has no key/value pairs
h.size number of key/value pairs in h

Hashes as final method arguments

Ruby allows hash without curly braces when calling a method whose last argument is a hash; If hash is first argument, not only the curly braces are needed, but also the entire arguemnt list need to be in parentheses.

def add_to_camera_database(owner, info)
  camera = Camera.new
  camera.owner = owner
  camera.year_made = info[:year_made]
  camera.model = info[:model]
end

add_to_camera_database("Tom",
  year_made: 1991,
  model: "Leica M")

Named arguments

def m(a:, b:)
  p a, b
end

m(a: 1, b: 2) # Ruby matches a and b which are required arguments

def m(a: 1, b: 2) # default values make arguments optional
end

If the method’s parameter list includes a double-starred name, calling a method using keyword arguments not declared by the method will end up sponging up all unknown keyword arguments into a hash:

def m(a: 1, b: 2, **c)
  p a, b, c
end

m(x:1, y:2)
# 1
# 2
# {:x=>1, :y=>2}

Ranges

A range is an object with a start point and an end point.

Create a range with Range.new or literal constructor.

r = Range.new(1, 100) # 1..100
r = 1..100 # literal range construction 1..100
r = 1...100 # exclusive range
r = Range.new(1, 100, true) # exclusive range 1...100

Range logic

r = 1..10
r.begin # 1
r.end # 10
r.exclude_end? # false
r = "a".."z"
r.cover?("a") # true
r.cover?("abc") # true
r.cover?("A") # false
r.cover?([]) # false
r.include?("a") # true
r.include?("abc") # false
r = 1.0..2.0
r.include?(1.5) # true
"This is a sample string"[10..-5] # sample st

Sets

Create a new Set by providing a collection object, can also provide a code block which every item in the collection object is passed through.

cameras = ["Leica M", "X-Pro3", "GX9", "Leica M"]
unique_cameras = Set.new(cameras) # #<Set: {"Leica M", "X-Pro3", "GX9"}>
unique_cameras = Set.new(cameras) {|c| c.upcase} # #<Set: {"LEICA M", "X-PRO3", "GX9"}>

Set operations

camera_brands = Set.new(["Leica", "Fujifilm", "Olympus"])
closed_brands = Set.new(["Olympus"])
optics_brands = Set.new(["Zeiss", "Leica"])
camera_brands - closed_brands # <Set: {"Leica", "Fujifilm"}>
camera_brands + optics_brands # <Set: {"Leica", "Fujifilm", "Olympus", "Zeiss"}>
camera_brands & optics_brands # <Set: {"Leica"}>

Subset and superset

Enumerable and Enumerator

Collection objects in Ruby typically include the Enumerable module for common functionalities.

class C
  include Enumerable
  def each
    # code
  end
end

The job of each is to yield items to a supplied code block, one at a time.

class Gender
  include Enumerable
  def each
    yield "boy"
    yield "girl"
  end
end

g = Gender.new
g.each do |gender|
  puts "Gender: #{gender}"
end

# output:
# Gender: boy
# Gender: girl

find works by calling each:

g = Gender.new
g.find {|gender| g.start_with?('b') }
# output: boy

Enumerable Boolean queries

A number of Enumerable methods return true or false depending on whether one or more elements matching certain criteria.

method meaning
include? whether the enumerable includes specified object
all? whether ALL elements match query
any? whether ANY element matches query
one? is there one and only one element matching query
none? is there NO element matching query

Enumerable searching and selecting

find locates the first element in an array for which the code block returns true, failing to find any element that passes code block test would return nil.

[1,2,3,4,5,6,7,8,9].find {|n| n > 3}
# output: 4

As a result, if the array has nil as an element, looking for nil would always return nil. Use include? as a work around to check whether the array has nil as an element. Alternatively, provide a Proc object as an argument to find, the function will be called if find fails.

failure = lambda { 11 }
over_ten = [1,2,3,4,5,6].find(failure) {|n| n > 10 }
# output: 11

find_all (the same as select) returns a new collection containing all the elements of the original collection that match the criteria in the code block, empty collection is returned if none is found.

select generally returns an array, include using Enumerable in your own class, but for hashes and sets, select returns a hash or set, respectively.

a = [1,2,3,4,5,6,7,8,9,10]
a.find_all {|item| item > 5 } # [6, 7, 8, 9, 10]
a.select {|item| item > 100 } # []

Arrays, hashes and sets have a bang version select! that reduces the collection permanently to only those elements that passed the selection test.

reject finds out which elements do not return a true value when yielded to the code block:

a.reject {|item| item > 5} # [1, 2, 3, 4, 5]

There’s also the bang version reject!

Selecting on threequal matches with grep

Enumerable#grep selects from an enumerable object based on case-equality operator ===.

misc = [12, "hi", 10..20, "bye"]
misc.grep(String) # ["hi", "bye"]
misc.grep(10..20) # [12]

enumerable.grep(expression) is equivalent to:

enumerable.select {|element| expression === element }

Organizing selection results with group_by and partition

colors = %w(red orange yellow green blue indigo violet)
colors.group_by {|color| color.size }
# output:
# {3=>["red"], 6=>["orange", "yellow", "indigo", "violet"], 5=>["green"], 4=>["blue"]}
class Camera
  attr_accessor :price
  def initialize(options)
    self.price = options[:price]
  end
  def below(limit)
    (0...limit) === price
  end
end

cameras = 900.step(1200, 50).map {|p| Camera.new(:price => p) }

cameras.partition { |camera| camera.below(1000) }
# output:
# [[#<Camera:0x00007feb871e48b0 @price=900>, #<Camera:0x00007feb871e4838 @price=950>], [#<Camera:0x00007feb871e4798 @price=1000>, #<Camera:0x00007feb871e46f8 @price=1050>, #<Camera:0x00007feb871e4680 @price=1100>, #<Camera:0x00007feb871e4608 @price=1150>, #<Camera:0x00007feb871e4590 @price=1200>]]

Element-wise operations

class Die
  include Enumerable
  def each
    loop do
      yield rand(6) + 1
    end
  end
end

d = Die.new
d.each do |roll|
  if roll == 6
    puts "Start"
  end
end

cameras
# [#<Camera:0x00007feb871e48b0 @price=900>, #<Camera:0x00007feb871e4838 @price=950>, #<Camera:0x00007feb871e4798 @price=1000>, #<Camera:0x00007feb871e46f8 @price=1050>, #<Camera:0x00007feb871e4680 @price=1100>, #<Camera:0x00007feb871e4608 @price=1150>, #<Camera:0x00007feb871e4590 @price=1200>]

cameras.take_while {|c| c.below(1000) }
# [#<Camera:0x00007feb871e48b0 @price=900>, #<Camera:0x00007feb871e4838 @price=950>]

cameras.drop_while {|c| c.below(1000) }
# [#<Camera:0x00007feb871e4798 @price=1000>, #<Camera:0x00007feb871e46f8 @price=1050>, #<Camera:0x00007feb871e4680 @price=1100>, #<Camera:0x00007feb871e4608 @price=1150>, #<Camera:0x00007feb871e4590 @price=1200>]

cameras.min {|a,b| a.price <=> b.price}
# <Camera:0x00007feb871e48b0 @price=900>

cameras.max {|a,b| a.price <=> b.price}
# <Camera:0x00007feb871e4590 @price=1200>
a = Array(1..10)

a.each_slice(3) {|slice| p slice }
# [1, 2, 3]
# [4, 5, 6]
# [7, 8, 9]
# [10]

a.each_cons(3) {|slice| p slice }
# [1, 2, 3]
# [2, 3, 4]
# [3, 4, 5]
# [4, 5, 6]
# [5, 6, 7]
# [6, 7, 8]
# [7, 8, 9]
# [8, 9, 10]

There’s a family of slice_ methods.

(1..10).slice_before { |num| num % 2 == 0 }.to_a
# [[1], [2, 3], [4, 5], [6, 7], [8, 9], [10]]

[1,2,3,3,4,5,6,6,7,8,8,8,9,10].slice_when { |i,j| i == j }.to_a
# [[1, 2, 3], [3, 4, 5, 6], [6, 7, 8], [8], [8, 9, 10]]

Enumerable#cycle yields all the elements in the object again and again in a loop, providing an integer argument asks the loop to run that many times.

Enumerable reduction with inject

inject works by initializing an accumulator oject and iterating through a collection, performing a calculation on each iteration and resetting the acumulator for purposes of the next iteration, to the result of that calculation.

[1,2,3,4].inject(0) {|acc,n| acc + n } # 10
[1,2,3,4].inject(:+)

Map

cameras = %w(Leica Fujifilm Olympus)
cameras.map {|camera| camera.upcase} # ["LEICA", "FUJIFILM", "OLYMPUS"]

# use a symbol argument as a block
cameras.map(&:upcase)

Be careful with block evaluation, map only cares about return value of puts which is always nil:

array = [1,2,3,4,5]
result = array.map {|n| puts n * 100 } # [nil, nil, nil, nil, nil]

Strings as enumerables

Sorting enumerables

In order to make a class instance sortable:

  1. define <=> for the class
  2. place multiple instances in a container, e.g. array
  3. sort the container

In cases where no <=> is defined for objects, a block can be supplied on the fly to indicate how they should be sorted.

sorted_camera = cameras.sort do |a, b|
  a.price <=> b.price
end
cameras.sort_by {|c| c.price }
cameras.sort_by(&:price)

Enumeraors

e = Enumerator.new do |y|
  y << 1
  y << 2
  y << 3
end

e = Enumerator.new do |y|
  (1..3).each { |i| y << i }
end

e.to_a # [1, 2, 3]
e.map {|x| x * 10} # [10, 20, 30]
e.select {|x| x > 1} # [2, 3]
e.take(2) # [1, 2]
a = [1, 2, 3, 4, 5]
e = Enumerator.new do |y|
  total = 0
  until a.empty?
    total += a.pop
    y << total
  end
end

e.take(2) # [5, 9], popping and adding last two
a # [1, 2, 3]
e.to_a # [3, 5, 6], each would pop and add to total until empty

Attaching enumerators to other objects

When the enumerator need to yield something, it gets the necessary value by triggering the next yield from the object to which it is attached via the designated method.

cameras = %w(Leica Olympus Canon Nikon)
e = cameras.enum_for(:select)
e.each {|c| c.include?('o')} # ["Canon", "Nikon"]

Any further arguments provided to enum_for are passed through to the method to which the enumerator is being attached.

e = cameras.enum_for(:inject, "Names: ")
e.each {|string, name| string << "#{name}..." }
# "Names: Leica...Olympus...Canon...Nikon..."
e = cameras.map
e.each {|camera| camera.capitalize}

Regular Expression

Examples of patterns:

Regular expression are instances of Regexp class, which has literal constructor // and %r{}:

>> //.class
=> Regexp

>> %r{}.class
=> Regexp

Any pattern matching operation involves a regexp and a string. The simplest way to find out whether there’s a match between a pattern and a string is with the match method or its sibling match?. Ruby also features a pattern-matching operator =~:

"A quick brown fox jumps over a lazy dog.".match?(/fox/)
/fox/.match?("A quick brown fox jumps over a lazy dog.")

puts "Match!" if /Hello/ =~ "Hello world!"
puts "Match!" if "Hello world!" =~ /Hello/
symbol meaning
//, %r{} instance of Regexp class
=~ determines if a match exists
. matches any character except \n
\ escape character, treat next character as literal
[] surrounds a character class, matches either character between [ and ]
^ 1. negates a character or character class, matches anything except what follows ^
^ 2. matches the expression at the start of a line
\d matches any digit
\D matches anything except a digit
\w matches any digit, alphabets or underscore
\W matches anything except a digit, alphabets or underscore
\s matches any whitespace characters (space, tab, newline)
\S matches anything except a white space character
{} matches a character or character class a specific number of times
$ matches the expression at the end of a line
+ matches one or more occurrences of the character or character class
* matches zero or more occurrences of the character or character class

A regexp includes three possible components:

  1. literal characters
  2. the dot wildcard (.), matches any character except \n
  3. character class

The dot widecard character matches any character at some point in the pattern, except of a newline.

>> /.ization/.match?("internationalization")
=> true
regex = %r{[ps]lot}
regex.match?("slot") # true
regex.match?("alot") # false

/[a-z]/
/[A-Fa-f0-9]/

%r{[^A-Fa-f0-9]} # not hex number

MatchData

Use parentheses to specify captures in regexp construction.

e.g. to extract book name from the following line:

line = 'the old man and the sea, Hemingway, Mr., writer'
regex = /([A-Za-z\s]+),[A-Za-z\s]+,[\s]?(Mrs?\.)/
m = regex.match(line)
m[0] # "the old man and the sea, Hemingway, Mr."
line = "My phone number is (456) 193-8123."
regex = /\((\d{3})\)\s+(\d{3})-(\d{4})/
m = regex.match(line) # #<MatchData "(456) 193-8123" 1:"456" 2:"193" 3:"8123">
m[0] # "(456) 193-8123"
m[1] # "456"
m[2] # "193"
m[3] # "8123"

>> /((a)((b)c))/.match("abc")
=> #<MatchData "abc" 1:"abc" 2:"a" 3:"bc" 4:"b">

File and IO

The IO class handles all input and output streams either by itself or via its descendant classes, particularly File.

STDIN.select {|line| line =~ /\A[A-Z]/ }
We're only interested in
lines that begin with
Uppercase letters
=> ["We're only interested in\n", "Uppercase letters\n"]

record = File.open("/tmp/record", "w")
old_stdout = $stdout
$stdout = record
$stderr = $stdout
puts "Z record" # written to /tmp/record
z = 10 / 0 # written to /tmp/record

Keyboard input

line = gets
c = STDIN.getc

File operations

The built-in class File is a subclass of IO.

To open and close a file:

file = File.new("file.txt")
file.close

Line-based file reading

Since File is enumerable, you can read each line by:

file.each {|line| puts "Line: #{line}" }

File has class methods for reading files:

content = File.read("text.txt")
lines = File.readlines("text.txt")

File position seeking

file.rewind // 0
file.pos // 0
file.gets // "first line\n"
file.pos // 11

file.seek(10, IO::SEEK_SSET) // seek to specific byte
file.seek(3, IO::SEEK_CUR) // seek 3 bytes from current position
file.seek(-10, IO::SEEK_END) // 10 bytes from end of the file

Writing to files

Write mode is indicated by the second argument when opening the file:

file = File.new("test.txt", "w")
file.puts "This is the first line."
file.close

Using class method to open a file has advantage that you don’t need to explicitly close it:

File.open("book.txt") do |f|
  while line = f.gets
    puts "Line: #{line}"
  end
end

Since File is also Enumerable, the above code can simply be:

File.open("book.txt") do |f|
  f.each do |line|
    puts "Line: #{line}"
  end
end

IO Query

FileTest.exist?("file.txt")
FileTest.empty?("file.txt")
FileTest.directory?("/usr/me/Books")
FileTest.file?("file.txt")
FileTest.symlink?("mockdata.json")
FileTest.readable?("/usr/root")
FileTest.writable?("/usr/root/file.txt")
FileTest.executable?("/scripts/shell.sh")
FileTest.size("file.txt")
FileTest.zero?("file.txt")

Working with directories

Dir is like File but targeting directories.

d = Dir.new("/usr/me/Books")
d.entries
Dir.entries("/usr/me/Books")
# both above returns entries listed under directory /usr/me/Books

entries.delete_if { |entry| entry =~ /^\./ } # delete entries from array if starting with dot
entries.map! { |entry| File.join(d.path, entry) }
entries.delete_if { |entry| !File.file?(entry) } # delete if not regular file

Directory globbing: Get an array containing all ePub books:

Dir["/usr/me/Books/*.ePub"]

The glob method lets you specify more details for globbing behaviour. Below does a case-insensitive glob including hidden dot files in the results:

Dir.glob("/usr/me/Books/*MarkTwain*.mobi", File::FNM_CASEFOLD | File::DOTMATCH)

Create a new directory:

Dir.mkdir("ebooks")

Check if a directory is empty:

Dir.empty?("ebooks")

Change context to a directory:

Dir.chdir("ebooks") do
  File.open("newbook.txt", "w") do |f|
    f.puts "a new line in a new file"
  end
end

Remove a directory:

Dir.rmdir("ebooks")