R
Ruby Quiz
by Robert Dober
First of all I would like to thank all submitters.
One of the best features of the Ruby Quiz is it tolerance brings up new ideas so
often.
In order to honor this feature I will first present Daniel Finnie's solution. It
just abbreviates all defined methods. This very useful a feature which I will
demonstrate after discussion of the code.
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
         send(method, *args, &blk)
     Â
      # Multiple possibilities, return an array of the possibilities.
      else
         "Ambigous abbreviation #{method} -> " +
"#{ possibleMethods.join(", ")}"
      end
   end
end
Nothing very complicated there, just define method_missing in Object as a catch
all for undefined methods in any object. Than he creates an array of all methods
which are legal completions of a potential abbreviation. In case there are none
an original NoMethodError is mimicked. In case there is exactly one it is
executed via send, please note the (method, *args, &blk) syntax, the &blk part
has been forgotten in some solutions but is vital in #method_missing. And
eventually if there are more Daniel returned an array of all completions for
testing. I will discuss this later.
I have adapted this to return a message explaining what completions exist for
the abbreviation. Why? Well I just fired up my irb and requested Daniel's
solution, see what I got, much less typing for free. Here are some excerpts of
my irb session.
irb(main):001:0> require 'sol2'
=> true
irb(main):002:0> "a".le
=> 1
irb(main):003:0> "abba".le
=> 4
irb(main):004:0> "Hello World".spl
=> ["Hello", "World"]
irb(main):005:0> "Hi there Daniel".sp
=> ["Hi", "there", "Daniel"]
irb(main):006:0> "Hi there Daniel".s
=> "Ambigous abbreviation s ->Â select, slice, sub!, squeeze, send, split,
size, strip, succ!, squeeze!, sub, slice!, scan, sort, swapcase, swapcase!,
sum, singleton_methods, succ, sort_by, strip!"
irb(main):007:0> 12.x
NoMethodError: undefined method `x' for 12:Fixnum
            from ./sol2.rb:9:in `method_missing'
            from (irb):7
Quite nice as a side product, no?
Let us turn towards solutions which respected the idea of defining certain
abbreviations, being interpreted in Command Line Interfaces for example. It was
extremely difficult to chose a solution because most of the solutions had their
strong parts. I eventually decided to comment on Donald Ball's solution as it
was maybe the most readable solution for myself. I had discussed this with James
who favored other solutions for other reasons and I will mention these reasons
shortly. As always there is something in every solution so take your time and
read them.
Ok here is Donald's code - slightly modified again.
require 'set'
module AutoComplete
   module ClassMethods
      attr_reader :abbrs
      def abbrev(*args)
         # TODO abbrs might be better implemented as a sorted set
         @abbrs ||= Set.new
         @abbrs += args
      end
   end
   module ObjectMethods
      def method_missing(id, *args, &blk)
         # if it is an exact match, there is no corresponding
method
# or else it would have been called
         if self.class.abbrs.include?(id)
            super
         end
         s = id.to_s
         len = s.length
         # find all abbreviations which begin with id and have
# active methods
         matches = self.class.abbrs.select { |abbr|
abbr.to_s[0,len] == s && respond_to?(abbr)
}
         if matches.length == 0
            super
         elsif matches.length == 1
            send(matches[0], *args, &blk)
         else
            matches
         end
      end
   end
end
class Object
   extend AutoComplete::ClassMethods
   include AutoComplete::ObjectMethods
end
Although this can be done more concisely I eventually started to like the
explicit way.
Lots of little details can be changed and they were in other solutions. Not
everybody wanted a NoMethodError thrown in case an abbreviation target was an
abbreviation of a different method and was not there.
Ken Bloom was the first to point out that it was a bad idea to return an array
of abbreviations in case of ambiguities. I completely agree, that was only for
the test code anyway.
A great majority of the solutions follow this idea which is certainly a must
under some circumstances. I imagine a DSL which is not interactive and where the
interpreter has no choice than to throw an Exception, good thinking here.
Ken's solution has three features noteworthy, first he added the list of
possible completions as an attribute of the exception he throws, secondly he
just used the abbrev Standard Library Module and thirdly he used #super in
#method_missing to raise a NoMethodError. super in that case calls
Kernel#method_missing. This took me some time to figure out. I guess the road to
Ruby mastery is not answering quizzes but submitting quizzes and understanding
the solutions. But you will find some of these features in other solutions too.
Let us finish the summary with his code, another astonishing example of how much
can be done with so little code in Ruby.
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
Many thanx to everyone....
First of all I would like to thank all submitters.
One of the best features of the Ruby Quiz is it tolerance brings up new ideas so
often.
In order to honor this feature I will first present Daniel Finnie's solution. It
just abbreviates all defined methods. This very useful a feature which I will
demonstrate after discussion of the code.
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
         send(method, *args, &blk)
     Â
      # Multiple possibilities, return an array of the possibilities.
      else
         "Ambigous abbreviation #{method} -> " +
