Skip to content

Commit

Permalink
RubyQuiz #7: Countdown
Browse files Browse the repository at this point in the history
  • Loading branch information
mthelander committed Apr 13, 2012
1 parent 7453d0f commit fb3ce53
Show file tree
Hide file tree
Showing 2 changed files with 162 additions and 0 deletions.
111 changes: 111 additions & 0 deletions 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
51 changes: 51 additions & 0 deletions 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

0 comments on commit fb3ce53

Please sign in to comment.