Skip to content

Instantly share code, notes, and snippets.

@stacietaylorcima
Last active January 16, 2018 01:46
Show Gist options
  • Save stacietaylorcima/2c1a3635d14a41ba81c882210ab81126 to your computer and use it in GitHub Desktop.
Save stacietaylorcima/2c1a3635d14a41ba81c882210ab81126 to your computer and use it in GitHub Desktop.

Ruby: Hashes

Concept Description
Hash A unordered data structure that allows for storage of key and value pairs
Access We can access the value of a given key by passing the key to the hash like hash_name[:key]
Iteration We can use a for in loop to iterate over the key/value pairs in a hash
Hash arguments We can use hashes to pass arguments to a method call

Objectives:

  • Define the Hash data type in Ruby.
  • Demonstrate how to access elements in a Hash.
  • Demonstrate how to iterate over the key/value pairs in a Hash.
  • Understand how to use Hashes to pass arguments into methods.

The Hash Class

  • A Hash is a dictionary-like collection of unique keys and their values. Also called associative arrays, they are similar to Arrays, but where an Array uses integers as its index, a Hash allows you to use any object type.
  • A hash gives us a better way to store our data because we can access it more intuitively.

Write a Hash:

hash_name = {key: value}
  • Hash name can be any approved variable name.
  • To the right of the = operator, we see an open curly bracket, followed by a key-value pair and a closing curly bracket.
  • key can be any word or number as long as it's not repeated in the hash.
  • value can be any type of object, even a Hash itself! Let's take a look at a few ways to initiate a Hash:
dog = Hash.new # using the #new method
dog = {} # initializing an empty hash
dog = {name: "Charlie"} # using literal form, preferred!
dog = {:name => "Charlie"} # using the old hash rocket syntax

Accessing Values

  • Format for accessing a value for a key in a Hash:
hash[expression]
  • Let's take a look at the following Hash.
  • Tip: Breaking Hashes into seperate lines improves readability.
person = {
  name: "Megan",
  wallet: {"5 dollar bills" => 2},
  city_of_birth: "New York"
}
  • More than one word?

    • If we need a key to contain more than one word, we’ll need to either use a string like "5 dollar bills", or an underscore separating each word as in city_of_birth above.
  • Ask the Person's Name:

    • Pass the key into the Hash.
person[:name]
#=> "Megan"
  • Ask for Wallet Info:
    • Notice that the value is a Hash itself.
person[:wallet]
#=> {"5 dollar bills" => 2}
  • Get the Nuber of 5 Dollar Bills in Wallet:
    • Using hash chaining, ask person for wallet value, then "5 dollar bill" value.
person[:wallet]["5 dollar bills"] #=> returns 2

OR

  • Store wallet hash into megans_wallet variable, then call (using the string) for "5 dollar bills".
megans_wallet = person[:wallet] #stores the wallet hash into megans_wallet
megans_wallet["5 dollar bills"] #=> 2
  • Get Megan's Birth City:
    • This code is shorter and more efficient, since it doesn’t use memory space to store a variable with the value of that key. To get Megan's city of birth, we'll need a Symbol:
person[:city_of_birth] #=> returns "New York"
  • Using Integers as Keys:

    • Using integers as keys in your Hash will require you to use the old Hash rocket syntax, like: key => value.
  • Using Expressions to Access Values:

    • The expression passed into the square brackets will be evaluated, and the result will be the key that is sent with the request.
hash = {1 => "one", 2 => "two"}
number_one = 1

puts hash[number_one] #=> returns "one"
puts hash[1 + 1] #=> returns "two"
  • Default Values for Non-Existing Keys:
    • One very useful default behavior with hashes is the default values provided for keys that do not exist.
answers = {capital_of_texas: "Austin", number_of_wheels_in_bicycle: 2}
answers[:meaning_of_life] #=> nil.
  • We asked the answers hash for the value of the key :meaning_of_life and instead of raising an error, it simply returns nil.
  • The useful thing about that is that nil is falsey. We can use this to see if a value exists in a key:
#above could also be written as:
if answers[:meaning_of_life].nil? #returns true if object called on is nil
  p "that key is not defined"
end

#since nil is falsey, we don't need to compare it with nil
unless answers[:meaning_of_life] # returns true if anything other than false or nil
  p "that key is not defined"
end
  • Checking if a Key is Defined? has_key?
    • Checking if a key is defined in a hash is such a common operation that there is a method we can use to ask the hash if a key is defined.
    • The has_key? method takes an argument, and if the argument is a key defined on the hash the method was called on, it returns as true.
