Reputation: 71
I am creating a method to test if a string contains only array elements in a game of scrabble. Given this array:
hand = ["b", "l", "c", "o", "h", "e", "a"] if word is "beach"
Originally I used this block:
def uses_available_letters?(word, letters_in_hand)
input = word.split("")
input.index{ |x| !letters_in_hand.include?(x) }.nil?
end
Given this array:
hand = ["b", "l", "c", "o", "h", "e", "a", "c"]
if word = "beach", method returns true. However, if word = "beeeach", it will still return true even though the array only contains 1 "e".
SO, I tried deleting the array element after it was compared:
def uses_available_letters?(word, letters_in_hand)
input = word.split("")
input.each do
if letters_in_hand.include?(input[i])
letters_in_hand.delete(input[i])
else
return false
end
end
end
BUT, given "beacch", false is returned even though there are 2 c's in the array. So it seems every like letter is being deleted.
Send help!
Upvotes: 1
Views: 320
Reputation: 2957
def uses_available_letters?(word, hand)
result = true
word.split('').each do |letter|
if hand.include?(letter)
hand.delete_at(hand.index(letter))
else
result = false
end
end
result
end
2.6.5 :065 > uses_available_letters?('beeeach', %w[b l e h c a e e])
=> true
2.6.5 :066 > uses_available_letters?('beach', %w[b l e h c a])
=> true
2.6.5 :067 > uses_available_letters?('beeeach', %w[b l e h c a])
=> false
2.6.5 :068 > uses_available_letters?('beeeach', %w[b l e d h e c e r a])
=> true
Upvotes: 1
Reputation: 110725
Here are two ways that could be done.
Compare counting hashes
def uses_available_letters?(word, letters_in_hand)
word_tally = word.each_char.tally
letters_in_hand_tally = letters_in_hand.tally
word_tally.all? { |k,v| letters_in_hand_tally[k].to_i >= v }
end
letters_in_hand = ["b", "l", "c", "e", "o", "h", "e", "a"]
uses_available_letters?("belch", letters_in_hand)
#=> true
uses_available_letters?("belche", letters_in_hand)
#=> true
uses_available_letters?("beleche", letters_in_hand)
#=> false
When word = "beleche"
,
word_tally
#=> {"b"=>1, "e"=>3, "l"=>1, "c"=>1, "h"=>1}
letters_in_hand_tally
#=> {"b"=>1, "l"=>1, "c"=>1, "e"=>2, "o"=>1, "h"=>1, "a"=>1}
See Enumerable#tally, which was introduced in Ruby v2.7. To support earlier versions of Ruby one could use the form of Hash::new that takes an argument (here zero), called the default value, and no block.
def uses_available_letters?(word, letters_in_hand)
word_tally = tally_ho(word.chars)
letters_in_hand_tally = tally_ho(letters_in_hand)
word_tally.all? { |k,v| letters_in_hand_tally[k].to_i >= v }
end
def tally_ho(arr)
arr.each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
end
If letters_in_hand_tally
does not have a key k
, letters_in_hand_tally[k] #=> nil
, resulting in the comparison nil >= v
, which would raise an exception. It is for that reason I wrote letters_in_hand_tally[k].to_i
, as nil.to_i #=> 0
and all values of word_tally
will be positive integers.
One could alternatively write the block as follows:
{ |k,v| letters_in_hand_tally.key?(k) && letters_in_hand_tally[k] >= v }
Treat duplicate letters in letters_in_hand
as distinct letters
This second method assumes letters_in_hand
is given by a string, rather than an array. For example:
letters_in_hand = "blceohea"
Of course we could always form that string from the array as a first step, but I think it's more pleasing for letters_in_hand
to be a string, just as word
is. The following method makes use of the fact that String#index accepts an optional second argument that equals the index into the string where the search is to begin:
def uses_available_letters?(word, letters_in_hand)
start_index = Hash.new(0)
word.each_char.all? do |c|
i = letters_in_hand.index(c, start_index[c])
return false if i.nil?
start_index[c] = i + 1
end
true
end
uses_available_letters?("belch", letters_in_hand)
#=> true
uses_available_letters?("belche", letters_in_hand)
#=> true
uses_available_letters?("belecher", letters_in_hand)
#=> false
Upvotes: 0
Reputation: 211670
The trouble with Ruby array operations is most treat them as a set of unique values, as in:
%w[ a a b b c c ] - %w[ a b c ]
# => []
Where that removes every a
, b
and c
, not just the first. The same goes for delete
unless you use a very specific index, but that gets messy in a hurry since deleting shuffles the remaining indexes, etc.
I'd consider storing the hand as a letter/count pair, as in:
def hand_count(str)
str.chars.group_by(&:itself).map { |l,a| [ l, a.length ] }.to_h
end
Where that gives you a hash instead of an array:
hand_count('example')
# => {"e"=>2, "x"=>1, "a"=>1, "m"=>1, "p"=>1, "l"=>1}
So now you can write a "sub" method:
def hand_sub(str, sub)
hand = hand_count(str)
hand_count(sub).each do |l, c|
# Unless the letter is present in the hand...
unless (hand.key?(l))
# ...this isn't possible.
return false
end
# Subtract letter count
hand[l] -= c
# If this resulted in a negative number of remaining letters...
if (hand[l] < 0)
# ...this isn't possible.
return false
end
end
# Convert back into a string...
hand.map do |l, c|
# ...by repeating each letter count times.
l * c
end.join
end
Where that works quite simply:
hand_sub('example', 'exam')
# => "epl"
hand_sub('example', 'expel')
# => "am"
hand_sub('example', 'plexi')
# => false
hand_sub('example', 'ell')
# => false
Upvotes: 1