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 Quiz by submitting ideas as often as you can:
3. Enjoy!
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
When you stop and think about it, methods like gets(), while handy, are still
pretty low level. In running Ruby Quiz I'm always seeing solutions with helper
methods similar to this:
# by Markus Koenig
def ask(prompt)
loop do
print prompt, ' '
$stdout.flush
s = gets
exit if s == nil
s.chomp!
if s == 'y' or s == 'yes'
return true
elsif s == 'n' or s == 'no'
return false
else
$stderr.puts "Please answer yes or no."
end
end
end
Surely we can make something like that better! We don't always need Rails or a
GUI framework and there's no reason writing a command-line application can't be
equally smooth.
This week's Ruby Quiz is to start a module called HighLine (for high-level,
line-oriented interface). Ideally this module would eventually cover many
aspects of terminal interaction, but for this quiz we'll just focus on getting
input.
What I really think we need here is to take a page out of the optparse book.
Here are some general ideas:
age = ask("What is your age?", Integer, :within => 0..105)
num = eval "0b#{ ask( 'Enter a binary number.',
String, :validate => /^[01_]+$/ ) }"
if ask_if("Would you like to continue?") # ...
None of these ideas are etched in stone. Feel free to call your input method
prompt() or use a set of classes. Rework the interface any way you like. Just
be sure to tell us how to use your system.
The goal is to provide an easy-to-use, yet robust method of requesting input.
It should free the programmer of common concerns like calls to chomp() and
ensuring valid input.
Not that this necessarily mitigates the educational value of this Quiz,
but: EasyPrompt sort of does what you're saying below.
http://easyprompt.rubyforge.org/
Example:
irb(main):001:0> require 'easyprompt'
=> true
irb(main):002:0> prompt = EasyPrompt.new
=> #<EasyPrompt:0x5a42a0
@stdout=#<EasyPrompt::MockableStdout:0x5a3e04>>
irb(main):003:0> fname = prompt.ask( "What's your first name?" )
What's your first name? John
=> "John"
irb(main):004:0> lname = prompt.ask( "What's your last name?", "Doe" )
What's your last name? [Doe]
=> "Doe"
irb(main):005:0> correct = prompt.ask( "Is your name #{ fname } #{
lname }?", true, :boolean )
Is your name John Doe? [y]
=> true
It's mockable, too! Everything must be mockable.
Francis Hwang
http://fhwang.net/
> Hi,
>
> Not that this necessarily mitigates the educational value of this
> Quiz, but: EasyPrompt sort of does what you're saying below.
>
> http://easyprompt.rubyforge.org/
Thanks for the link. I wasn't aware of this project. I like it.
James Edward Gray II
--
Bill Atkins
> > Just
> > be sure to tell us how to use your system.
> >
> > The goal is to provide an easy-to-use, yet robust method of requesting
> > input.
> > It should free the programmer of common concerns like calls to chomp()
> > and
> > ensuring valid input.
> >
> >
>
>
> --
> Bill Atkins
--
Bill Atkins
> Is using a class instead of a module allowed?
Definitely! Rework the interface any way you like.
James Edward Gray II
> What I really think we need here is to take a page out of the optparse book.
> Here are some general ideas:
>
> age = ask("What is your age?", Integer, :within => 0..105)
> num = eval "0b#{ ask( 'Enter a binary number.',
> String, :validate => /^[01_]+$/ ) }"
Do not ever do that.
irb(main):004:0> Integer("0b1011")
=> 11
irb(main):005:0> "1011".to_i(2)
=> 11
irb(main):006:0> "foo\nbar" =~ /^foo$/
=> 0
--
Christian Neukirchen <chneuk...@gmail.com> http://chneukirchen.org
> Ruby Quiz <ja...@grayproductions.net> writes:
>
>> What I really think we need here is to take a page out of the
>> optparse book.
>> Here are some general ideas:
>>
>> age = ask("What is your age?", Integer, :within => 0..105)
>> num = eval "0b#{ ask( 'Enter a binary number.',
>> String, :validate => /^[01_]+$/ ) }"
>
> Do not ever do that.
>
> irb(main):004:0> Integer("0b1011")
> => 11
> irb(main):005:0> "1011".to_i(2)
> => 11
>
> irb(main):006:0> "foo\nbar" =~ /^foo$/
> => 0
I'm not exactly sure what you're trying to show here. Even 0 would be
a fine binary number. However, the point was that I have created a
safe eval() because ask() should not return anything that doesn't
validate.
Obviously, using Integer() is better style though.
James Edward Gray II
I think he meant along the lines of:
irb --simple-prompt
>> p "looks ok!" if "01010\nsystem('rm -rf /')" =~ /^[01_]+$/
"looks ok!"
Regards,
Bill
> I think he meant along the lines of:
>
> irb --simple-prompt
>>> p "looks ok!" if "01010\nsystem('rm -rf /')" =~ /^[01_]+$/
> "looks ok!"
Oops, my Perl habits are showing. My bad. The Regexp should be
/\A[01_]+\Z/. I'll correct it on the Ruby Quiz site.
Thanks.
James Edward Gray II
Given the existence of this library, do we still want to proceed with this
Quiz? :)
I suppose it will be worth it simply because we will see various
implementations and APIs for this concept, even if we are duplicating
EasyPrompt.
But sometimes I think programmers have too much of a penchant for
continually re-inventing the wheel...
Ryan Leavengood
I think it's a good one for any newbies out there to have a shot at.
Reinventing wheels is a good thing to do for the purpose of learning to
program.
Maybe others of us might look at how EasyPrompt might be extended.
Cheers,
Dave
> James Edward Gray II wrote:
>> On Apr 22, 2005, at 9:24 AM, Francis Hwang wrote:
>>
>>> Hi,
>>>
>>> Not that this necessarily mitigates the educational value of this
>>> Quiz, but: EasyPrompt sort of does what you're saying below.
>>>
>>> http://easyprompt.rubyforge.org/
>>
>>
>> Thanks for the link. I wasn't aware of this project. I like it.
>
> Given the existence of this library, do we still want to proceed with
> this
> Quiz? :)
Heck yeah! Take a glance back at what I didn't know when I ran the
Quoted Printable quiz and it turned out just fine. If we used what I
don't know as a barrier, there wouldn't be any quizzes! ;)
> I suppose it will be worth it simply because we will see various
> implementations and APIs for this concept, even if we are duplicating
> EasyPrompt.
I wouldn't declare EasyPrompt robust. Did you glance at the API? It's
well... Easy. :) I'm confident I can add features to it in under an
hour. It's not even capable of some of the ideas I'm tossing around in
the quiz.
What it did do for me, was give me more ideas. Have yourself a look,
then think bigger... :)
> But sometimes I think programmers have too much of a penchant for
> continually re-inventing the wheel...
We all know that's a good coding habit, but I really don't think it
applies much to Ruby Quiz.
To me, Ruby Quiz is about writing regular code to keep our skills in
shape. The goal is to find interesting exercises to support that. I
use a lot of methods to find them. Sometimes, I dream them up (like
this week). Some are suggested.
Other times I adapt challenges I've seen elsewhere. Obviously, these
have been worked before, some even in Ruby. No big deal. If I ruled
out all those ideas, we would miss out on some great little puzzles
that have taught me a lot. That's how I know they're good and worth
using. They're proven. By contrast, some of my ideas flop and nobody
works them.
Ruby Quiz isn't a professional software coding assignment. (You
certainly won't be paid for it!) It's gaming for super geeks. Use it
to sharpen your reflexes. Give yourself little challenges going in:
"This will be the sexiest solution to prime numbers ever written; I'll
solve this sucker in five lines; this weather program will scrape and
digitally modify Google's satellite photos to add rain and snow above
your own house, because I can do that and it would be awesome; etc."
Then when the big job comes along, you'll have all those experiences to
draw on, putting you that much ahead in the big game. (I've actually
adapted quiz solutions to job problems!)
"Of course, that's just my opinion. I could be wrong." --Dennis Miller
James Edward Gray II
Yeah I agree this quiz has potential for both newbies and more experienced
Rubyists. I will certainly try my hand in it. But I have a rule about not
reading code or APIs for other projects similar to something I'm solving,
so as not to taint my own problem-solving and API design. So I probably
won't look at EasyPrompt until I've written up my quiz solution...
Ryan
Well consider me convinced. I certainly do see the value in Ruby Quizzes.
Actually, I'll admit that the last Ruby Quiz was a big influence in
bringing me back to being more active in the Ruby community.
In fact I'd like to thank for the work you do in preparing the quizzes and
summarizing them at the end. It is appreciated.
Anyhow, you can expect my solution to this quiz on Sunday (even if it
slightly reinvents the EasyPrompt wheel ;)
Ryan
> In fact I'd like to thank for the work you do in preparing the quizzes
> and
> summarizing them at the end. It is appreciated.
My pleasure.
I myself am eternally grateful to all the people who work the quizzes
and submit ideas to me. Without all the wonderful help, I would be
sunk. Thanks so much!
Ruby Quiz has really grown into more than I expected of it. I only
started it because I'm a quiz junkie and Ruby seemed lacking in the
programming challenges area when I showed up. I did it for me, not
you! <laughs>
But things change.
Now most of my time goes into running the quiz and I don't often end up
with enough left over to actually work them. I don't regret it though.
The Ruby Quiz site has become a killer resource for just reading
interesting code, if I do say so myself. There are some really great
idioms in the solutions all over that site.
I say we've built something great here and I'm proud to be a part of
it. Thanks again!
James Edward Gray II
> On Apr 22, 2005, at 10:56 AM, Ryan Leavengood wrote:
>> I suppose it will be worth it simply because we will see various
>> implementations and APIs for this concept, even if we are duplicating
>> EasyPrompt.
>
> I wouldn't declare EasyPrompt robust. Did you glance at the API?
> It's well... Easy. :) I'm confident I can add features to it in
> under an hour. It's not even capable of some of the ideas I'm tossing
> around in the quiz.
>
> What it did do for me, was give me more ideas. Have yourself a look,
> then think bigger... :)
Agreed. EasyPrompt is a pretty quick little thing; I just released it
'cause I wrote it once, then needed it again in another piece of code.
The only mildly difficult thing in it is the MockCommandLineUser.
Mostly I was just pointing it out as an informational aside.
But if people want me to add specific things, by all means let me know!
Francis Hwang
http://fhwang.net/
Intra-line editing?
History via (or a la) Readline?
ANSI Colors?
Hal
(who secretly misses the good old days of command lines)
Maybe eventually a readline replacement :) no more libreadline required!
> Hal
> (who secretly misses the good old days of command lines)
Mark
(who learned to like the command line when he learned it wasn't always DOS)
> On 4/22/05, Hal Fulton <hal...@hypermetrics.com> wrote:
>> Intra-line editing?
>> History via (or a la) Readline?
>> ANSI Colors?
>
> Maybe eventually a readline replacement :) no more libreadline
> required!
Well, maybe I need to backpedal a bit and say "if people have specific
things they'd like me to add that wouldn't require me to drop my other
15 projects, let me know."
Though the idea of replacing readline is intriguing ... Would the main
benefit of that be just getting away from an external lib dependency
that could make distribution harder? (And if so, I wonder if there are
any plans for RubyGems or RPA to interface with other
package-management systems so installing a Ruby lib based on a non-Ruby
lib could be easier.)
Francis Hwang
http://fhwang.net/
Haha... I wasn't really calling your bluff, just dreaming.
> Though the idea of replacing readline is intriguing ... Would the main
> benefit of that be just getting away from an external lib dependency
> that could make distribution harder?
One possible benefit might be easy access to "completion" tricks --
write a completion routine in Ruby and just pass the name of it
or whatever.
Hal
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 :output, :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
:) I wasn't trying to make more work for you. I was only half serious,
anyway. I know *I* don't have time right now to do this...
> Though the idea of replacing readline is intriguing ... Would the main
> benefit of that be just getting away from an external lib dependency
> that could make distribution harder?
On MacOSX, Apple doesn't ship libreadline due to licensing
restrictions, so there has never been readline support in Apple's
standard ruby install, which is very lame, and a source of endless
nuby problems. If I was starting a project to recreate readline, it
would be to convert one more of the default extensions to a more
comfortable license, making it easier for people to distribute.
cheers,
Mark
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
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[i].each do |validator|
error_message = nil
valid, err_msg, alt = validator.call(resp)
if !valid and !err_msg
while !@error_messages[i]
i = i + 1
break if i == @error_messages.length
end
klass_error_message = @error_messages[i]
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[i] != @default_response
ps << wrap_default(@choices[i]) + "\t" + @items[i] + "\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
> # 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
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
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...
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
> 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