if answers.has_key?(:meaning_of_life)
  # do something awesome
end
  • Looking for a Value? has_value?

    • There is also a has_value? method that looks through the values instead.
  • Change Default Behavior of Returning nil:

    • If we want to change the default behavior of returning nil for keys that are not found, we can simply say so when we initialize the hash:
answers = Hash.new(42)
answers[:meaning_of_life] #=> 42.

Adding & Removing Values

  • Add Keys to a Hash:
    • Once a hash has been defined, we can add new keys and change the values of existing keys just as easily as accessing them.
recipes = {}
recipes[:cereal] = "Pour cereal in bowl. Add milk. Enjoy."
recipes["boiled eggs"] = "Put eggs in small saucepan with boiling water. Remove after 7 minutes. Peel eggs. Enjoy."
  • Turn Array into Hash:
numbers = ["zero", "one", "two"]
numbers_hash = {}

numbers.each_with_index do |element, index|
  numbers_hash[index] = element
end
  • Delete Keys from a Hash:
    • To remove a key from a hash, we could use the delete method.
    • This takes the key you want to remove from the Hash as an argument and returns the value of the key if it was successful, or it returns nil if the key wasn't found.
person = {name: "Arya"}
person.delete(:name) #=> "Arya"
  • If we are feeling more destructive, the Hash class also responds to the clear method. It will delete every key-value pair in the Hash.

Iteration

  • Like the Array class, the Hash class has a method called each that lets you iterate over every key-value pair in a Hash.
person = {name: "Janet", age: "25", number_of_cats: 1}
person.each do |key, value|
  puts "Key is #{key} and value is #{value}"
end

=begin
returns:

Key is name and it's value is Janet
Key is age and it's value is 25
Key is number_of_cats and it's value is 1

=end
  • Iterate Over ONLY Keys or ONLY Values:
  • There are also versions that only iterate over keys or values:
person = {name: "Janet", age: "25", number_of_cats: 1}
person.each_key do |key|
  puts "Key is #{key} but no value is passed :("
end

=begin
returns:

