Ruby Training

From bibbleWiki
Jump to navigation Jump to search

Introduction

This is to summarised what I learn on the ruby Training

Returning if true

Quite liked this. On do the return if true

  def remaining_minutes_in_oven(actual_minutes_in_oven)
    return EXPECTED_MINUTES_IN_OVEN - actual_minutes_in_oven if actual_minutes_in_oven < EXPECTED_MINUTES_IN_OVEN
    raise 'Please implement the Lasagna#remaining_minutes_in_oven method'
  end

Symbols

Symbols are just an identifier

class User
  STATES = [:active, :inactive, :banned]
...

Provide you pass the same thing, they are the same thing e.g.

User.new(:active)


Predicate Method

So in ruby you have a predicate method of a method that returns true or false which is, by convention, not enforced, a method with a ?. The example below returns true if status equal true.

class User
  STATES = [:active, :inactive, :banned]

  def initialize(status)
    raise "Invalid state" unless STATES.include?(status)
    @status = status
  end

  def active?
    @status == :active
  end
end

This became a bit more obvious the usage with the example with ranges

module Chess
  RANKS = 1..8
  FILES = 'A'..'H'

  def self.valid_square?(rank, file)
    RANKS.include?(rank) && FILES.include?(file)
  end
end

Interpolation

This is how to interpolation works

my_var1 = "HELLO"
my_var2 = "FRED WAS HERE #{my_var1}"
puts my_var2

Better is

my_var1 = "HELLO1"
my_var2 = 123
my_var3 = "FRED WAS HERE %s, %d" % [my_var1, my_var2]
puts my_var3

Arrays

These have a lot of functions e.g. array_name.blah which include

  • last
  • first
  • max
  • sort
  • reverse

Series

This was very fancy. In ruby you can yields consecutive groups to the block. Or generate groups from a block. (sliding windows). So with this you can do

class Series
  def initialize(series_string)
    @series = series_string.chars
  end

  def slices(slice_length)
    raise ArgumentError if slice_length > @series.length || slice_length <= 0

    @series.each_cons(slice_length).map(&:join)
  end
end

Series.new('918493904243').slices(5)
=>["91849", "18493", "84939", "49390", "93904", "39042", "90424", "04243"]

LINQ in Ruby?

So this is what was given

class BirdCount
  def self.last_week
    [0, 2, 5, 3, 7, 8, 4]
  end

  def initialize(birds_per_day)
    @birds_per_day = birds_per_day
  end

  def yesterday
    @birds_per_day[-2]
  end

  def total
    @birds_per_day.sum
  end

  def busy_days
    @birds_per_day.count { |day| day >= 5 }
  end

  def day_without_birds?
    @birds_per_day.any? { |day| day == 0 }
  end
end

Which looks canna complicated but really it just like this in C#

public class BirdCount
{
    public int[] BirdsPerDay { get; }

    public BirdCount(int[] birdsPerDay)
    {
        BirdsPerDay = birdsPerDay;
    }
}

var birds = new BirdCount(new[] { 0, 2, 5, 3, 7, 8, 4 });
birds.Where(day => day >= 5).Count();


Other functions include

fibonacci = [0, 1, 1, 2, 3, 5, 8, 13]

fibonacci.count  { |number| number == 1 }   #=> 2
fibonacci.any?   { |number| number == 6 }   #=> false
fibonacci.select { |number| number.odd? }   #=> [1, 1, 3, 5, 13]
fibonacci.all?   { |number| number < 20 }   #=> true
fibonacci.map    { |number| number * 2  }   #=> [0, 2, 2, 4, 6, 10, 16, 26]

Mapping over arrays

This was quite hard for me. I think just not used to syntax. Once I see the answer it is obvious. compact removes the nil values in an array.

# So an example of the data is

    shoes = { price: 30.00, name: "Shoes", quantity_by_size: { s: 1, xl: 4 } }
    coat = { price: 65.00, name: "Coat", quantity_by_size: { s: 2 } }
    handkerchief = { price: 19.99, name: "Handkerchief", quantity_by_size: { m: 3, l: 2 } }
    items = [shoes, coat, handkerchief]

