├── README ├── Rakefile.rb ├── answers ├── csv_formatter.rb └── csv_formatter_test.rb ├── array_queue ├── array_queue.rb └── array_queue_test.rb ├── checkpoint ├── checkpoint.rb └── checkpoint_test.rb ├── csv_writer ├── csv_writer.rb └── csv_writer_test.rb ├── matcher ├── matcher.rb └── matcher_test.rb ├── person ├── person.rb └── person_test.rb ├── points ├── bird.rb ├── bird_test.rb ├── button.rb ├── button_test.rb └── point.rb ├── robot ├── machine.rb ├── machine_spec.rb ├── report.rb ├── report_spec.rb ├── robot.rb └── robot_spec.rb ├── simple_queue ├── simple_queue.rb └── simple_queue_test.rb ├── svg-after ├── sparkline.rb ├── sparky.rb └── svg.rb ├── svg-before └── sparky.rb ├── template ├── template.rb └── template_test.rb ├── theater ├── agency.rb ├── booking_test.rb └── theater.rb ├── tic_tac_toe ├── tic_tac_toe.rb └── tic_tac_toe_test.rb ├── timelog ├── features │ ├── step_definitions │ │ └── timelog_steps.rb │ ├── support │ │ └── env.rb │ └── timelog.feature ├── timelog.rb └── timelog_test.rb ├── timer ├── timer.rb └── timer_test.rb └── ucalc ├── calc_controller.rb ├── calc_controller_spec.rb ├── calc_screen.rb ├── calculator.rb ├── calculator_spec.rb ├── dimension.rb ├── value.rb └── value_spec.rb /README: -------------------------------------------------------------------------------- 1 | Home of the code samples for the Ruby Refactoring Workbook. 2 | 3 | To download the code in Zip or Tar format, visit http://github.com/kevinrutherford/rrwb-code/downloads. 4 | 5 | -------------------------------------------------------------------------------- /Rakefile.rb: -------------------------------------------------------------------------------- 1 | require 'rake/testtask' 2 | require 'reek/rake_task' 3 | require 'spec/rake/spectask' 4 | 5 | desc 'run the ucalc specs' 6 | task :ucalc do 7 | Dir.chdir('ucalc') { sh 'spec *_spec.rb' } 8 | end 9 | 10 | Reek::RakeTask.new do |t| 11 | t.source_files = '**/*.rb' 12 | end 13 | 14 | Rake::TestTask.new do |t| 15 | t.libs = Dir['*'] 16 | t.test_files = '**/*_test.rb' 17 | end 18 | 19 | desc 'runs all tests and specs' 20 | task :default => [:test, :ucalc] 21 | -------------------------------------------------------------------------------- /answers/csv_formatter.rb: -------------------------------------------------------------------------------- 1 | class CsvFormatter 2 | 3 | def format(lines) 4 | lines.collect { |line| write_line(line) }.join("\n") 5 | end 6 | 7 | private 8 | 9 | def write_line(fields) 10 | fields.collect { |field| write_field(field) }.join(",") 11 | end 12 | 13 | def write_field(field) 14 | case field 15 | when /,/ then quote_and_escape(field) 16 | when /"/ then quote_and_escape(field) 17 | else field 18 | end 19 | end 20 | 21 | def quote_and_escape(field) 22 | "\"#{field.gsub(/\"/, "\"\"")}\"" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /answers/csv_formatter_test.rb: -------------------------------------------------------------------------------- 1 | require 'csv_formatter' 2 | require 'test/unit' 3 | 4 | class CsvFormatterTest < Test::Unit::TestCase 5 | 6 | def setup 7 | @csv = CsvFormatter.new 8 | end 9 | 10 | def test_no_lines 11 | assert_equal("", @csv.format([])) 12 | end 13 | 14 | def test_no_quotes_or_commas 15 | assert_equal("", @csv.format([[]])) 16 | assert_equal("only one field", 17 | @csv.format([["only one field"]])) 18 | assert_equal("two,fields", 19 | @csv.format([["two", "fields"]])) 20 | assert_equal(",contents,several words included", 21 | @csv.format([["", "contents", "several words included"]])) 22 | assert_equal("two\nlines", 23 | @csv.format([["two"], ["lines"]])) 24 | end 25 | 26 | def test_commas_and_quotes 27 | assert_equal('",","embedded , commas","trailing,"', 28 | @csv.format([[',', 'embedded , commas', 'trailing,']])) 29 | assert_equal('"""","multiple """""" quotes"""""', 30 | @csv.format([['"', 'multiple """ quotes""']])) 31 | assert_equal('"commas, and ""quotes""",simple', 32 | @csv.format([['commas, and "quotes"', 'simple']])) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /array_queue/array_queue.rb: -------------------------------------------------------------------------------- 1 | class ArrayQueue < Array 2 | 3 | def add_rear(s) 4 | self << s 5 | end 6 | 7 | def remove_front 8 | self.delete_at(0) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /array_queue/array_queue_test.rb: -------------------------------------------------------------------------------- 1 | require 'array_queue' 2 | require 'test/unit' 3 | 4 | class ArrayQueueTest < Test::Unit::TestCase 5 | def test_queue_invariant 6 | q = ArrayQueue.new 7 | q.add_rear("E1") 8 | q.add_rear("E2") 9 | assert_equal("E1", q.remove_front) 10 | assert_equal("E2", q.remove_front) 11 | assert_equal(0, q.length) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /checkpoint/checkpoint.rb: -------------------------------------------------------------------------------- 1 | module Checkpoint 2 | def checkpoint 3 | @state = var_values 4 | end 5 | 6 | def var_values 7 | result = {} 8 | instance_variables.each do |var| 9 | result[var] = instance_variable_get var 10 | end 11 | result 12 | end 13 | 14 | def changes 15 | var_values.reject { |k,v| k == "@state" || @state[k] == v } 16 | end 17 | 18 | end 19 | 20 | class Object 21 | include Checkpoint 22 | end 23 | -------------------------------------------------------------------------------- /checkpoint/checkpoint_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'checkpoint' 3 | 4 | class Customer 5 | attr_reader :first, :last, :ssn 6 | 7 | def initialize(first, last, ssn) 8 | @first, @last, @ssn = first, last, ssn 9 | end 10 | 11 | def marries(other) 12 | @last = other.last 13 | end 14 | end 15 | 16 | class CheckpointTest < Test::Unit::TestCase 17 | def test_one_variable_changed 18 | martha = Customer.new "Martha", "Jones", "12-345-6789" 19 | jack = Customer.new "Jack", "Harkness", "97-865-4321" 20 | martha.checkpoint 21 | martha.marries(jack) 22 | assert_equal({"@last" => "Harkness"}, martha.changes) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /csv_writer/csv_writer.rb: -------------------------------------------------------------------------------- 1 | class CsvWriter 2 | 3 | def write(lines) 4 | lines.each { |line| write_line(line) } 5 | end 6 | 7 | private 8 | 9 | def write_line(fields) 10 | if (fields.length == 0) 11 | puts 12 | else 13 | write_field(fields[0]) 14 | 1.upto(fields.length-1) do |i| 15 | print "," 16 | write_field(fields[i]) 17 | end 18 | puts 19 | end 20 | end 21 | 22 | def write_field(field) 23 | case field 24 | when /,/ then write_quoted(field) 25 | when /"/ then write_quoted(field) 26 | else print(field) 27 | end 28 | end 29 | 30 | def write_quoted(field) 31 | print "\"" 32 | print field.gsub(/\"/, "\"\"") 33 | print "\"" 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /csv_writer/csv_writer_test.rb: -------------------------------------------------------------------------------- 1 | require 'csv_writer' 2 | require 'test/unit' 3 | 4 | class CsvWriterTest < Test::Unit::TestCase 5 | 6 | def test_writer 7 | writer = CsvWriter.new 8 | lines = [] 9 | lines << [] 10 | lines << ['only one field'] 11 | lines << ['two', 'fields'] 12 | lines << ['', 'contents', 'several words included'] 13 | lines << [',', 'embedded , commas, included', 'trailing,'] 14 | lines << ['"', 'embedded " quotes', 'multiple """ quotes""'] 15 | lines << ['mixed commas, and "quotes"', 'simple field'] 16 | 17 | # Expected: 18 | # -- (empty line) 19 | # only one field 20 | # two,fields 21 | # ,contents,several words included 22 | # ",","embedded , commas, included","trailing," 23 | # """","embedded "" quotes","multiple """""" quotes""""" 24 | # "mixed commas, and ""quotes""",simple field 25 | 26 | writer.write(lines) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /matcher/matcher.rb: -------------------------------------------------------------------------------- 1 | class Matcher 2 | def match(expected, actual, clip_limit, delta) 3 | # Clip "too-large" values 4 | actual = actual.map { |val| [val, clip_limit].min } 5 | 6 | # Check for length differences 7 | return false if actual.length != expected.length 8 | 9 | # Check that each entry is within expected +/- delta 10 | actual.each_index { |i| 11 | return false if (expected[i] - actual[i]).abs > delta 12 | } 13 | return true 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /matcher/matcher_test.rb: -------------------------------------------------------------------------------- 1 | require 'matcher' 2 | require 'test/unit' 3 | 4 | class MatcherTest < Test::Unit::TestCase 5 | 6 | def setup 7 | @numbers = [10, 50, 30, 98] 8 | end 9 | 10 | def test_different_lengths_rejected 11 | assert !Matcher.new.match(@numbers, @numbers + [1], 100, 5) 12 | assert !Matcher.new.match(@numbers + [1], @numbers, 100, 5) 13 | end 14 | 15 | def test_different_lengths_rejected_with_clipping 16 | @numbers << 103 17 | assert !Matcher.new.match(@numbers, @numbers + [1], 100, 5) 18 | assert !Matcher.new.match(@numbers + [1], @numbers, 100, 5) 19 | end 20 | 21 | def test_variation_within_delta_accepted 22 | assert Matcher.new.match(@numbers, [12, 55, 25, 100], 100, 5) 23 | end 24 | 25 | def test_clipped_variation_within_delta_accepted 26 | assert Matcher.new.match(@numbers, [12, 55, 25, 110], 100, 5) 27 | end 28 | 29 | def test_variation_greater_than_delta_rejected 30 | assert !Matcher.new.match(@numbers, [10, 60, 30, 98], 100, 5) 31 | end 32 | 33 | end 34 | -------------------------------------------------------------------------------- /person/person.rb: -------------------------------------------------------------------------------- 1 | Person = Struct.new('Person', :last, :first, :middle) 2 | -------------------------------------------------------------------------------- /person/person_test.rb: -------------------------------------------------------------------------------- 1 | require 'stringio' 2 | require 'test/unit' 3 | 4 | require 'person' 5 | 6 | class PersonClient < Test::Unit::TestCase 7 | 8 | def client1(out, person) 9 | out.write(person.first) 10 | out.write(" ") 11 | if person.middle != nil 12 | out.write(person.middle) 13 | out.write(" ") 14 | end 15 | out.write(person.last) 16 | end 17 | 18 | def client2(person) 19 | result = person.last + ", " + person.first 20 | if (person.middle != nil) 21 | result += " " + person.middle 22 | end 23 | return result 24 | end 25 | 26 | def client3(out, person) 27 | out.write(person.last) 28 | out.write(", ") 29 | out.write(person.first) 30 | if (person.middle != nil) 31 | out.write(" ") 32 | out.write(person.middle) 33 | end 34 | end 35 | 36 | def client4(person) 37 | return person.last + ", " + 38 | person.first + 39 | ((person.middle == nil) ? "" : " " + person.middle) 40 | end 41 | 42 | def test_clients 43 | bobSmith = Person.new("Smith", "Bob", nil) 44 | jennyJJones = Person.new("Jones", "Jenny", "J") 45 | 46 | out = StringIO.new 47 | client1(out, bobSmith) 48 | assert_equal("Bob Smith", out.string) 49 | 50 | out = StringIO.new 51 | client1(out, jennyJJones) 52 | assert_equal("Jenny J Jones", out.string) 53 | 54 | assert_equal("Smith, Bob", client2(bobSmith)) 55 | assert_equal("Jones, Jenny J", client2(jennyJJones)) 56 | 57 | out = StringIO.new 58 | client3(out, bobSmith) 59 | assert_equal("Smith, Bob", out.string) 60 | 61 | out = StringIO.new 62 | client3(out, jennyJJones) 63 | assert_equal("Jones, Jenny J", out.string) 64 | 65 | assert_equal("Smith, Bob", client4(bobSmith)) 66 | assert_equal("Jones, Jenny J", client4(jennyJJones)) 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /points/bird.rb: -------------------------------------------------------------------------------- 1 | # bird.rb 2 | require 'point.rb' 3 | 4 | class Bird 5 | attr_accessor :location 6 | 7 | def initialize max_x, max_y 8 | @@max_x = max_x 9 | @@max_y = max_y 10 | @location = Point.new 0, 0 11 | end 12 | 13 | def move_by(point) 14 | @location.x = (@location.x + point.x) % @@max_x 15 | @location.y = (@location.y + point.y) % @@max_y 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /points/bird_test.rb: -------------------------------------------------------------------------------- 1 | require 'bird' 2 | require 'test/unit' 3 | 4 | class BirdTest < Test::Unit::TestCase 5 | def test_location_wraps 6 | bird = Bird.new 100, 200 7 | bird.move_by(Point.new(80, 100)) 8 | bird.move_by(Point.new(50, 100)) 9 | assert_equal(30, bird.location.x) 10 | assert_equal(0, bird.location.y) 11 | end 12 | end 13 | 14 | -------------------------------------------------------------------------------- /points/button.rb: -------------------------------------------------------------------------------- 1 | #button.rb 2 | require 'point.rb' 3 | 4 | class Button 5 | attr_accessor :name 6 | attr_accessor :x, :y 7 | 8 | def initialize name, x_limit, y_limit 9 | @name = name 10 | @xmax = x_limit 11 | @ymax = y_limit 12 | @x = 0 13 | @y = 0 14 | end 15 | 16 | def move_to(x, y) 17 | @x = limit(x, @xmax) 18 | @y = limit(y, @ymax) 19 | end 20 | 21 | private 22 | def limit(v, vmax) 23 | result = v 24 | while result >= vmax 25 | result -= vmax 26 | end 27 | while result < 0 28 | result += vmax 29 | end 30 | result 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /points/button_test.rb: -------------------------------------------------------------------------------- 1 | require 'button.rb' 2 | require 'test/unit' 3 | 4 | class ButtonTest < Test::Unit::TestCase 5 | def test_button_stays_in_bounds 6 | button = Button.new('Click here', 50, 75) 7 | button.move_to(100, 78) 8 | assert_equal(0, button.x) 9 | assert_equal(3, button.y) 10 | end 11 | 12 | def test_negative_positions_moved_positive 13 | button = Button.new('New', 100, 15) 14 | button.move_to(-15, -5) 15 | assert_equal(85, button.x) 16 | assert_equal(10, button.y) 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /points/point.rb: -------------------------------------------------------------------------------- 1 | class Point 2 | attr_accessor :x, :y 3 | 4 | def initialize x, y 5 | @x = x 6 | @y = y 7 | end 8 | end 9 | 10 | -------------------------------------------------------------------------------- /robot/machine.rb: -------------------------------------------------------------------------------- 1 | class Machine 2 | attr_reader :name, :bin 3 | 4 | def initialize(name, location) 5 | @name = name 6 | @location = location 7 | end 8 | 9 | def take 10 | result = @bin 11 | @bin = nil 12 | return result 13 | end 14 | 15 | def put(bin) 16 | @bin = bin 17 | end 18 | end -------------------------------------------------------------------------------- /robot/machine_spec.rb: -------------------------------------------------------------------------------- 1 | require 'machine' 2 | 3 | describe Machine do 4 | before :each do 5 | @machine = Machine.new("Oven", "middle") 6 | end 7 | 8 | it 'should initially have no bin' do 9 | @machine.bin.should be_nil 10 | end 11 | 12 | it 'should accept things into its bin' do 13 | @machine.put("chips") 14 | @machine.bin.should == "chips" 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /robot/report.rb: -------------------------------------------------------------------------------- 1 | class Report 2 | def Report.report(out, machines, robot) 3 | out.print "FACTORY REPORT\n" 4 | machines.each do |machine| 5 | out.print "Machine #{machine.name}" 6 | out.print " bin=#{machine.bin}" if machine.bin != nil 7 | out.print "\n" 8 | end 9 | out.print "\n" 10 | out.print "Robot" 11 | if robot.location != nil 12 | out.print " location=#{robot.location.name}" 13 | end 14 | out.print " bin=#{robot.bin}" if robot.bin != nil 15 | out.print "\n" 16 | out.print "========\n" 17 | end 18 | end -------------------------------------------------------------------------------- /robot/report_spec.rb: -------------------------------------------------------------------------------- 1 | require 'machine' 2 | require 'robot' 3 | require 'report' 4 | require 'stringio' 5 | 6 | describe Report do 7 | it 'should report the state of everything' do 8 | line = [] 9 | line << Machine.new("mixer", "left") 10 | 11 | extruder = Machine.new("extruder", "center") 12 | extruder.put("paste") 13 | line << extruder 14 | 15 | oven = Machine.new("oven", "right") 16 | oven.put("chips") 17 | line << oven 18 | 19 | robot = Robot.new 20 | robot.move_to(extruder) 21 | robot.pick 22 | 23 | out = StringIO.new 24 | Report.report(out, line, robot) 25 | 26 | expected = < 19 | 20 | #{x_axis} 21 | #{sparkline} 22 | #{spark} 23 | 24 | } 25 | end 26 | 27 | private 28 | 29 | def x_axis 30 | " 31 | #{SVG.line(0, 0, @y_values.length, 0, '#999', 1)}" 32 | end 33 | 34 | def sparkline 35 | points = [] 36 | @y_values.each_index { |i| points << "#{i},#{@y_values[i]}" } 37 | " 38 | #{SVG.polyline(points, 'none', '#333', 1)}" 39 | end 40 | 41 | SQUARE_SIDE = 4 42 | SPARK_COLOR = 'red' 43 | 44 | def spark 45 | centre_x = @y_values.length-1 46 | centre_y = @y_values[-1] 47 | rect = SVG.rect(centre_x-(SQUARE_SIDE/2), 48 | centre_y-(SQUARE_SIDE/2), 49 | SQUARE_SIDE, SQUARE_SIDE, SPARK_COLOR, 'none', 0) 50 | text = SVG.text(centre_x+6, centre_y+4, @final_value, 51 | 'Verdana', 9, SPARK_COLOR) 52 | " 53 | #{rect} 54 | 55 | #{text}" 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /svg-after/sparky.rb: -------------------------------------------------------------------------------- 1 | require 'sparkline' 2 | 3 | def zero_or_one() rand(2) end 4 | 5 | def one_or_minus_one 6 | (zero_or_one * 2) - 1 7 | end 8 | 9 | def next_value(y_values) 10 | y_values[-1] + one_or_minus_one 11 | end 12 | 13 | def y_values 14 | result = [0] 15 | 1000.times { result << next_value(result) } 16 | result 17 | end 18 | 19 | puts Sparkline.new(y_values).to_svg 20 | -------------------------------------------------------------------------------- /svg-after/svg.rb: -------------------------------------------------------------------------------- 1 | module SVG 2 | def self.rect(centre_x, centre_y, width, height, fill, 3 | stroke, stroke_width) 4 | %Q{} 8 | end 9 | 10 | def self.text(x, y, msg, font_family, font_size, fill) 11 | %Q{#{msg}} 14 | end 15 | 16 | def self.line(x1, y1, x2, y2, stroke, stroke_width) 17 | %Q{} 19 | end 20 | 21 | def self.polyline(points, fill, stroke, stroke_width) 22 | %Q{} 25 | end 26 | end -------------------------------------------------------------------------------- /svg-before/sparky.rb: -------------------------------------------------------------------------------- 1 | NUMBER_OF_TOSSES = 1000 2 | BORDER_WIDTH = 50 3 | 4 | def toss 5 | 2 * (rand(2)*2 - 1) 6 | end 7 | 8 | def values(n) 9 | a = [0] 10 | n.times { a << (toss + a[-1]) } 11 | a 12 | end 13 | 14 | def spark(centre_x, centre_y, value) 15 | " 18 | #{value}" 21 | end 22 | 23 | $tosses = values(NUMBER_OF_TOSSES) 24 | points = [] 25 | $tosses.each_index { |i| points << "#{i},#{200-$tosses[i]}" } 26 | 27 | data = " 29 | 30 | 32 | 34 | #{spark(NUMBER_OF_TOSSES-1, 200-$tosses[-1], $tosses[-1])} 35 | " 36 | 37 | puts "Content-Type: image/svg+xml 38 | Content-Length: #{data.length} 39 | 40 | #{data}" 41 | -------------------------------------------------------------------------------- /template/template.rb: -------------------------------------------------------------------------------- 1 | module Template 2 | def template(source_template, req_id) 3 | template = String.new(source_template) 4 | 5 | # Substitute for %CODE% 6 | template_split_begin = template.index("%CODE%") 7 | template_split_end = template_split_begin + 6 8 | template_part_one = 9 | String.new(template[0..(template_split_begin-1)]) 10 | template_part_two = 11 | String.new(template[template_split_end..template.length]) 12 | code = String.new(req_id) 13 | template = 14 | String.new(template_part_one + code + template_part_two) 15 | 16 | # Substitute for %ALTCODE% 17 | template_split_begin = template.index("%ALTCODE%") 18 | template_split_end = template_split_begin + 9 19 | template_part_one = 20 | String.new(template[0..(template_split_begin-1)]) 21 | template_part_two = 22 | String.new(template[template_split_end..template.length]) 23 | altcode = code[0..4] + "-" + code[5..7] 24 | puts template_part_one + altcode + template_part_two 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /template/template_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'template' 3 | 4 | class TemplateTest < Test::Unit::TestCase 5 | include Template 6 | 7 | def test_missing_interval 8 | puts 'Expected output: Code is 5678901234; alt code is 56789-012' 9 | template 'Code is %CODE%; alt code is %ALTCODE%', '5678901234' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /theater/agency.rb: -------------------------------------------------------------------------------- 1 | class Agency 2 | def self.book(num_reqd, theater) 3 | free_seats = [] 4 | theater.seats.each_with_index do |item, index| 5 | free_seats << index if item == '-' 6 | end 7 | return nil if free_seats.empty? 8 | free_seats[0..num_reqd] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /theater/booking_test.rb: -------------------------------------------------------------------------------- 1 | require 'agency' 2 | require 'theater' 3 | require 'test/unit' 4 | 5 | class BookingTest < Test::Unit::TestCase 6 | def test_two_seats_anywhere 7 | adelphi = Theater.new('x-xxxx-xxxx') 8 | assert_equal([1,6], Agency.book(2, adelphi)) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /theater/theater.rb: -------------------------------------------------------------------------------- 1 | class Theater 2 | attr_reader :seats 3 | 4 | def initialize(seats) 5 | @seats = seats.split(//) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /tic_tac_toe/tic_tac_toe.rb: -------------------------------------------------------------------------------- 1 | class Game 2 | attr_accessor :board 3 | 4 | def initialize(s, position=nil, player=nil) 5 | @board = s.dup 6 | @board[position] = player unless position == nil 7 | end 8 | 9 | def move(player) 10 | (0..8).each do |i| 11 | if board[i,1] == '-' 12 | game = play(i, player) 13 | return i if game.winner() == player 14 | end 15 | end 16 | 17 | (0..8).each { |i| return i if board[i,1] == '-' } 18 | return -1 19 | end 20 | 21 | def play(i, player) 22 | Game.new(board, i, player) 23 | end 24 | 25 | def winner 26 | if board[0,1] != '-' && board[0,1] == board[1,1] && 27 | board[1,1] == board[2,1] 28 | return board[0,1] 29 | end 30 | if board[3,1] != '-' && board[3,1] == board[4,1] && 31 | board[4,1] == board[5,1] 32 | return board[3,1] 33 | end 34 | if board[6,1] != '-' && board[6,1] == board[7,1] && 35 | board[7,1] == board[8,1] 36 | return board[6,1] 37 | end 38 | return '-' 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /tic_tac_toe/tic_tac_toe_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'tic_tac_toe' 3 | 4 | class GameTest < Test::Unit::TestCase 5 | 6 | def test_default_move 7 | game = Game.new("XOX" + 8 | "OX-" + 9 | "OXO") 10 | assert_equal(5, game.move('X')) 11 | 12 | game = Game.new("XOX" + 13 | "OXO" + 14 | "OX-") 15 | assert_equal(8, game.move('O')) 16 | 17 | game = Game.new("---" + 18 | "---" + 19 | "---") 20 | assert_equal(0, game.move('X')) 21 | 22 | game = Game.new("XXX" + 23 | "XXX" + 24 | "XXX") 25 | assert_equal(-1, game.move('X')) 26 | end 27 | 28 | def test_find_winning_move 29 | game = Game.new("XO-" + 30 | "XX-" + 31 | "OOX") 32 | assert_equal(5, game.move('X')) 33 | end 34 | 35 | def test_win_conditions 36 | game = Game.new("---" + 37 | "XXX" + 38 | "---") 39 | assert_equal('X', game.winner()) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /timelog/features/step_definitions/timelog_steps.rb: -------------------------------------------------------------------------------- 1 | When /I create a new timelog database/ do 2 | `rm -f timelog.txt` 3 | end 4 | 5 | When /^I run timelog (.*)$/ do |args| 6 | timelog(args) 7 | end 8 | 9 | Then /^stdout equals "([^\"]*)"$/ do |report| 10 | @last_stdout.should == report 11 | end 12 | 13 | Then /a total of ([\d\.]+) hours are reported/ do |expected| 14 | total_line = @last_stdout.split("\n")[-1] 15 | hrs = total_line.split[1].to_f 16 | hrs.should == expected.to_f 17 | end -------------------------------------------------------------------------------- /timelog/features/support/env.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'tempfile' 3 | require 'spec/expectations' 4 | require 'fileutils' 5 | 6 | class TimelogWorld 7 | def run(cmd) 8 | stderr_file = Tempfile.new('timelog-world') 9 | stderr_file.close 10 | @last_stdout = `#{cmd} 2> #{stderr_file.path}` 11 | @last_exit_status = $?.exitstatus 12 | @last_stderr = IO.read(stderr_file.path) 13 | end 14 | 15 | def timelog(args) 16 | ENV['TL_DIR'] = '.' 17 | run("ruby timelog.rb #{args}") 18 | end 19 | end 20 | 21 | World do 22 | TimelogWorld.new 23 | end 24 | -------------------------------------------------------------------------------- /timelog/features/timelog.feature: -------------------------------------------------------------------------------- 1 | Feature: Report time 2 | Reporting time using the timelog application 3 | 4 | Background: 5 | Given I create a new timelog database 6 | And I run timelog -u fred -h 6 proj1 7 | And I run timelog -u jim -h 7 proj1 8 | And I run timelog -u alice -h 4.5 proj1 9 | 10 | Scenario: project total 11 | When I run timelog proj1 12 | Then a total of 17.5 hours are reported 13 | 14 | Scenario: project_total_for_missing_project 15 | When I run timelog proj2 16 | Then a total of 0 hours are reported 17 | 18 | Scenario: user_total 19 | When I run timelog --user fred proj1 20 | Then a total of 6 hours are reported 21 | 22 | Scenario: user_total_for_missing_user 23 | When I run timelog --user harry proj1 24 | Then a total of 0 hours are reported 25 | 26 | Scenario: user_total_for_missing_project 27 | When I run timelog --user fred proj2 28 | Then a total of 0 hours are reported 29 | -------------------------------------------------------------------------------- /timelog/timelog.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/ruby 2 | # 3 | # Usage: 4 | # 5 | # timelog [--user USERNAME] [[--date d] [--hours] hrs] project 6 | # 7 | 8 | require 'ostruct' 9 | require 'optparse' 10 | require 'optparse/date' 11 | 12 | def parse_options(argv) 13 | options = OpenStruct.new 14 | OptionParser.new do |opts| 15 | opts.banner = "Usage: #{$0} [options] project_name" 16 | 17 | opts.on("-d", "--date DATE", Date, 18 | "Specify the date on which hours were worked") do |d| 19 | options.date = d 20 | end 21 | opts.on("-h", "--hours NUM", Float, 22 | "The number of hours worked") do |hrs| 23 | options.hours = hrs 24 | end 25 | opts.on("-u", "--user USERNAME", String, 26 | "Log time for a different user") do |user| 27 | options.user = user 28 | end 29 | opts.on_tail("-?", "--help", "Show this message") do 30 | puts opts 31 | exit 32 | end 33 | end.parse! 34 | 35 | if argv.length < 1 36 | puts "Usage: #{$0} [options] project_name" 37 | exit 38 | end 39 | 40 | if argv.length == 2 41 | hours = argv.shift 42 | options.hours = hours.to_f 43 | end 44 | 45 | if options.hours && options.hours <= 0.0 46 | raise OptionParser::InvalidArgument, hours 47 | end 48 | 49 | options.project = argv[0] 50 | options 51 | end 52 | 53 | TIMELOG_FOLDER = ENV['TL_DIR'] || '/var/log/timelog' 54 | TIMELOG_FILE_NAME = 'timelog.txt' 55 | TIMELOG_FILE = TIMELOG_FOLDER + '/' + TIMELOG_FILE_NAME 56 | 57 | def report(options) 58 | records = IO.readlines(TIMELOG_FILE) 59 | records = records.grep(/^#{options.project},/) 60 | records = records.grep(/,#{options.user},/) if options.user 61 | months = Hash.new(0.0) 62 | total = 0.0 63 | records.each do |record| 64 | project, user, date, hours = record.split(/,/) 65 | total += hours.to_f 66 | y, m, d = date.split(/-/) 67 | months["#{y}-#{m}"] += hours.to_f 68 | end 69 | lines = months.keys.sort.map { |month| 70 | "%-7s %8.1f" % [month, months[month]] 71 | } 72 | lines << "Total %8.1f" % total 73 | lines.join("\n") 74 | end 75 | 76 | def log(options) 77 | options.user ||= ENV['USERNAME'] 78 | options.date ||= Date.today.to_s 79 | File.open TIMELOG_FILE, 'a+' do |f| 80 | f.puts "#{options.project},#{options.user},#{options.date},#{options.hours}" 81 | end 82 | end 83 | 84 | if __FILE__ == $PROGRAM_NAME 85 | options = parse_options(ARGV) 86 | if options.hours.nil? 87 | puts report(options) 88 | else 89 | log(options) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /timelog/timelog_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | load 'timelog.rb' 3 | 4 | class TimelogTest < Test::Unit::TestCase 5 | def setup 6 | @varlog_size = File.size(TIMELOG_FILE) if File.exist?(TIMELOG_FILE) 7 | File.delete(TIMELOG_FILE_NAME) if File.exist?(TIMELOG_FILE_NAME) 8 | ENV['TL_DIR'] = '.' 9 | assert_equal('', `ruby timelog/timelog.rb -u fred -h 6 proj1`) 10 | assert_equal('', `ruby timelog/timelog.rb -u jim -h 7 proj1`) 11 | assert_equal('', `ruby timelog/timelog.rb -u alice -h 4.5 proj1`) 12 | end 13 | 14 | def teardown 15 | if File.exist?(TIMELOG_FILE) 16 | assert_equal(@varlog_size, File.size(TIMELOG_FILE), 17 | "log file #{TIMELOG_FILE} should be unchanged") 18 | end 19 | File.delete(TIMELOG_FILE_NAME) if File.exist?(TIMELOG_FILE_NAME) 20 | end 21 | 22 | def test_project_total 23 | rpt = `ruby timelog/timelog.rb proj1`.split("\n")[-1] 24 | assert_equal(17.5, rpt.split[1].to_f) 25 | end 26 | 27 | def test_project_total_for_missing_project 28 | rpt = `ruby timelog/timelog.rb proj2`.split("\n")[-1] 29 | assert_equal(0, rpt.split[1].to_f) 30 | end 31 | 32 | def test_user_total 33 | rpt = `ruby timelog/timelog.rb --user fred proj1`.split("\n")[-1] 34 | assert_equal(6, rpt.split[1].to_f) 35 | end 36 | 37 | def test_user_total_for_missing_user 38 | rpt = `ruby timelog/timelog.rb --user harry proj1`.split("\n")[-1] 39 | assert_equal(0, rpt.split[1].to_f) 40 | end 41 | 42 | def test_user_total_for_missing_project 43 | rpt = `ruby timelog/timelog.rb --user fred proj2`.split("\n")[-1] 44 | assert_equal(0, rpt.split[1].to_f) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /timer/timer.rb: -------------------------------------------------------------------------------- 1 | module Timer 2 | def times(env) 3 | value_s = env['interval'] 4 | if value_s == nil 5 | raise "interval missing" 6 | end 7 | value = Integer(value_s) 8 | 9 | if value <= 0 10 | raise "interval should be > 0" 11 | end 12 | check_interval = value 13 | 14 | value_s = env['duration'] 15 | raise "duration missing" if value_s.nil? 16 | value = Integer(value_s) 17 | if value <= 0 18 | raise "duration should be > 0" 19 | end 20 | if (value % check_interval) != 0 21 | raise "duration should be multiple of interval" 22 | end 23 | monitor_time = value 24 | 25 | value_s = env['departure'] 26 | if value_s.nil? 27 | raise "departure missing" 28 | end 29 | value = Integer(value_s) 30 | raise "departure should be > 0" if value <= 0 31 | if (value % check_interval) != 0 32 | raise "departure should be multiple of interval" 33 | end 34 | departure_offset = value 35 | [check_interval, monitor_time, departure_offset] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /timer/timer_test.rb: -------------------------------------------------------------------------------- 1 | require 'test/unit' 2 | require 'timer' 3 | 4 | class TimerTest < Test::Unit::TestCase 5 | include Timer 6 | 7 | def test_values_decoded 8 | interval, time, departure = times 'interval'=>"3", 'duration' => "12", 'departure' => "6" 9 | assert_equal 3, interval 10 | assert_equal 12, time 11 | assert_equal 6, departure 12 | end 13 | 14 | def test_missing_interval_raises_runtime_error 15 | assert_raise RuntimeError do 16 | times 'duration' => "12", 'departure' => "5" 17 | end 18 | end 19 | 20 | def test_non_integer_interval_raises_argument_error 21 | assert_raise ArgumentError do 22 | times 'interval' => "fred", 'duration' => "12", 'departure' => "5" 23 | end 24 | assert_raise ArgumentError do 25 | times 'interval' => "fred 3", 'duration' => "12", 'departure' => "5" 26 | end 27 | assert_raise ArgumentError do 28 | times 'interval' => "3 fred 3", 'duration' => "12", 'departure' => "5" 29 | end 30 | end 31 | 32 | def test_non_positive_interval_raises_runtime_error 33 | assert_raise RuntimeError do 34 | times 'interval' => "0", 'duration' => "12", 'departure' => "5" 35 | end 36 | assert_raise RuntimeError do 37 | times 'interval' => "-24", 'duration' => "12", 'departure' => "5" 38 | end 39 | end 40 | 41 | def test_missing_duration_raises_runtime_error 42 | assert_raise RuntimeError do 43 | times 'interval' => "12", 'departure' => "5" 44 | end 45 | end 46 | 47 | def test_non_integer_duration_raises_argument_error 48 | assert_raise ArgumentError do 49 | times 'interval' => "3", 'duration' => "sally", 'departure' => "5" 50 | end 51 | assert_raise ArgumentError do 52 | times 'interval' => "3", 'duration' => "sally 12", 'departure' => "5" 53 | end 54 | assert_raise ArgumentError do 55 | times 'interval' => "3", 'duration' => "12sally", 'departure' => "5" 56 | end 57 | end 58 | 59 | def test_non_positive_duration_raises_runtime_error 60 | assert_raise RuntimeError do 61 | times 'interval' => "3", 'duration' => "0", 'departure' => "5" 62 | end 63 | assert_raise RuntimeError do 64 | times 'interval' => "3", 'duration' => "-33", 'departure' => "5" 65 | end 66 | end 67 | 68 | def test_duration_not_a_multiple_of_interval_raises_runtime_error 69 | assert_raise RuntimeError do 70 | times 'interval' => "27", 'duration' => "12", 'departure' => "5" 71 | end 72 | end 73 | 74 | def test_missing_departure_raises_runtime_error 75 | assert_raise RuntimeError do 76 | times 'interval' => "3", 'duration' => "15" 77 | end 78 | end 79 | 80 | def test_non_integer_departure_raises_argument_error 81 | assert_raise ArgumentError do 82 | times 'interval' => "3", 'duration' => "12", 'departure' => "five" 83 | end 84 | assert_raise ArgumentError do 85 | times 'interval' => "3", 'duration' => "12", 'departure' => "five5" 86 | end 87 | assert_raise ArgumentError do 88 | times 'interval' => "3", 'duration' => "12", 'departure' => "5five" 89 | end 90 | end 91 | 92 | def test_non_positive_departure_raises_runtime_error 93 | assert_raise RuntimeError do 94 | times 'interval' => "3", 'duration' => "15", 'departure' => "0" 95 | end 96 | assert_raise RuntimeError do 97 | times 'interval' => "3", 'duration' => "33", 'departure' => "-15" 98 | end 99 | end 100 | 101 | def test_departure_not_a_multiple_of_interval_raises_runtime_error 102 | assert_raise RuntimeError do 103 | times 'interval' => "3", 'duration' => "12", 'departure' => "5" 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /ucalc/calc_controller.rb: -------------------------------------------------------------------------------- 1 | require 'calculator' 2 | require 'value' 3 | require 'dimension' 4 | 5 | class Calc_Controller 6 | 7 | def initialize calculator 8 | @calculator = calculator 9 | @calculated = false 10 | end 11 | 12 | def digit n 13 | @calculator.extend(n) 14 | end 15 | 16 | def unit arg 17 | if @calculator.is_calculated 18 | @calculator.pop 19 | @calculator.push(Value.new(0, arg)) 20 | else 21 | value = @calculator.top 22 | @calculator.pop 23 | value *= (Value.new 1, arg) 24 | @calculator.push value 25 | end 26 | 27 | @calculator.is_calculated = false 28 | end 29 | 30 | def push 31 | @calculator.push(Value.new(0, Dimension.new)) 32 | @calculator.is_calculated = false 33 | end 34 | 35 | def pop 36 | @calculator.pop 37 | end 38 | 39 | def cab 40 | a = @calculator.top 41 | @calculator.pop 42 | b = @calculator.top 43 | @calculator.pop 44 | c = @calculator.top 45 | @calculator.pop 46 | 47 | @calculator.push b 48 | @calculator.push a 49 | @calculator.push c 50 | 51 | @calculator.is_calculated = true 52 | end 53 | 54 | def swap 55 | @calculator.swap 56 | end 57 | 58 | def plus 59 | @calculator.plus 60 | end 61 | 62 | def subtract 63 | @calculator.minus 64 | end 65 | 66 | def times 67 | @calculator.times 68 | end 69 | 70 | def divide 71 | @calculator.divide 72 | end 73 | 74 | def plus_old 75 | @calculator.binary_op(lambda{|a,b| a+b}) 76 | end 77 | 78 | def to_s 79 | @calculator.to_s 80 | end 81 | 82 | end 83 | -------------------------------------------------------------------------------- /ucalc/calc_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'value' 2 | require 'calculator' 3 | require 'calc_controller' 4 | 5 | # Run with: spec calc_controller_spec.rb --format specdoc 6 | 7 | describe Calc_Controller, "for an empty calculator" do 8 | before do 9 | @calc = Calculator.new(Value.new(0, Dimension.new)) 10 | @controller = Calc_Controller.new @calc 11 | end 12 | 13 | it "extends digits" do 14 | @controller.digit 3 15 | @controller.digit 4 16 | @calc.to_s.should == '34' 17 | end 18 | 19 | it "clears number and extends units when the top is calculated" do 20 | @calc.push(Value.new(1, Dimension.new)) 21 | @calc.push(Value.new(2, Dimension.new)) 22 | @calc.plus 23 | @controller.unit(Dimension.new({"m"=>2})) 24 | @calc.to_s.should == '0*m^2' 25 | end 26 | 27 | it "extends units without clearing when the top was freshly pushed" do 28 | @calc.push(Value.new(7, Dimension.new)) 29 | @controller.unit(Dimension.new({"s"=>-1})) 30 | @calc.to_s.should == "7*1/s" 31 | end 32 | 33 | it "pushes an empty value" do 34 | @calc.push(Value.new(8, Dimension.new)) 35 | @controller.push 36 | @calc.to_s.should == '0' 37 | end 38 | 39 | it "can pop what was pushed" do 40 | @calc.push(Value.new(9, Dimension.new)) 41 | @controller.pop 42 | @controller.to_s.should == '0' 43 | end 44 | 45 | it "can rotate (cab) what's on the stack" do 46 | @calc.push 'c' 47 | @calc.push 'b' 48 | @calc.push 'a' 49 | @controller.to_s.should == 'a' # check setup 50 | 51 | @controller.cab 52 | @controller.to_s.should == 'c' 53 | @controller.pop 54 | @controller.to_s.should == 'a' 55 | @controller.pop 56 | @controller.to_s.should == 'b' 57 | end 58 | 59 | it "can swap the top two stack elements" do 60 | @calc.push 'b' 61 | @calc.push 'a' 62 | @controller.to_s.should == 'a' # check setup 63 | @controller.swap 64 | @controller.to_s.should == 'b' 65 | end 66 | 67 | it "can apply operations" do 68 | @calc.push(Value.new(10, Dimension.new)) 69 | @calc.push(Value.new(11, Dimension.new)) 70 | @controller.plus 71 | @calc.push(Value.new(9, Dimension.new)) 72 | @controller.subtract 73 | @calc.push(Value.new(3, Dimension.new)) 74 | @controller.times 75 | @calc.push(Value.new(4, Dimension.new)) 76 | @controller.divide 77 | @controller.to_s.should == '9' 78 | end 79 | 80 | end 81 | -------------------------------------------------------------------------------- /ucalc/calc_screen.rb: -------------------------------------------------------------------------------- 1 | require 'tk' 2 | require 'value' 3 | require 'calculator' 4 | require 'calc_controller' 5 | 6 | @my_font = TkFont.new('helvetica 20 bold') 7 | @calculator = Calculator.new(Value.new 0, Dimension.new) 8 | @controller = Calc_Controller.new @calculator 9 | 10 | def push 11 | @controller.push 12 | @my_text.value = @controller 13 | end 14 | 15 | def pop 16 | @controller.pop 17 | @my_text.value = @controller 18 | end 19 | 20 | def cab 21 | @controller.cab 22 | @my_text.value = @controller 23 | end 24 | 25 | def swap 26 | @controller.swap 27 | @my_text.value = @controller 28 | end 29 | 30 | def plus 31 | @controller.plus 32 | @my_text.value = @controller 33 | end 34 | 35 | def minus 36 | @controller.subtract 37 | @my_text.value = @controller 38 | end 39 | 40 | def times 41 | @controller.times 42 | @my_text.value = @controller 43 | end 44 | 45 | def divide 46 | @controller.divide 47 | @my_text.value = @controller 48 | end 49 | 50 | def extend_unit arg 51 | @controller.unit(arg) 52 | @my_text.value = @controller 53 | end 54 | 55 | def extend_number n 56 | @controller.digit(n) 57 | @my_text.value = @controller 58 | end 59 | 60 | def plus_old 61 | @calculator.binary_op(lambda{|a,b| a+b}) 62 | @my_text.value = @calculator 63 | end 64 | 65 | def make_button frame, name, p 66 | TkButton.new(frame, :text=>name, :font=>@my_font, :command =>p) 67 | end 68 | 69 | def make_digit root, number 70 | make_button(root, number, proc{extend_number number}) 71 | end 72 | 73 | def make_unit root, unit 74 | make_button(root, unit, proc{extend_unit unit}) 75 | end 76 | 77 | root = TkRoot.new { title "Calculator" } 78 | 79 | output_frame = TkFrame.new(root).pack( 80 | 'side'=>'top', 81 | 'padx'=>10, 82 | 'pady'=>10, 83 | 'fill'=>'both') 84 | 85 | button_frame = TkFrame.new(root).pack( 86 | 'side'=>'bottom', 87 | 'padx'=>10, 88 | 'pady'=>10) 89 | 90 | @my_text = TkVariable.new 91 | 92 | @calculated_result = TkEntry.new(output_frame) { 93 | width 75 94 | font @my_font 95 | state 'readonly' 96 | justify 'right' 97 | border 5 98 | }.pack( 99 | 'fill'=>'y', 100 | 'expand'=>'true') 101 | 102 | @calculated_result.textvariable = @my_text 103 | @my_text.value = @calculator 104 | 105 | b0 = make_digit(button_frame, 0) 106 | b1 = make_digit(button_frame, 1) 107 | b2 = make_digit(button_frame, 2) 108 | b3 = make_digit(button_frame, 3) 109 | b4 = make_digit(button_frame, 4) 110 | b5 = make_digit(button_frame, 5) 111 | b6 = make_digit(button_frame, 6) 112 | b7 = make_digit(button_frame, 7) 113 | b8 = make_digit(button_frame, 8) 114 | b9 = make_digit(button_frame, 9) 115 | 116 | bm = make_unit(button_frame, Dimension.new({'m'=>1})) 117 | b1m = make_unit(button_frame, Dimension.new({'m'=>-1})) 118 | bk = make_unit(button_frame, Dimension.new({'k'=>1})) 119 | b1k = make_unit(button_frame, Dimension.new({'k'=>-1})) 120 | bs = make_unit(button_frame, Dimension.new({'s'=>1})) 121 | b1s = make_unit(button_frame, Dimension.new({'s'=>-1})) 122 | 123 | b_plus = make_button(button_frame, '+', proc{plus}) 124 | b_minus = make_button(button_frame, '-', proc{minus}) 125 | b_times = make_button(button_frame, '*', proc{times}) 126 | b_divide = make_button(button_frame, '/', proc{divide}) 127 | 128 | b_push = make_button(button_frame, 'Push', proc{push}) 129 | b_pop = make_button(button_frame, 'Pop', proc{pop}) 130 | b_swap = make_button(button_frame, 'Swap', proc{swap}) 131 | b_cab = make_button(button_frame, 'CAB', proc{cab}) 132 | 133 | spaceholder = TkLabel.new(button_frame) 134 | 135 | buttons = [ 136 | b7, b8, b9, bm, b1m, b_plus, b_push, 137 | b4, b5, b6, bk, b1k, b_minus, b_pop, 138 | b1, b2, b3, bs, b1s, b_times, b_swap, 139 | spaceholder, b0, spaceholder, spaceholder, spaceholder, b_divide, b_cab] 140 | 141 | items_per_row = 7 142 | 143 | buttons.each_index { |i| 144 | buttons[i].grid( 145 | 'column'=>(i%items_per_row), 146 | 'row'=>(i/items_per_row), 147 | 'sticky'=>'news', 148 | 'padx'=>5, 149 | 'pady'=>5) 150 | } 151 | 152 | Tk.mainloop -------------------------------------------------------------------------------- /ucalc/calculator.rb: -------------------------------------------------------------------------------- 1 | require 'value' 2 | 3 | class Calculator 4 | attr_accessor :is_calculated 5 | 6 | def initialize start 7 | @default = start #Value.new 0, Dimension.new 8 | @stack = [] 9 | @is_calculated = true 10 | end 11 | 12 | def default 13 | @default.clone 14 | end 15 | 16 | def top 17 | return default if @stack.size < 1 18 | @stack[-1] 19 | end 20 | 21 | def push value 22 | @is_calculated = false 23 | @stack.push value 24 | end 25 | 26 | def extend value 27 | start = @is_calculated ? default : top 28 | pop 29 | push start.extend(value) 30 | end 31 | 32 | def pop 33 | @is_calculated = true 34 | @stack.pop 35 | end 36 | 37 | def plus 38 | v2 = @stack.pop 39 | v1 = @stack.pop 40 | begin 41 | result = v1 + v2 42 | rescue 43 | result = default 44 | end 45 | 46 | @stack.push(result) 47 | @is_calculated = true 48 | self 49 | end 50 | 51 | def minus 52 | v2 = @stack.pop 53 | v1 = @stack.pop 54 | begin 55 | result = v1 - v2 56 | rescue 57 | result = default 58 | end 59 | 60 | @stack.push(result) 61 | @is_calculated = true 62 | self 63 | end 64 | 65 | def times 66 | v2 = @stack.pop 67 | v1 = @stack.pop 68 | begin 69 | result = v1 * v2 70 | rescue 71 | result = default 72 | end 73 | 74 | @stack.push(result) 75 | @is_calculated = true 76 | self 77 | end 78 | 79 | def divide 80 | v2 = @stack.pop 81 | v1 = @stack.pop 82 | begin 83 | result = v1 / v2 84 | rescue 85 | result = default 86 | end 87 | 88 | @stack.push(result) 89 | @is_calculated = true 90 | self 91 | end 92 | 93 | def binary_op_old op 94 | v2 = @stack.pop 95 | v1 = @stack.pop 96 | begin 97 | result = op.call(v1,v2) 98 | rescue 99 | result = default 100 | end 101 | 102 | @stack.push(result) 103 | @is_calculated = true 104 | self 105 | end 106 | 107 | def swap 108 | a = top 109 | pop 110 | b = top 111 | pop 112 | 113 | push a 114 | push b 115 | 116 | @is_calculated = true 117 | end 118 | 119 | def to_s 120 | top.to_s 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /ucalc/calculator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'value' 2 | require 'calculator' 3 | 4 | # Run with: spec calculator_spec.rb --format specdoc 5 | 6 | describe Calculator, "for an empty calculator" do 7 | before do 8 | @calc = Calculator.new(Value.new(0, Dimension.new)) 9 | end 10 | 11 | it "starts out considered to be calculated" do 12 | @calc.is_calculated.should == true 13 | end 14 | 15 | it "has the default value all the way down" do 16 | (@calc.top).should == Value.new(0, Dimension.new) 17 | @calc.pop 18 | (@calc.top).should == Value.new(0, Dimension.new) 19 | end 20 | 21 | it "has values you push" do 22 | @calc.push 20 23 | (@calc.top).should == 20 24 | @calc.is_calculated.should == false 25 | end 26 | 27 | it "can pop what was pushed" do 28 | @calc.push 17 29 | @calc.push 19 30 | @calc.push 20 31 | @calc.pop 32 | @calc.pop 33 | (@calc.top).should == 17 34 | end 35 | 36 | it "treats a pushed-and-popped item as newly calculated" do 37 | @calc.push 17 38 | @calc.push 18 39 | @calc.pop 40 | @calc.is_calculated.should == true 41 | end 42 | 43 | end 44 | 45 | describe Calculator, "for a calculator with values" do 46 | before do 47 | @calc = Calculator.new(0) 48 | @calc.push 2 49 | @calc.push 15 50 | @calc.push 5 51 | end 52 | 53 | it "can add" do 54 | @calc.plus 55 | @calc.plus 56 | @calc.top.should == 22 57 | @calc.is_calculated.should == true 58 | end 59 | 60 | it "can subtract" do 61 | @calc.minus 62 | @calc.top.should == 10 63 | @calc.is_calculated.should == true 64 | end 65 | 66 | it "can multiply" do 67 | @calc.times 68 | @calc.top.should == 75 69 | @calc.is_calculated.should == true 70 | end 71 | 72 | it "can divide" do 73 | @calc.divide 74 | @calc.top.should == 3 75 | @calc.is_calculated.should == true 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /ucalc/dimension.rb: -------------------------------------------------------------------------------- 1 | class Dimension 2 | attr_reader :dimensions 3 | 4 | def initialize unit2int={} 5 | @dimensions = new_hash(unit2int) 6 | end 7 | 8 | def clone 9 | Dimension.new(new_hash(@dimensions)) 10 | end 11 | 12 | def ==(other) 13 | return dimensions == other.dimensions 14 | end 15 | 16 | def *(other) 17 | new_dimensions = new_hash(dimensions) 18 | other.dimensions.each_pair { 19 | |key, value| 20 | sum = dimensions[key] + value 21 | new_dimensions[key] = sum 22 | new_dimensions.delete(key) if sum == 0 23 | } 24 | Dimension.new new_dimensions 25 | end 26 | 27 | def -@ 28 | new_dimensions = new_hash(dimensions) 29 | dimensions.each_pair{ 30 | |key, value| 31 | new_dimensions[key] = -value 32 | } 33 | Dimension.new new_dimensions 34 | end 35 | 36 | def /(other) 37 | self * -other 38 | end 39 | 40 | def to_s 41 | return "" if dimensions.size == 0 42 | 43 | positives = "" 44 | negatives = "" 45 | 46 | dimensions.each{|key, value| 47 | positives += '*' + format(key, value) if value > 0 48 | negatives += '*' + format(key, -value) if value < 0 49 | } 50 | 51 | if (positives.length == 0) 52 | positives = "1" 53 | else 54 | positives = positives[1..-1] 55 | end 56 | 57 | if (negatives.length > 0) 58 | negatives = negatives[1..-1] 59 | end 60 | 61 | return positives if (negatives.length == 0) 62 | return positives + "/" + negatives 63 | end 64 | 65 | def format key, value 66 | return key if value == 1 67 | return key + "^" + value.to_s 68 | end 69 | 70 | private 71 | def new_hash initial_value 72 | result = Hash.new{|hash, key| hash[key] = 0 } 73 | result.merge!(initial_value) 74 | result 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /ucalc/value.rb: -------------------------------------------------------------------------------- 1 | require 'dimension' 2 | 3 | class Value 4 | attr_reader :number, :dimension 5 | 6 | def initialize number, dimension 7 | @number = number 8 | @dimension = dimension 9 | end 10 | 11 | def clone 12 | Value.new(@number, @dimension.clone) 13 | end 14 | 15 | def extend v 16 | return Value.new(number * 10 + v, dimension) if v.kind_of? Integer 17 | return Value.new(number, dimension * v) 18 | end 19 | 20 | def +(other) 21 | raise "can't mix apples and oranges" if dimension != other.dimension 22 | Value.new(number + other.number, dimension) 23 | end 24 | 25 | def -(other) 26 | raise "can't mix apples and oranges" if dimension != other.dimension 27 | Value.new(number - other.number, dimension) 28 | end 29 | 30 | def *(other) 31 | Value.new(number * other.number, dimension * other.dimension) 32 | end 33 | 34 | def /(other) 35 | Value.new(number / other.number, dimension / other.dimension) 36 | end 37 | 38 | def ==(other) 39 | (number == other.number) and (dimension == other.dimension) 40 | end 41 | 42 | def dimension 43 | @dimension 44 | end 45 | 46 | def to_s 47 | suffix = @dimension.to_s 48 | return @number.to_s if suffix.size == 0 49 | @number.to_s + '*' + @dimension.to_s 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /ucalc/value_spec.rb: -------------------------------------------------------------------------------- 1 | require 'value' 2 | 3 | # Run with: spec value_spec.rb --format specdoc 4 | 5 | describe Value, "for dimension-less numbers" do 6 | before do 7 | @v1 = Value.new 8, Dimension.new 8 | @v2 = Value.new 4, Dimension.new 9 | end 10 | 11 | it "computes like normal arithmetic" do 12 | (@v1 + @v2).should == (Value.new 12, Dimension.new) 13 | (@v1 - @v2).should == (Value.new 4, Dimension.new) 14 | (@v1 * @v2).should == (Value.new 32, Dimension.new) 15 | (@v1 / @v2).should == (Value.new 2, Dimension.new) 16 | end 17 | 18 | it "just returns the number as its string form" do 19 | @v1.to_s.should == '8' 20 | end 21 | end 22 | 23 | describe Value, "for compatible dimensions" do 24 | before do 25 | @v1 = Value.new 8, Dimension.new({"m"=>2}) 26 | @v2 = Value.new 4, Dimension.new({"m"=>2}) 27 | end 28 | 29 | it "addition and subtraction have the same dimension" do 30 | (@v1 + @v2).should == (Value.new 12, Dimension.new({"m"=>2})) 31 | (@v1 - @v2).should == (Value.new 4, Dimension.new({"m"=>2})) 32 | end 33 | 34 | it "multiplication results in product of dimensions" do 35 | (@v1 * @v2).should == (Value.new 32, Dimension.new({"m"=>4})) 36 | end 37 | 38 | 39 | it "division results in division of dimensions" do 40 | (@v1 / @v2).should == (Value.new 2, Dimension.new) 41 | end 42 | 43 | end 44 | 45 | describe Value, "for incompatible dimensions" do 46 | before do 47 | @v1 = Value.new 8, Dimension.new({"m"=>2}) 48 | @v2 = Value.new 4, Dimension.new({"s"=>-3}) 49 | end 50 | 51 | it "error if addition and subtraction have differing dimensions" do 52 | lambda{(@v1 + @v2)}.should raise_error 53 | lambda{(@v1 - @v2)}.should raise_error 54 | end 55 | 56 | it "multiplication should multiply dimensions" do 57 | (@v1 * @v2).should == (Value.new 32, Dimension.new({"m"=>2, "s"=>-3})) 58 | end 59 | 60 | it "division should invert and multiply dimensions" do 61 | (@v1 / @v2).should == (Value.new 2, Dimension.new({"m"=>2, "s"=>3})) 62 | end 63 | 64 | it "should multiply number times dimension for string form" do 65 | @v1.to_s.should == '8*m^2' 66 | end 67 | end 68 | 69 | describe Value, "for unreduced dimensions" do 70 | before do 71 | @v1 = Value.new 18, Dimension.new({"t"=>1}) 72 | @v2 = Value.new 3, Dimension.new({"t"=>-2}) 73 | end 74 | 75 | it "should have computed empty dimension be equivalent to a new empty dimension" do 76 | (@v1 / @v1).should == (Value.new 1, Dimension.new) 77 | end 78 | 79 | it "should have computed complex dimension be equivalent to a reduced dimension" do 80 | ((@v1 * @v2) * @v2).should == (Value.new 162, Dimension.new({"t"=>-3})) 81 | end 82 | end 83 | 84 | describe Value, "to extend" do 85 | before do 86 | @v1 = Value.new(8, Dimension.new({"t"=>2})) 87 | end 88 | 89 | it "should grow with either number or dimension" do 90 | @v1.extend(4).should == (Value.new 84, Dimension.new({"t"=>2})) 91 | @v1.extend(Dimension.new({"m"=>-1})).should == (Value.new 8, Dimension.new({"t"=>2, "m"=>-1})) 92 | end 93 | end 94 | --------------------------------------------------------------------------------