[QUIZ] 1-800-THE-QUIZ (#20)

Discussion in 'Ruby' started by Ruby Quiz, Feb 18, 2005.

  1. Ruby Quiz

    Ruby Quiz Guest

    The three rules of Ruby Quiz:

    1. Please do not post any solutions or spoiler discussion for this quiz until
    48 hours have passed from the time on this message.

    2. Support Ruby Quiz by submitting ideas as often as you can:

    http://www.rubyquiz.com/

    3. Enjoy!

    -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

    Many companies like to list their phone numbers using the letters printed on
    most telephones. This makes the number easier to remember for customers. A
    famous example being 1-800-PICK-UPS.

    This week's quiz is to write a program that will show a user possible matches
    for a list of provided phone numbers.

    Your script should behave as a standard Unix filter, reading from files
    specified as command-line arguments or STDIN when no files are given. Each line
    of these files will contain a single phone number.

    For each phone number read, your filter should output all possible word
    replacements from a dictionary. Your script should try to replace every digit
    of the provided phone number with a letter from a dictionary word; however, if
    no match can be made, a single digit can be left as is at that point. No two
    consecutive digits can remain unchanged and the program should skip over a
    number (producing no output) if a match cannot be made.

    Your script should allow the user to set a dictionary with the -d command-line
    option, but it's fine to use a reasonable default for your system. The
    dictionary is expected to have one word per line.

    All punctuation and whitespace should be ignored in both phone numbers and the
    dictionary file. The program should not be case sensative, letting "a" == "A".
    Output should be capital letters and digits separated at word boundaries with a
    single dash (-), one possible word encoding per line. For example, if your
    program is fed the number:

    873.7829

    One possible line of output is

    USE-RUBY

    According to my dictionary.

    The number encoding on my phone is:

    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

    Feel free to use that, or the encoding on your own phone.
    Ruby Quiz, Feb 18, 2005
    #1
    1. Advertising

  2. I wrote a solution with a stdin/out interface and a webrick interface.
    You can test the webrick interface at http://v-jix.homeip.net:2005/ .
    My dictionary is 2of4brif.txt . I'm going to post my code after the
    no-spoiler period.

    --
    Jannis Harder
    Jannis Harder, Feb 19, 2005
    #2
    1. Advertising

  3. Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20)

    Hello Group,

    i once again found the time to do the ruby quiz. I liked the quiz
    because it was short, and on the other hand harder than I thought. I
    skipped the part about skipping letters in my first try, and when I had
    to add it it made me think quite a bit. (I first skipped letters instead
    of numbers, because I got confused in my datastructure.)

    Now, heres my solution:

    I build a tree index of the dictionary indexed by numbers and search
    this tree.

    Thanks for the quiz James!


    class DictionaryNode < Array
    # Terminal info
    attr_reader :words

    def initialize
    super()
    @words = []
    end
    end

    # A tree-index of the dictionary. Indexed by phone key number.
    # This is at the same time node and Dictionary.
    class Dictionary
    def initialize(encoding)
    super()
    @encoding = {}
    @inverse_encoding = {}

    encoding.each do | k, v |
    @encoding[k] = v.split(/\s+/).map{|c| c[0]}
    end

    # Create map from characters to numbers
    @inverse_encoding = @encoding.inject({}) { | r, (k, v) |
    v.each do | l | r[l] = k end
    r
    }
    @root = DictionaryNode.new
    end

    # Helper method for rekursive adding of words to the dictionary
    private
    def add_recursive(node, word, suffix)
    if suffix.empty?
    node.words << word
    return node
    end
    add_recursive(node[@inverse_encoding[suffix[0]]] ||= DictionaryNode.new, word, suffix[1..-1])
    end

    # Add words to the dictionary
    public
    def add(word)
    suffix = word.unpack('C*')
    add_recursive(@root, word, suffix)
    self
    end

    # Load a wordlist from a file, which contains one word per line.
    # Ignores punctuation and whitespace.
    def load_wordlist(file)
    file.each do |w|
    w.gsub!(/[^A-Za-z]/, '')
    next if w.empty?
    w.upcase!
    self.add(w)
    end
    self
    end

    private
    # Search words and return (in the block) words and the unmatched rest of the number
    def sub_find_noskip(node, number, &block)
    # Return words found so far
    block[node.words.map{|w|w.dup}, number] unless node.words.empty?
    # No more digits, so stop searching here
    return node if number.empty?
    # Search for longer words
    sub_find_noskip(node[number[0]], number[1..-1], &block) if node[number[0]]
    end

    # Search words and return (in the block) words and the unmatched rest of the number.
    # Allows to skip parts of the words, returning the skipped positions as a binary array.
    def sub_find(node, number, skipped = [], &block)
    # Return words found so far
    block[node.words.map{|w|w.dup}, number, skipped] unless node.words.empty?
    # No more digits, so stop searching here
    return node if number.empty?
    # Search for longer words
    sub_find(node[number[0]], number[1..-1], skipped + [false], &block) if node[number[0]]
    # If previous digit was not skipped, allow to skip this one
    sub_find(node, number[1..-1], skipped + [true], &block) if !skipped[-1]
    end

    public
    # Skipping makes this a bit ugly
    def find(number, options)
    result = []
    if options.allow_skips
    sub_find(@root, number) do | words, rest_number, skipped |
    # Interleave skipped numbers
    needle = []
    skipped.zip(number).each_with_index do |(s,n), i|
    needle << [n, i] if s
    end
    words.each do | w |
    needle.each do | (n, i) | w.insert(i, n.to_s) end
    end

    if rest_number.empty?
    result.concat(words)
    else
    find(rest_number, options).each do | sentence |
    words.each do | w |
    result << w + '-' + sentence
    end
    end
    end
    end
    else
    sub_find_noskip(@root, number) do | words, rest_number |
    if rest_number.empty?
    result.concat(words)
    else
    find(rest_number, options).each do | sentence |
    words.each do | w |
    result << w + '-' + sentence
    end
    end
    end
    end
    end
    result
    end
    end

    encodings = {
    :james => {
    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'},

    :logic => {
    0 => 'A B',
    1 => 'C D',
    2 => 'E F',
    3 => 'G H',
    4 => 'I J K',
    5 => 'L M N',
    6 => 'O P Q',
    7 => 'R S T',
    8 => 'U V W',
    9 => 'X Y Z'
    }
    }

    require 'optparse'

    class PhonewordOptions < OptionParser
    attr_reader :dictionary, :encoding, :format, :allow_skips, :help, :encoding_help
    def initialize
    super()
    @dictionary = '/usr/share/dict/words'
    @encoding = :james
    @format = :plain
    @allow_skips = true
    @help = false
    @encoding_help = false
    self.on("-d", "--dictionary DICTIONARY", String) { | v | @dictionary = v }
    self.on("-e", "--encoding ENCODING", String,
    "How the alphabet is encoded to phonenumbers. james or logic are supported.") { | v | @encoding = v.downcase.to_sym }
    self.on("-p", "--plain", 'One result per found number, no other information') { @format = :plain }
    self.on("-v", "--verbose", 'Prefix the result with the number') { @format = :verbose }
    self.on("-s", "--skips", "--allow_skips", "--allow-skips", 'Allow to skip one adjacent number while matching. ',
    'Gives lots of ugly results, but james asked for it.') { @allow_skips = true }
    self.on("-c", "--no-skips", "Don't leave numbers in the detected words") { @allow_skips = false }
    self.on("-?", "--help") { @help = true }
    self.on("--supported-encodings", "--encoding-help", "List the supported encodings") { @encoding_help = true }
    end
    end

    options = PhonewordOptions.new
    options.parse!(ARGV)

    if options.help
    puts options
    exit
    end

    if options.encoding_help or !encodings[options.encoding]
    puts "Possible encodings:"
    puts encodings.to_a.sort_by{|(k,v)|k.to_s}.map{|(k,v)| "#{k}:\n"+v.map{|(n,e)|" #{n}: #{e}"}.sort.join("\n")}
    exit
    end
    dictionary = Dictionary.new(encodings[options.encoding]).load_wordlist(File.open(options.dictionary))

    output = {
    :plain => lambda do | number, sentence | sentence end,
    :verbose => lambda do | number, sentence | "#{number.ljust(15)}: #{sentence}" end }

    ARGF.each do | number |
    number.strip!
    dictionary.find(number.gsub(/[^0-9]/, '').unpack('C*').map{|n|n - ?0}, options).each do | sentence |
    puts output[options.format][number, sentence]
    end
    end
    Brian Schröder, Feb 20, 2005
    #3
  4. Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20)

    On Mon, 21 Feb 2005 05:40:00 +0900
    Brian Schröder <> wrote:

    > Hello Group,
    >
    > i once again found the time to do the ruby quiz. I liked the quiz
    > because it was short, and on the other hand harder than I thought. I
    > skipped the part about skipping letters in my first try, and when I had
    > to add it it made me think quite a bit. (I first skipped letters instead
    > of numbers, because I got confused in my datastructure.)
    >
    > Now, heres my solution:
    >
    > I build a tree index of the dictionary indexed by numbers and search
    > this tree.
    >
    > Thanks for the quiz James!
    >


    Ooops, I didn't send the final version, and I did not include the link to my solution, so here is another try:

    This version loads the dictionary a lot faster (3 times as fast as the old
    version) because it does not create as many intermediate objects. Also I measured that upcasing and gsub'ing is faster on the long string than on the individual short strings.

    Browse the solution online and in full color at:

    http://ruby.brian-schroeder.de/quiz/phoneword/

    or directly at

    http://ruby.brian-schroeder.de/quiz/phoneword/browse/phoneword-rb.html

    And to show it in all its glory (now loading the dictionary 3 times as fast:)


    # Nodes in the Dictionary.
    class DictionaryNode < Array
    # Terminal info
    attr_reader :words

    def initialize
    super()
    @words = []
    end
    end

    # A tree-indexed version of the dictionary that allows efficent searching by number 2 alphabet mapping.
    class Dictionary
    def initialize(encoding)
    super()
    @encoding = {}
    @inverse_encoding = {}

    encoding.each do | k, v |
    @encoding[k] = v.split(/\s+/).map{|c| c[0]}
    end

    # Create map from characters to numbers
    @inverse_encoding = @encoding.inject({}) { | r, (k, v) |
    v.each do | l | r[l] = k end
    r
    }
    @root = DictionaryNode.new
    end

    # Helper method for rekursive adding of words to the dictionary
    private
    def add_recursive(node, word, position)
    if word.length == position
    node.words << word
    return node
    end
    add_recursive(node[@inverse_encoding[word[position]]] ||= DictionaryNode.new, word, position + 1)
    end

    # Add words to the dictionary
    public
    def add(word)
    add_recursive(@root, word, 0)
    self
    end

    # Load a wordlist from a file, which contains one word per line.
    # Ignores punctuation and whitespace.
    def load_wordlist(file)
    file.read.gsub!(/[^A-Za-z\n]/, '').upcase!.each do |w|
    w.chomp!
    next if w.empty?
    self.add(w)
    end
    self
    end

    private
    # Search words and return (in the block) words and the unmatched rest of the number
    def sub_find_noskip(node, number, &block)
    # Return words found so far
    block[node.words.map{|w|w.dup}, number] unless node.words.empty?
    # No more digits, so stop searching here
    return node if number.empty?
    # Search for longer words
    sub_find_noskip(node[number[0]], number[1..-1], &block) if node[number[0]]
    end

    # Search words and return (in the block) words and the unmatched rest of the number.
    # Allows to skip parts of the words, returning the skipped positions as a binary array.
    def sub_find(node, number, skipped = [], &block)
    # Return words found so far
    block[node.words.map{|w|w.dup}, number, skipped] unless node.words.empty?
    # No more digits, so stop searching here
    return node if number.empty?
    # Search for longer words
    sub_find(node[number[0]], number[1..-1], skipped + [false], &block) if node[number[0]]
    # If previous digit was not skipped, allow to skip this one
    sub_find(node, number[1..-1], skipped + [true], &block) if !skipped[-1]
    end

    public
    # Skipping makes this a bit ugly
    def find(number, options)
    result = []
    if options.allow_skips
    sub_find(@root, number) do | words, rest_number, skipped |
    # Interleave skipped numbers
    needle = []
    skipped.zip(number).each_with_index do |(s,n), i|
    needle << [n, i] if s
    end
    words.each do | w |
    needle.each do | (n, i) | w.insert(i, n.to_s) end
    end

    if rest_number.empty?
    result.concat(words)
    else
    find(rest_number, options).each do | sentence |
    words.each do | w |
    result << w + '-' + sentence
    end
    end
    end
    end
    else
    sub_find_noskip(@root, number) do | words, rest_number |
    if rest_number.empty?
    result.concat(words)
    else
    find(rest_number, options).each do | sentence |
    words.each do | w |
    result << w + '-' + sentence
    end
    end
    end
    end
    end
    result
    end
    end

    encodings = {
    :james => {
    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'},

    :logic => {
    0 => 'A B',
    1 => 'C D',
    2 => 'E F',
    3 => 'G H',
    4 => 'I J K',
    5 => 'L M N',
    6 => 'O P Q',
    7 => 'R S T',
    8 => 'U V W',
    9 => 'X Y Z'
    }
    }

    require 'optparse'

    class PhonewordOptions < OptionParser
    attr_reader :dictionary, :encoding, :format, :allow_skips, :help, :encoding_help
    def initialize
    super()
    @dictionary = '/usr/share/dict/words'
    @encoding = :james
    @format = :plain
    @allow_skips = true
    @help = false
    @encoding_help = false
    self.on("-d", "--dictionary DICTIONARY", String) { | v | @dictionary = v }
    self.on("-e", "--encoding ENCODING", String,
    "How the alphabet is encoded to phonenumbers. james or logic are supported.") { | v | @encoding = v.downcase.to_sym }
    self.on("-p", "--plain", 'One result per found number, no other information') { @format = :plain }
    self.on("-v", "--verbose", 'Prefix the result with the number') { @format = :verbose }
    self.on("-s", "--skips", "--allow_skips", "--allow-skips", 'Allow to skip one adjacent number while matching. ',
    'Gives lots of ugly results, but james asked for it.') { @allow_skips = true }
    self.on("-c", "--no-skips", "Don't leave numbers in the detected words") { @allow_skips = false }
    self.on("-?", "--help") { @help = true }
    self.on("--supported-encodings", "--encoding-help", "List the supported encodings") { @encoding_help = true }
    end
    end

    options = PhonewordOptions.new
    options.parse!(ARGV)

    if options.help
    puts options
    exit
    end

    if options.encoding_help or !encodings[options.encoding]
    puts "Possible encodings:"
    puts encodings.to_a.sort_by{|(k,v)|k.to_s}.map{|(k,v)| "#{k}:\n"+v.map{|(n,e)|" #{n}: #{e}"}.sort.join("\n")}
    exit
    end

    dictionary = Dictionary.new(encodings[options.encoding]).load_wordlist(File.open(options.dictionary))

    output = {
    :plain => lambda do | number, sentence | sentence end,
    :verbose => lambda do | number, sentence | "#{number.ljust(15)}: #{sentence}" end }

    ARGF.each do | number |
    number.strip!
    dictionary.find(number.gsub(/[^0-9]/, '').unpack('C*').map{|n|n - ?0}, options).each do | sentence |
    puts output[options.format][number, sentence]
    end
    end
    Brian Schröder, Feb 20, 2005
    #4
  5. On Feb 19, 2005, at 5:14 PM, Jannis Harder wrote:

    > I wrote a solution with a stdin/out interface and a webrick interface.
    > You can test the webrick interface at http://v-jix.homeip.net:2005/ .
    > My dictionary is 2of4brif.txt . I'm going to post my code after the
    > no-spoiler period.


    Couldn't reach it. Can you post le code? :D

    --
    Jordi
    Jordi Bunster, Feb 21, 2005
    #5
  6. Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20)

    On Feb 20, 2005, at 3:50 PM, Brian Schröder wrote:

    > This version loads the dictionary a lot faster (3 times as fast as the
    > old
    > version) because it does not create as many intermediate objects. Also
    > I measured that upcasing and gsub'ing is faster on the long string
    > than on the individual short strings.


    Must be something wrong with my old iBook, because here it takes
    aaaaages for anything to even appear.

    Makes me wonder if I'm running it correctly:

    echo 5467945 | ruby phoneword.rb -v -p -s -d /usr/share/dict/words

    Or is the problem just that I/O intensive?

    --
    Jordi
    Jordi Bunster, Feb 21, 2005
    #6
  7. Re: [SOLUTION] 1-800-THE-QUIZ (#20)

    Here's my code:
    (wordizer.rb)
    #!/usr/bin/env ruby
    class Wordizer
    def
    initialize(dict,map=[nil,nil,"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"])
    @map=map.map do |z| #@map=map.map ... w00t
    if z
    "[#{z}]"
    else
    "[^\x00-\xFF]"
    end
    end
    case dict
    when String
    @dict=dict.split(/\s+/)
    when Array
    @dict=dict
    when File
    @dict=dict.readlines.map{|z|z.strip}
    end
    end
    def wordize(number,mulnum=false)
    number=number.to_s
    numa = number.split('').map{|z|@map[z.to_i]}
    positions=[[0,false]]
    words = [nil]*(number.size+1)
    until positions.empty?
    positions.uniq!
    pos,num = positions.shift
    words[pos]= nil if words[pos] and words[pos].empty?
    words[pos]||=@dict.grep(mkre(numa[pos..-1]))

    words[pos].map{|z|z.size if z}.uniq.each do |len|
    positions.push([pos+len,false]) if pos+len<=number.size
    end
    if ((not num) or mulnum)and pos<number.size
    words[pos]<<number[pos,1]
    if !positions.include?([pos+1,false])
    positions.push([pos+1,true])
    end
    end

    end
    out = recwalk(words,mulnum).compact.sort{ |a,b|
    ac = a.gsub(/[^-]/,'').size
    bc = b.gsub(/[^-]/,'').size
    if ac == bc
    a<=>b
    else
    ac<=>bc
    end
    }.map{|z|z.upcase!;if mulnum;z.gsub!(/([0-9])-(?=[0-9])/,'\1');end;z}
    out.delete(number) if mulnum
    out
    end
    private
    def mkre(number)
    cc=0
    re="#{number.shift}"
    number.each do |z|
    cc+=1
    re<<"(#{z}"
    end
    re<<(")?"*cc)
    /^#{re}$/i
    end
    def recwalk(words,mulnum)
    que=[[nil,0,false]]
    out=[]
    until que.empty?
    pre,pos,num,left = que.shift
    if pos == words.size-1
    out << pre
    next
    end
    words[pos].map do |z|
    newnum = (z =~ /[0-9]/)
    que << ["#{pre ? pre+'-' : ''}#{z}",pos+z.size,newnum] if mulnum
    or ((num and not newnum) or not num)
    end if words[pos]
    que.uniq!
    end

    out
    end
    end
    if __FILE__ == $0
    require 'optparse'

    dict="2of4brif.txt"
    map=[nil,nil,"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"]
    mulnum=false
    opts = OptionParser.new do |opts|
    opts.banner = "Usage: #$0 [options] [phone number file]"
    opts.on("-d","--dict TEXTFILE","Specify the dictionary") do |file|

    dict=File.expand_path(file)

    end

    opts.on("-m","--map MAPPING",
    "Specify a custom mapping for a number",
    " Format: number=characters",
    " Example: -m0 -m1 -m2=abc -m3=def ...") \
    do |mapping|
    if mapping !~ /^([0-9])(=(.*))$/
    $stderr.puts "#$0: invalid mapping"
    exit 1
    else
    map[$1.to_i]=$3
    end
    end

    opts.on("-n","--condig","Allow consecutive digits in the output") do

    mulnum=true

    end

    opts.on_tail("-h", "--help", "Show this message") do
    puts opts
    exit
    end
    end

    opts.parse!(ARGV)

    begin
    f = File.open(dict)
    ARGF.pos
    rescue
    $stderr.puts "#$0: #$!"
    exit 1
    end

    w = Wordizer.new(f,map)


    while e=gets
    e.tr!("^0-9","")
    puts w.wordize(e,mulnum)
    end
    f.close
    end
    __END__

    And the Server:
    (server.rb)
    #!/usr/bin/env ruby
    require 'webrick'
    require 'wordizer'
    include WEBrick
    PRE = '<?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
    <head>
    <title>Phone Number Wordizer</title>
    </head>
    <body>
    '
    POST =' <form action="/" method="get">
    <div>Phone number: <input type="text" name="pn"%VAL% /></div>
    <div><input type="checkbox" name="condig"%C% />Allow consecutive
    digits</div>
    <div><input type="submit" name="action" value="Go!" /></div>
    </form>
    <div><small>by <a href="mailto:">Jannis
    Harder</a></small></div>
    </body>
    </html>'

    s = HTTPServer.new( :port => (ARGV[0]||2005).to_i )

    $inwork = []
    $cache = [nil]*(ARGV[1]||150).to_i
    f=File.open(File.expand_path(ARGV[1]||"2of4brif.txt"))
    $w=Wordizer.new(f)
    f.close




    def msg(msg)
    " <p><strong>#{msg}</strong></p>\n"
    end
    def connum(condig,number)
    (condig ? 'a' : 'b')+number
    end

    s.mount_proc("/") do |req, res|
    res.body = PRE.dup
    if req.query["pn"]
    number = req.query["pn"].tr("^0-9","")
    condig = req.query["condig"]
    cnum = connum(condig,number)
    if number.size == 0
    elsif number.size > 15
    res.body << msg("Phone number too long.")
    elsif e = $cache.find{|z|z and z[0]==cnum}
    if e[1].empty?
    res.body << msg("No match found")
    else
    res.body << msg("Results:")
    res.body << " <div>"+e[1].join("</div>\n
    <div>")+"</div><p></p>\n"
    end
    $cache[$cache.index(e),1]=[]
    $cache << e
    else
    Thread.new(number) do
    $inwork << cnum
    $cache << [cnum, $w.wordize(number,condig)]
    $cache.shift
    $inwork.delete(number)
    end unless $inwork.include? cnum

    res['Refresh']="1;url=/?pn=#{WEBrick::HTTPUtils.escape(req.query['pn'])}#{
    req.query['condig'] ? '&condig=on' : ''}&action=Go%21"
    res.body << msg("Please wait...")
    end
    end
    res.body << POST.gsub(/(%VAL%|%C%)/) {
    case $1
    when "%VAL%"
    if req.query["pn"]
    ' value="'+WEBrick::HTMLUtils.escape(req.query["pn"])+'"'
    else
    ''
    end
    when "%C%"
    if req.query["condig"]
    ' checked="checked"'
    else
    ''
    end
    end
    }
    res['Content-Type'] = "text/html"
    end
    s.mount_proc("/favicon.ico") do
    end

    trap("INT"){ exit! }
    s.start
    __END__

    --
    Jannis Harder
    iorcc - International Obfuscated Ruby Code Contest
    http://iorcc.dyndns.org/ irc://irc.freenode.net/iorcc
    Jannis Harder, Feb 22, 2005
    #7
  8. Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20)

    Hello Jordi,

    you found a bug. Your number makes my code enter into an infinite
    loop. I'll fix it and repost.

    Thanks,

    Brian


    On Tue, 22 Feb 2005 06:26:16 +0900, Jordi Bunster <> wrote:
    >
    > On Feb 20, 2005, at 3:50 PM, Brian Schröder wrote:
    >
    > > This version loads the dictionary a lot faster (3 times as fast as the
    > > old
    > > version) because it does not create as many intermediate objects. Also
    > > I measured that upcasing and gsub'ing is faster on the long string
    > > than on the individual short strings.

    >
    > Must be something wrong with my old iBook, because here it takes
    > aaaaages for anything to even appear.
    >
    > Makes me wonder if I'm running it correctly:
    >
    > echo 5467945 | ruby phoneword.rb -v -p -s -d /usr/share/dict/words
    >
    > Or is the problem just that I/O intensive?
    >
    > --
    > Jordi
    >
    >
    Brian Schröder, Feb 22, 2005
    #8
  9. Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20)

    On Tue, 22 Feb 2005 16:59:02 +0900
    Brian Schröder <> wrote:

    > Hello Jordi,
    >
    > you found a bug. Your number makes my code enter into an infinite
    > loop. I'll fix it and repost.
    >
    > Thanks,
    >
    > Brian
    >
    >
    > On Tue, 22 Feb 2005 06:26:16 +0900, Jordi Bunster <> wrote:
    > >
    > > On Feb 20, 2005, at 3:50 PM, Brian Schröder wrote:
    > >
    > > > This version loads the dictionary a lot faster (3 times as fast as the
    > > > old
    > > > version) because it does not create as many intermediate objects. Also
    > > > I measured that upcasing and gsub'ing is faster on the long string
    > > > than on the individual short strings.

    > >
    > > Must be something wrong with my old iBook, because here it takes
    > > aaaaages for anything to even appear.
    > >
    > > Makes me wonder if I'm running it correctly:
    > >
    > > echo 5467945 | ruby phoneword.rb -v -p -s -d /usr/share/dict/words
    > >
    > > Or is the problem just that I/O intensive?
    > >
    > > --
    > > Jordi
    > >
    > >

    >


    Thanks to Jordi who found a testcase that broke my code I reworked the solution. Now I use a different approach to skipping numbers. I create the possible skips for a given number, ignore the skipped numbers, search a real solution for the rest and reinject the skipped numbers into the solution.

    That is a lot nicer, and also the code is now cleaner. Additionally I
    improved loading of the wordlist once again, made -v a true verbose
    switch and added abit of description.

    I hope I'm not annoying people with this long posts.

    As always: The nice and colorfull solutions is at:

    http://ruby.brian-schroeder.de/quiz/phoneword/

    or directly at

    http://ruby.brian-schroeder.de/quiz/phoneword/browse/phoneword-rb.html

    Best regards,

    Brian


    #!/usr/bin/ruby

    # Nodes in the Dictionary.
    class DictionaryNode < Array
    # Terminal info
    attr_reader :words

    def initialize
    super(10)
    @words = []
    end
    end

    # A tree-indexed version of the dictionary that allows efficent searching by number 2 alphabet mapping.
    class Dictionary
    def initialize(encoding)
    super()
    @encoding = {}
    @inverse_encoding = {}

    encoding.each do | k, v |
    @encoding[k] = v.split(/\s+/).map{|c| c[0]}
    end

    # Create map from characters to numbers
    @inverse_encoding = @encoding.inject({}) { | r, (k, v) |
    v.each do | l | r[l] = k end
    r
    }
    @root = DictionaryNode.new
    end

    # Helper method for rekursive adding of words to the dictionary
    private
    def add_recursive(node, word, position)
    if word.length == position
    node.words << word
    return node
    end
    add_recursive(node[@inverse_encoding[word[position]]] ||= DictionaryNode.new, word, position + 1)
    end

    # Add words to the dictionary
    public
    def add(word)
    add_recursive(@root, word, 0)
    self
    end

    # Load a wordlist from a file, which contains one word per line.
    # Ignores punctuation and whitespace.
    def load_wordlist(file, options)
    $stderr.print "Loading dictionary... " if options.verbose
    start = Time.new
    file.read.gsub(/[^A-Za-z\n]/, '').upcase!.split($/).uniq!.each do |w|
    w.chomp!
    next if w.empty? or w.length <= options.min_length
    self.add(w)
    end
    $stderr.puts "built dictionary in %f seconds" % (Time.new-start).to_f if options.verbose
    self
    end

    private
    # Search words and return (in the block) words and the unmatched rest of the number
    def sub_find(node, number, &block)
    # Return words found so far
    block[node.words.map{|w|w.dup}, number] unless node.words.empty?
    # No more digits, so stop searching here
    return node if number.empty?
    # Search for longer words
    sub_find(node[number[0]], number[1..-1], &block) if node[number[0]]
    end

    private
    # Calculate all allowed skip patterns for a number of a given length
    def skips(s, length)
    return if length == 0
    result = skips(s + [false], length-1)
    result.concat(skips(s + [true], length-1)) unless s[-1]
    result
    end

    public
    # Skipping makes this a bit ugly
    def find_noskip(number)
    result = []
    sub_find(@root, number) do | words, rest_number |
    if rest_number.empty?
    result.concat(words)
    else
    find_noskip(rest_number).each do | sentence |
    words.each do | w |
    result << w + '-' + sentence
    end
    end
    end
    end
    result
    end

    # Skipping makes this a bit ugly
    def find(number)
    result = []
    skips([], number.length).each do | skipped |

    # Create the injector that can inject the skipped numbers back into the word
    injector = []
    skipped.zip(number).each_with_index do |(s,n), i|
    injector << [n.to_s, i] if s
    end

    # We search for words built from the unskipped digits
    unskipped_digits = number.zip(skipped).select{|(d, s)| !s}.map{|(d,s)|d}
    sentences = find_noskip(unskipped_digits)
    # Inject the skipped digits back into the found sentences
    sentences.each do | s |
    injector.each do | (n, i) | s.insert(i, n) end
    end

    result.concat(sentences)
    end
    result
    end
    end

    encodings = {
    :james => {
    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'},

    :logic => {
    0 => 'A B',
    1 => 'C D',
    2 => 'E F',
    3 => 'G H',
    4 => 'I J K',
    5 => 'L M N',
    6 => 'O P Q',
    7 => 'R S T',
    8 => 'U V W',
    9 => 'X Y Z'
    }
    }

    require 'optparse'

    class PhonewordOptions < OptionParser
    attr_reader :dictionary, :encoding, :format, :allow_skips, :help, :encoding_help, :verbose, :min_length
    def initialize
    super()
    @dictionary = '/usr/share/dict/words'
    @encoding = :james
    @format = :plain
    @allow_skips = true
    @help = false
    @encoding_help = false
    @verbose = false
    @ignore_non_alpha = false
    @min_length = 1
    self.on("-d", "--dictionary DICTIONARY", String) { | v | @dictionary = v }
    self.on("-e", "--encoding ENCODING", String,
    "How the alphabet is encoded to phonenumbers. james or logic are supported.") { | v | @encoding = v.downcase.to_sym }
    self.on("-p", "--plain", 'One result per found number, no other information. (Default)') { @format = :plain }
    self.on("-f", "--full", 'Prefix the result with the number') { @format = :full }
    self.on("-v", "--verbose", 'Make more noise') { @verbose = true }
    self.on("-s", "--skips", "--allow_skips", "--allow-skips", 'Allow to skip one adjacent number while matching. (Default)',
    'Gives lots of ugly results, but james asked for it.') { @allow_skips = true }
    self.on("-c", "--no-skips", "Don't leave numbers in the detected words") { @allow_skips = false }
    self.on("-m" "--min-length", "Minimum length of accepted words.",
    "Use this to ignore one-letter words that make the output quite uninteresting.", Integer) { | v | @min_length = v }
    self.on("-?", "--help") { @help = true }
    self.on("--supported-encodings", "--encoding-help", "List the supported encodings") { @encoding_help = true }
    end
    end

    options = PhonewordOptions.new
    begin
    options.parse!(ARGV)
    rescue => e
    puts e
    puts options
    exit
    end

    if options.help
    puts options
    exit
    end

    if options.encoding_help or !encodings[options.encoding]
    puts "Possible encodings:"
    puts encodings.to_a.sort_by{|(k,v)|k.to_s}.map{|(k,v)| "#{k}:\n"+v.map{|(n,e)|" #{n}: #{e}"}.sort.join("\n")}
    exit
    end

    dictionary = Dictionary.new(encodings[options.encoding]).load_wordlist(File.open(options.dictionary), options)

    output = {
    :plain => lambda do | number, sentence | sentence end,
    :full => lambda do | number, sentence | "#{number.ljust(15)}: #{sentence}" end }

    method = {true => :find, false => :find_noskip }

    ARGF.each do | number |
    number.strip!
    number = number.gsub(/[^0-9]/, '').unpack('C*').map{|n|n - ?0}
    $stderr.puts "Searching for #{number}" if options.verbose
    dictionary.send(method[options.allow_skips], number).each do | sentence |
    puts output[options.format][number, sentence]
    end
    end
    Brian Schröder, Feb 22, 2005
    #9
  10. Ruby Quiz

    Lee Marlow Guest

    Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20)

    ------=_NextPart_000_0135_01C518EF.C6101400
    Content-Type: text/plain;
    charset="US-ASCII"
    Content-Transfer-Encoding: 7bit

    Attached is my solution.

    Enjoy

    -----Original Message-----
    From: Ruby Quiz [mailto:]
    Sent: Friday, February 18, 2005 6:58 AM
    To: ruby-talk ML
    Subject: [QUIZ] 1-800-THE-QUIZ (#20)

    The three rules of Ruby Quiz:

    1. Please do not post any solutions or spoiler discussion for this quiz until
    48 hours have passed from the time on this message.

    2. Support Ruby Quiz by submitting ideas as often as you can:

    http://www.rubyquiz.com/

    3. Enjoy!

    -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

    Many companies like to list their phone numbers using the letters printed on most telephones. This makes the number easier to
    remember for customers. A famous example being 1-800-PICK-UPS.

    This week's quiz is to write a program that will show a user possible matches for a list of provided phone numbers.

    Your script should behave as a standard Unix filter, reading from files specified as command-line arguments or STDIN when no files
    are given. Each line of these files will contain a single phone number.

    For each phone number read, your filter should output all possible word replacements from a dictionary. Your script should try to
    replace every digit of the provided phone number with a letter from a dictionary word; however, if no match can be made, a single
    digit can be left as is at that point. No two consecutive digits can remain unchanged and the program should skip over a number
    (producing no output) if a match cannot be made.

    Your script should allow the user to set a dictionary with the -d command-line option, but it's fine to use a reasonable default for
    your system. The dictionary is expected to have one word per line.

    All punctuation and whitespace should be ignored in both phone numbers and the dictionary file. The program should not be case
    sensative, letting "a" == "A".
    Output should be capital letters and digits separated at word boundaries with a single dash (-), one possible word encoding per
    line. For example, if your program is fed the number:

    873.7829

    One possible line of output is

    USE-RUBY

    According to my dictionary.

    The number encoding on my phone is:

    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

    Feel free to use that, or the encoding on your own phone.

    ------=_NextPart_000_0135_01C518EF.C6101400
    Content-Type: application/octet-stream;
    name="phonewords.rb"
    Content-Transfer-Encoding: 7bit
    Content-Disposition: attachment;
    filename="phonewords.rb"

    #!/usr/bin/env ruby
    require 'set'
    require 'optparse'

    dict = nil

    ARGV.options do |opts|
    opts.banner = "Usage: ruby #{__FILE__} [options] [input files]"
    opts.on('Options:')
    opts.on('--dictionary DICTFILE', '-d', 'Specify dictionary file') { |file|
    File.open(file) { |f| dict = f.readlines }
    }
    opts.on("--help", "-h", "This text") { puts opts; exit 0 }

    opts.parse!
    end

    l2n = {}
    %w{ABC DEF GHI JKL MNO PQRS TUV WXYZ}.each_with_index { |letters, num|
    letters.scan(/./).each { |c|
    l2n[c] = "#{num + 2}"
    }
    }

    dict = %w{use ruby a quick brown fox jumped over the lazy laz laxx dog lazyfox f azyfox} unless dict

    num_dict = {}
    dict.each { |word|
    num_word = ''
    upword = word.upcase
    upword.scan(/./).each { |c|
    num_word << l2n[c]
    }
    (num_dict[num_word] ||= []) << upword
    }

    def build_word_list(position_list, phnumber, words = Set.new, word = '')
    position = word.length - word.count('-')
    if position >= position_list.size
    word.chop! while word[-1, 1] == '-'
    words << word
    return
    end
    position_list[position].each { |word_ary|
    next unless word_ary
    word_ary.each { |w|
    new_word = word.empty? ? "#{w}" : "#{word}-#{w}"
    build_word_list(position_list, phnumber, words, new_word)
    build_word_list(position_list, phnumber, words, "#{new_word}-#{phnumber[position + w.length, 1]}")
    }
    }
    words
    end

    while phone = gets
    next if phone.gsub!(/[^\d]/, '').empty?
    digits = phone.scan(/./)
    position_list = Array.new(digits.size)
    digits.each_with_index { |d, i|
    length_list = position_list = Array.new(digits.size - i)
    num_word = ''
    (i...digits.size).each { |j|
    num_word << digits[j]
    length_list[j - i] = num_dict[num_word]
    }
    }

    build_word_list(position_list, phone, build_word_list(position_list, phone), phone[0,1]).each { |w|
    puts w
    }
    end

    ------=_NextPart_000_0135_01C518EF.C6101400--
    Lee Marlow, Feb 22, 2005
    #10
  11. Ruby Quiz

    Dave Burt Guest

    Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20)

    "Brian Schröder" <> wrote:
    > i once again found the time to do the ruby quiz. I liked the quiz
    > because it was short, and on the other hand harder than I thought.


    Me too.

    > I
    > skipped the part about skipping letters in my first try, and when I had
    > to add it it made me think quite a bit. (I first skipped letters instead
    > of numbers, because I got confused in my datastructure.)


    I tackled it in bits, too, roughly corresponding to my 3 main functions:
    match, combine and new_phone_numbers. (See the end of this message for a
    link to the program.)

    Step 0: setup
    Get a map get a digit corresponding to any character. {'A'=>'2',
    'B'=>'2'...}
    Read dictionary into a hash mapping digit-strings to an array of the words
    they can make (uppercase).
    {'228' => ['BAT', 'CAT'], ...}

    Step 1: match
    Check every possible substring of the number for matches in the dictionary
    map. (Initially, I just printed these underneath the string in the
    corresponding position. I thought I was nearly there.) To move on, I got
    this function to produce an array of all these matches, each match being
    represented like this: {:start => 1, :length => 3, :words => ['BAT', 'CAT']}

    Step 2: combine
    Combine gets all non-overlapping combinations of matches from the above
    step, and returns an array of combinations. Each combination is an array of
    matches (see above... I really should have made Match a class, hey?).

    Step 3: new_phone_numbers
    Iterates through each word in each match in each combination... except it's
    not that simple. 3 combinations x 3 matches each x 1 word each = 9
    solutions, no worries. 3 combinations x 3 matches each x 3 words each = 243
    solutions. Each word in each match has to be shown with every word in every
    other match in the combination. Enter an array of indexes - an index for
    each match to keep track of which word it's up to (the variable's called
    "index"). So the array of indexes starts at each match's last word, and gets
    decremented until it's [0,0...].
    Now we've got that tricky loop sorted out, the easy part: substitute the
    current word (using index) from each match into the number, and finally
    reject the number if it's got 2 digits in a row.

    And here's the final product.
    http://www.dave.burt.id.au/ruby/phone_words.rb

    Cheers,
    Dave
    Dave Burt, Feb 23, 2005
    #11
  12. Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20)

    On Feb 22, 2005, at 4:04 PM, Lee Marlow wrote:

    > Attached is my solution.


    Just FYI, I get some pretty unusual output when I run this on the quiz
    phone number using /usr/share/dict/words as my dictionary. Here's a
    little of what I see:

    $ echo '873.7829' | ruby phonewords.rb -d /usr/share/dict/words
    8-Q
    -7-TA
    URF
    -T
    -Y
    T
    -3-PU
    -Z
    U
    -ES
    -AW
    UR
    -7-T
    -Y
    US
    -Q
    -A
    U
    -3-P
    -2-Z
    URD
    -U
    ...

    James Edward Gray II
    James Edward Gray II, Feb 23, 2005
    #12
  13. Re: [SOLUTION] 1-800-THE-QUIZ (#20)

    On Feb 22, 2005, at 12:56 AM, Jannis Harder wrote:

    > Here's my code:
    > (wordizer.rb)


    I must have gremlins today. I'm breaking all kinds of programs.

    [snip beginning of code]

    > def wordize(number,mulnum=false)
    > number=number.to_s
    > numa = number.split('').map{|z|@map[z.to_i]}
    > positions=[[0,false]]
    > words = [nil]*(number.size+1)
    > until positions.empty?
    > positions.uniq!
    > pos,num = positions.shift
    > words[pos]= nil if words[pos] and words[pos].empty?
    > words[pos]||=@dict.grep(mkre(numa[pos..-1]))
    > words[pos].map{|z|z.size if z}.uniq.each do |len|
    > positions.push([pos+len,false]) if pos+len<=number.size
    > end
    > if ((not num) or mulnum)and pos<number.size
    > words[pos]<<number[pos,1] if
    > !positions.include?([pos+1,false])


    At first, copying and pasting this code gave me a syntax error.
    Changing the above to:

    words[pos]<<number[pos,1]
    if !positions.include?([pos+1,false])

    Seemed to fix that issue.

    [snip more code]

    > begin f = File.open(dict)
    > ARGF.pos
    > rescue
    > $stderr.puts "#$0: #$!"
    > exit 1
    > end


    Then when I would try:

    $ echo '873.7829' | ruby wordizer.rb -d /usr/share/dict/words

    The above chunk of code would issue an "Illegal Seek" error.
    Commenting out ARGF.pos seem to get me over that hump.

    However at that point, issuing the same command seems to cause an
    infinite loop. I couldn't figure out what was going on there, so I
    gave up.

    Just thought I would share, in case I uncovered anything...

    James Edward Gray II
    James Edward Gray II, Feb 23, 2005
    #13
  14. Ruby Quiz

    Lee Marlow Guest

    Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20)

    ------=_NextPart_000_035A_01C519BA.F4D9C0D0
    Content-Type: text/plain;
    charset="US-ASCII"
    Content-Transfer-Encoding: 7bit

    Here is the new version. I just needed to chomp the dictionary words. I wrote and tested it using cygwin and my own test
    dictionary, IO.readlines seems to keep the \n for each line on linux but not on cygwin.

    The output against /usr/share/dict/words doesn't seem too helpful since each letter is listed as its own word.

    Sorry it's so slow running against a large dictionary.

    -----Original Message-----
    From: James Edward Gray II [mailto:]
    Sent: Wednesday, February 23, 2005 1:23 PM
    To: ruby-talk ML
    Subject: Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20)

    On Feb 22, 2005, at 4:04 PM, Lee Marlow wrote:

    > Attached is my solution.


    Just FYI, I get some pretty unusual output when I run this on the quiz phone number using /usr/share/dict/words as my dictionary.
    Here's a little of what I see:

    $ echo '873.7829' | ruby phonewords.rb -d /usr/share/dict/words 8-Q -7-TA URF -T -Y T -3-PU -Z U -ES -AW UR -7-T -Y US -Q -A U -3-P
    -2-Z URD -U ...

    James Edward Gray II



    ------=_NextPart_000_035A_01C519BA.F4D9C0D0
    Content-Type: application/octet-stream;
    name="phonewords.rb"
    Content-Transfer-Encoding: 7bit
    Content-Disposition: attachment;
    filename="phonewords.rb"

    #!/usr/bin/env ruby
    require 'set'
    require 'optparse'

    dict = nil

    ARGV.options do |opts|
    opts.banner = "Usage: ruby #{__FILE__} [options] [input files]"
    opts.on('Options:')
    opts.on('--dictionary DICTFILE', '-d', 'Specify dictionary file') { |file|
    File.open(file) { |f| dict = f.readlines }
    }
    opts.on("--help", "-h", "This text") { puts opts; exit 0 }

    opts.parse!
    end

    l2n = {}
    %w{ABC DEF GHI JKL MNO PQRS TUV WXYZ}.each_with_index { |letters, num|
    letters.scan(/./).each { |c|
    l2n[c] = "#{num + 2}"
    }
    }

    dict = %w{use ruby a quick brown fox jumped over the lazy laz laxx dog lazyfox f azyfox} unless dict

    num_dict = {}
    dict.each { |word|
    num_word = ''
    upword = word.chomp.upcase
    upword.scan(/./).each { |c|
    num_word << l2n[c]
    }
    (num_dict[num_word] ||= []) << upword
    }

    def build_word_list(position_list, phnumber, words = Set.new, word = '')
    position = word.length - word.count('-')
    if position >= position_list.size
    word.chop! while word[-1, 1] == '-'
    words << word
    return
    end
    position_list[position].each { |word_ary|
    next unless word_ary
    word_ary.each { |w|
    new_word = word.empty? ? "#{w}" : "#{word}-#{w}"
    build_word_list(position_list, phnumber, words, new_word)
    build_word_list(position_list, phnumber, words, "#{new_word}-#{phnumber[position + w.length, 1]}")
    }
    }
    words
    end

    while phone = gets
    next if phone.gsub!(/[^\d]/, '').empty?
    digits = phone.scan(/./)
    position_list = Array.new(digits.size)
    digits.each_with_index { |d, i|
    length_list = position_list = Array.new(digits.size - i)
    num_word = ''
    (i...digits.size).each { |j|
    num_word << digits[j]
    length_list[j - i] = num_dict[num_word]
    }
    }

    build_word_list(position_list, phone, build_word_list(position_list, phone), phone[0,1]).each { |w|
    puts w
    }
    end

    ------=_NextPart_000_035A_01C519BA.F4D9C0D0--
    Lee Marlow, Feb 23, 2005
    #14
  15. Ruby Quiz

    Dave Burt Guest

    Diagrams in RDoc (was Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20))

    "Brian Schröder" <> wrote:
    > ...
    > As always: The nice and colorfull solutions is at:
    >
    > http://ruby.brian-schroeder.de/quiz/phoneword/
    > ...


    Hi Brian,

    I like your class diagrams at
    http://ruby.brian-schroeder.de/quiz/phoneword/doc/

    How did you generate that <map> and pop it into all the pages? (If I had to
    guess, I'd say there was some kind of RubyGraphViz at work somewhere in the
    picture, but I don't, so I won't.)

    Cheers,
    Dave
    Dave Burt, Feb 24, 2005
    #15
  16. Re: Diagrams in RDoc (was Re: [SOLUTION] [QUIZ] 1-800-THE-QUIZ (#20))

    Dave Burt ha scritto:

    > Hi Brian,
    >
    > I like your class diagrams at
    > http://ruby.brian-schroeder.de/quiz/phoneword/doc/
    >
    > How did you generate that <map> and pop it into all the pages? (If I had to
    > guess, I'd say there was some kind of RubyGraphViz at work somewhere in the
    > picture, but I don't, so I won't.)


    I guess it is the -d option of rdoc, and in that case, yes, it is using
    graphviz/dot. It comes with standard ruby, cool ain't it?
    gabriele renzi, Feb 24, 2005
    #16
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. okaminer
    Replies:
    2
    Views:
    529
    IPGrunt
    Feb 16, 2005
  2. Peter Swanson

    Re: Website width for 800 x 600 pixel

    Peter Swanson, Aug 8, 2003, in forum: HTML
    Replies:
    0
    Views:
    474
    Peter Swanson
    Aug 8, 2003
  3. Peter Swanson

    Re: Website width for 800 x 600 pixel

    Peter Swanson, Aug 8, 2003, in forum: HTML
    Replies:
    14
    Views:
    9,514
    Some One
    Aug 10, 2003
  4. Ruby Quiz

    [SUMMARY] 1-800-THE-QUIZ (#20)

    Ruby Quiz, Feb 24, 2005, in forum: Ruby
    Replies:
    7
    Views:
    120
    Dave Burt
    Feb 25, 2005
  5. iMath
    Replies:
    15
    Views:
    387
    Chris Angelico
    Sep 30, 2012
Loading...

Share This Page