Taylor Tompkins
Taylor Tompkins

Reputation: 71

Checking that string contains only unique array elements, repeating letters

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

Answers (3)

Sergio Belevskij
Sergio Belevskij

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

Cary Swoveland
Cary Swoveland

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

tadman
tadman

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

Related Questions