class BoutiqueInventory
  def initialize(items)
    @items = items
  end

  def item_names
    @items.map { |item| item[:name] }.flatten.sort
  end

  def cheap
    @items.map { |item| item[:price] < 30.00 ? item : nil }.compact
  end

  def out_of_stock
    @items.map { |item| item[:quantity_by_size].empty? ? item : nil }.compact
  end

  def stock_for_item(name)
    @items.map { |item| item[:name] == name ? item[:quantity_by_size] : nil }.compact.first || {}
  end

  def total_stock
    @items.map { |item| item[:quantity_by_size].values.sum }.sum
  end

  private
  attr_reader :items
end

Openstruct

With this we create an object from a hashes. E.g.

attributes = { name: "Jeremy Walker", age: 21, location: "Nomadic" }
person = OpenStruct.new(attributes)

# You can now do
person.name
#=> Jeremy Walker

person.location
#=> Nomadic

So for out example above we can do

# So an example of the data is

    shoes = { price: 30.00, name: "Shoes", quantity_by_size: { s: 1, xl: 4 } }
    coat = { price: 65.00, name: "Coat", quantity_by_size: { s: 2 } }
    handkerchief = { price: 19.99, name: "Handkerchief", quantity_by_size: { m: 3, l: 2 } }
    items = [shoes, coat, handkerchief]

class BoutiqueInventory
  def initialize(items)
    # @items = items
    @items = items.map { |item| OpenStruct.new(item) }
  end

  def item_names
    # @items.map { |item| item[:name] }.flatten.sort
    # First Bash
    # items.map { |item| item.name }.sort
    items.map(&:name).sort
  end

  def total_stock
    # @items.map { |item| item[:quantity_by_size].values.sum }.sum
    # First Bash
    # items.sum do |item|
    #  item.quantity_by_size.values.sum
    # end
    items.map(&:quantity_by_size).map(&:values).map(&:sum).sum
  end

  private
  attr_reader :items
end

Decomposing Arrays and Hashes

I think it probably is just arrays as hashes will need to be converted to arrays

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> x, y, z = fruits_inventory
>> x
=> {:apple=>6, :banana=>2, :cherry=>3}
>> y
=> nil

Instead use to_a

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> fruits_inventory.to_a
=> [[:apple, 6], [:banana, 2], [:cherry, 3]]
>> x, y, z = fruits_inventory.to_a
>> x
=> [:apple, 6]

Once a array, we can decompose stuff any way we choose using splats to capture remaining. Did actually quite like this.

>> fruits = ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]
>> x, *middle, y, z = fruits
>> y
=> "melon"
>> middle
=> ["banana", "cherry", "orange", "kiwi"]

Composing Arrays and Hashes

Composing can be done with single splat (*) for arrays

>> fruits = ["apple", "banana", "cherry"]
>> more_fruits = ["orange", "kiwi", "melon", "mango"]

# fruits and more_fruits are unpacked and then their elements are packed into combined_fruits
>> combined_fruits = *fruits, *more_fruits

>> combined_fruits
=> ["apple", "banana", "cherry", "orange", "kiwi", "melon", "mango"]

And a double splat (**)for hashes

>> fruits_inventory = {apple: 6, banana: 2, cherry: 3}
>> more_fruits_inventory = {orange: 4, kiwi: 1, melon: 2, mango: 3}

# fruits_inventory and more_fruits_inventory are unpacked into key-values pairs and combined.
>> combined_fruits_inventory = {**fruits_inventory, **more_fruits_inventory}

# then the pairs are packed into combined_fruits_inventory
>> combined_fruits_inventory
=> {:apple=>6, :banana=>2, :cherry=>3, :orange=>4, :kiwi=>1, :melon=>2, :mango=>3}

Splats with Arguments

Maybe the only sensible thing to say about this is like Gary Eats ("let's give a go"). If they can fit it in they will. It suggests
def my_method(<positional_arguments>, *arguments, <positional_arguments>, <keyword_arguments>, **keyword_arguments)

def my_method(a, b, *arguments,c, symbol_bash:, **d)
  p a
  p b
  p arguments
  p c
  p symbol_bash
  p d
end

my_method(1, 2, 3, 4, 5, 6, symbol_bash: 100, a: 1, b: 2)

Ruby String functions

translate characters (tr)

Just substitute a for b

"metal-oxide".tr("-", " ")

global substitute with regex

So will never learn regex just to difficult for me. Know how to solve so not an issue.

  • [...] means set
  • ^ inside bracket means not like maths
  • a-z means what it says a,b,...z
  • A-Z means what it says A,B,...Z
  • \s means whitespace, space, tab, newline, carriage return

