lylyly
lylyly

Reputation: 93

Ruby: how to count the frequencies of the letters

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

Answers (2)

Cary Swoveland
Cary Swoveland

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

Silvio Mayolo
Silvio Mayolo

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

Related Questions