Sorting hash fast runtime in Ruby - ruby

I'm trying to build a method in Ruby (2.6.3) to return the top 100 objects with the highest count from an array with 1_000_000+ elements. But I need this method to be really fast. I have benchmark two possible solutions:
arr = []
1_000_000.times { arr << Faker::Internet.public_ip_v4_address}
def each_counter(arr)
counter = Hash.new(0)
arr.each do |item|
counter[item] += 1
end
counter.sort_by{|k,v| -v } [0..99]
end
def each_with_obj_counter(arr)
results = arr.each_with_object (Hash.new(0)) { |e, h| h[e] += 1}
results.sort_by{|k,v| -v } [0..99]
end
And the results are:
user system total real
each: 1.360000 0.141000 1.501000 (1.505116)
each_with_obj: 0.859000 0.047000 0.906000 (0.910877)
Is there a better solution for this? I've been asked to upgrade the method in order to provide a quick response under 300ms. Is that possible?

Try this.
def doit(arr, top_n)
arr.each_with_object(Hash.new(0)) { |n,h| h[n] += 1 }
.max_by(top_n, &:last)
end
For example,
a = %w| a b c d e f g h i j |
#=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j"]
arr = 50.times.map { a.sample }
#=> ["i", "d", "i", "g", "f", "f", "e", "h", "g", "i",
# "b", "c", "b", "b", "a", "g", "j", "f", "e", "h",
# "b", "d", "h", "g", "d", "d", "g", "j", "b", "h",
# "g", "g", "g", "f", "d", "b", "h", "j", "c", "e",
# "d", "d", "c", "b", "f", "h", "g", "j", "d", "h"]
doit(arr, 5)
#=> [["g", 9], ["d", 8], ["h", 7], ["b", 7], ["f", 5]]
Note that in this example,
arr.each_with_object(Hash.new(0)) { |n,h| h[n] += 1 }
#=> {"i"=>3, "d"=>8, "g"=>9, "f"=>5, "e"=>3,
# "h"=>7, "b"=>7, "c"=>3, "a"=>1, "j"=>4}
Let's try a poor man's benchmark.
def each_with_obj_counter(arr, top_n)
results = arr.each_with_object (Hash.new(0)) { |e, h| h[e] += 1}
results.sort_by{|k,v| -v } [0..top_n-1]
end
require 'time'
top_n = 20
n = 1_000_000
arr = n.times.map { rand(1_000) }
arr.size
#=> 1000000
arr.first(10)
#=> [68, 259, 168, 79, 809, 38, 912, 398, 243, 850]
t = Time.now
each_with_obj_counter(arr, top_n)
Time.now - t
#=> 0.140723
t = Time.now
doit(arr, top_n)
Time.now - t
#=> 0.132715
Both methods returned the array
[[516, 1090], [740, 1086], [788, 1085], [392, 1085], [440, 1083],
[568, 1081], [890, 1081], [688, 1080], [306, 1078], [982, 1078],
[841, 1075], [447, 1074], [897, 1074], [630, 1072], [600, 1072],
[500, 1071], [20, 1071], [282, 1071], [410, 1070], [918, 1070]]
With repeated runs (and for larger array sizes) there was no clear winner. While the results were not quite what I was hoping for, they may be of general interest. I've noticed before that, for Ruby, sort is blazingly fast.

I would use Enumerable#tally to do the calculation, hoping that this build-in Ruby method is better optimized than summing the elements with Ruby hashes. This method was added in Ruby 2.7.
And just returning the elements with the maximum counters with Enumerable#max_by is certainly faster than completely sorting the hash first.
arr.tally.max_by(100, &:last)

