fmi11
fmi11

Reputation: 85

How to Add Values to a Hash in Ruby

I have done much research on this topic, but in every circumstance I attempt, the values appear to be replaced in the hash. After the person opts to enter a new ID, I would like the next person's name and age to be added to the hash. Could someone explain to me why the keys and values are being replaced?

class Person
  def initialize(name, age)
    if name != nil || age != nil
      if @people != nil
        @people[name.__id__] = age.__id__
      else
        @people = {name => age}
      end
    else
      puts "Invalid credentials."
    end
  end

  attr_reader :people
end

class MainInit
  def initialize()
    options = ["y", "yes", "n", "no"]
    choice = "y"
    while choice.downcase == "y" || choice.downcase == "yes"
      p "Enter Name:"
      inputname = gets.chomp
      p inputname

      p "Enter Age:"
      inputage = gets.chomp
      p inputage

      person = Person.new(inputname, inputage)
      p person.people

      p "Enter another ID?"
      choice = gets.chomp
      until options.include? choice.downcase
        p "Invalid Choice"
        p "Enter another ID?"
        choice = gets.chomp
      end
    end
  end
end

MainInit.new

Upvotes: 0

Views: 250

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110675

Let me both explain why you are having the problem you describe and also offer some suggestions for how you might change your code.

class Person

In class Person, you need to save your list of persons at the class level, which means the use of either a class instance variable (e.g., @people) or a class variable (e.g., @@people). I am with the majority of Rubiests in prefering the former. (The reasons are beyond the scope of this answer, but you will find a lot written on the subject by simply Googling, "Ruby 'class instance variables' versus 'class variables'". The inner quotes--the only ones you enter--help narrow the search.)

To define a class instance variable, @people, we just enter it as follows:

class Person
  @people = {}
  class << self
    attr_accessor :people
  end
  def initialize(name, age)
    self.class.people[name] = age
  end
end

The @ means it is an instance variable. As soon as Ruby reads class Person, it sets self to Person. It then reads @people = {} and makes that an instance variable of Person. By contrast, if you were to initialize @person within, say, an initialize method, self would at that time be an instance of Person, so @person would be a normal instance variable. (Aside: we could have both a class instance variable @person and an instance variable @person, and Ruby would treat them as differently as it would @night and @day.)

In order for objects to access @people we define an accessor. If we just entered attr_accessor :person, Ruby would create an accessor for a regular instance variable @person. Instead we enter class << self, which directs Ruby to associate what follows, until end is reached, with the class.

Each time a new instance of Person is created, for a given name and age,

self.class.people[name] = age

adds an element to the hash @person, since self.class is Person and people is the accessor.

Now look at the class MainInit

class MainInit

class MainInit
  def initialize
    loop do
      name = nil
      loop do
        print 'Enter Name: '
        name = gets.strip
        break unless name.empty?
      end  
      puts "name is #{name}"
      age = nil
      loop do
        print 'Enter Age: '
        age = gets.strip
        case age
        when /^\d+$/ && ('10'..'120')
          break
        else
          puts 'age must be between 10 and 120'
        end  
      end  
      puts "age is #{age}"
      person = Person.new(name, age)
      puts "people is now #{Person.people}"
      loop do
        print "Enter another ID? "
        case gets.chomp.downcase
        when 'n', 'no'
          return
        when 'y', 'yes'
          break
        else
          puts 'Invalid choice'
        end  
      end
    end
  end  
end

loop do...end

You see that in several places I have used loop do...end with break to exit a loop. I'm a big fan of this construct, as compared to loop while... or or until..., in part because it avoids the need to enter a starting condition to get into the loop and then repeat the same condition withing the loop. I also just think it looks cleaner.

Any variables created within the loop cease to exist when you leave the loop, so if you want a variable's value (e.g., name and age), you must reference the variable outside of the beginning of the loops. That is why (and the only reason) I have name = nil and age = nil. It didn't have to be nil; I could have initialized them to anything.

Use of case statement

The loop for getting age uses this case statement:

case age
when /^\d+$/ && ('10'..'120')
  ...
end  

This requires some explanation. The case statement uses String#===, rather than String#== to obtain truthy values. Therefore when /^\d+$/ is equivalent to:

/^\d+$/ === age

which is the same as

/^\d+$/ =~ age

The regex simply ensures that all characters of age are digits (e.g., "39).

Similarly,

('10'..'120') === age

is the same as

('10'..'120').cover?(age)

Odds and Ends

I used String#strip in place of String#chomp. Both remove ending newline characters, but strip also removes spaces the user may have entered at the beginning or end of the input string.

For strings, I mostly used single quotes, but double-quotes are needed for string interpolation. For example, I initially wrote puts 'name is #{name}'. That printed name is #{name}. After changing that to puts "name is #{name}", it correctly printed name is Debra.

Example

MainInit.new
Enter Name: Debra       
name is Debra
Enter Age: 29
age is 29
people is now {"Debra"=>"29"}
Enter another ID? y
Enter Name: Billy-Bob
name is Billy-Bob
Enter Age: 58
age is 58
people is now {"Debra"=>"29", "Billy-Bob"=>"58"}
Enter another ID? u
Invalid choice
Enter another ID? n

Upvotes: 0

nPn
nPn

Reputation: 16728

I think the reason the key-value pairs are being replaced is this:

The statement in your initialize method

if @people != nil

will always evaluate to false. initialize is called when you create a new object, so by default @people has not been defined or set yet, so each time you call

 person = Person.new(inputname, inputage)

it creates a new Person rather than adding the new person to an exiting Hash (which is what I think you are trying to do).

It might work if you make people a class variable (@@people), but it seems like you just want to create a Hash in your main program and then add the new entries in there.

So something like this

people = Hash.new # Or even just people = {}

Then when you have a new name / age entry to add

people[name] = age

I have not tried it, but I think your entire program should be reduced to something like this:

people = Hash.new
options = ["y", "yes", "n", "no"]
    choice = "y"
    while choice.downcase == "y" || choice.downcase == "yes"
      p "Enter Name:"
      inputname = gets.chomp
      p inputname

      p "Enter Age:"
      inputage = gets.chomp
      p inputage

      #person = Person.new(inputname, inputage)
      people[inputname] = inputage
      person = people[inputname]
      p person.people

      p "Enter another ID?"
      choice = gets.chomp
      until options.include? choice.downcase
        p "Invalid Choice"
        p "Enter another ID?"
        choice = gets.chomp
      end

Upvotes: 1

Related Questions