[SUMMARY] HighLine (#29)

Discussion in 'Ruby' started by Ruby Quiz, Apr 28, 2005.

  1. Ruby Quiz

    Ruby Quiz Guest

    There weren't a lot of solutions this week, but they all had interesting
    elements. I had a really hard time selecting what to show in the summary, but
    the interests of time and space demand that I choose one.

    Let's have a look at Mark Sparshatt's solution. We'll jump right into the ask()
    method, which was really the main thrust of this quiz:

    module HighLine
    # prompt = text to display
    # type can be one of :string, :integer, :float, :bool or a proc
    # if it's a proc then it is called with the entered string. If the
    # input cannot be converted then it should throw an exception
    # if type == :bool then y,yes are converted to true. n,no are
    # converted to false. All other values are rejected.
    #
    # options should be a hash of validation options
    # :validate => regular expresion or proc
    # if validate is a regular expression then the input is matched
    # against it
    # if it's a proc then the proc is called and the input is accepted
    # if it returns true
    # :between => range
    # the input is checked if it lies within the range
    # :above => value
    # the input is checked if it is above the value
    # :below => value
    # the input is checked if it is less than the value
    # :default => string
    # if the user doesn't enter a value then the default value
    # is returned
    # :base => [b, o, d, x]
    # when asking for integers this will take a number in binary,
    # octal, decimal or hexadecimal
    def ask(prompt, type, options=nil)
    begin
    valid = true

    default = option(options, :default)
    if default
    defaultstr = " |#{default}|"
    else
    defaultstr = ""
    end

    base = option(options, :base)

    print prompt, "#{defaultstr} "
    $stdout.flush
    input = gets.chomp

    if default && input == ""
    input = default
    end

    #comvert the input to the correct type
    input = case type
    when :string: input
    when :integer: convert(input, base) rescue valid = false
    when :float: Float(input) rescue valid = false
    when :bool
    valid = input =~ /^(y|n|yes|no)$/
    input[0] == ?y
    when Proc: input = type.call(input) rescue valid = false
    end

    #validate the input
    valid &&= validate(options, :validate) do |test|
    case test
    when Regexp: input =~ test
    when Proc: test.call(input)
    end
    end
    valid &&= validate(options, :within) { |range| range === input}
    valid &&= validate(options, :above) { |value| input > value}
    valid &&= validate(options, :below) { |value| input < value}

    puts "Not a valid value" unless valid
    end until valid

    return input
    end

    # ...

    The comment above the method explains what it expects to be passed, in nice
    detail. You can see that Mark added several options to those suggested in the
    quiz. Mark also hit on a fun feature: Allow the type parameter to be a Proc.
    My own solution used this trick and I was surprised at the flexibility it lended
    to the method. Let's move on to the code itself.

    The method starts off by calling a helper option() to fetch :default and :base.
    We haven't seen the code for that yet, but it's easy to assume what it does at
    this point and we can mentally translate option(options, :default) to
    options[:default] for now. Note that if a :default is given, the code sets up a
    string to display it to the user.

    The next little chunk of code displays the prompt (with trailing :default
    string). flush() is called right after that, to be sure the output is not
    buffered. Then a line is read from the keyboard. If the line of input was
    empty and a :default was set, the next if statement makes the switch.

    The following case statement reassigns input, based on the type of conversion
    requested. :string gets no translation, :integer calls the helper method
    convert() we'll examine later, :float uses Float(), :bool has a clever check
    that returns true if the first character is a ?y, and finally if type is a Proc
    object it is called with the input.

    There are a two other points of interest in this chunk of code. First there are
    a lot of colons used in there, thanks to some new Ruby syntax. when ... : is
    the same as when ... then, which is the older way to stuff the condition and
    result on the same line. This works for if statements now too.

    The other point of interest is that the code is constantly updating the valid
    variable. If and exception is thrown or a :bool question was given something
    other than "y", "n", "yes", or "no", valid is set to false. If you glance back
    at the top of the method you'll see that valid started out true, but it may not
    be when we're done here. We'll see the effects of that in a bit.

    Next up we have a bunch of calls to another helper called validate(). It seems
    to take some code and return a true or false response based on how the code
    executed. If you reread the initial comment at this point, you'll see that it
    explains all those blocks and what they are checking for. The neat trick here
    is that all of these results are &&=ed with valid. && requires two truths each
    time it is evaluated, so valid will only stay true if it was true when we got
    here and every single validate() call returns true. This made for a pretty
    clean process, I though.

    We now see that we get a warning if we didn't provide valid input (by any
    required condition). We also find the end of a begin ... end until valid
    construct, which is a rare Ruby loop that is similar to do ... until in other
    languages. When input is returned outside that loop, we know it must be valid.

    Here's the other quiz suggested method:

    #...

    #asks a yes/no question
    def ask_if(prompt)
    ask(prompt, :bool)
    end

    #...

    Obviously, that's just a simplification of ask().

    Let's get to those helper methods now:

    #...

    private

    #extracts a key from the options hash
    def option(options, key)
    result = nil
    if options && options.key?(key)
    result = options[key]
    end
    result
    end

    #helper function for validation
    def validate(options, key)
    result = true
    if options && options.key?(key)
    result = yield options[key]
    end
    result
    end

    #converts a string to an integer
    #input = the value to convert
    #base = the numeric base of the value b,o,d,x
    def convert(input, base)
    if base
    if ["b", "o", "d", "x"].include?(base)
    input = "0#{base}#{input}"
    value = Integer(input)
    else
    value = Integer(input)
    end
    else
    value = Integer(input)
    end

    value
    end
    end

    # ...

    option() simply checks that options were provided and that they included the
    requested key. If so, the matching value is returned. Otherwise, nil is
    returned. validate() is nearly identical, save that it yields the value to a
    provided block and returns the result of that block. convert() just reality
    checks the provided base and calls Integer().

    Finally, here are some simple tests showing the method calls:

    if __FILE__ == $0
    include HighLine
    #string input using a regexp to validate, returns test as the
    # default value
    p ask( "enter a string, (all lower case)", :string,
    :validate => /^[a-z]*$/, :default => "test" )
    #string input using a proc to validate
    p ask( "enter a string, (between 3 and 6 characters)", :string,
    :validate => proc { |input| (3..6) === input.length} )

    #integer intput using :within
    p ask("enter an integer, (0-10)", :integer, :within => 0..10)
    #float input using :above
    p ask("enter a float, (> 6)", :float, :above => 6)

    #getting a binary value
    p ask("enter a binary number", :integer, :base => "b")

    #using a proc to convert the a comma seperated list into an array
    p ask("enter a comma seperated list", proc { |x| x.split(/,/)})

    p ask_if("do you want to continue?")
    end

    Be sure and look over examples two and six, which use Procs for validation and
    type. It's crazy how powerful you can make something when you open it up to
    extension by Ruby code.

    Ryan Leavengood's solution is class based, instead of using a module. That
    allows you to assign the input and output streams, for working with sockets
    perhaps. That adds an object construction step though. In my solution, also
    class based, I solved that by allowing an option to import top level shortcuts
    for casual usage.

    Ryan used a custom MockIO object and the ability to redirect his streams to
    create a nice set of unit tests. I did the same thing using the standard
    StringIO library.

    Finally, do look over the list() method in Ryan's code, that provides simple
    menu selection. Neat stuff.

    My thanks to Ryan, Mark, Sean, and Dave for jumping right in and working yet
    another wacky idea of mine.

    Tomorrow Gavin Kistner is back with a second submitted quiz topic! (That makes
    all of you who haven't submitted even one yet look really bad.) It's a fun
    topic too, guaranteed to be a Barrel of Monkeys...
     
    Ruby Quiz, Apr 28, 2005
    #1
    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. Ruby Quiz

    [QUIZ] HighLine (#29)

    Ruby Quiz, Apr 22, 2005, in forum: Ruby
    Replies:
    8
    Views:
    157
    James Edward Gray II
    Apr 22, 2005
  2. Ryan Leavengood

    [SOLUTION] HighLine (#29)

    Ryan Leavengood, Apr 24, 2005, in forum: Ruby
    Replies:
    6
    Views:
    137
    James Edward Gray II
    Apr 29, 2005
  3. mark sparshatt

    [SOLUTION] Highline (#29)

    mark sparshatt, Apr 24, 2005, in forum: Ruby
    Replies:
    0
    Views:
    140
    mark sparshatt
    Apr 24, 2005
  4. James Edward Gray II

    [ANN] HighLine 0.2.0

    James Edward Gray II, Apr 29, 2005, in forum: Ruby
    Replies:
    2
    Views:
    113
    James Edward Gray II
    Apr 29, 2005
  5. James Edward Gray II

    [ANN] HighLine 0.3.0 -- Now with ANSI colors!

    James Edward Gray II, May 4, 2005, in forum: Ruby
    Replies:
    0
    Views:
    114
    James Edward Gray II
    May 4, 2005
Loading...

Share This Page