Make Sure You're Testing the Right Things
First of all, your methodology is flawed because you don't have a fixture. As a result, the contents of arr will vary each time you run the experiment, which will cause your benchmarks to vary.
Secondly, there's so much variance that a million IPv4 records might yield as few as one or two duplicates for each record. As an example, using the same array-building you used:
arr.count
#=> 1000000
arr.tally.invert.count
#=> 2
1_000_000 - arr.tally.keys.count
#=> 135
That means out of a million records, this test instance found only about 135 non-unique records, and no more than two duplicates of any IP address on this particular run. I'm not sure how you define "the top 100" in such a case.
Getting to 50 Milliseconds with Different Data
That said, given a more sensible test it isn't too hard to get to about 0.05 seconds with MRI, especially on a fast machine. For example, with Ruby 3.0.2 and IRB's built-in measure command:
arr = []
1_000_000.times { arr << (1..1_000).to_a.sample }
measure
top_100 = arr.tally.invert.sort.reverse.lazy.take(100); nil
processing time: 0.054442s
Note: With Ruby 3.0.2, the conversion back to a Hash no longer seems to guarantee insertion order, so it's up to you whether there's any point in calling #to_h on the result. It didn't affect my time much; it simply made the results less useful.
Using #sort_by isn't much faster, although in some cases it might perform slightly better depending on the size of the Hash or the data it contains. For example:
top_100 = arr.tally.sort_by { _2 }.reverse.take(100); nil
processing time: 0.052266s
TruffleRuby managed the same jobs in about 10 milliseconds less than MRI. If milliseconds count, consider using a different Ruby engine than MRI.
Next Steps
It's not hard to get the type of processing you want under 300 milliseconds. The real problem seems to be that you have to do the equivalent of a full table scan (e.g. processing a million mostly-unique records) in a way that provides few opportunities for differentiation of the results, or optimizations for parsing the data.
If you really need that sort of number crunching for slow-to-parse data, delegate to your database, storage engine, external map-reduce process, or other tools. Even if you could figure a way to use Ractors or otherwise split the job into some sort of multi-core threaded algorithm, it's unlikely that you'll be able to process a million records in pure Ruby much faster without it being a lot more trouble than it's worth. YMMV, though.
Use External Tools to Get to Under a Millisecond
It's also worth pointing out that the features of higher-level languages like Ruby, as well as formatting and terminal I/O, are often more expensive than the data crunching itself. Just as a point of comparison, you can take the same million numbers and parse them in the shell faster than in Ruby, although Ruby does a much better job of generating large volumes of data, manipulating complex data structures, and providing convenience methods. For example:
File.open('numbers.txt', ?w) do |f|
1_000_000.times { f.puts Random.rand(1_000) }
end
#=> 1000000
~> time printf "Count: %s, Value: %s\n" \
(string split -nr " " \
(gsort -rn numbers.txt | guniq -cd | ghead -100))
Count: 962, Value: 999
Count: 1000, Value: 998
Count: 968, Value: 997
Count: 1006, Value: 996
# remainder of items trimmed for space
________________________________________________________
Executed in 1.45 millis fish external
usr time 492.00 micros 492.00 micros 0.00 micros
sys time 964.00 micros 964.00 micros 0.00 micros
Even with the relatively expensive I/O to the terminal, this took only 1.45 milliseconds total. For further comparison, storing the output in a variable for later formatting took only 20 microseconds.
~> time set top_100 (string split -nr " " \
(gsort -rn numbers.txt | guniq -cd | ghead -100))
________________________________________________________
Executed in 20.00 micros fish external
usr time 20.00 micros 20.00 micros 0.00 micros
sys time 2.00 micros 2.00 micros 0.00 micros
The point here isn't to avoid using Ruby, especially since several of the answers are already well under your 300 millisecond threshold. Rather, the point is that if you need more raw speed then you can get it, but you may need to think outside the box if you want to get down into the realm of microseconds.

Related

Count array with condition

So I have a array of characters and I'd like to display all permutations of a given size meeting a certain condition. For instance, if my array contains 'L', 'E' and 'A' and I choose to display all permutations of size 3 that ends with 'L'. There are two possibilities, ["A", "E", "L"] and ["E", "A", "L"].
My problem is: how can I count the number of possibilities and print all the possibilities within the same each? Here's what I have so far:
count = 0
combination_array.select do |item|
count += 1 if item.last == 'L'
puts "#{item} " if item.last == 'L'
end
It works fine, but I have to write the condition 2 times and also I can't write before displaying all possibilities. I've created a method
def count_occurrences(arr)
counter = 0
arr.each do |item|
counter += 1 if item.last == 'L'
end
counter
end
but I still have to repeat my condition (item.last == 'L'). it doesn't seem very efficient to me.
You could use each_cons (docs) to iterate through each set of 3 items, and count (docs) in block form to have Ruby count for you without constructing a new array:
matches = [["E", "A", "L"], ["A", "E", "L"]]
match_count = data.each_cons(3).count do |set|
if matches.include?(set)
puts set.to_s
return true
end
end
If you really dislike the conditional block, you could technically simplify to a one-liner:
stuff_from_above.count do |set|
matches.include?(set) && !(puts set.to_s)
end
This takes advantage of the fact that puts always evaluates to nil.
And if you're feeling extra lazy, you can also write ["A", "E", "L"] as %w[A E L] or "AEL".chars.
If you specifically want to display and count permutations that end in "L", and the array arr is known to contain exactly one "L", the most efficient method is to simply generate permutations of the array with "L" removed and then tack "L" onto each permutation:
arr = ['B', 'L', 'E', 'A']
str_at_end = 'L'
ar = arr - [str_at_end]
#=> ["B", "E", "A"]
ar.permutation(2).reduce(0) do |count,a|
p a + [str_at_end]
count += 1
end
#=> 6
displaying:
["B", "E", "L"]
["B", "A", "L"]
["E", "B", "L"]
["E", "A", "L"]
["A", "B", "L"]
["A", "E", "L"]
If you want to do something else as well you need to state specifically what that is.
Note that the number of permutations of the elements of an array of size n is simply n! (n factorial), so if you only need the number of permutations with L at the end you could compute that as factorial(arr.size-1), where factorial is a simple method you would need to write.

