Method Auto Completion (#110)

The three rules of Ruby Q.:

  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 Q. by submitting ideas as often as you can:

http://www.rubyquiz.com/

  1. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps
everyone
on Ruby T. follow the discussion. Please reply to the original quiz
message,
if you can.

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

by Robert D.

Command Line Interfaces very often support command abbreviations The
purpose of
this quiz is to automatically dispatch to methods on unambiguous
abbreviations,
ask the user for clarification in case of ambiguous abbreviations and
raise a
NoMethodError in case of an abbreviation that cannot be matched to any
command.

Behavior of other methods defined in a class shall not be altered.

Be creative about the interface and about behavior. I have OTOH defined
a small
test suite that makes assumptions about the interface and behavior. But
the test
suite is only there for your convenience.

What is said below applies to the test suite and shall in no way inhibit
any
alternative ideas.

class Mine
abbrev :step, :next, :stop
abbrev :exit
end

Mine.new.e # should resolve to exit
Mine.new.st # should prompt the user
Mine.new.a # should still raise a NoMethodError

Abbreviation targets themselves are not expanded.

class Nine
abbrev :hash
abbrev :has
end

Nine.new.ha # => [:hash, :has]
Nine.new.has # => NoMethodError

class Nine
def has; 42; end
end
Nine.new.has # => 42

In order to allow for automated testing the test code shall not prompt
the user
in case of an ambiguous abbreviation but return an array containing all
(and
only all) possible completions as symbols. Note that the test suite sets
the
global variable $TESTING to a true value for your convenience.

http://rubyquiz.com/test-abbrev.rb

Last week I was kind of embarrassed that Bob chose to feature my late,
poorly commented, somewhat opaque solution, but I certainly appreciate
the inclusive spirit of Ruby Q… Here’s a more timely submission for
the auto completion problem.


class Object
def self.abbrev(*args)
module_eval <<-EOS
@@abbrevs ||= []
@@abbrevs += args

def method_missing(m)
  # abbrev targets themselves are not supposed to be expanded
  raise NoMethodError if @@abbrevs.include?(m)

  # which abbrev targets could match m, and which of those

correspond to methods?
matches = @@abbrevs.select do |sym|
sym.to_s.index(m.to_s) == 0 && methods.include?(sym.to_s)
end

  case matches.size
    when 0
      raise NoMethodError
    when 1
      self.send(matches.first)
    else
      # multiple matches, pass them back to the user
      return matches if $TESTING
      puts matches.join(" ")
  end
end
EOS

end
end

This code passes the supplied tests, and I also added another test
when an earlier version of my code was doing things it shouldn’t have:


check for unintended effects on other classes

class Test3 < Test::Unit::TestCase
def setup
@foo_class = Class.new {
abbrev :start
def start; nil end
}
@bar_class = Class.new {
def start; nil end
}
end

def test1
f = @foo_class.new
b = @bar_class.new
assert_raise NoMethodError do
b.send(:sta)
end
end
end

I don’t feel very confident in the approach I’ve taken, so I’m eager
to hear critique and see other solutions.

Krishna

On Fri, 19 Jan 2007 22:45:05 +0900, Ruby Q. wrote:

ask the user for clarification in case of ambiguous abbreviations and raise a

end
Nine.new.has # => 42

In order to allow for automated testing the test code shall not prompt the user
in case of an ambiguous abbreviation but return an array containing all (and
only all) possible completions as symbols. Note that the test suite sets the
global variable $TESTING to a true value for your convenience.

http://rubyquiz.com/test-abbrev.rb

Returning an array when the answer is ambiguous is a very bad way to do
this. Instead, I throw an exception (you can get the candidates from the
#candidates attribute of the exception).

I also saw no reason to specifically name the methods that get
abbreviated
– rather, abbreviation works on all methods in the object and its super
classes.

require ‘abbrev’

class AmbiguousExpansionError < StandardError
attr_accessor :candidates
def initialize(name,possible_methods)
super(“Ambiguous abbreviaton: #{name}\n”+
“Candidates: #{possible_methods.join(”, “)}”)
@candidates=possible_methods
end
end

module Abbreviator
def method_missing name,*args
abbrevs=methods.abbrev
return send(abbrevs[name.to_s],*args) if abbrevs[name.to_s]
meths=abbrevs.reject{|key,value| key!~/^#{name}/}.values.uniq
raise AmbiguousExpansionError.new(name, meths) if meths.length>1
return super(name,*args)
end
end

Here is my solution.
It does not pass the supplied tests since it uses a different interface
when
there are multiple matches : It a raises a MultipleMethods exception
with an
array of the possible matches.

There is an additional abbrev method in Module which defines the methods
to be
abbreviated. If used with the single argument ‘true’ it enables
abbreviations
for all methods of the object.
the ‘abbreviated_methods’ private method of Object does the intersection
of
the defined object class’s abbreviated methods and the current actual
methods
of the object.
Then the abbreviation are caught with method_missing.

quiz110.rb :

class MultipleMethods < NoMethodError
attr_reader :methods
def initialize(methods)
@methods = methods
super("Multiple Choices : " + @methods.join(’ '))
end
end

class Module
attr_reader :abbreviated_methods

private

Defines a list of methods to be abbreviated.

If auto = true, then all the methods are abbreviated

class Test

abbrev # All the methods of Test will be abbreviated

end

class Test2

abbrev :first, :last # Only the methods first and last may be

abbreviated

end

If multiple choices are possible, a MultipleMethods exception is

raised
which contains a list of the matches.
def abbrev(auto = true, *args)
if auto.respond_to? :to_sym
@abbreviated_methods ||= []
@abbreviated_methods += args.collect {|m| m.to_sym }.unshift
auto.to_sym
elsif args.empty?
auto ? @abbreviated_methods = [] : @abbreviated_methods = nil
else
raise ArgumentError
end
end
end

class Object
alias :abbrev_method_missing :method_missing
def method_missing(sym, *args)
found = abbreviated_methods.select { |m| m.to_s =~ /^#{sym}/ }
if found.empty?
abbrev_method_missing(sym, *args)
elsif found.size == 1
send found.first, *args
else
raise MultipleMethods.new(found)
end
end

private
def abbreviated_methods
if self.class.abbreviated_methods.nil?
[]
elsif self.class.abbreviated_methods.empty?
methods
else
self.class.abbreviated_methods & methods.collect {|m| m.to_sym}
end
end
end

My Solution doesn’t pass all tests, since i think that when a method
:aa is defined it should offer completion from :a to :aa (even when
the method doesn’t exist in the first place but is defined later on)

Parked at Loopia

Parked at Loopia - forgot to remove the other stuff :expressionless: and
somehow it didn’t set highlighting to ruby…

On 1/19/07, Ruby Q. [email protected] wrote:

ask the user for clarification in case of ambiguous abbreviations and raise a

    end
    Nine.new.has # => 42

In order to allow for automated testing the test code shall not prompt the user
in case of an ambiguous abbreviation but return an array containing all (and
only all) possible completions as symbols. Note that the test suite sets the
global variable $TESTING to a true value for your convenience.

    http://rubyquiz.com/test-abbrev.rb

My Solution doesn’t pass all tests, since i think that when a method
:aa is defined it should offer completion from :a to :aa (even when
the method doesn’t exist in the first place but is defined later on)

http://pastie.caboo.se/34631

It’s nothing fancy, just a quick 15 minutes-hack, was fun though :slight_smile:

^manveru

This is my solution. Probably my fastest and shortest ruby quiz
solution, but no less fun! Props to the creator of this quiz.

In technicolor glory: Parked at Loopia
Also attached. And pasted below.

class Object
def method_missing(method, *args, &blk)
# Gather all possible methods it could be.
possibleMethods = self.methods.select {|x| x =~
/^#{Regexp.escape(method.to_s)}/ }

case possibleMethods.size
# No matching method.
when 0
  raise NoMethodError.new("undefined method `#{method}' for

#{self.inspect}:#{self.class.name}")

# One matching method, call it.
when 1
  method = possibleMethods.first
  self.send(method, *args, &blk)

# Multiple possibilities, return an array of the possibilities.
else
  possibleMethods
end

end
end