[SOLUTION] HighLine (#29)

Discussion in 'Ruby' started by Ryan Leavengood, Apr 24, 2005.

  1. Here is my solution for this quiz. Nothing too fancy really, but it does
    the job. I suspect if I used this more for various applications I'd find
    more clever things to do. But I did replace some command-line
    input/output in another program of mine with this library, and it
    definitely cleaned things up. It certainly makes sense to put this kind
    of stuff in a library instead of always rolling your own.

    I never did look at the EasyPrompt code, but I suspect this code might
    look very similar because of my use of a MockIO object (I did notice
    that much from the EasyPrompt summary.) But I suppose it is good that
    the unit tests and MockIO object are more lines of code than the library :)

    I would appreciate feedback on the API design as well as the unit test
    design. I feel the unit tests are too coupled to the implementation
    (particularly on my checking of the output.)

    Ryan Leavengood

    CODE (beware of wrapping):
    #--------------------------------------
    # HighLine command-line input library
    #
    # Copyright (C) 2005 Ryan Leavengood
    #
    # Released under the Ruby license
    #--------------------------------------

    class String
    def pad_if_needed
    self[-1].chr != ' ' ? self + ' ' : self
    end
    end

    class HighLine
    attr_accessor :io_out, :io_in
    def initialize(io_out=$stdout, io_in=$stdin)
    @io_out, @io_in = io_out, io_in
    end

    def ask(question, default=nil)
    q = question.pad_if_needed
    q += "[#{default}] " if default
    answer = validation_loop(q) do |input|
    input.size > 0 or default
    end
    answer.size > 0 ? answer : default
    end

    def ask_if?(question)
    answer = validation_loop(question.pad_if_needed+'(y,n) ') do |input|
    %w(y n yes no).include?(input.downcase)
    end
    answer.downcase[0,1] == 'y'
    end

    def ask_int(question, range=nil)
    validation_loop(question) do |input|
    input =~ /\A\s*-?\d+\s*\Z/ and (not range or
    range.member?(input.to_i))
    end.to_i
    end

    def ask_float(question, range=nil)
    validation_loop(question) do |input|
    input =~ /\A\s*-?\d+(.\d*)?\s*\Z/ and (not range or
    range.member?(input.to_f))
    end.to_f
    end

    def header(title)
    dashes = '-'*(title.length+4)
    io_out.puts(dashes)
    io_out.puts("| #{title} |")
    io_out.puts(dashes)
    end

    def list(items, prompt=nil)
    items.each_with_index do |item, i|
    @io_out.puts "#{i+1}. #{item}"
    end
    valid_range = '1'..items.length.to_s
    prompt = "Please make a selection: " unless prompt
    answer = validation_loop(prompt) do |input|
    valid_range.member?(input)
    end
    # Though the list is shown using a 1-indexed list, return 0-indexed
    return answer.to_i-1
    end

    def validation_loop(prompt)
    loop do
    @io_out.print prompt.pad_if_needed
    answer = @io_in.gets
    if answer
    answer.chomp!
    if yield answer
    return answer
    end
    end
    end
    end
    end

    # Unit Tests
    if $0 == __FILE__
    class MockIO
    attr_accessor :eek:utput, :input

    def initialize
    reset
    end

    def reset
    @index = 0
    @input=nil
    @output=''
    end

    def print(*a)
    @output << a.join('')
    end

    def puts(*a)
    if a.size > 1
    @output << a.join("\n")
    else
    @output << a[0] << "\n"
    end
    end

    def gets
    if @input.kind_of?(Array)
    @index += 1
    @input[@index-1]
    else
    @input
    end
    end
    end

    require 'test/unit'

    class TC_HighLine < Test::Unit::TestCase
    def initialize(name)
    super(name)
    @mock_io = MockIO.new
    @highline = HighLine.new(@mock_io, @mock_io)
    end

    def setup
    @mock_io.reset
    end

    def test_ask
    question = 'Am I the coolest?'
    @mock_io.input = [nil, '', "\n", "yes\n"]
    assert_equal(@mock_io.input[-1].chomp, @highline.ask(question))
    assert_equal((question+' ')*4, @mock_io.output)
    end

    def test_ask_default
    question = 'Where are you from? '
    default = 'Florida'
    @mock_io.input = [nil, "\n"]
    assert_equal(default, @highline.ask(question, default))
    assert_equal((question+"[#{default}] ")*2, @mock_io.output)
    end

    def test_ask_if
    question = 'Is Ruby the best programming language? '
    @mock_io.input = [nil, "0\n", "blah\n", "YES\n"]
    assert_equal(true, @highline.ask_if?(question))
    assert_equal((question+'(y,n) ')*4, @mock_io.output)
    end

    def test_ask_int
    question = 'Give me a number:'
    @mock_io.input = [nil, '', "\n", ' ', "blah\n", " -4 \n"]
    assert_equal(-4, @highline.ask_int(question))
    assert_equal((question+' ')*6, @mock_io.output)
    @mock_io.reset
    @mock_io.input = [nil, '', "\n", ' ', "blah\n", "3604\n"]
    assert_equal(3604, @highline.ask_int(question))
    assert_equal((question+' ')*6, @mock_io.output)
    end

    def test_ask_int_range
    question = 'How old are you?'
    @mock_io.input = [nil, '', "\n", ' ', "blah\n", "106\n", "28\n"]
    assert_equal(28, @highline.ask_int(question, 0..105))
    assert_equal((question+' ')*7, @mock_io.output)
    end

    def test_ask_float
    question = 'Give me a floating point number:'
    @mock_io.input = [nil, '', "\n", ' ', "blah\n", " -4.3 \n"]
    assert_equal(-4.3, @highline.ask_float(question))
    assert_equal((question+' ')*6, @mock_io.output)
    @mock_io.reset
    @mock_io.input = [nil, '', "\n", ' ', "blah\n", "560\n"]
    assert_equal(560.0, @highline.ask_float(question))
    assert_equal((question+' ')*6, @mock_io.output)
    end

    def test_ask_float_range
    question = 'Give me a floating point number between 5.0 and 13.5:'
    @mock_io.input = [nil, '', "\n", ' ', "blah\n", " -4.3 \n",
    "4.9\n", "13.6\n", "7.55\n"]
    assert_equal(7.55, @highline.ask_float(question, 5.0..13.5))
    assert_equal((question+' ')*9, @mock_io.output)
    end

    def test_header
    title = 'HighLine Manual'
    @highline.header(title)
    output = "-------------------\n| HighLine Manual
    |\n-------------------\n"
    assert_equal(output, @mock_io.output)
    end

    def test_list
    items = ['Ruby','Python','Perl']
    prompt = 'Please choose your favorite programming language: '
    @mock_io.input = [nil, "0\n", "blah\n", "4\n", "1\n"]
    assert_equal(0, @highline.list(items, prompt))
    assert_equal("1. Ruby\n2. Python\n3. Perl\n#{prompt * 5}",
    @mock_io.output)
    end
    end
    end
     
    Ryan Leavengood, Apr 24, 2005
    #1
    1. Advertising

  2. Here's my solution.

    I ran out of time, mainly for documentation, but I think I've got
    something worth showing here. I'm using unit tests, so you can read
    those (primarily "tc_highline.rb") to see how my module works.

    It covers the basics in the quiz and is pretty open for new additions.
    The killer feature is the type you specify in to ask(). It's really
    powerful in that it can take a Proc that does the conversion to
    whatever you like. agree(), my version of ask_if() from the quiz, is
    implemented in these terms:

    def agree( yes_or_no_question )
    ask( yes_or_no_question,
    lambda { |a| a =~ /\AY(?:es)?\Z/i ? true : false } )
    end

    Here are some other examples from my test cases:

    def test_membership
    @input << "112\n-541\n28\n"
    @input.rewind

    answer = @terminal.ask("Tell me your age.", Integer) do |q|
    q.member = 0..105
    end
    assert_equal(28, answer)
    end

    def test_reask
    number = 61676
    @input << "Junk!\n" << number << "\n"
    @input.rewind

    answer = @terminal.ask("Favorite number? ", Integer)
    assert_kind_of(Integer, number)
    assert_instance_of(Fixnum, number)
    assert_equal(number, answer)
    assert_equal( "Favorite number? " +
    "You must enter a valid Integer.\n" +
    "? ", @output.string )

    # ...
    end

    def test_type_conversion
    # ...

    @input.truncate(@input.rewind)
    number = 10.5002
    @input << number << "\n"
    @input.rewind

    answer = @terminal.ask( "Favorite number? ",
    lambda { |n| n.to_f.abs.round } )
    assert_kind_of(Integer, answer)
    assert_instance_of(Fixnum, answer)
    assert_equal(11, answer)

    # ...

    @input.truncate(@input.rewind)
    @input << "6/16/76\n"
    @input.rewind

    answer = @terminal.ask("Enter your birthday.", Date)
    assert_instance_of(Date, answer)
    assert_equal(16, answer.day)
    assert_equal(6, answer.month)
    assert_equal(76, answer.year)

    # ...

    @input.truncate(@input.rewind)
    @input << "gen\n"
    @input.rewind

    answer = @terminal.ask("Select a mode: ", [:generate, :run])
    assert_instance_of(Symbol, answer)
    assert_equal:)generate, answer)
    end

    def test_validation
    @input << "system 'rm -rf /'\n105\n0b101_001\n"
    @input.rewind

    answer = @terminal.ask("Enter a binary number: ") do |q|
    q.validate = /\A(?:0b)?[01_]+\Z/
    end
    assert_equal("0b101_001", answer)
    end

    I had a lot of fun working on this library and may just keep working on
    it to see if I can't turn it into something genuinely useful.

    You can load my library two different ways:

    # This first way loads the class system, useful if you want to manage
    many HighLine
    # objects, say for socket work.
    require "highline"
    # Or you can take the easy out an import methods to the top level.
    require "highline/import"

    You can find my code here:

    http://rubyquiz.com/highline.zip

    Enjoy.

    James Edward Gray II
     
    James Edward Gray II, Apr 27, 2005
    #2
    1. Advertising

  3. Re: [SOLUTION] HighLine (#29) [LONG]

    # This solution provides a framework for handling user input at a higher level
    # than "gets" and "chomp". Sorry to James for being so late...
    #
    # Usage:
    #
    # Define a class which inherits from HighLine::ValueInput,
    # HighLine::ChoiceInput or HighLine::MenuInput, using the class methods
    # (see below) to define the way the input will be handled. Then call the
    # #ask class method to prompt for the input, passing additional definitions
    # in an optional block (see examples at end of file).
    #
    #
    # Class Methods (all classes):
    #
    # transform <Proc object p> [, *args]
    # Calls r = p.call(r, *args), where r is the user's response
    # transform symbol [, *args]
    # Calls r = r.method(symbol).call(*args)
    # transform :with_my, symbol [, *args]
    # Calls r = self.method(symbol).call(r, *args), where self is an instance
    # of the class
    #
    # Transformations are applied to the input in the order in which they were
    # defined, and they are inherited from parent classes cumulatively (is that
    # a word?)
    #
    # synonym <base string>, <synonym string> [, *<synonym strings>]
    # Creates a transformation which maps each synonym string to the
    # base string.
    #
    # okay_if <Proc object p> [, *args]
    # Uses the return value of p.call(r, *args) to determine if the validation
    # phase can be skipped. (r is the user's response after transformation)
    # okay_if <Regexp re>
    # As above, with the result of r =~ re
    # okay_if symbol [, *args]
    # As above, with the result of r.method(symbol).call(*args)
    # okay_if :with_my, symbol [, *args]
    # As above, with the result of self.method(symbol).call(r, *args)
    #
    # Definitions for okay values are inherited.
    #
    # error_message string
    # Causes the string to be printed if the validation phase fails
    # error_message symbol
    # As above, with the return value from self.method(symbol).call(r)
    # error_message <Proc object p>
    # As above, with the return value from p.call(r)
    #
    # A class will only use one error_message definition. See the section on
    # validation below for more details about the difference in error handling
    # between ValueInput and ChoiceInput/MenuInput.
    #
    #
    # Class Methods (ValueInput):
    #
    # format_hint string
    # A string which is appended to the prompt. The default
    # ValueInput#prompt_suffix method wraps it in square brackets, e.g.
    # "[YYYY-MM-DD]".
    #
    # Format hints are inherited, but not cumulatively--if a class provides one,
    # it will override any hints in its parent classes.
    #
    # validate
    #
    # The syntax for the validate method is the same as that for okay_if.
    # However, procs and methods that it causes to be called have different
    # return signatures:
    # bool -> indicates validity
    # bool, str_or_nil -> as above, plus an error message that overrides
    # any error_message definitions in the class
    # bool, str_or_nil, val -> as above, plus an alternate version of the
    # response string
    #
    # When an alternate value is returned, subsequent validations that would
    # have operated on the response string will operate on this alternate value
    # instead (see the description of the #ask method below for details on using
    # this value).
    #
    # Validators are cumulatively inherited, along with the error_message
    # defintions (so if a validator from a parent class fails, the error_message
    # from that class will be used).
    #
    # output_format
    #
    # The syntax for output_format is the same as that for transform. It provides
    # a way to format the response after it has been validated. Like validators,
    # output format procedures will operate on the response or on the alternate
    # value if one is present.
    #
    #
    # Class Methods (ChoiceInput, MenuInput):
    #
    # choices *args
    # Adds its arguments to the list of valid string responses for the class.
    # The default ChoiceInput#prompt_suffix method displays it like this--
    # "[y/(n)/m]". The parentheses indicate the default answer (see #ask below).
    #
    # Instead of using validators, ChoiceInput classes simply check for a
    # response that is in the list of choices.
    #
    # header string
    # (MenuInput only) A string that is printed before the list of choices is
    # displayed.
    #
    # items *args
    # (MenuInput only) The arguments are matched to the choices.
    #
    # When one of the choices is selected, the matching element in the list of
    # items is returned as the alternate value. The default
    # MenuInput#prompt_suffix displays the header string, and one line for each
    # choice in the format "#{choice}\t#{item}\n".
    #
    # The #ask method
    #
    # ask(prompt, default_value=nil)
    #
    # When the ask method is called, things get done in this order:
    # 1. The prompt is printed, with the result of #prompt_suffix appended.
    # 2. $stdin_gets is called, and the raw input is saved.
    # 3. All defined transformations are applied to the response.
    # 4. If any okay_if tests pass, the response is returned.
    # 5. If the response is empty, and there is a default_value, the default is
    # returned.
    # 6. Validation occurs. For ChoiceInput/MenuInput, this just involves
    # matching the response to the list of choices. For ValueInput, all
    # defined validation tests are run.
    # 7. If validation succeeded, all output_format rules are applied.
    # 8. If validation failed, and an error message was returned, it is printed,
    # and the process loops back to step 1.
    #
    # The return value is an object of class ResponseString, which is a subclass
    # of normal String. It provides a #raw_input method, an #error_message method,
    # a #valid? method, and and #alternate method, for getting details about the
    # response.

    module HighLine

    class BaseInput

    def self.get_error_message
    @em ||= nil
    end

    def self.error_message(em)
    @em = em
    end

    def self.get_transformers
    @ts ||= []
    end

    def self.transform(*ts)
    @ts ||= []
    @ts << ts
    end

    def self.get_okays
    @oi ||= []
    end

    def self.okay_if(*oi)
    @oi ||= []
    @oi << oi
    end

    def self.synonym(*syns)
    default = syns[0]
    synonyms = syns[1..-1]
    transform :with_my, :synonymize, synonyms, default
    end

    def self.ask(prompt, default_response=nil, &block)
    prompt = prompt + " " if prompt !~ /\s$/ and !prompt.empty?
    if block_given?
    klass = Class.new(self, &block)
    else
    klass = self
    end
    inputter = klass.new(default_response)
    while true
    print prompt
    $stdout.flush
    response = inputter.gets
    break if response.valid?
    if response.error_message
    puts response.error_message
    $stdout.flush
    end
    end
    return response
    end

    def initialize(default_response)
    @default_response = default_response
    @klasses = []
    klass = self.class
    while klass.respond_to? :get_transformers
    @klasses.unshift(klass)
    klass = klass.superclass
    end
    @okays = @klasses.collect { |klass|
    curry_okays(klass.get_okays)
    }.flatten
    @transformers = @klasses.collect { |klass|
    curry_transformers(klass.get_transformers)
    }.flatten
    end

    def gets
    print prompt_suffix
    $stdout.flush
    raw_input = $stdin.gets.chomp
    response = raw_input.dup
    @transformers.each do |transformer|
    response = transformer.call(response)
    end
    if @default_response and response.empty?
    return ResponseString.new(@default_response, raw_input, true)
    end

    @okays.each do |okay|
    if okay.call(response)
    return ResponseString.new(response, raw_input, true)
    end
    end

    do_validate(response, raw_input)
    end

    def prompt_suffix
    if @default_response and !@default_response.empty?
    "(#{@default_response}) "
    else
    ""
    end
    end

    def synonymize(r, synonyms, default)
    return r unless synonyms.member? r
    default
    end

    def curry_error_message(klass_error_message)
    case klass_error_message
    when Symbol
    proc { |r| method(klass_error_message).call(r) }
    when Proc
    proc { |r| klass_error_message.call(r) }
    when String
    proc { |r| klass_error_message }
    end
    end

    def curry_transformers(klass_transformers)
    klass_transformers.collect do |transformer|
    p_name = transformer[0]
    args = transformer[1..-1]
    case p_name
    when Symbol
    if p_name == :with_my
    proc { |r| method(args[0]).call(r, *args[1..-1]) }
    else
    proc { |r| r.method(p_name).call(*args) }
    end
    when Proc
    proc { |r| p_name.call(r, *args) }
    end
    end
    end

    def curry_okays(klass_okays)
    klass_okays.collect { |okay|
    p_name = okay[0]
    args = okay[1..-1]
    case p_name
    when Symbol
    if p_name == :with_my
    proc { |r| method(args[0]).call(r, *args[1..-1]) }
    else
    proc { |r| r.method(p_name).call(*args) }
    end
    when Regexp
    proc { |r| r.to_s =~ p_name }
    when Proc
    proc { |r| p_name.call(r, *args) }
    end
    }.compact
    end

    def do_validate(response, raw_input)
    ResponseString.new(response, raw_input, true)
    end
    end

    class ValueInput < BaseInput

    def self.get_validators
    @vld ||= []
    end

    def self.validate(*vld)
    @vld ||= []
    @vld << vld
    end

    def self.get_output_formats
    @of ||= []
    end

    def self.output_format(*of)
    @of ||= []
    @of << of
    end

    def self.get_format_hint
    @fh ||= nil
    end

    def self.format_hint(fh)
    @fh = fh
    end

    def initialize(default_response)
    super
    klasses = @klasses[1..-1]
    @validators = klasses.collect { |klass|
    curry_okays(klass.get_validators)
    }
    @error_messages = klasses.collect do |klass|
    curry_error_message(klass.get_error_message)
    end
    @output_formats = klasses.collect { |klass|
    curry_transformers(klass.get_output_formats)
    }.flatten
    klasses.reverse.each do |klass|
    @format_hint = klass.get_format_hint
    break if @format_hint
    end
    end

    def prompt_suffix
    super << (@format_hint ? "[#{@format_hint}] " : "")
    end

    def do_validate(response, raw_input)
    valid = true
    err_msg = nil
    @alternate = nil
    resp = response
    @validators.each_index do |i|
    @validators.each do |validator|
    error_message = nil
    valid, err_msg, alt = validator.call(resp)
    if !valid and !err_msg
    while !@error_messages
    i = i + 1
    break if i == @error_messages.length
    end
    klass_error_message = @error_messages
    err_msg = klass_error_message.call(resp) if klass_error_message
    end
    if alt
    resp = alt
    @alternate = alt
    end
    break unless valid
    end
    break unless valid
    end
    resp = response if @output_formats.empty?
    @output_formats.each do |output_format|
    resp = output_format.call(resp)
    end
    ResponseString.new(resp, raw_input, valid, err_msg, @alternate)
    end
    end

    class ChoiceInput < BaseInput
    error_message :default_error_message

    def self.get_choices
    @cs ||= []
    end

    def self.choices(*cs)
    if !get_choices.empty?
    raise SyntaxError, "cannot add multiple choice sets", caller
    end
    @cs = cs
    end

    def initialize(default_response)
    super
    @choices = self.class.get_choices
    @klasses.reverse.each do |klass|
    @error_message = curry_error_message(klass.get_error_message)
    break if @error_message
    end
    end

    def wrap_default(choice)
    choice == @default_response ? "(#{choice})" : choice
    end

    def prompt_suffix
    "[" + @choices.collect { |ch| wrap_default(ch) }.join('/') + "] "
    end

    def default_error_message(response)
    "Please enter one of #{@choices[0..-2].join(', ')} or #{@choices[-1]}"
    end

    def do_validate(response, raw_input)
    error_message = nil
    valid = @choices.member? response
    if !valid
    error_message = @error_message.call(response)
    end
    ResponseString.new(response, raw_input, valid, error_message)
    end
    end

    class MenuInput < ChoiceInput

    def self.get_items
    @its ||= []
    end

    def self.items(*its)
    @its ||= []
    if get_choices.empty?
    raise SyntaxError, "choices must be added before items", caller
    end
    if its.length != @cs.length
    raise SyntaxError, "number of items must match choices", caller
    end
    @its = its
    end

    def self.get_header
    @hdr ||= nil
    end

    def self.header(hdr)
    @hdr = hdr
    end

    def initialize(default_response)
    super
    klasses = @klasses[2..-1]
    @items = self.class.get_items
    klasses.reverse.each do |klass|
    @header = klass.get_header
    break if @header
    end
    end

    def prompt_suffix
    if @header
    ps = "#{@header}\n"
    else
    ps = "\n"
    end
    0.upto(@choices.length - 1) do |i|
    ps << " " if @choices != @default_response
    ps << wrap_default(@choices) + "\t" + @items + "\n"
    end
    return ps
    end

    def default_error_message(response)
    "Please select one of the given options"
    end

    def do_validate(response, raw_input)
    rs = super
    rs.alternate = @items[@choices.index(response)] if rs.valid?
    rs
    end
    end

    class ResponseString < String
    attr_accessor :alternate
    attr_reader :error_message, :raw_input

    def initialize(resp, raw, valid, err_msg=nil, alt=nil)
    @raw_input = raw
    @valid = valid
    @error_message = err_msg
    @alternate = alt || resp
    super(resp)
    end

    def valid?
    @valid
    end
    end

    end

    if __FILE__ == $0

    require 'date'

    class IntegerInput < HighLine::ValueInput
    validate /^\d+$/
    validate proc { |r| [true, nil, r.to_i] }
    end

    puts IntegerInput.ask("Enter a number from 1 to 10, or Q to quit:") {
    okay_if /^q$/i
    validate :between?, 1, 10
    }

    class DateInput < HighLine::ValueInput
    validate :with_my, :check_date

    def check_date(r)
    begin
    test_date = Date.parse(r)
    rescue
    false
    else
    [true, nil, test_date]
    end
    end
    end

    puts DateInput.ask("Enter a date:") {
    output_format :to_s
    error_message "That is not a date!"
    }

    class YesOrNo < HighLine::ChoiceInput
    transform :downcase
    choices "y", "n"
    synonym "y", "yes", "oui", "si"
    end

    puts YesOrNo.ask("Is your computer turned on?", "y")

    class EditorMenu < HighLine::MenuInput
    header "Please select an editor:"
    choices "1", "2", "3"
    items "vim", "vim", "vim"
    error_message "There are no other editors!"
    end

    puts EditorMenu.ask("", "1").alternate

    end
     
    Sean E. McCardell, Apr 28, 2005
    #3
  4. Re: [SOLUTION] HighLine (#29) [LONG]

    On Apr 27, 2005, at 6:33 PM, Sean E. McCardell wrote:

    > # This solution provides a framework for handling user input at a
    > higher level
    > # than "gets" and "chomp".


    Wow. I'm looking through this a bit to see what you've done here.
    Very impressive.

    I feel pretty dumb for registering my solution with RubyForge today
    now. :D

    Any chance you could give us a few simple examples of usage? For
    example, how do the quiz examples translate to this system?

    > Sorry to James for being so late...


    I'm the one who should apologize. I finished up the summary earlier
    today. ;)

    James Edward Gray II
     
    James Edward Gray II, Apr 28, 2005
    #4
  5. Ryan Leavengood

    Dave Burt Guest

    Here's my solution:

    http://www.dave.burt.id.au/ruby/highline.rb

    It's inspired a fair bit by OptParse, and I tried to make it very flexible
    and smart in how it accepts options. The code features a little
    meta-programming (so you can do "retries 1" or "validation 1..10" in a block
    passed to the Prompt.new), optional named arguments, and inference of
    arguments' meaning by class somewhat like OptParse#on does.

    It's not as easily mockable as EasyPrompt - you would have to do something
    tricky like this:
    class Prompt
    def print(*args)
    my_alternate_output_stream.print *args
    end
    def gets
    my_alternate_input_stream.gets
    end
    end

    I think the code you write to use it is cleaner and more straighforward than
    the examples from the quiz question and the EasyPrompt doco. Here are those
    examples and more:

    # Example usage from the Quiz
    age = ask("What is your age?", Integer, 0..105 )
    num = ask('Enter a binary number.') {|s| not s =~ /[^01]/ }.to_i(2)
    if ask("Would you like to continue?", TrueClass) # ...

    # Example usage from EasyPrompt documentation

    irb(main):003:0> fname = prompt.ask( "What's your first name?" )
    What's your first name? John
    => "John"
    irb(main):004:0> lname = ask("What's your last name?", "Doe")
    What's your last name? [Doe]
    => "Doe"
    irb(main):005:0> ok = ask("Is your name #{ fname } #{ lname }?", TrueClass)
    Is your name John Doe? [Yn]
    => true

    # Extra examples
    s = ask
    i = ask("How many strikes will be allowed?", 3, 0..(1.0/0.0))
    s = ask("Give me a word with "foo" in it:", /foo/)
    i = ask("Give me a number divisible by 3:", Integer) {|i| i % 3 == 0 }
    a = ask("Give me three or more numbers:", Array) do |x|
    x.each {|n| Float(n) }
    x.size >= 3 or puts "I need more things than that!"
    end.map {|n| n.to_f }

    # And you can also get a reusable Prompt object:
    p = Prompt.new("A word with foo in it?") do
    validation /foo/i
    default "Foo"
    end
    p = Prompt.new("A word with foo in it?", "Foo", /foo/i)
    s = p.ask
     
    Dave Burt, Apr 28, 2005
    #5
  6. Re: [SOLUTION] HighLine (#29) [LONG]

    On 11:52 Thu 28 Apr , James Edward Gray II wrote:
    > Any chance you could give us a few simple examples of usage? For
    > example, how do the quiz examples translate to this system?


    Sure thing. Here goes:

    require 'highline'

    # This might be useful for someone implementing COMMAND.COM in Ruby :)

    class DiskError < HighLine::ChoiceInput
    choices "abort", "retry", "fail"
    synonym "abort", "a"
    synonym "retry", "r"
    synonym "fail", "f"
    end

    result = DiskError.ask("Error reading drive A:")

    # And the output will look like:
    # Error reading drive A: [abort/retry/fail]
    # The user will continue to be prompted until "abort", "retry", or
    # "fail" is entered (or one of their synonyms, "a", "r", or "f"

    # For the age example from the quiz, I would do:

    class IntegerInput < HighLine::ValueInput
    validate /^\d+$/
    # when a validation procedure returns a three-element array,
    # the second element can be an error message, and the third
    # element will be used as an alternate test value (instead of the
    # user's response string) for subsequent validation tests.
    validate proc { |r| [true, nil, r.to_i] }
    end

    age = IntegerInput.ask("Enter your age:") {
    validate :between?, 0, 105
    }.alternate

    # The #alternate method of the returned object gives you access to
    # the alternate test value, if any, created during validation. In this
    # case, it is an Integer

    # And for an indirect way of finding an age, here's one that demonstrates
    # using an instance method for validation:

    require 'date'
    class DateInput < HighLine::ValueInput
    validate :with_my, :ensure_date

    def ensure_date(response)
    begin
    test_date = Date.parse(response)
    rescue ArgumentError
    false
    else
    [true, nil, test_date]
    end
    end
    end

    birthday = DateInput.ask("When were you born?") {
    # output_format, like validate, operates on an alternate test value
    # if one was created during validation. This just calls #to_s on that
    # value, so you always get anwers in the form "YYYY-MM-DD", even if
    # you enter something like "April 28th, 2005"
    output_format :to_s
    error_message "Please enter a valid date"
    validate proc { |r| [r <= Date.today, "You can't be from the future!"] }
    }

    Hope this helps,

    --Sean
     
    Sean E. McCardell, Apr 28, 2005
    #6
  7. Re: [SOLUTION] HighLine (#29) [LONG]

    On Apr 28, 2005, at 5:01 PM, Sean E. McCardell wrote:

    > On 11:52 Thu 28 Apr , James Edward Gray II wrote:
    >> Any chance you could give us a few simple examples of usage? For
    >> example, how do the quiz examples translate to this system?

    >
    > Sure thing. Here goes:


    [snip great examples]

    > Hope this helps,


    It was impressive. Thanks for sharing!

    James Edward Gray II
     
    James Edward Gray II, Apr 29, 2005
    #7
    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:
    156
    James Edward Gray II
    Apr 22, 2005
  2. mark sparshatt

    [SOLUTION] Highline (#29)

    mark sparshatt, Apr 24, 2005, in forum: Ruby
    Replies:
    0
    Views:
    140
    mark sparshatt
    Apr 24, 2005
  3. Ruby Quiz

    [SUMMARY] HighLine (#29)

    Ruby Quiz, Apr 28, 2005, in forum: Ruby
    Replies:
    0
    Views:
    166
    Ruby Quiz
    Apr 28, 2005
  4. James Edward Gray II

    [ANN] HighLine 0.2.0

    James Edward Gray II, Apr 29, 2005, in forum: Ruby
    Replies:
    2
    Views:
    112
    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