From fb3ce530859562aa922931696d48d3861880873d Mon Sep 17 00:00:00 2001 From: Michael Thelander Date: Fri, 13 Apr 2012 09:23:42 -0700 Subject: [PATCH] RubyQuiz #7: Countdown --- countdown/countdown.rb | 111 ++++++++++++++++++++++++++++++++++++ countdown/countdown_spec.rb | 51 +++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 countdown/countdown.rb create mode 100644 countdown/countdown_spec.rb diff --git a/countdown/countdown.rb b/countdown/countdown.rb new file mode 100644 index 0000000..83f95f1 --- /dev/null +++ b/countdown/countdown.rb @@ -0,0 +1,111 @@ +module RubyQuiz + class Countdown + attr_reader :tree + + def initialize(target, source) + @target, @source, = target.to_f, source.map(&:to_f) + + @tree = Node.new(0, :+) + @source.product([:+]).each do |n| + next unless should_continue?(n) + @tree << build(Node.new(*n), @source) + end + end + + def operators + @operators ||= [:+, :-, :*, :/] + end + + def build(node, possibilities, depth = 1) + node.tap do |node| + sub_possibilities = possibilities.reject { |n| n == node.value } + return node if sub_possibilities.empty? + sub_possibilities.product(operators()) do |p| + next unless should_continue?(p) + node << build(Node.new(p.first, p.last), sub_possibilities, depth + 1) + end + end + end + + def should_continue?(p) + return false if p.empty? + case p.first + when 1 then ![:*, :/].include?(p.last) + when 0 then ![:-, :+].include?(p.last) + else true + end + end + + def evaluate(solution, depth = 0) + while (i = solution.rindex(?()) + j = solution[i..solution.size-1].index(?)) + solution[i..j] = _eval(solution[i+1..j-1]) + end + _eval(reduce(solution)) + end + + def _eval(solution) + solution = reduce(solution) + op = :+ + solution.reduce(0) do |result, term| + case term + when Fixnum then result = result.send(op, term) + when Symbol then op = term; result + end + end + end + + def reduce(terms) + 0.upto(terms.size) do |i| + a, op, b = terms[i..i+2] + if [:*, :/].include?(op) + terms[i..i+2] = a.send(op, b) + end + end + + terms + end + + def solve + find_solution(@tree, '') + end + + def find_solution(node, solution, depth = 0) + solution.chop! if solution =~ /.*[-+*\/]$/ + return solution if valid_solution?(solution) + + node.children.each do |child| + parenthesized = solution.empty? ? '' : "(#{solution})" + s = find_solution(child, child.term + parenthesized, depth + 1) + return s if valid_solution?(s) + end + + solution + end + + def valid_solution?(expression) + eval(expression) == @target + end + end + + class Node + attr_accessor :children, :visited + attr_reader :value, :op + + def initialize(value, op, children = []) + @value, @op, @children = value, op, children + end + + def <<(node) + @children << node + end + + def to_s + "#@op #@value [ #@children ]" + end + + def term + "#@value#@op" + end + end +end diff --git a/countdown/countdown_spec.rb b/countdown/countdown_spec.rb new file mode 100644 index 0000000..88b6267 --- /dev/null +++ b/countdown/countdown_spec.rb @@ -0,0 +1,51 @@ +require '~/work/rubyquiz/countdown/countdown.rb' +require 'benchmark' +include RubyQuiz + +describe Countdown do + describe '#build' do + it 'should build a tree' do + countdown = Countdown.new(3, [1, 2]) + countdown.tree.should.to_s == Node.new(0, :+, [ + [ 1.0, :+, [ Node.new(2.0, :+), Node.new(2.0, :-), Node.new(2.0, :*), Node.new(2.0, :/) ] ], + [ 2.0, :+, [ Node.new(1.0, :+), Node.new(1.0, :-), Node.new(1.0, :*), Node.new(1.0, :/) ] ], + ]).to_s + end + end + + describe '#solve' do + it 'should calculate a solution' do + target = 522 + countdown = Countdown.new(target, [100, 5, 5, 2, 6, 8]) + solution = countdown.solve + p solution + solution.should_not be_nil + eval(solution).should == target + end + end + + describe '#evaluate' do + it 'should evaluate an expression faster than eval' do + countdown = Countdown.new(3, [1, 3]) + countdown.evaluate([3, :+, 9]).should == 12 + countdown._eval([3, :+, 9]).should == 12 + countdown.evaluate([?(, 3, :+, 9, ?), :*, 12]).should == 144 + time = Benchmark.realtime do + 1000.times { countdown.evaluate([?(, 3, :+, 9, ?), :*, 12]) } + end + puts "Countdown#evaluate time: #{time}ms" + + time = Benchmark.realtime do + 1000.times { eval "(3+9)*12" } + end + puts "eval time: #{time}ms" + end + end + + describe '#reduce' do + it 'should eliminate multiplication and division' do + countdown = Countdown.new(3, [1, 3]) + countdown.reduce([3, :+, 8, :*, 9, :+, 2, :/, 2]).should == [3, :+, 72, :+, 1] + end + end +end