Ruby sort subarray in place

If I have an array in Ruby, foo, how can a sort foo[i..j] in-place?
I tried calling foo[i..j].sort! but it didn't sort the original array, just returned a sorted part of it.
If you want to sort part of an array you need to reinject the sorted parts. The in-place modifier won't help you here because foo[i..j] returns a copy. You're sorting the copy in place, which really doesn't mean anything to the original array.
So instead, replace the original slice with a sorted version of same:
test = %w[ z b f d c h k z ]
test[2..6] = test[2..6].sort
# => ["c", "d", "f", "h", "k"]
test
# => ["a", "b", "c", "d", "f", "h", "k", "q"]

Algorithm for generating abstract keys in Ruby

I have an array with each letter
alphabet = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
input ="cg?"
Then i'm creating sub-keys
sub_keys = (2..letters.length).flat_map do |n|
letters.combination(n).map(&:join).map{|n| n = n.length.to_s + n.chars.sort.join }
end.uniq
Which gives me
sub_keys = ["2?c, 2?g, 2cg, 3cg?"]
Now i want to transform my sub_keys to get expected output which is
keys = [2ac, 2bc, 2cc, 2c, 2ag, 2bg, 2cg, 2dg, 2eg, 2fg, 2gg, 2g, 3acg, 3bcg, 3ccg, 3cdg, 3ceg, 3cfg, 3cgg, 3cg]
As you can see some keys like: 2c, 2g, 3cg stand out from the rest. In my code 2c acts as 2cd, 2ce, 2cf etc.. This is the output i want and my code for it is here:
keys_with_blanks = sub_keys.select{ |k| k.include? "?" }.map{ |k| k.gsub("?","") }
keys = keys_with_blanks.map do |k|
current_letters = choose_letters(all_letters, k)
k = current_letters.map{ |l| l.gsub(l,l+k) }
end.flatten.map{ |k| k.chars.sort.join }.uniq
keys += keys_with_blanks
For making less keys as the input grows, i decided to choose only the letters i need from the alphabet in each iteration instead of all of them, hence the line current_letters = choose_letters(all_letters, k)
choose_letters:
def choose_letters(letters, key)
letters = letters.select{ |l| l <= key.chars.last }
end
Let's say if i have a key key = "2c" then choose_letters will return ["a","b","c"]
This way i only make the keys i need. Up to about 13 character input its quite fast. The problem start when input is 14 or 15 characters long. Then it takes 3 or 4 seconds to make all the keys.
THIS IS THE SLOW PART
keys = keys_with_blanks.map do |k|
current_letters = choose_letters(all_letters, k)
k = current_letters.map{ |l| l.gsub(l,l+k) }
end.flatten.map{ |k| k.chars.sort.join }.uniq
It takes about 3-4 seconds for input = "abcdefghijklmn?"
QUESTION
How can the code be refactored to generate keys faster? My latest upgrade was to impletent choose_letters method, so keys like "2cd", "2ce", "2cf" and so on wouldn't generate, because they work exactly like "2c". Now it generates only the keys i need, so it's a little faster, but still.. Can it be faster?

Ruby: How to create a letter counting function

