Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 100 additions & 63 deletions exercises/alphametics/example.rb
Original file line number Diff line number Diff line change
@@ -1,98 +1,135 @@
module BookKeeping
VERSION = 3
VERSION = 4
end

class Alphametics
def solve(puzzle)
letters = Hash.new(0)
puzzle.scan(/[a-zA-Z]/) { |w| letters[w] += 1 }

possible_values(letters.keys) do |letters_values|
return letters_values if valid?(puzzle, letters_values)
end
def self.solve(equation)
new.solve(equation)
end

nil
def solve(equation)
@prime_solver = AlphaSolver.new(equation)
solve_using_partials
end

private

def possible_values(letters)
(0..9).to_a.combination(letters.length) do |combined_integers|
combined_integers.permutation do |permutated_integers|
yield permutated_integers.map.with_index { |integer, index|
[letters[index], integer]
}.to_h
end
attr_accessor :prime_solver

def solve_using_partials
prime_solver.partial_solutions.each do |partial_solution|
sub_solver = AlphaSolver.new(prime_solver.partial_equation(partial_solution))
sub_solution = sub_solver.first_solution
return sub_solution.merge(partial_solution) if sub_solution
end
{}
end

def valid?(puzzle, letters_values)
equation = puzzle.gsub(/[a-zA-Z]/, letters_values)
Equation.new(equation).valid?
end
end

class Equation
attr_reader :equation
class AlphaSolver

def initialize(equation)
@equation = equation
def initialize(input_equation)
@input_equation = input_equation.gsub('^', '**')
@puzzle = Puzzle.new(input_equation)
end

def valid?
return false if has_leading_zeros?
def partial_solutions
AlphaSolver.new(puzzle.simplified).all_solutions
end

expression, result = equation.split('==')
def all_solutions
numeric_permutations.map { |values| result_table if solution?(values) }.compact
end

numbers = []
operators = []
def first_solution
numeric_permutations.each { |values| return result_table if solution?(values) }
nil
end

expression.scan(/\d+|\+|\-|\*|\/|\^/).each do |token|
case token
when /^\d+$/
numbers.push(token.to_i)
when '+', '-', '*', '/', '^'
calculate_last(numbers, operators) if has_precedence?(operators, token)
operators.push(token)
end
end
def partial_equation(partial_solution)
input_equation.tr(partial_solution.keys.join, partial_solution.values.join)
end

until operators.empty?
calculate_last(numbers, operators)
end
private

attr_reader :input_equation, :puzzle
attr_accessor :proposed_values

numbers.last == result.to_i
def solution?(values)
self.proposed_values = values.join
proposed_equation_qualified? && proposed_equation_evaluates?
end

private
def proposed_equation
input_equation.tr(puzzle_letters, proposed_values)
end

def has_leading_zeros?
equation.match(/^0\d+|\D0\d+/)
def numeric_permutations
puzzle.numeric_permutations
end

def has_precedence?(operators, token)
return false if operators.empty?
prev_operator = operators.last
def puzzle_letters
puzzle.letters
end

case token
when '+', '-'
prev_operator == '*' || prev_operator == '/' || prev_operator == '^'
when '*', '/'
prev_operator == '^'
else
false
end
def proposed_equation_qualified?
(proposed_equation =~ /\b0\d+/).nil?
end

def calculate_last(numbers, operators)
right = numbers.pop
left = numbers.pop
operator = as_ruby_operator(operators.pop)
result = left.send(operator, right)
numbers.push(result)
def proposed_equation_evaluates?
eval(proposed_equation)
end

def as_ruby_operator(operator)
operator == '^' ? '**' : operator
def result_table
Hash[puzzle_letters.chars.zip(result_numbers)]
end

def result_numbers
proposed_values.chars.map(&:to_i)
end

end

class Puzzle

PATTERNS = {mod_10: ' % 10',
adjacent_letters: /(\b)([A-Z]{1,})([A-Z])/,
equation_left_side: /(.*)( == )/}

def initialize(string_equation)
@string_equation = string_equation
end

def letters
@letters ||= string_equation.scan(/[A-Z]/).uniq.join
end

def numeric_permutations
@numeric_permutations ||= unused_numbers.to_a.permutation(letter_count)
end

def simplified
@simplified ||= string_equation
.gsub(PATTERNS[:adjacent_letters], "\\1\\3")
.gsub(PATTERNS[:equation_left_side], "(\\1)#{PATTERNS[:mod_10]}\\2")
end

private

attr_reader :string_equation

def letter_count
@letter_count ||= letters.length
end

def unused_numbers
@unused_numbers ||= (0..9).to_a.map(&:to_s) - used_numbers
end

def used_numbers
@used_numbers ||= string_equation.gsub(PATTERNS[:mod_10], '').scan(/\d/).uniq
end

end