Key is name but no value is passed :(
Key is age but no value is passed :(
Key is number_of_cats but no value is passed :(

=end

person.each_value do |value|
  puts "Value is #{value} but no key is passed :("
end

=begin
returns:

Value is Janet but no key is passed :(
Value is 25 but no key is passed :(
Value is 1 but no key is passed :(

=end
  • Iterate over Key-Value Pairs:
    • We can also iterate over key-value pairs in a Hash using a for loop:
    • Every loop iteration in our variable passenger is an object of the Array class.
      • The first element in that array will be the symbol that represents the key we are currently looking at.
      • The second element will be the value that the key contains.
car_passenger_list = {
  driver: 'Rick',
  front_passenger: 'Glenn',
  rear_left_passenger: nil,
  rear_right_passenger: 'Daryl'
}

for passenger in car_passenger_list
  p "key-value pair: #{passenger}"
end

=begin
returns:

key-value pair: [:driver, "Rick"]
key-value pair: [:front_passenger, "Glenn"]
key-value pair: [:rear_left_passenger, nil]
key-value pair: [:rear_right_passenger, "Daryl"]

=end
  • Write a method that will let a machine scan a vehicle and report on the occupants:
car_passenger_list = {
  driver: 'Rick',
  front_passenger: 'Glenn',
  rear_left_passenger: nil,
  rear_right_passenger: 'Daryl'
}

def halt_who_goes_there(car_passenger_list)
  for passenger in car_passenger_list
    unless passenger.last.nil? #unless the value of the current key-value pair is nil
      p "#{passenger.last} is the #{passenger.first.to_s}"
    else
      p "There is no #{passenger.first.to_s}"
    end
  end
end

halt_who_goes_there(car_passenger_list)

=begin
returns:

Rick is the driver
Glenn is the front_passenger
There is no rear_left_passenger
Daryl is the rear_right_passenger

=end
  • It's not quite right. passenger.first (we could use passenger[0]) gives us a Symbol.
  • We use the to_s method to turn that into a String.
  • However, those underscores don’t look correct.Take another look at them:
car_passenger_list = {
  driver: 'Rick',
  front_passenger: 'Glenn',
  rear_left_passenger: nil,
  rear_right_passenger: 'Daryl'
}

def halt_who_goes_there(car_passenger_list)
  for passenger in car_passenger_list
    unless passenger.last.nil? #unless the value of the current key-value pair is nil
      p "#{passenger.last} is the #{passenger.first.to_s.split('_').join(' ')}"
    else
      p "There is no #{passenger.first.to_s.split('_').join(' ')}"
    end
  end
end

halt_who_goes_there(car_passenger_list)

=begin
returns:

Rick is the driver
Glenn is the front passenger
There is no rear left passenger
Daryl is the rear right passenger

=end
  • We split the string at every underscore, which gave us an Array with every word as an element, and then we joined every element of that array into a string by separating each word with a space!
  • There is some refactoring that we can do, such as storing the seat position for each iteration to avoid using the same method chain twice. You should always write code that does what is expected and then refactor later.

Additional Methods and Use Cases

  • How many key-value pairs are in a Hash?
    • To find out, we can use the length, size or count methods:
language_proficiency = {ruby: true, javascript: true, swift: false}
language_proficiency.length #=> 3
language_proficiency.size #=> 3
language_proficiency.count #=> 3
  • Ask a hash if it has no pairs
    • empty? method:
wallet = {}
wallet.empty? #=> true
  • Get an array containing all of the keys or values in a hash
    • Using the keys or values methods:
numbers = {one: "uno", two: "dos", three: "tres"}

numbers.keys #=> [:one, :two, :three]
numbers.values #=> ["uno", "dos", "tres"]
  • Pass named parameters into methods
    • This opens up our classes to a clean interface, and makes it easier for us to transition to using Rails later on.
def check_out(name, options = {})
  p "Welcome to Ruby mart! Let's check you on outta here:"
  return "Good-bye, window shopper" if options.empty?
  options.each do |k, v|
    p "#{k} for $#{v}"
  end
  p "Thank you for shopping, #{name}!"
end
  • In the code above, we could call that method with just one argument, and it would use that method as name. However, we could also pass a hash as a second argument, and it will list the pairs in the hash like this:
check_out("Megan", oranges: 2, potatoes: 2, milk: 10)

=begin
returns:

Welcome to Ruby mart! Let's check you on outta here:
oranges for $2
potatoes for $2
milk for $10
Thank you for shopping, Megan!

=end
  • Note that if the options hash is either the last parameter or the only parameter in the method signature, we can use an implicit hash like we did in the method call above. If not, we’d need to wrap the key-value pairs in curly brackets.
@stacietaylorcima
Copy link
Author

stacietaylorcima commented Jan 15, 2018

Exercises - Word Frequency Table

  • Define a method called frequency_table that will take a String as an argument and return a frequency table (using the Hash class), where each key is a word in the String and it's value is the number of times the word occurs in the sentence. The method should not be case sensitive. Assume that the input will always be a String. Return an empty Hash for an empty String.
def frequency_table(string)
  word_hash = {}
  string.downcase.split.each do |word|
    word_hash[word] = word_hash[word] ? word_hash[word] += 1 : 1 
  end
  word_hash
end

@stacietaylorcima
Copy link
Author

Exercises - Settings Attributes

  • Define a class named User. Its initialize method should take a Hash as an argument. We'll name the argument config and set a default value for the argument to an empty Hash:
class User
  def initialize(config = {})
  end
end
  • This config = {} the syntax supplies a "default argument" for initialize. If someone initializes a User instance without a config argument, the config variable in the method will automatically be set to the default we gave it--an empty Hash.

  • The config argument should be used to set any of the following attributes on a user: name, email, bio, age, and sex. If an attribute is not provided in the Hash argument, the initialize method should default it to a value to "n/a". For example:

class User
  def initialize(config = {})
    @name = config[:name] || "n/a"
    @email = config[:email] || "n/a"
    ...
  end
end
  • _Setting default values is a very common task in Ruby. A basic way to do this is by using the || assignment operator, which means "or". _

  • We'll also need to access the instance variables set in our initialize method. To do this, we can use the attr_accessor method declaration. The attr_accessor method also lets us declare multiple attributes on one line. For example:

class User
  attr_accessor :name, :email

  def initialize(config = {})
    @name = config[:name] || "n/a"
    @email = config[:email] || "n/a"
    # ...
  end
end
  • Finish writing the User class and initialize method to handle all of the required attributes.
class User
  attr_accessor :name, :email, :bio, :age, :sex
  def initialize(config= {})
    @name = config[:name] || "n/a" 
    @email = config[:email] || "n/a"
    @bio = config[:bio] || "n/a"
    @age = config[:age] || "n/a" 
    @sex = config[:sex] || "n/a" 
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment