Reputation: 93
HELP: do a frequency method: calculate the frequency each letter appears in “str” and assign the output to “letters”. The method should count lower and uppercase as the same letter. but I have the error about NoMethodError: undefined method Thank you
here is my code
class MyString
attr_accessor :str
attr_reader :letters
def initialize
@str = "Hello World!"
@letters = Hash.new()
end
def frequency
@letters = @str.split(" ").reduce(@letters) { |h, c| h[c] += 1; h}
end
def histogram
#@letters.each{|key,value| puts "#{key} + ':' + value.to_s + '*' * #{value}" }
end
end
The error shows :
irb(main):009:0> txt1.frequency
NoMethodError: undefined method `+' for nil:NilClass
from assignment.rb:11:in `block in frequency'
from assignment.rb:11:in `each'
from assignment.rb:11:in `reduce'
from assignment.rb:11:in `frequency'
from (irb):9
from /usr/bin/irb:12:in `<main>'
Upvotes: 1
Views: 2160
Reputation: 110755
Whenever one uses a counting hash (@Silvio's answer) one can instead use Enumerable#group_by, which is what I've done here.
str = "It was the best of times, it was the worst of times"
str.gsub(/[[:punct:]\s]/, '').
downcase.
each_char.
group_by(&:itself).
each_with_object({}) { |(k,v),h| h[k] = v.size }
#=> {"i"=>4, "t"=>8, "w"=>3, "a"=>2, "s"=>6, "h"=>2, "e"=>5,
# "b"=>1, "o"=>3, "f"=>2, "m"=>2, "r"=>1}
The steps are as follows.
a = str.gsub(/[[:punct:]\s]/, '')
#=> "Itwasthebestoftimesitwastheworstoftimes"
b = a.downcase
#=> "itwasthebestoftimesitwastheworstoftimes"
e = b.each_char
#=> #<Enumerator: "itwasthebestoftimesitwastheworstoftimes":each_char>
f = e.group_by(&:itself)
#=> {"i"=>["i", "i", "i", "i"],
# "t"=>["t", "t", "t", "t", "t", "t", "t", "t"],
# ...
# "r"=>["r"]}
f.each_with_object({}) { |(k,v),h| h[k] = v.size }
#=> < return value shown above >
Let's look more closely at the last step. The first key-value pair of the hash f
is passed to the block as a two-element array, together with the initial value of the hash h
:
(k,v), h = [["i", ["i", "i", "i", "i"]], {}]
#=> [["i", ["i", "i", "i", "i"]], {}]
Applying the rules of disambiguation (or decomposition), we obtain the following.
k #=> "i"
v #=> ["i", "i", "i", "i"]
h #=> {}
The block calculation is performed:
h[k] = v.size
#=> h["i"] = 4
So now
h => { "i"=>4 }
The next key-value pair is passed to the block, along with the current value of h
:
(k,v), h = [["t", ["t", "t", "t", "t", "t", "t", "t", "t"]], { "i"=>4 }]
#=> [["t", ["t", "t", "t", "t", "t", "t", "t", "t"]], {"i"=>4}]
k #=> "t"
v #=> ["t", "t", "t", "t", "t", "t", "t", "t"]
h #=> {"i"=>4}
h[k] = v.size
#=> 8
So now
h #=> {"i"=>4, "t"=>8}
The remaining calculations are similar.
The method Enumerable#tally, which made its debut in Ruby v2.7, is purpose-built for this task:
str.gsub(/[[:punct:]\s]/, '').downcase.each_char.tally
#=> {"i"=>4, "t"=>8, "w"=>3, "a"=>2, "s"=>6, "h"=>2,
# "e"=>5, "b"=>1, "o"=>3, "f"=>2, "m"=>2, "r"=>1}
Upvotes: 3
Reputation: 70397
When you try to add 1
to a value in the hash that doesn't exist, it tries to add 1
to nil
, which isn't allowed. You can change the hash so that the default value 0
, not nil
.
@letters = Hash.new(0)
Now, your program right now is counting word frequencies, not letter frequencies (split(" ")
splits on spaces, not on each character). To split on each character, use the appropriately named each_char
method.
@letters = @str.each_char.reduce(@letters) { |h, c| h[c] += 1; h}
Upvotes: 4