From: Jesse Merriman Date: 2007-06-26T20:02:04+09:00 Subject: [QUIZ][SOLUTION] LSRC Name Picker (#129) --Boundary-00=_YHPgGLx5wXMeysx Content-Type: Multipart/Mixed; boundary="Boundary-00=_YHPgGLx5wXMeysx" --Boundary-00=_YHPgGLx5wXMeysx Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit Content-Disposition: inline My solution uses SQLite to store a DB of names, and RMagick to display chosen names. Consider the display part a first draft - I think I'll redo it, maybe in a completely different way - and submit again later, though the backend shouldn't change. There are 4 files that roughly follow MVC: names_db.rb: Wrapper around a SQLite::Database (M) pick.rb: Main script (C) pickerface.rb: Interface stuff (V) name_image.rb: RMagick stuff (V) The DB stores just one thing: names. When a name is chosen, its simply deleted. I thought of marking the names as either picked or unpicked, or storing chosen names in another table, but I'm not sure if that would be used, and in any event it would be easy enough to add if needed. When pick.rb is run without a command option, it either creates a DB and starts adding names if the DB doesn't exist, or picks and displays a name if it does. See the top of pick.rb, or run pick.rb --help for a full usage message. For all text-mode-only operations, names can be Unicode, but RMagick doesn't seem to like that, so they can't be displayed nicely. Magick::ImageList.animate is used for displaying, so it won't work for any name on Windows. I bracket chosen names in stars taken from: http://lonestarrubyconf.com/stylesheets/lsrc_2007/images/LSRC_big_title.jpg I'm not sure this list will accept messages containing graphics, so you'll have to fetch this image and place it in the same directory as my code: http://www.jessemerriman.com/images/ruby_quiz/129_lsrc_name_picker/star_small.png -- Jesse Merriman jessemerriman@warpmail.net http://www.jessemerriman.com/ --Boundary-00=_YHPgGLx5wXMeysx Content-Type: application/x-ruby; name="names_db.rb" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="names_db.rb" require 'sqlite' # NamesDB wraps up a SQLite::Database. class NamesDB class AlreadyAdded < StandardError; end class NonexistantDB < StandardError; end NamesTable = 'names' # Initialize a new NamesDB. # - filename: The filename of the DB. # - create: If true, and the DB doesn't exist, it'll be created. If false, # and the DB doesn't exist, raise a NonexistantDB exception. # - optimize: Enable optimizations. def initialize filename, create = false, optimize = true raise NonexistantDB if not create and not File.exists? filename @filename = filename @db = SQLite::Database.new filename create_names_table if @db.table_info(NamesTable).empty? enable_optimizations if optimize prepare_statements self end # Add a name to the DB. def add_name name begin @statements[:add_name].execute! NamesTable, name rescue SQLite::Exceptions::SQLException raise AlreadyAdded.new, "#{name} is already in the DB!" end end # Delete a name from the DB. def delete_name name @statements[:delete_name].execute! NamesTable, name end # Yield each name in the DB. def each_name @statements[:each_name].execute!(NamesTable) { |res| yield res[0] } end # Clear the DB of all names. def clear @statements[:clear].execute! NamesTable end # Destroy the DB. def destroy @db.close File.delete @filename end # Return a randomly-chosen name, or nil if there are non left. def pick res = @statements[:pick].execute! NamesTable res.empty? ? nil : res[0][0] end private def create_names_table @db.execute <<-SQL, NamesTable CREATE TABLE ? ( name TEXT PRIMARY KEY ); SQL end def enable_optimizations @db.execute 'PRAGMA default_synchronous = OFF;' end def prepare_statements @statements = {} @statements[:add_name] = @db.prepare 'INSERT INTO ? (name) VALUES (?);' @statements[:delete_name] = @db.prepare 'DELETE FROM ? WHERE name = ?;' @statements[:each_name] = @db.prepare 'SELECT name FROM ?;' @statements[:clear] = @db.prepare 'DELETE FROM ?; VACUUM;' @statements[:pick] = @db.prepare <<-SQL SELECT name FROM ? ORDER BY random() LIMIT 1; SQL end end --Boundary-00=_YHPgGLx5wXMeysx Content-Type: application/x-ruby; name="pickerface.rb" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="pickerface.rb" require 'names_db' require 'name_image' # Pickerface handles the interface for pick.rb. class Pickerface def initialize filename @db_file = filename self end # Read names from standard input and add the to the DB. def add_names puts 'Enter names to add, one per line. Blank or ^D to stop.' db = NamesDB.new @db_file, true while name = $stdin.gets and name != "\n" begin db.add_name name.chomp rescue NamesDB::AlreadyAdded => ex $stderr.puts ex end end end # List all names to standard output. def list_names existing_db do |db| puts str = "Contents of #{@db_file}:" puts '-' * str.length db.each_name { |name| puts name } end end # Clear the DB of all names. def clear existing_db do |db| db.clear puts "#{@db_file} has been cleared of all names." end end # Randomly choose a name, write it to standard output, and delete it from the # DB. def pick_simple pick { |name| puts name } end # Randomly choose a name, display it in a fancy way, and delete it from the # DB. def pick_fancy pick { |name| NameImage.fancy(name).animate } end # Like pick_fancy, but save the image to the given filename instead of # displaying it. def save_fancy filename pick { |name| NameImage.fancy(name).write(filename) } end # Destroy the DB. def destroy existing_db do |db| db.destroy puts "#{@db_file} has been destroyed." end end private # Yield a NamesDB if @db_file exists, otherwise print an error. def existing_db begin yield NamesDB.new(@db_file) rescue NamesDB::NonexistantDB $stderr.puts "Error: #{@db_file} does not exist!" end end # Yield a randomly-picked name, or a message that there are none left. def pick existing_db do |db| if name = db.pick db.delete_name name yield name else yield 'No one left!' end end end end --Boundary-00=_YHPgGLx5wXMeysx Content-Type: application/x-ruby; name="pick.rb" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="pick.rb" #!/usr/bin/env ruby # = Ruby Quiz 129: LSRC Name Picker # Author: Jesse Merriman # # == Usage # # pick.rb [OPTION] [DB-FILE] # # -h, --help: # Print out usage message. # # -a, --add-names: # Take a list of names on standard input and add them to the DB. Creates the # DB if it does not exist. # # -l, --list-names: # List out all names in the DB to standard output, if it exists. # # -c, --clear-names: # Clear the DB of all names, if it exists. # # -p, --pick-simple # Randomly choose an unchosen name from the DB, if it exists. # # -f, --pick-fancy # Like --pick, but display the chosen name in a fancy way. # # -s, --save-fancy FILENAME # Like --pick-fancy, but save the graphic to FILENAME instead of displaying. # Very SLOW. # # -d, --destroy-db: # Destroy the DB, if it exists. # # If no option is given, the default is --add-names if no DB exists, or # --pick-fancy if it does. # # DB-FILE defaults to 'names.db'. require 'pickerface' require 'getoptlong' require 'rdoc/usage' if __FILE__ == $0 Opts = GetoptLong.new( [ '--help', '-h', GetoptLong::NO_ARGUMENT ], [ '--add-names', '-a', GetoptLong::NO_ARGUMENT ], [ '--list-names', '-l', GetoptLong::NO_ARGUMENT ], [ '--clear-names', '-c', GetoptLong::NO_ARGUMENT ], [ '--pick-simple', '-p', GetoptLong::NO_ARGUMENT ], [ '--pick-fancy', '-f', GetoptLong::NO_ARGUMENT ], [ '--save-fancy', '-s', GetoptLong::REQUIRED_ARGUMENT ], [ '--destroy-db', '-d', GetoptLong::NO_ARGUMENT ] ) # Handle arguments. op, args = nil, [] Opts.each do |opt, arg| case opt when '--help'; RDoc::usage when '--add-names'; op = :add_names when '--list-names'; op = :list_names when '--clear-names'; op = :clear when '--pick-simple'; op = :pick_simple when '--pick-fancy'; op = :pick_fancy when '--save-fancy'; op = :save_fancy; args << arg when '--destroy-db'; op = :destroy end end # Setup default arguments. ARGV.empty? ? db_file = 'names.db' : db_file = ARGV.first if op.nil? File.exists?(db_file) ? op = :pick_fancy : op = :add_names end # Run. Pickerface.new(db_file).send(op, *args) end --Boundary-00=_YHPgGLx5wXMeysx Content-Type: application/x-ruby; name="name_image.rb" Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="name_image.rb" require 'RMagick' # A few methods for creating images of names with RMagick. module NameImage StarFile = './star_small.png' Colors = { :bg => 'black', :border_in => 'darkblue', :border_out => 'black', :text_stroke => 'darkblue', :text_fill => 'gold' } # Create a fancy animated ImageList. def NameImage.fancy name img = add_ripple(plain(name)) img.border! 4, 4, Colors[:border_in] img.border! 4, 4, Colors[:border_out] img = add_stars img shake img end # Create a rather plain Image. def NameImage.plain name img = Magick::Image.new(1, 1) { self.background_color = Colors[:bg] } gc = Magick::Draw.new metrics = gc.get_multiline_type_metrics img, name img.resize! metrics.width * 4, metrics.height * 4 gc.annotate(img, 0, 0, 0, 0, name) do |gc| gc.font_family = 'Verdana' gc.font_weight = Magick::LighterWeight gc.pointsize = 40 gc.gravity = Magick::SouthEastGravity gc.stroke = Colors[:text_stroke] gc.fill = Colors[:text_fill] end img end # Create a new image consisting of img and a rippling reflection under it. def NameImage.add_ripple img img_list = Magick::ImageList.new xform = Magick::AffineMatrix.new 1.0, 0.0, Math::PI/4.0, 1.0, 0.0, 0.0 img_list << img img_list << img.wet_floor(0.5, 0.7).affine_transform(xform). rotate(90).wave(2, 10).rotate(-90) img_list.append true end # Create a new image consisting of img with stars on either side. def NameImage.add_stars img begin star = Magick::ImageList.new(StarFile).first img_list = Magick::ImageList.new img_list << star.copy img_list << img img_list << star img_list.append false rescue Magick::ImageMagickError $stderr.puts "Couldn't open #{StarFile}. Did you download it?" img end end # Create an animated ImageList of img shaking. def NameImage.shake img animation = Magick::ImageList.new 20.times { animation << img.copy } 0.3.step(0.6, 0.3) do |deg| animation << img.rotate(deg) animation << img.rotate(-deg) end animation << img.radial_blur(6) animation.delay = 5 animation.iterations = 10000 animation end end --Boundary-00=_YHPgGLx5wXMeysx-- --Boundary-00=_YHPgGLx5wXMeysx--