I wouldn't have asked for help without first spending a few hours trying to figure out my error but I'm at a wall. So there is my attempt but I'm getting false when I try to pass the argument though and I'm not sure why. I know there are other ways to solve this problem that are a little shorter but I'm more interested in trying to get my code to work. Any help is much appreciated.
Write a method that takes in a string. Your method should return the most common letter in the array, and a count of how many times it appears.
def most_common_letter(string)
idx1 = 0
idx2 = 0
counter1 = 0
counter2 = 0
while idx1 < string.length
idx2 = 0
while idx2 < string.length
if string[idx1] == string[idx2]
counter1 += 1
end
idx2 += 1
end
if counter1 > counter2
counter2 = counter1
counter1 = 0
var = [string[idx1], counter2]
end
idx1 += 1
end
return var
end
puts("most_common_letter(\"abca\") == [\"a\", 2]: #{most_common_letter("abca") == ["a", 2]}")
puts("most_common_letter(\"abbab\") == [\"b\", 3]: #{most_common_letter("abbab") == ["b", 3]}")
I didn't rewrite your code because I think it's important to point out what is wrong with the existing code that you wrote (especially since you're familiar with it). That said, there are much more 'ruby-like' ways to go about this.
The issue
counter1 is only being reset if you've found a 'new highest'. You need to reset it regardless of whether or not a new highest number has been found:
def most_common_letter(string)
idx1 = 0
idx2 = 0
counter1 = 0
counter2 = 0
while idx1 < string.length
idx2 = 0
while idx2 < string.length
if string[idx1] == string[idx2]
counter1 += 1
end
idx2 += 1
end
if counter1 > counter2
counter2 = counter1
# counter1 = 0 THIS IS THE ISSUE
var = [string[idx1], counter2]
end
counter1 = 0 # this is what needs to be reset each time
idx1 += 1
end
return var
end
Here's what the output is:
stackoverflow master % ruby letter-count.rb
most_common_letter("abca") == ["a", 2]: true
most_common_letter("abbab") == ["b", 3]: true
I think you're aware there are way better ways to do this but frankly the best way to debug this is with a piece of paper. "Ok counter1 is now 1, indx2 is back to zero", etc. That will help you keep track.
Another bit of advice, counter1 and counter2 are not very good variable names. I didn't realize what you were using them for initially and that should never be the case, it should be named something like current_count highest_known_count or something like that.
Your question has been answered and #theTinMan has suggested a more Ruby-like way of doing what you want to do. There are many other ways of doing this and you might find it useful to consider a couple more.
Let's use the string:
string = "Three blind mice. Oh! See how they run."
First, you need to answer a couple of questions:
do you want the frequency of letters or characters?
do you want the frequency of lowercase and uppercase letters combined?
I assume you want the frequency of letters only, independent of case.
#1 Count each unique letter
We can deal with the case issue by converting all the letters to lower or upper case, using the method String#upcase or String#downcase:
s1 = string.downcase
#=> "three blind mice. oh! see how they run."
Next we need to get rid of all the characters that are not letters. For that, we can use String#delete1:
s2 = s1.delete('^a-z')
#=> "threeblindmiceohseehowtheyrun"
Now we are ready to convert the string s2 to an an array of individual characters2:
arr = s2.chars
#=> ["t", "h", "r", "e", "e", "b", "l", "i", "n", "d",
# "m", "i", "c", "e", "o", "h", "s", "e", "e", "h",
# "o", "w", "t", "h", "e", "y", "r", "u", "n"]
We can combine these first three steps as follows:
arr = string.downcase.gsub(/[^a-z]/, '').chars
First obtain all the distinct letters present, using Array.uniq.
arr1 = arr.uniq
#=> ["t", "h", "r", "e", "b", "l", "i", "n",
# "d", "m", "c", "o", "s", "w", "y", "u"]
Now convert each of these characters to a two-character array consisting of the letter and its count in arr. Whenever you need convert elements of a collection to something else, think Enumerable#map (a.k.a. collect). The counting is done with Array#count. We have:
arr2 = arr1.map { |c| [c, arr.count(c)] }
#=> [["t", 2], ["h", 4], ["r", 2], ["e", 6], ["b", 1], ["l", 1],
# ["i", 2], ["n", 2], ["d", 1], ["m", 1], ["c", 1], ["o", 2],
# ["s", 1], ["w", 1], ["y", 1], ["u", 1]]
Lastly, we use Enumerable#max_by to extract the element of arr2 with the largest count3:
arr2.max_by(&:last)
#=> ["e", 6]
We can combine the calculation of arr1 and arr2:
arr.uniq.map { |c| [c, arr.count(c)] }.max_by(&:last)
and further replace arr with that obtained earlier:
string.downcase.gsub(/[^a-z]/, '').chars.uniq.map { |c|
[c, arr.count(c)] }.max_by(&:last)
#=> ["e", 6]
String#chars returns a temporary array, upon which the method Array#uniq is invoked. As alternative, which avoids the creation of the temporary array, is to use String#each_char in place of String#chars, which returns an enumerator, upon which Enumerable#uniq is invoked.
The use of Array#count is quite an inefficient way to do the counting because a full pass through arr is made for each unique letter. The methods below are much more efficient.
#2 Use a hash
With this approach we wish to create a hash whose keys are the distinct elements of arr and each value is the count of the associated key. Begin by using the class method Hash::new to create hash whose values have a default value of zero:
h = Hash.new(0)
#=> {}
We now do the following:
string.each_char { |c| h[c.downcase] += 1 if c =~ /[a-z]/i }
h #=> {"t"=>2, "h"=>4, "r"=>2, "e"=>6, "b"=>1, "l"=>1, "i"=>2, "n"=>2,
# "d"=>1, "m"=>1, "c"=>1, "o"=>2, "s"=>1, "w"=>1, "y"=>1, "u"=>1}
Recall h[c] += 1 is shorthand for:
h[c] = h[c] + 1
If the hash does not already have a key c when the above expression is evaluated, h[c] on the right side is replaced by the default value of zero.
Since the Enumerable module is included in the class Hash we can invoke max_by on h just as we did on the array:
h.max_by(&:last)
#=> ["e", 6]
There is just one more step. Using Enumerable#each_with_object, we can shorten this as follows:
string.each_char.with_object(Hash.new(0)) do |c,h|
h[c.downcase] += 1 if c =~ /[a-z]/i
end.max_by(&:last)
#=> ["e", 6]
The argument of each_with_object is an object we provide (the empty hash with default zero). This is represented by the additional block variable h. The expression
string.each_char.with_object(Hash.new(0)) do |c,h|
h[c.downcase] += 1 if c =~ /[a-z]/i
end
returns h, to which max_by(&:last) is sent.
#3 Use group_by
I will give a slightly modified version of the Tin Man's answer and show how it works with the value of string I have used. It uses the method Enumerable#group_by:
letters = string.downcase.delete('^a-z').each_char.group_by { |c| c }
#=> {"t"=>["t", "t"], "h"=>["h", "h", "h", "h"], "r"=>["r", "r"],
# "e"=>["e", "e", "e", "e", "e", "e"], "b"=>["b"], "l"=>["l"],
# "i"=>["i", "i"], "n"=>["n", "n"], "d"=>["d"], "m"=>["m"],
# "c"=>["c"], "o"=>["o", "o"], "s"=>["s"], "w"=>["w"],
# "y"=>["y"], "u"=>["u"]}
used_most = letters.max_by { |k,v| v.size }
#=> ["e", ["e", "e", "e", "e", "e", "e"]]
used_most[1] = used_most[1].size
used_most
#=> ["e", 6]
In later versions of Ruby you could simplify as follows:
string.downcase.delete('^a-z').each_char.group_by(&:itself).
transform_values(&:size).max_by(&:last)
#=> ["e", 6]
See Enumerable#max_by, Object#itself and Hash#transform_values.
1. Alternatively, use String#gsub: s1.gsub(/[^a-z]/, '').
2. s2.split('') could also be used.
3. More or less equivalent to arr2.max_by { |c, count| count }.
It's a problem you'll find asked all over Stack Overflow, a quick search should have returned a number of hits.
Here's how I'd do it:
foo = 'abacab'
letters = foo.chars.group_by{ |c| c }
used_most = letters.sort_by{ |k, v| [v.size, k] }.last
used_most # => ["a", ["a", "a", "a"]]
puts '"%s" was used %d times' % [used_most.first, used_most.last.size]
# >> "a" was used 3 times
Of course, now that this is here, and it's easily found, you can't use it because any teacher worth listening to will also know how to search Stack Overflow and will find this answer.

What is the Big-O complexity for this "Telephone Words" Algorithm?

This isn't homework, just an interview question I found on the web that looks interesting.
So I took a look at this first: Telephone Words problem -- but it seems to be poorly worded/created some controversy. My question is pretty much the same, except my question is more about the time complexity behind it.
You want to list all the possible words when given a 10-digit phone number as your input. So here is what I have done:`
def main(telephone_string)
hsh = {1 => "1", 2 => ["a","b","c"], 3 => ["d","e","f"], 4 => ["g","h","i"],
5 => ["j","k","l"], 6 => ["m","n","o"], 7 => ["p","q","r","s"],
8 => ["t","u","v"], 9 => ["w","x","y","z"], 0 => "0" }
telephone_array = telephone_string.split("-")
three_number_string = telephone_array[1]
four_number_string = telephone_array[2]
string = ""
result_array = []
hsh[three_number_string[0].to_i].each do |letter|
hsh[three_number_string[1].to_i].each do |second_letter|
string = letter + second_letter
hsh[three_number_string[2].to_i].each do |third_letter|
new_string = string + third_letter
result_array << new_string
end
end
end
second_string = ""
second_result = []
hsh[four_number_string[0].to_i].each do |letter|
hsh[four_number_string[1].to_i].each do |second_letter|
second_string = letter + second_letter
hsh[four_number_string[2].to_i].each do |third_letter|
new_string = second_string + third_letter
hsh[four_number_string[3].to_i].each do |fourth_letter|
last_string = new_string + fourth_letter
second_result << last_string
end
end
end
end
puts result_array.inspect
puts second_result.inspect
end
First off, this is what I hacked together in a few minutes time, no refactoring has been done. So I apologize for the messy code, I just started learning Ruby 6 weeks ago, so please bear with me!
So finally to my question: I was wondering what the time complexity of this method would be. My guess is that it would be O(n^4) because the second loop (for the four letter words) is nested four times. I'm not really positive though. So I would like to know whether that is correct, and if there is a better way to do this problem.
This is actually a constant time algorithm, so O(1) (or to be more explicit, O(4^3 + 4^4))
The reason this is a constant time algorithm is that for each digit in the telephone number, you're iterating through a fixed number (at most 4) of possible letters, that's known beforehand (which is why you can put hsh statically into your method).
One possible optimization would be to stop searching when you know there are no words with the current prefix. For example, if the 3-digit number is "234", you can ignore all strings that start with "bd" (there are some bd- words, like "bdellid", but none that are 3-letters, at least in my /usr/share/dict/words).
From the original phrasing, I would assume that is requesting all of the possibilities, instead of the number of possibilities as output.
Unfortunately, if you need to return every combination, there is no way to lower the complexity below that determined by the specified keys.
If it were simply the number, it could be in constant time. However, to print them all out, the end result depends highly on assumptions:
1) Assuming that all of the words you are checking for are composed solely of letters, you only need to check against the eight keys from 2 to 9. If this is incorrect, just sub out 8 in the function below.
2) Assuming the layout of all keys is exactly as set up here (no octothorpes or asterisks), with the contents of the empty arrays taking up no space in the final word.
{
1 => [],
2 => ["a", "b", "c"],
3 => ["d", "e", "f"],
4 => ["g", "h", "i"],
5 => ["j", "k", "l"],
6 => ["m", "n", "o"],
7 => ["p", "q", "r", "s"],
8 => ["t", "u", "v"],
9 => ["w", "x", "y", "z"],
0 => []
}
At each stage, you would simply check the number of possibilities for the next step, and append each possible choice to the end of a string. If you were to do, so, the minimum time would be (essentially) constant time (0, if the number consisted of all ones and zeros). However, the function would be O(4^n), where n reaches a maximum at 10. The largest possible number of combinations would be 4^10, if they hit 7 or nine each time.
As for your code, I would recommend a single loop, with a few basic nested loops. Here is the code, in Ruby, although I haven't run it, so there may be syntax errors.
def get_words(number_string)
hsh = {"2" => ["a", "b", "c"],
"3" => ["d", "e", "f"],
"4" => ["g", "h", "i"],
"5" => ["j", "k", "l"],
"6" => ["m", "n", "o"],
"7" => ["p", "q", "r", "s"],
"8" => ["t", "u", "v"],
"9" => ["w", "x", "y", "z"]}
possible_array = hsh.keys
number_array = number_string.split("").reject{|x| possible_array.include?(x)}
if number_array.length > 0
array = hsh[number_array[0]]
end
unless number_array[1,-1].nil?
number_array.each do |digit|
new_array = Array.new()
array.each do |combo|
hsh[digit].each do |new|
new_array = new_array + [combo + new]
end
end
array = new_array
end
new_array
end

Resources