Ruby – Phone Number to Word Combinations

I was recently looking for a phone-number-to-words Ruby solution but could not find one. I could not even find a good algorithm in any language. This told me that I needed to write one to share with others. The following code lets you convert a phone number to possible word(s) combinations using a dictionary.

Please feel free to comment with feedback or improvements.


require 'csv'

# This class provides methods for converting phone numbers 
# to possible word matches.
#
# How to use this file:
# => ruby phone_number_converter.rb -n NUMBER
# => ruby phone_number_converter.rb -f NUMBER_FILE
# => ruby phone_number_converter.rb -n NUMBER -d DICTIONARY_FILE
# => ruby phone_number_converter.rb -f NUMBER_FILE -d DICTIONARY_FILE
#
class PhoneNumberConverter
  attr_accessor :number, :number_file, :dictionary_file

  class << self
    # This method provides all the number-to-letters
    # options for conversions.
    #
    # @return [Hash]
    def keypad
      {
        '1' => %w{1},
        '2' => %w{A B C},
        '3' => %w{D E F},
        '4' => %w{G H I},
        '5' => %w{J K L},
        '6' => %w{M N O},
        '7' => %w{P Q R S},
        '8' => %w{T U V},
        '9' => %w{W X Y Z}
      }
    end

    # This method extracts all the options provided in 
    # the command and assigns them to class variables.
    def extract_options
      ARGV.each_with_index do |arg,index|
        following_arg = ARGV[index + 1].to_s
        case arg
        when '-n'
          @number = following_arg
        when '-f'
          @number_file = following_arg
        when '-d'
          @dictionary_file = following_arg
        end
      end
    end

    # This method parses a provided dictionary file and returns
    # an array of words or returns the default dictionary array.
    #
    # @return [Array]
    def dictionary
      if @dictionary_file
        CSV.read(@dictionary_file).flatten
      else
        %w{use ruby}
      end.map(&:downcase)
    end

    # This method generates a list of phone numbers from a specified
    # file or uses one number provided in the command.
    #
    # @return [Array]
    def numbers
      if @number_file
        CSV.read(@number_file).flatten
      else
        [@number]
      end.map{|n| clean_number(n) }
    end

    # This method cleans the provided phone number.
    #
    # @param [String] number
    #  The provided phone number.
    #
    # @return [String]
    def clean_number(number)
      number.gsub(/\D/, '').strip
    end

    # This method returns an array of digits from
    # the provided number.
    #
    # @param [String] number
    #  The provided phone number.
    #
    # @return [Array]
    def array_of_numbers(number)
      number.to_s.split(//)
    end

    # This method generates all the possible partitions
    # from the provided phone number.
    #
    # [1,2,3,4] => [[[1],[2,3,4]],[[1,2],[3,4]],[[1],[2,3,4]]] ...
    #
    # @param [Array] arr_nums
    #  An array of digits from the phone number.
    #
    # @return [Array]
    def partitions(arr_nums)
      (0...arr_nums.length)
      .flat_map{|i| (1...arr_nums.length).to_a.combination(i).to_a }
      .map{|cut| i = -1; arr_nums.slice_before{cut.include?(i += 1)}.to_a }
    end

    # This method generates all the possible word(s) matches
    # for the provided number and dictionary.
    #
    # @return [Array]
    def permutations
      extract_options

      numbers.collect do |phone_number|
        partitions = partitions(array_of_numbers(phone_number))
        partitions.map do |partition| 
          partition.map do |numbers|
            numbers.map do |number|
              keypad[number]
            end.inject(&:product).map do |perms| 
              numbers.one? ? perms : perms.join
            end.select do |word| 
              dictionary.include?(word.downcase) || word.length == 1
            end.collect do |word|
              dictionary.include?(word.downcase) ? word : keypad.select{|k,v| v.include?(word) }.first[0]
            end
          end.inject(&:product).collect do |word_option| 
            word_option.is_a?(Array) ? word_option.join('-') : word_option
          end
        end.reject(&:empty?).flatten.reject do |word_option| 
          word_option.length < array_of_numbers(phone_number).length || word_option =~ /[0-9]-[0-9]/
        end.uniq
      end
    end
  end
end

permutations = PhoneNumberConverter::permutations

permutations.each do |phone_number|
  puts phone_number
end

Thanks to sawa for helping out with a part of it.

Leave a Reply