Best of Ruby Quiz Pragmatic programmers phần 5 pot

29 262 0
Best of Ruby Quiz Pragmatic programmers phần 5 pot

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

ANSWER 7. HIGHLINE 110 elsif @type == Symbol string.to_sym elsif @type == Regexp Regexp.new(string) elsif @type.is_a?(Array) @type.extend(OptionParser::Completion) @type.complete(string).last elsif [Date, DateTime].include?(@type) @type.parse(string) elsif @type.is_a?(Proc) @type[string] end end def accept?( answer_object ) @member. nil? or @member.member?(answer_object) end def valid?( string ) @validate.nil? or string =~ @validate end end end This i s really just a data class. It sets a bunch of defaults and then allows the user to change them to fit their needs by passing the object to a block in initialize( ). Inside the block, the user can use the accessors to set details for the answer they are aft er. The only meth od really wort h discussing here is convert( ). You can see that it supports many types the answer can be converted into including Integer, Symbol, or even DateTime. This method can do two interesting forms of conversion. First, if the @type (answer_type from the HighLine layer) is set to an Array of values, the method will autocomplete the user’s answer to a matching value, using code borrowed from Option- Parser. Finally, if you set @type to a Proc object, it will be called to handle whatever custom conversion you need. Glance back at HighLine.agree( ) if you want to see an example. So far, we’ve seen the class system, which could be used directly via require "highline" when needed. Most of the time, though, we would prob- ably prefer global access to these methods. For that, HighLine provides another file you could load with require "highline/import": Report erratum ANSWER 7. HIGHLINE 111 highline/highline/import.rb #!/usr/local/bin/ruby -w require "highline" require "forwardable" $terminal = HighLine.new module Kernel extend Forwardable def_delegators :$terminal, :agree, :ask, :say end The idea here is that w e can stick a HighLine object in a g l obal variable and then just modify Kernel to delegate bare agree( ), ask( ), or say( ) calls to that object. The standard library, Forwardable, handles the latter part of that process for us via def_delegators( ). You just give it the name of the object to handle the calls and a list of methods to forward. Notice that Kernel needs to extend Forwardable to gain access to def_delegators( ). This l i brary proved helpful enough to me that I continued to develop it and made it available t o the Ruby community through RubyForge. HighLine has grown and matured from the original quiz submission and now supports many , many features. Recently, a second devel- oper, Greg Brown, signed on, bringing a comprehensive menu frame- work to the project. If you would like to play with the library, see http://highline.rubyforge.org/ for instructions on obtaining the latest release. Additional Exercises 1. Create the ASCII table feature mentioned in the discussion of Ryan’s header( ) method. 2. Work up a patch t o add thi s f eat ure to the HighLine library on Ruby- Forge. 3. Extend your solution to fetch an entire Array of answers from the user. Report erratum ANSWER 8. ROMAN NUMERALS 112 Answer 8 From page 18 Roman Numerals Solving this quiz is easy, but how easy? Well, the problem gives us the conversion chart, which is just crying out to be a Hash: roman_numerals/simple.rb ROMAN_MAP = { 1 => "I", 4 => "IV", 5 => "V", 9 => "IX", 10 => "X", 40 => "XL", 50 => "L", 90 => "XC", 100 => "C", 400 => "CD", 500 => "D", 900 => "CM", 1000 => "M" } That’s the version from my code, but most solutions used something very similar. From there we just need to_roman( ) and to_arabic( ) methods, right? Sounded like too much work for a lazy bum like me, so I cheated. If you build a conversion table, you can get away with just doing the con- version one way: roman_numerals/simple.rb ROMAN_NUMERALS = Array.new(3999) do |index| target = index + 1 ROMAN_MAP.keys.sort { |a, b| b <=> a }.inject( "") do |roman, div| times, target = target.divmod(div) roman << ROMAN_MAP[div] * times end end Report erratum ANSWER 8. ROMAN NUMERALS 113 This is the to_roman( ) method many solutions hit on. I just used mine to fill an Array. The algorithm here isn’t too tough. Divide the target number by each value there is a Roman numeral for copy the numeral that many times reduce the target, and repeat. Ruby’s divmod( ) is great for this. From ther e, it’s trivial to wrap a Unix filter around the Array. However, I do like to validate input, so I did one more little prep task: roman_numerals/simple.rb IS_ROMAN = / ^ M{0,3} (?:CM|DC{0,3}|CD|C{0,3}) (?:XC|LX{0,3}|XL|X{0,3}) (?:IX|VI{0,3}|IV|I{0,3}) $ /ix IS_ARABIC = /^(?:[123]\d{3}|[1-9]\d{0,2})$/ That first Regexp is a validator for the Roman let ter combinations we accept, split up by powers of ten. The second Regexp is a pattern to match 1 3999, a number in the range we can convert to and from. Now, we’re ready for the Unix filter wrapper: roman_numerals/simple.rb if __FILE__ == $0 ARGF.each_line() do |line| line.chomp! case line when IS_ROMAN then puts ROMAN_NUMERALS.index(line) + 1 when IS_ARABIC then puts ROMAN_NUMERALS[line.to_i - 1] else raise "Invalid input: #{line}" end end end In English that says, for each line of input, see whether it matches IS_ROMAN, and if it does, look it up in the Array. If it doesn’t match IS_ROMAN but does match IS_ARABIC, index into the Array to get the match. If none of that is true, complain about the broken input. Saving Some Memory If you don’t want to build the Array, you just need to create the other converter. It ’s not hard. J E Bailey’s script did both, so let’s look at that: Report erratum ANSWER 8. ROMAN NUMERALS 114 roman_numerals/dual_conversions.r b #!/usr/bin/env ruby @data = [ ["M" , 1000], ["CM" , 900], ["D" , 500], [ "CD" , 400], ["C" , 100], ["XC" , 90], ["L" , 50], [ "XL" , 40], ["X" , 10], ["IX" , 9], ["V" , 5], [ "IV" , 4], [ "I" , 1] ] @roman = %r{^[CDILMVX]*$} @arabic = %r{^[0-9]*$} def to_roman(num) reply = "" for key, value in @data count, num = num.divmod(value) reply << (key * count) end reply end def to_arabic(rom) reply = 0 for key, value in @data while rom.index(key) == 0 reply += value rom.slice!(key) end end reply end $stdin.each do |line| case line when @roman puts to_arabic(line) when @arabic puts to_roman(line.to_i) end end Report erratum ANSWER 8. ROMAN NUMERALS 115 Joe Asks. . . toRoman( ) or to_roman( )? The methods in J E’s solution were originally toRoman( ) and toArabic( ). These method names use an unusual (in Ruby cir- cles) naming convention often referred to as camelCase. Typi- cal Ruby style is to name methods and variables in snake_case (such as to_roman( ) and to_arabic( )). We do typically use a variant of the former (with a capital first letter) in the names of classes and modules, though. Why is this importa nt? Well, with a ny language first you need to learn the grammar, but eventually you want to know the slang, right? Same thing. Someday you may want to write Ruby the way that Ruby gurus do. I told you we all used something similar to my Hash. Here it’s just an Array of tuples. Right below that, you’ll see J E’s data identifying Regexp declarations. They’re not as exact as my versions, but certainly they are easier on the eyes. Next we see a to_roman( ) method, w hich looks very familiar. The imple- mentation is almost identical to mine, but it comes out a little cleaner here since it isn’t used to load an Array. Then we r each the method of interest, to_arabic( ). The method starts by setting a reply variable to 0. Then it hunts for each Roman numeral in the rom String, increments reply by that value, and removes that numeral from the String. The ordering of the @data Array ensures that an XL or IV will be found before an X or I. Finally, the code provides the quiz-specified Unix filter behavior. Again, this is very similar to my own solution, but with conversion routines going both ways. Romanizing Ruby Those are simple solutions, but let’s jump over to Dave Burt’s code for a little Ruby voodoo. Dave’s code builds a module, RomanNumerals, with Report erratum ANSWER 8. ROMAN NUMERALS 116 to_integer( ) and from_integer( ), similar to what we’ve discussed previ- ously. The module also defines is_roman_numeral?( ) for checking exactly what the name claims and some helpful constants such as DI GITS, MAX, and REGEXP. roman_numerals/roman_numerals.r b # Contains methods to convert integers to Roman numeral strings, and vice versa. module RomanNumerals # Maps Roman numeral digits to their integer values DIGITS = { ' I' => 1, ' V' => 5, ' X' => 10, ' L' => 50, ' C' => 100, ' D' => 500, ' M' => 1000 } # The largest integer representable as a Roman numerable by this module MAX = 3999 # Maps some integers to their Roman numeral values @@digits_lookup = DIGITS.inject({ 4 => ' IV' , 9 => ' IX' , 40 => ' XL' , 90 => ' XC' , 400 => ' CD' , 900 => ' CM' }) do |memo, pair| memo.update({pair.last => pair.first}) end # Based on Regular Expression Grabbag in the O' Reilly Perl Cookbook, #6.23 REGEXP = /^M*(D?C{0,3}|C[DM])(L?X{0,3}|X[LC])(V?I{0,3}|I[VX])$/i # Converts +int+ to a Roman numeral def self.from_integer(int) return nil if int < 0 || int > MAX remainder = int result = ' ' @@digits_lookup.keys.sort.reverse.each do |digit_value| while remainder >= digit_value remainder -= digit_value result += @@digits_lookup[digit_value] end break if remainder <= 0 end result end Report erratum ANSWER 8. ROMAN NUMERALS 117 # Converts +roman_string+, a Roman numeral, to an integer def self.to_integer(roman_string) return nil unless roman_string.is_roman_numeral? last = nil roman_string.to_s.upcase.split(//).reverse.inject(0) do |memo, digit| if digit_value = DIGITS[digit] if last && last > digit_value memo -= digit_value else memo += digit_value end last = digit_value end memo end end # Returns true if +string+ is a Roman numeral. def self.is_roman_numeral?(string) REGEXP =~ string end end I doubt we need to go over that code again, but I do want to point out one clever point. Notice how Dave uses a neat dance to keep things like IV out of DIGITS. In doing so, we see the unusual construct memo.update({pair.last => pair.first}), instead of the seemingly more natural memo[pair.last] = pair.first. The r eason is that the former returns the Hash itself, satisfying the continuous update cycle of inject( ). Anyway, the module is a small chunk of Dave’s code, and the rest is fun. Let’s see him put it to use: roman_numerals/roman_numerals.r b class String # Considers string a Roman numeral, # and converts it to the corresponding integer. def to_i_roman RomanNumerals.to_integer(self) end # Returns true if the subject is a Roman numeral. def is_roman_numeral? RomanNumerals.is_roman_numeral?(self) end end class Integer # Converts this integer to a Roman numeral. def to_s_roman RomanNumerals.from_integer( self) || ' ' end end Report erratum ANSWER 8. ROMAN NUMERALS 118 First, he adds converters t o String and Integer. This allows you to code things such as the following: puts "In the year #{1999.to_s_roman} " Fun, but there’s mor e. For Dave’s final magic trick he defines a class: roman_numerals/roman_numerals.r b # Integers that look like Roman numerals class RomanNumeral attr_reader :to_s, :to_i @@all_roman_numerals = [] # May be initialized with either a string or an integer def initialize(value) case value when Integer @to_s = value.to_s_roman @to_i = value else @to_s = value.to_s @to_i = value.to_s.to_i_roman end @@all_roman_numerals[to_i] = self end # Factory method: returns an equivalent existing object if such exists, # or a new one def self.get(value) if value.is_a?(Integer) to_i = value else to_i = value.to_s.to_i_roman end @@all_roman_numerals[to_i] || RomanNumeral.new(to_i) end def inspect to_s end # Delegates missing methods to Integer, converting arguments to Integer, # and converting results back to RomanNumeral def method_missing(sym, *args) unless to_i.respond_to?(sym) raise NoMethodError.new( "undefined method ' #{sym}' for #{self}:#{self.class}") end result = to_i.send(sym, *args.map {|arg| arg.is_a?(RomanNumeral) ? arg.to_i : arg }) case result Report erratum ANSWER 8. ROMAN NUMERALS 119 when Integer RomanNumeral.get(result) when Enumerable result.map do |element| element.is_a?(Integer) ? RomanNumeral.get(element) : element end else result end end end If you use the factory met hod get( ) to create these objects, it’s efficient with reuse, always giving you the same object for the same value. Note that method_missing( ) basically delegates t o Integer at the end, so you can treat these objects mostly as Integer objects. This class allows you to code things like thus: IV = RomanNumeral.get(4) IV + 5 # => IX Even better, though, is that Dave removes the need f or that first step with the following: roman_numerals/roman_numerals.r b # Enables uppercase Roman numerals to be used interchangeably with integers. # They are autovivified RomanNumeral constants # Synopsis: # 4 + IV #=> VIII # VIII + 7 #=> XV # III ** III #=> XXVII # VIII.divmod(III) #=> [II, II] def Object.const_missing sym unless RomanNumerals::REGEXP === sym.to_s raise NameError.new( "uninitialized constant: #{sym}") end const_set(sym, RomanNumeral.get(sym)) end This makes it so that Ruby will automatically turn constants like IX into RomanNumeral objects as needed. That’s just smooth. Finally, the listing at the top of the facing page shows Dave’s actual solution to the quiz using the previous tools: Report erratum [...]... unless $board[square].nil? # otherwise calculate all jumps x, y = square[0] - ?a, square[1, 1].to_i - 1 steps = Array.new [-1, 1].each do |s_off| [-2, 2].each do |l_off| [[s_off, l_off], [l_off, s_off]].each do |(x_off, y_off)| next_x, next_y = x + x_off, y + y_off next if next_x < 0 or next_x > 7 next if next_y < 0 or next_y > 7 steps . :rock][rand(3)] end end 30 Ruby Quiz is hosted on the Ruby Talk mailing list, and you will often se e discussion there about the problems. You can find more information about this mailing list for general Ruby discussion. JEGPaperPlayer AJBRandomPlayer: 51 1.0 JEGPaperPlayer: 489.0 AJBRandomPlayer Wins AJBRandomPlayer vs. JEGQueuePlayer AJBRandomPlayer: 499 .5 JEGQueuePlayer: 50 0 .5 JEGQueuePlayer Wins Outthinking a Random Player Of course,. vs. JEGPaperPlayer CNBiasInverter: 9 95. 0 JEGPaperPlayer: 5. 0 CNBiasInverter Wins CNBiasInverter vs. JEGQueuePlayer CNBiasInverter: 653 .5 JEGQueuePlayer: 346 .5 CNBiasInverter Wins 32 The unusual

Ngày đăng: 12/08/2014, 09:21

Từ khóa liên quan

Tài liệu cùng người dùng

Tài liệu liên quan