Reputation: 85
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
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
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