2

I'm trying to write some code that will loop through an array of strings, clean up the entries, and then add the cleaned up entries to a hash that tracks the frequency with which each word appears. This was my first solution:

puts("Give me your text.")
text = gets.chomp

words = text.split
frequencies = Hash.new(0)
words.map! do |word|
  word.tr("\",.", "")
end
words.each do |word|
  frequencies[word] += 1
end

It works fine, but looping through the array twice feels very inefficient, so I've been trying to find a way to do it one go and stumbled upon the following:

puts("Give me your text.")
text = gets.chomp

words = text.split
frequencies = Hash.new(0)
words.each_with_index do |word, index|
  words[index].tr!("\",.", "")
  frequencies[word] += 1
end

Based on my understanding of each_with_index, this shouldn't work, but somehow it does, and the hash receives the clean version of each string: https://repl.it/B9Gw. What's going on here? And is there a different way to solve this problem without looping twice?

EDIT: After some reading, I was able to solve the problem using just one loop in the following way:

puts("Give me your text.")
text = gets.chomp

words = text.split
frequencies = Hash.new(0)
for i in 0..words.length-1
  words[i].tr!("\",.", "")
  frequencies[words[i]] += 1
end

However, this is more of a JS or C++ solution and doesn't seem like idiomatic Ruby. Are there any other options? Also, why does the each_with_index approach even work?

1 Answer 1

3

You are using the String#tr! method, which modifies the string destructively instead of returning a new string. The fact that you are looking it up on the hash again (using words[index]) doesn't change anything, because the string object is still the same - so the word you use to modify the frequencies hash is also modified.

And is there a different way to solve this problem without looping twice?

An obvious way would be to use the same logic that you used, but without the with_index (which isn't making any difference here anyway). I would advise using the non-destructive String#tr instead of String#tr!, to make it more clear which strings have been cleaned and which have not.

frequencies = Hash.new(0)
words.each do |word|
  cleaned = word.tr("\",.", "")
  frequencies[cleaned] += 1
end

If you want to make clear the map phase of the process and still only loop once, you can leverage ruby's lazy enumerators:

frequencies = Hash.new(0)
cleaned_words = words.lazy.map { |word| word.tr("\",.", "") }

cleaned_words.each do |cleaned|
  frequencies[cleaned] += 1
end

Here, even though we do a map and then an each, the collection is only traversed once, and ruby doesn't create any intermediary arrays.

Sign up to request clarification or add additional context in comments.

1 Comment

I'm not sure I follow. My understanding is that word is assigned a certain value before each pass through the loop, so the changes made to the array by words[index].tr!("\",.", "") shouldn't affect it. Consider the following example: array = [1, 2, 3] array.each_with_index do |element, index| array[index] += 1 puts element end The values that get printed out here are the original values in the array. Also, your alternate solution isn't equivalent because it leaves the original array unchanged, and I would like it to get updated.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.