"#{ possibleMethods.join(", ")}"
      end
   end
end
Nothing very complicated there, just define method_missing in Object as a catch
all for undefined methods in any object. Than he creates an array of all methods
which are legal completions of a potential abbreviation. In case there are none
an original NoMethodError is mimicked. In case there is exactly one it is
executed via send, please note the (method, *args, &blk) syntax, the &blk part
has been forgotten in some solutions but is vital in #method_missing. And
eventually if there are more Daniel returned an array of all completions for
testing. I will discuss this later.
I have adapted this to return a message explaining what completions exist for
the abbreviation. Why? Well I just fired up my irb and requested Daniel's
solution, see what I got, much less typing for free. Here are some excerpts of
my irb session.
irb(main):001:0> require 'sol2'
=> true
irb(main):002:0> "a".le
=> 1
irb(main):003:0> "abba".le
=> 4
irb(main):004:0> "Hello World".spl
=> ["Hello", "World"]
irb(main):005:0> "Hi there Daniel".sp
=> ["Hi", "there", "Daniel"]
irb(main):006:0> "Hi there Daniel".s
=> "Ambigous abbreviation s ->Â select, slice, sub!, squeeze, send, split,
size, strip, succ!, squeeze!, sub, slice!, scan, sort, swapcase, swapcase!,
sum, singleton_methods, succ, sort_by, strip!"
irb(main):007:0> 12.x
NoMethodError: undefined method `x' for 12:Fixnum
            from ./sol2.rb:9:in `method_missing'
            from (irb):7
Quite nice as a side product, no?
Let us turn towards solutions which respected the idea of defining certain
abbreviations, being interpreted in Command Line Interfaces for example. It was
extremely difficult to chose a solution because most of the solutions had their
strong parts. I eventually decided to comment on Donald Ball's solution as it
was maybe the most readable solution for myself. I had discussed this with James
who favored other solutions for other reasons and I will mention these reasons
shortly. As always there is something in every solution so take your time and
read them.
Ok here is Donald's code - slightly modified again.
require 'set'
module AutoComplete
   module ClassMethods
      attr_reader :abbrs
      def abbrev(*args)
         # TODO abbrs might be better implemented as a sorted set
         @abbrs ||= Set.new
         @abbrs += args
      end
   end
   module ObjectMethods
      def method_missing(id, *args, &blk)
         # if it is an exact match, there is no corresponding
method
# or else it would have been called
         if self.class.abbrs.include?(id)
            super
         end
         s = id.to_s
         len = s.length
         # find all abbreviations which begin with id and have
# active methods
         matches = self.class.abbrs.select { |abbr|
abbr.to_s[0,len] == s && respond_to?(abbr)
}
         if matches.length == 0
            super
         elsif matches.length == 1
            send(matches[0], *args, &blk)
         else
            matches
         end
      end
   end
end
class Object
   extend AutoComplete::ClassMethods
   include AutoComplete::ObjectMethods
end
Although this can be done more concisely I eventually started to like the
explicit way.
Lots of little details can be changed and they were in other solutions. Not
everybody wanted a NoMethodError thrown in case an abbreviation target was an
abbreviation of a different method and was not there.
Ken Bloom was the first to point out that it was a bad idea to return an array
of abbreviations in case of ambiguities. I completely agree, that was only for
the test code anyway.
A great majority of the solutions follow this idea which is certainly a must
under some circumstances. I imagine a DSL which is not interactive and where the
interpreter has no choice than to throw an Exception, good thinking here.
Ken's solution has three features noteworthy, first he added the list of
possible completions as an attribute of the exception he throws, secondly he
just used the abbrev Standard Library Module and thirdly he used #super in
#method_missing to raise a NoMethodError. super in that case calls
Kernel#method_missing. This took me some time to figure out. I guess the road to
Ruby mastery is not answering quizzes but submitting quizzes and understanding
the solutions. But you will find some of these features in other solutions too.
Let us finish the summary with his code, another astonishing example of how much
can be done with so little code in Ruby.
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
Many thanx to everyone....