Portfolio: Phone Number Word Finder

By

View raw: phone-numbers.rb

#!/usr/bin/ruby -w
# -*- coding: utf-8 -*-
# 
# Copyright by Scott Severance
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

# This is a little command-line tool to find words corresponding to a given
# phone number. It is meant to run interactively from the command line. The
# Ubuntu/Debian package 'scowl' should be installed for the best results.

################################################################################
# INITIAL SETUP:
# At least one of $dictionary and $dictDir must be set properly.
# Set the following to the path of a myspell/hunspell dictionary on your system.
# This will also work with a plain text file containing one word per line.
$dictionary = '/usr/share/hunspell/en_US.dic'
# Set the following to the scowl directory.
$dictDir = '/usr/share/dict/scowl'
# Set the following to the encoding of the dictionary file. It is specified in
# the .dic file's corresponding .aff file.
$encoding = 'iso-8859-1'
################################################################################

begin
  require 'readline'
  $readline = true
rescue LoadError
  puts "The readline library is not available on this system. Command editing
        will be limited.".gsub(/^[ ]+/, '')
  $readline = false
end

class NoNumberError < Exception
end

class PhoneWords
  @@wordListCounter = 0

  def initialize(num=nil)
    @number = num
    makeWordList unless defined? @@WordList
  end

  def listWords
    @@WordList
  end

  def makeWordList
    startTime = Time.now.to_f
    @@wordListCounter += 1
    puts "Building the word list. This may take a while."

    # Choose the files to examine for words
    #fileBlacklist = [ /(british|canadian|proper-names|roman-numerals|hacker|variant)/, /^[.]+$/ ]
    fileList = []
    begin
      fileList = Dir.entries($dictDir).delete_if do |x|
        delete = false
        if x =~ /(british|canadian|proper-names|roman-numerals|hacker|variant)/ || File.directory?(x)
          delete = true
        end
        delete
      end
    rescue Errno::ENOENT
      $stderr.puts "The directory '#{$dictDir}' doesn't exist. Skipping..."
    end
    fileList.collect! { |x| "#{$dictDir}/#{x}" }.push($dictionary)

    # Make the word list
    wordList = []
    fileList.each do |filename|
      begin
        File.open(filename) do |file|
          file.each do |line|
            line = line.encode('utf-8', $encoding).upcase.chomp
            line.gsub!(/^([^\/\t ]*).*/,'\1').gsub!(/[^A-Z]+/,'')
            wordList.push(line) if line.length > 0
          end
        end
      rescue Errno::ENOENT
        $stderr.puts "The file '#{filename}' doesn't exist. Skipping..."
      end
    end
    @@WordList = wordList.sort.uniq
    endTime = Time.now.to_f
    printf "Finished building the word list. It took %.3f seconds to complete.\nThe list contains %d words.\n\n", (endTime - startTime), @@WordList.length
  end

  private

  def trimWordList(len,wordList)
    wordList.select { |word| word.length == len }
  end

  def splitNumber(numberStr)
    x = numberStr.gsub(/[^\d-]*/,'')
    x.gsub!(/-/,'1')
    x.split(/[01]+/)
  end

  def numToWord(numberStr,wordList)
    correlation = {
                    'A' => 2, 'B' => 2, 'C' => 2,
                    'D' => 3, 'E' => 3, 'F' => 3,
                    'G' => 4, 'H' => 4, 'I' => 4,
                    'J' => 5, 'K' => 5, 'L' => 5,
                    'M' => 6, 'N' => 6, 'O' => 6,
                    'P' => 7, 'R' => 7, 'S' => 7, #'Q' => 7,
                    'T' => 8, 'U' => 8, 'V' => 8,
                    'W' => 9, 'X' => 9, 'Y' => 9, #'Z' => 9
                  }
    digits = numberStr.split(//)
    digits.map! { |n| n.to_i }
    words = Array.new
    wordList.each do |word|
      i = 0
      word.split(//).each do |letter|
      break unless correlation[letter] == digits[i]
        if i == word.length - 1
          words.push(word)
          break
        end
        i += 1
      end
    end
    words
  end

  public

  attr_reader :number

  def number=(n)
    @number = n.to_s
  end

  def generate(print_results = false)
    raise NoNumberError, "No number specified" unless @number
    result = []

    phoneNumber = splitNumber(@number)    
    phoneNumber.each do |ph|
      next if ph.length <= 1
      wordList = trimWordList(ph.length,@@WordList)
      options = numToWord(ph,wordList)
      if print_results
        case options.length
          when 0 then printf "There are no options for the number %s.\n", ph
          when 1 then printf "The only option for the number %s is %s.\n", ph, options[0]
          else printf "The options for the number %s are: %s.\n", ph, options.join(', ')
        end
      end
      result.push({:number => ph, :options => options})
    end
    result
  end
end

def parseCommand(input)
  case input
    when /^grep /i then runGrep input.gsub(/grep (.*)/i,'\1')
    when /^(exit|q(uit)?|bye)$/i then runExit
    when nil then runExit
    when /\d/ then runNumber input
    when /^l(ist)?/i
      list = PhoneWords.new.listWords
      puts list.join(', ')
      puts "Words in list: #{list.length}"
    when /^h(elp)?|\?/i then runHelp
    else runError input
  end
end

def runGrep(pattern)
  pat = pattern.gsub(/^\/?(.*)\//,'\1') # Extract the patern from any slashes
  begin
    re = Regexp.new(pat,"i")
  rescue RegexpError
    puts "ERROR: Invalid regular expression"
    return false
  end
  p re
  puts PhoneWords.new.listWords.grep(re).join(', ')
end

def runExit
  puts ''
  exit 0
end

def runNumber(num)
  PhoneWords.new(num).generate(true)
end

def runHelp
  puts <<-EOT.gsub(/^ {4}/, '')
    Type any phone number to list the possibilities. The number is split on
    "0", "1", and "-". Any other non-digit characters are ignored.

    Other Commands:

      grep regex: Lists the words matching `regex`, a regular expression as
                  defined by Ruby. The regex automatically includes the /i
                  (case-insensitive) switch.

      list:       Lists all the words in the word list.

      exit, quit, q, or bye: Exits.
  EOT
end

def runError(input=nil)
  puts "Unrecognized input: " + input
end

def main()
  while true
    begin
      if $readline
        parseCommand Readline.readline('Command ("h" for help): ', true)
      else
        print "\nCommand (\"h\" for help): "
        parseCommand gets.chomp
      end
    rescue Interrupt
      runExit
    end
  end
end

main if $0 == __FILE__