Pregunta

When trying to find the frequency of letters in 'fantastic' I am having trouble understanding the given solution:

def letter_count(str)
  counts = {}

  str.each_char do |char|
    next if char == " "
    counts[char] = 0 unless counts.include?(char)
    counts[char] += 1
  end

  counts
end

I tried deconstructing it and when I created the following piece of code I expected it to do the exact same thing. However it gives me a different result.

blah = {}
x = 'fantastic'
    x.each_char do |char|
        next if char == " "
        blah[char] = 0 
            unless 
            blah.include?(char)
            blah[char] += 1
    end
blah
end

The first piece of code gives me the following

puts letter_count('fantastic')
>
{"f"=>1, "a"=>2, "n"=>1, "t"=>2, "s"=>1, "i"=>1, "c"=>1}

Why does the second piece of code give me

puts blah
>
{"f"=>0, "a"=>0, "n"=>0, "t"=>0, "s"=>0, "i"=>0, "c"=>0}

Can someone break down the pieces of code and tell me what the underlying difference is. I think once I understand this I'll be able to really understand the first piece of code. Additionally if you want to explain a bit about the first piece of code to help me out that'd be great as well.

¿Fue útil?

Solución

You can't split this line...

counts[char] = 0 unless counts.include?(char)

... over multiple line the way you did it. The trailing conditional only works on a single line.

If you wanted to split it over multiple lines you would have to convert to traditional if / end (in this case unless / end) format.

unless counts.include?(char)
  counts[char] = 0
end

Here's the explanation of the code...

# we define a method letter_count that accepts one argument str
def letter_count(str)

  # we create an empty hash 
  counts = {}

  # we loop through all the characters in the string... we will refer to each character as char
  str.each_char do |char|

    # we skip blank characters (we go and process the next character)
    next if char == " "

    # if there is no hash entry for the current character we initialis the 
    # count for that character to zero
    counts[char] = 0 unless counts.include?(char)

    # we increase the count for the current character by 1
    counts[char] += 1

  # we end the each_char loop
  end

  # we make sure the hash of counts is returned at the end of this method 
  counts

# end of the method
end

Otros consejos

Now that @Steve has answered your question and you have accepted his answer, perhaps I can suggest another way to count the letters. This is just one of many approaches that could be taken.

Code

def letter_count(str)
  str.downcase.each_char.with_object({}) { |c,h|
    (h[c] = h.fetch(c,0) + 1) if c =~ /[a-z]/ }
end

Example

letter_count('Fantastic')
  #=> {"f"=>1, "a"=>2, "n"=>1, "t"=>2, "s"=>1, "i"=>1, "c"=>1}

Explanation

Here is what's happening.

str = 'Fantastic'

We use String#downcase so that, for example, 'f' and 'F' are treated as the same character for purposes of counting. (If you don't want that, simply remove .downcase.) Let

s = str.downcase #=> "fantastic"

In

s.each_char.with_object({}) { |c,h| (h[c] = h.fetch(c,0) + 1) c =~ /[a-z]/ }

the enumerator String#each_char is chained to Enumerator#with_index. This creates a compound enumerator:

enum = s.each_char.with_object({})
  #=> #<Enumerator: #<Enumerator: "fantastic":each_char>:with_object({})>

We can view what the enumerator will pass to the block by converting it to an array:

enum.to_a
  #=> [["f", {}], ["a", {}], ["n", {}], ["t", {}], ["a", {}],
  #    ["s", {}], ["t", {}], ["i", {}], ["c", {}]]

(Actually, it only passes an empty hash with 'f'; thereafter it passes the updated value of the hash.) The enumerator with_object creates an empty hash denoted by the block variable h.

The first element enum passes to the block is the string 'f'. The block variable c is assigned that value, so the expression in the block:

(h[c] = h.fetch(c,0) + 1) if c =~ /[a-z]/

evaluates to:

(h['f'] = h.fetch('f',0) + 1) if 'f' =~ /[a-z]/ 

Now

c =~ /[a-z]/

is true if and only if c is a lowercase letter. Here

'f' =~ /[a-z]/ #=> true

so we evaluate the expression

h[c] = h.fetch(c,0) + 1

h.fetch(c,0) returns h[c] if h has a key c; else it returns the value of Hash#fetch's second parameter, which here is zero. (fetch can also take a block.)

Since h is now empty, it becomes

h['f'] = 0 + 1 #=> 1

The enumerator each_char then passes 'a', 'n' and 't' to the block, resulting in the hash becoming

h = {'f'=>1, 'a'=>1, 'n'=>1, 't'=>1 }

The next character passed in is a second 'a'. As h already has a key 'a',

h[c] = h.fetch(c,0) + 1

evaluates to

h['a'] = h['a'] + 1 #=> 1 + 1 => 2

The remainder of the string is processed the same way.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top