So substitute anything not in set with ""

phrase.gsub(/[^a-zA-Z\s]/, "")

Exceptions

So quick reminder of mainly rescue and exceptions

class SimpleCalculator
  ALLOWED_OPERATIONS = ['+', '/', '*'].freeze

  class UnsupportedOperation < StandardError; end

  def self.calculate(first_operand, second_operand, operation)
    unless ALLOWED_OPERATIONS.include?(operation)
      raise UnsupportedOperation, "Operation must be one of #{ALLOWED_OPERATIONS.join(', ')}"
    end

    raise ArgumentError, 'Operands must be numeric' unless first_operand.is_a?(Numeric) && second_operand.is_a?(Numeric)

    case operation
    when '+'
      "#{first_operand} + #{second_operand} = #{first_operand + second_operand}"
    when '/'
      begin
        "#{first_operand} / #{second_operand} = #{first_operand / second_operand}"
      rescue ZeroDivisionError
        'Division by zero is not allowed.'
      end
    when '*'
      "#{first_operand} * #{second_operand} = #{first_operand * second_operand}"
    end
  end
end

Floor and Ceil

Never does alot of this and always with unit tests so it does not matter. Floor round down, ceil round up. By default ruby floors so

  # This will round down 
  (score - 10) / 2

Reduce in Ruby

Took a while to get through this one because of the ruby syntax. This was the test case
Given this
one fish two fish red fish blue fish
return
{ "one" => 1, "fish" => 4, "two" => 1, "red" => 1, "blue" => 1 }

For typescript this is relatively easy. I don't know and will never know regex (getting better)

function wordCount(phrase: string): Record<string, number> {
<syntaxhighlight lang="rb">
  const words = phrase.match(/\b\w+'\w+|\b\w+\b/g) ?? [];

  return words.reduce<Record<string, number>>((acc, word) => {
    acc[word] = (acc[word] ?? 0) + 1;
    return acc;
  }, {});
}

So the equivalent for ruby is this

class Phrase
  attr_reader :phrase

  def initialize(phrase)
    @phrase = phrase.downcase
  end

  def word_count
    words = phrase.scan(/\b\w+'\w+|\b\w+\b/)
    words.each_with_object(Hash.new(0)) { |word, counts| counts[word] += 1 }
  end
end

And the regex /\b\w+'\w+|\b\w+\b/ Always seems to be hard until explained.

  • \b\w+'\w+ word boundary, letters, apostrophe, letters - This allows for apostophe. so Iain's, don't
  • Or
  • \b\w+ word boundary, letters

zip

Never seen this but u can zip things provided they are the same lengths. So a is compared with b and it counts how many not equal.

module Hamming
  def self.compute(a, b)
    raise ArgumentError unless a.length == b.length
    a.chars.zip(b.chars).count { |x, y| x != y }
  end
end

Scrabble Scoring

Something handy at last

class Scrabble 
  LETTER_SCORES = {
    "A" => 1, "E" => 1, "I" => 1, "O" => 1, "U" => 1,
    "L" => 1, "N" => 1, "R" => 1, "S" => 1, "T" => 1,
    "D" => 2, "G" => 2,
    "B" => 3, "C" => 3, "M" => 3, "P" => 3,
    "F" => 4, "H" => 4, "V" => 4, "W" => 4, "Y" => 4,
    "K" => 5,
    "J" => 8, "X" => 8,
    "Q" => 10, "Z" => 10
  }.freeze

  def initialize(string)
    @string = string
  end

  def score
    @string.each_char.sum { |ch| LETTER_SCORES.fetch(ch.upcase, 0) }
  end
end

And homework

class Scrabble 
  GROUPED_SCORES = {
    1 => "AEIOULNRST",
    2 => "DG",
    3 => "BCMP",
    4 => "FHVWY",
    5 => "K",
    8 => "JX",
    10 => "QZ"
  }.freeze

  LETTER_SCORES = GROUPED_SCORES.each_with_object({}) do |(points, letters), score_map|
    letters.each_char { |letter| score_map[letter] = points }
  end.freeze

  def initialize(string)
    @string = string
  end

  def score
    @string.each_char.sum { |ch| LETTER_SCORES.fetch(ch.upcase, 0) }
  end
end