-
-
Notifications
You must be signed in to change notification settings - Fork 531
[alphametics] Warn about long running tests in the common test data. #469
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5f4917d
336a911
ea2ad66
a270ac3
6286608
3ded5e9
a5dbabe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| 3 | ||
| 4 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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) | ||
| 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 = {} | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Invalid solutions now expect an empty hash rather than a |
||
| 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.) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the wording of this comment OK?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
|
@@ -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 | ||
| 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 |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
instanceclass method.Edit: It's NOT using an instance method.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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,optimsingshould beoptimising.