[SUMMARY] Phone Typing (#21)

The topic of this week's quiz was very interesting to me. I regret that I
couldn't steal enough time to try a solution of my own. However, I've enjoyed
playing with the solutions quite a bit. Thanks Hans!

So what's all the fuss about? Let's run a simple test, using Dave Burt's
solution (because it puts each step on a new line). The following is me typing
a simple sentence with the multitap method. (Note: I did modify one line of
Dave's code used here, to make the digits come last as they do on my phone and
in Brian's solution.) Forgive the length, but in a way it's telling:

  m_
  mw_
  mx_ # second keypress
  my_ # third keypress
  my _
  my w_
  my wg_
  my wh_ # second keypress
  my wi_ # third keypress
  my wid_
  my wie_ # second keypress
  my wif_ # third keypress
  my wifd_
  my wife_ # second keypress
  my wife _
  my wife d_
  my wife da_
  my wife dam_
  my wife dan_ # second keypress
  my wife dana_
  my wife danap_
  my wife danaq_ # second keypress
  my wife danar_ # third keypress
  my wife danas_ # fourth keypress
  my wife danas _
  my wife danas a_
  my wife danas b_ # second keypress
  my wife danas bg_
  my wife danas bh_ # second keypress
  my wife danas bi_ # third keypress
  my wife danas bip_
  my wife danas biq_ # second keypress
  my wife danas bir_ # third keypress
  my wife danas birt_
  my wife danas birtg_
  my wife danas birth_ # second keypress
  my wife danas birthd_
  my wife danas birthda_
  my wife danas birthdaw_
  my wife danas birthdax_ # second keypress
  my wife danas birthday_ # third keypress
  my wife danas birthday _
  my wife danas birthday g_
  my wife danas birthday h_ # second keypress
  my wife danas birthday i_ # third keypress
  my wife danas birthday ip_
  my wife danas birthday iq_ # second keypress
  my wife danas birthday ir_ # third keypress
  my wife danas birthday is_ # fourth keypress
  my wife danas birthday is _
  my wife danas birthday is a_
  my wife danas birthday is ap_
  my wife danas birthday is app_
  my wife danas birthday is apq_ # second keypress
  my wife danas birthday is apr_ # third keypress
  my wife danas birthday is aprg_
  my wife danas birthday is aprh_ # second keypress
  my wife danas birthday is apri_ # third keypress
  my wife danas birthday is aprij_
  my wife danas birthday is aprik_ # second keypress
  my wife danas birthday is april_ # third keypress
  my wife danas birthday is april _
  my wife danas birthday is april d_
  my wife danas birthday is april e_ # second keypress
  my wife danas birthday is april eg_
  my wife danas birthday is april eh_ # second keypress
  my wife danas birthday is april ei_ # third keypress
  my wife danas birthday is april eig_
  my wife danas birthday is april eigg_
  my wife danas birthday is april eigh_ # second keypress
  my wife danas birthday is april eight_
  my wife danas birthday is april eightg_
  my wife danas birthday is april eighth_ # second keypress

Here's the same exercise, with LetterWise:

  o_
  _ # had to push *
  m_
  my_
  my _
  my w_
  my wh_
  my w_ # had to push *
  my wi_
  my wid_
  my wi_ # had to push *
  my wif_
  my wife_
  my wife _
  my wife f_
  my wife _ # had to push *
  my wife d_
  my wife da_
  my wife dan_
  my wife danc_
  my wife dan_ # had to push *
  my wife dana_
  my wife danas_
  my wife danas _
  my wife danas a_
  my wife danas _ # had to push *
  my wife danas b_
  my wife danas bi_
  my wife danas bir_
  my wife danas birt_
  my wife danas birth_
  my wife danas birthe_
  my wife danas birth_ # had to push *
  my wife danas birthd_
  my wife danas birthda_
  my wife danas birthday_
  my wife danas birthday _
  my wife danas birthday h_
  my wife danas birthday _ # had to push *
  my wife danas birthday i_
  my wife danas birthday is_
  my wife danas birthday is _
  my wife danas birthday is a_
  my wife danas birthday is as_
  my wife danas birthday is a_ # had to push *
  my wife danas birthday is ar_
  my wife danas birthday is a_ # had to push *, a second time
  my wife danas birthday is ap_
  my wife danas birthday is app_
  my wife danas birthday is ap_ # had to push *
  my wife danas birthday is apr_
  my wife danas birthday is apri_
  my wife danas birthday is april_
  my wife danas birthday is april _
  my wife danas birthday is april f_
  my wife danas birthday is april _ # had to push *
  my wife danas birthday is april d_
  my wife danas birthday is april _ # had to push *, a second time
  my wife danas birthday is april e_
  my wife danas birthday is april ei_
  my wife danas birthday is april eig_
  my wife danas birthday is april eigh_
  my wife danas birthday is april eight_
  my wife danas birthday is april eighti_
  my wife danas birthday is april eight_ # had to push *
  my wife danas birthday is april eighth_

(I'm sure Dana will be looking forward to all your gifts this year.)

That's a pretty radical difference. Let me line up those stats:

                 Multitap LetterWise
    keypresses: 74 52
  second press: 20 (27%) 12 (23%)
   third press: 14 (19%) 2 (4%)
  fourth press: 2 (3%) 0

However, and this is purely subjective so feel free to completely ignore it, I'm
almost positive I entered the sentence faster with Multitap. Isn't that ironic?
The reason is that I can glance at the keypad and know exactly what I need to
do. With LetterWise, I had to wait and see what I got, before I could respond.
The document linked to by the quiz seemed to imply this was a phase I would get
past, eventually.

So, between the two methods explored in the solution, LetterWise is definitely a
by the numbers improvement. Subjectively though, it may take some getting use
to. Try it for yourself.

Oh, should I be talking about Ruby here? Right, let's get on that.

I want to take a look at Dave Burt's code that was used in the examples above,
but not all of it. I'll leave out his TK interface and a few classes mainly
useful in testing.

First, Dave combined the code Hans and I offered with the quiz to build a better
command-line interface. Here's the class for that:

  class CharPhonePad < PhonePad
    def initialize
      super
      Thread.new do
        loop do
          case c = self.class.read_char
          when 3, 4, 26, 27 # Break on Ctrl+C, Ctrl+D, Ctrl+Z, Escape
            break
          when 43 # convert '+' to '*' for ease of typing on numpad
            c = 42
          when 45, 46, 47 # convert '-', '.', '/' to '#' for ease of typing
            c = 35
          end
          notify_observers(c.chr)
        end
      end
    end
    def set_text(text, cursor)
      super(text, cursor)
      puts to_s
    end
    def to_s
      @text.dup.insert(@cursor, '_')
    end

    begin
      require "Win32API"
      def self.read_char
        c = Win32API.new("crtdll", "_getch", [], "L").Call
      end
    rescue LoadError
      def self.read_char
        system "stty raw -echo"
        STDIN.getc
      ensure
        system "stty -raw echo"
      end
    end
  end

Nothing real surprising there. Dave just extended Hans's framework with my
character reading methods. It's a nicer option than what I offered because you
can use the event system. Note the character case block adds some niceties for
using your keyboard number pad.

Next, here's Dave's Multipress/Multitap implementation:

  class TapHandler
    def initialize(phone_pad)
      @p = phone_pad
    end
  end

  class MultipressTapHandler < TapHandler
    attr_accessor :timeout
    def initialize(phone_pad, timeout = 1.5)
      super(phone_pad)
      @timeout = timeout
    end
    def process_event(ev)
      if (@last_event &&
          ev.digit == @last_event.digit &&
          Time.now - @last_event.when < timeout)
        char_set = PhonePad::InputMap[ev.digit]
        char_index = (char_set.index(@p.text[-1].chr) + 1) % char_set.size
        @p.set_text(@p.text[0..-2] + char_set[char_index], @p.cursor)
      else
      ### modified by JEG2 for discussion example ###
        @p.set_text(@p.text+PhonePad::InputMap[ev.digit][0], @p.cursor+1)
      ### actual code by Dave Burt ###
      # @p.set_text(@p.text+ev.digit, @p.cursor+1)
      end
      @last_event = ev
    end
  end

All the exciting work in there happens in process_event(). The if checks to see
if it has memorized a previous event. If it has and this new event is the same
digit and the timout for that press hasn't expired yet, the next character in
the InputMap is selected. In all other cases, the first character of the
InputMap is selected. That's all there is to Multitap.

  class LetterWiseTapHandler < TapHandler
    # method taken from:
    # MacKenzie, I. S., Kober, H., Smith, D., Jones, T., Skepner, E. (2001).
    # LetterWise: Prefix-based disambiguation for mobile text input.
    # Proceedings of the ACM Symposium on User Interface Software and
    # Technology - UIST 2001, pp. 111-120. New York: ACM.
    # http://www.yorku.ca/mack/uist01.html
    
    # A lot of the logic in this method is captured in the InputMap,
    # which maps prefixes of up to 3 letters and a key (0-9) onto
    # an array of letters inmost-likely-first order.
    
    #require 'yaml'
    # 1.8MB, ~ 6 seconds to load
    #InputMap = YAML.load_file('predict3.yaml')
    # 2.3MB, ~ 3 seconds to load
    InputMap = eval(File.read('predict3.rb'))
    
    def initialize(phone_pad)
      super(phone_pad)
      @cycle = ['*']
    end
    def process_event(ev)
      prefix = @p.text[/\w{0,3}$/]
      if ev.digit == '*' # change last letter
        @cycle.push @cycle.shift # rotate
        @p.set_text(@p.text[0..-2], @p.cursor - 1)
      elsif InputMap[prefix]
        @cycle = InputMap[prefix][ev.digit].dup
      else
        @cycle = InputMap[nil][ev.digit].dup
      end
      @cycle ||= %w[. ! - & @ $ * +]
      @p.set_text(@p.text + @cycle[0], @p.cursor + 1)
    end
  end

As the comments suggest, there's not a lot of logic in here. Surprisingly,
there isn't that much in the evaled predict3.rb file either. It's just one big
boring hash, which you can see for yourself, if you follow the links to Dave's
solution.

Again, process_event() is where the action is. Really, it just looks up the
last three letters entered in the InputMap to find the order of the letters (the
elsif part) or cycles to the next most likely letter (the if part). The system
seems to support some punctuation characters, but I couldn't figure out how to
get at them when using it.

Here's the code that kicks off the process:

  if $0 == __FILE__
    
    #p = CLIPhonePad.new # Command-line interface
    p = CharPhonePad.new # Raw command-line interface
    #p = TkPhonePad.new # TK interface
    
    # just dump digits pressed
    #tap_handler = NoOpTapHandler.new(p)
    # tap the shape of a letter on the keypad!
    #tap_handler = IconicTapHandler.new(p)
    # standard multi-press, 1.5s timeout
    #tap_handler = MultipressTapHandler.new(p)
    # letter-wise prediction; * to change letter
    tap_handler = LetterWiseTapHandler.new(p)

    p.register do |ev|
      tap_handler.process_event(ev)
    end

    Thread.list[0].join
  end

Dave, repeat after me, "optparse is my friend..." Be kind to the guy who plays
with your code, because it might be me. Here's that and more without the
comment dance:

  if $0 == __FILE__
    require "optparse"
    
    # defaults
    pad = :raw
    tap = :letterwise
    
    # parse options
    opts = OptionParser.new do |opts|
      opts.banner = "Usage: #$0 [OPTIONS]"
      
      opts.separator ""
      opts.separator "Specific Options:"
      
      opts.on( "-i INTERFACE",
               [:raw, :tk, :cli],
               "Interface you would like to use:",
               " raw, tk or cli." ) do |v|
        pad = v
      end
      opts.on( "-m MODE",
               [:letterwise, :multipress, :iconic, :noop],
               "The input algorithm to use:",
               " letterwise, multipress, iconic or noop." ) do |v|
        tap = v
      end
      opts.on( "-h", "-?", "--help",
               "Show this text." ) do
        puts opts
        exit
      end
    end
    opts.parse!(ARGV)
    
    # handle results
    case pad
    when :raw then p = CharPhonePad.new
    when :tk then p = TkPhonePad.new
    when :cli then p = CLIPhonePad.new
    end
    case tap
    when :letterwise then tap_handler = LetterWiseTapHandler.new(p)
    when :multipress then tap_handler = MultipressTapHandler.new(p)
    when :iconic then tap_handler = IconicTapHandler.new(p)
    when :noop then tap_handler = NoOpTapHandler.new(p)
    end

    # run program
    p.register { |ev| tap_handler.process_event(ev) }
    Thread.list[0].join
  end

Brian makes them even prettier, so scan his solution for an even better example.

My thanks to both Brian and Dave for playing with this one while I didn't have
time to. Also, thank you Hans for such a clever topic.

Tomorrows quiz takes us all the way to Rome...