Skip to content
Merged
Show file tree
Hide file tree
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
2 changes: 1 addition & 1 deletion exercises/alphametics/.version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3
4
100 changes: 55 additions & 45 deletions exercises/alphametics/alphametics_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,64 +3,75 @@
require 'minitest/autorun'
require_relative 'alphametics'

# Test data version:
# 8d8589f
# Test data version: 9dab356
class AlphameticsTest < Minitest::Test
def test_solve_short_puzzle

def test_puzzle_with_three_letters
# skip
expect = {
'I' => 1, 'B' => 9, 'L' => 0
}
actual = Alphametics.new.solve('I + BB == ILL')
assert_equal(expect, actual)
input = 'I + BB == ILL'
expected = { 'B' => 9, 'I' => 1, 'L' => 0 }
assert_equal expected, Alphametics.solve(input)
Copy link
Copy Markdown
Contributor Author

@Insti Insti Oct 24, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was: Alphametics.new.solve(input)
Now: Alphametics.solve(input)
Instantiating an object is unnecessary, so it now uses a instance class method.

Edit: It's NOT using an instance method.

Copy link
Copy Markdown
Contributor

@moveson moveson Oct 28, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the final test, 'AND + A + STRONG + OFFENSE + AS + A + GOOD = DEFENSE', the = should be ==. Also, in the commentary, optimsing should be optimising.

end

# This test has been commented out due its long runtime.
# def test_solve_long_puzzle
# skip
# expect = {
# 'S' => 9, 'E' => 5, 'N' => 6, 'D' => 7,
# 'M' => 1, 'O' => 0, 'R' => 8, 'Y' => 2
# }
# actual = Alphametics.new.solve('SEND + MORE == MONEY')
# assert_equal(expect, actual)
# end

def test_solution_must_have_unique_value_for_each_letter
skip
expect = nil
actual = Alphametics.new.solve('A == B')
assert_equal(expect, actual)
input = 'A == B'
expected = {}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invalid solutions now expect an empty hash rather than a nil value.
This may be controversial, but I strongly believe that a method should always return the same type of object.

assert_equal expected, Alphametics.solve(input)
end

def test_leading_zero_solution_is_invalid
skip
expect = nil
actual = Alphametics.new.solve('ACA + DD == BD')
assert_equal(expect, actual)
input = 'ACA + DD == BD'
expected = {}
assert_equal expected, Alphametics.solve(input)
end

def test_solve_puzzle_with_four_words
def test_puzzle_with_four_letters
skip
expect = {
'E' => 4, 'G' => 2, 'H' => 5, 'I' => 0,
'L' => 1, 'S' => 9, 'T' => 7
}
actual = Alphametics.new.solve('HE + SEES + THE == LIGHT')
assert_equal(expect, actual)
input = 'AS + A == MOM'
expected = { 'A' => 9, 'M' => 1, 'O' => 0, 'S' => 2 }
assert_equal expected, Alphametics.solve(input)
end

# This test has been commented out due its long runtime.
# def test_solve_puzzle_with_many_words
# skip
# expect = {
# 'A' => 5, 'D' => 3, 'E' => 4, 'F' => 7,
# 'G' => 8, 'N' => 0, 'O' => 2, 'R' => 1,
# 'S' => 6, 'T' => 9
# }
# actual = Alphametics.new.solve('AND + A + STRONG + OFFENSE + AS + A + GOOD = DEFENSE')
# assert_equal(expect, actual)
# end
def test_puzzle_with_six_letters
skip
input = 'NO + NO + TOO == LATE'
expected = { 'A' => 0, 'E' => 2, 'L' => 1, 'N' => 7,
'O' => 4, 'T' => 9 }
assert_equal expected, Alphametics.solve(input)
end

def test_puzzle_with_seven_letters
skip
input = 'HE + SEES + THE == LIGHT'
expected = { 'E' => 4, 'G' => 2, 'H' => 5, 'I' => 0,
'L' => 1, 'S' => 9, 'T' => 7 }
assert_equal expected, Alphametics.solve(input)
end

# The obvious algorithm can take a long time to solve this puzzle,
# but an optimised solution can solve it fairly quickly.
# (It's OK to submit your solution without getting this test to pass.)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the wording of this comment OK?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.

def test_puzzle_with_eight_letters
skip
input = 'SEND + MORE == MONEY'
expected = { 'D' => 7, 'E' => 5, 'M' => 1, 'N' => 6,
'O' => 0, 'R' => 8, 'S' => 9, 'Y' => 2 }
assert_equal expected, Alphametics.solve(input)
end

# The obvious algorithm can take a long time to solve this puzzle,
# but an optimised solution can solve it fairly quickly.
# (It's OK to submit your solution without getting this test to pass.)
def test_puzzle_with_ten_letters
skip
input = 'AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE'
expected = { 'A' => 5, 'D' => 3, 'E' => 4, 'F' => 7,
'G' => 8, 'N' => 0, 'O' => 2, 'R' => 1,
'S' => 6, 'T' => 9 }
assert_equal expected, Alphametics.solve(input)
end

# Problems in exercism evolve over time, as we find better ways to ask
# questions.
Expand All @@ -78,9 +89,8 @@ def test_solve_puzzle_with_four_words
#
# If you are curious, read more about constants on RubyDoc:
# http://ruby-doc.org/docs/ruby-doc-bundle/UsersGuide/rg/constants.html

def test_bookkeeping
skip
assert_equal 3, BookKeeping::VERSION
assert_equal 4, BookKeeping::VERSION
end
end
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
13 changes: 7 additions & 6 deletions exercises/alphametics/example.tt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ gem 'minitest', '>= 5.0.0'
require 'minitest/autorun'
require_relative 'alphametics'

# Test data version:
# <%= sha1 %>
class AlphameticsTest < Minitest::Test<% test_cases.each do |test_case| %>
# Test data version: <%= sha1 %>
class AlphameticsTest < Minitest::Test
<% test_cases.each do |test_case| %>

<%= test_case.runtime_comment %>
def <%= test_case.test_name %>
<%= test_case.skipped %>
expect = <%= test_case.expect %>
actual = <%= test_case.work_load %>
assert_equal(expect, actual)
<%= test_case.workload %>
end
<% end %>

<%= IO.read(XRUBY_LIB + '/bookkeeping.md') %>
def test_bookkeeping
skip
Expand Down
Loading