From 990ec8b39e9fd206986337ec7c7b46444c8b7a72 Mon Sep 17 00:00:00 2001 From: Shanthi Pendleton Date: Tue, 25 Oct 2016 15:20:56 +0100 Subject: [PATCH] Fast alphametics solver (uses simplified partial equations to limit set of viable permutations) --- exercises/allergies/allergies_test.rb | 1 - exercises/alphametics/example.rb | 163 +++++++++++------- exercises/atbash-cipher/atbash_cipher_test.rb | 1 - exercises/beer-song/beer_song_test.rb | 3 +- .../binary_search_tree_test.rb | 2 +- exercises/binary/example.rb | 1 - .../circular-buffer/circular_buffer_test.rb | 2 - exercises/clock/example.rb | 2 - exercises/connect/connect_test.rb | 1 - exercises/custom-set/custom_set_test.rb | 1 - exercises/diamond/diamond_test.rb | 1 - exercises/etl/etl_test.rb | 2 +- exercises/house/house_test.rb | 1 - exercises/largest-series-product/example.tt | 3 - .../largest_series_product_test.rb | 4 - exercises/linked-list/linked_list_test.rb | 2 +- exercises/meetup/meetup_test.rb | 1 - exercises/nth-prime/example.tt | 2 - exercises/nth-prime/nth_prime_test.rb | 2 - exercises/ocr-numbers/ocr_numbers_test.rb | 3 +- .../protein_translation_test.rb | 1 - exercises/proverb/proverb_test.rb | 4 +- exercises/queen-attack/queen_attack_test.rb | 6 +- .../robot-simulator/robot_simulator_test.rb | 2 +- exercises/strain/strain_test.rb | 4 +- 25 files changed, 111 insertions(+), 104 deletions(-) diff --git a/exercises/allergies/allergies_test.rb b/exercises/allergies/allergies_test.rb index e7f6b45aee..9a60b6559a 100755 --- a/exercises/allergies/allergies_test.rb +++ b/exercises/allergies/allergies_test.rb @@ -59,7 +59,6 @@ def test_allergic_to_lots_of_stuff def test_allergic_to_everything skip allergies = Allergies.new(255) - # rubocop:disable Metrics/LineLength expected = %w(eggs peanuts shellfish strawberries tomatoes chocolate pollen cats) assert_equal expected, allergies.list end diff --git a/exercises/alphametics/example.rb b/exercises/alphametics/example.rb index 2a20e14b26..49446d4326 100644 --- a/exercises/alphametics/example.rb +++ b/exercises/alphametics/example.rb @@ -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 \ No newline at end of file diff --git a/exercises/atbash-cipher/atbash_cipher_test.rb b/exercises/atbash-cipher/atbash_cipher_test.rb index 81cf01b539..d2c0dc5536 100755 --- a/exercises/atbash-cipher/atbash_cipher_test.rb +++ b/exercises/atbash-cipher/atbash_cipher_test.rb @@ -3,7 +3,6 @@ require 'minitest/autorun' require_relative 'atbash_cipher' -# rubocop:disable Style/MethodName class AtbashTest < Minitest::Test def test_encode_no assert_equal 'ml', Atbash.encode('no') diff --git a/exercises/beer-song/beer_song_test.rb b/exercises/beer-song/beer_song_test.rb index 11c51c61ef..01fc5614b7 100755 --- a/exercises/beer-song/beer_song_test.rb +++ b/exercises/beer-song/beer_song_test.rb @@ -3,7 +3,6 @@ require 'minitest/autorun' require_relative 'beer_song' -# rubocop:disable Metrics/LineLength class BeerSongTest < Minitest::Test def test_the_first_verse expected = "99 bottles of beer on the wall, 99 bottles of beer.\n" \ @@ -62,7 +61,7 @@ def test_a_few_verses assert_equal expected, BeerSong.new.verses(2, 0) end - def test_the_whole_song # rubocop:disable Metrics/MethodLength + def test_the_whole_song skip expected = <<-SONG 99 bottles of beer on the wall, 99 bottles of beer. diff --git a/exercises/binary-search-tree/binary_search_tree_test.rb b/exercises/binary-search-tree/binary_search_tree_test.rb index 3ae1496d55..16b6ae9b61 100755 --- a/exercises/binary-search-tree/binary_search_tree_test.rb +++ b/exercises/binary-search-tree/binary_search_tree_test.rb @@ -32,7 +32,7 @@ def test_inserting_right assert_equal 5, four.right.data end - def test_complex_tree # rubocop:disable Metrics/MethodLength + def test_complex_tree skip four = Bst.new 4 four.insert 2 diff --git a/exercises/binary/example.rb b/exercises/binary/example.rb index 80464cd24b..02847622bb 100644 --- a/exercises/binary/example.rb +++ b/exercises/binary/example.rb @@ -19,7 +19,6 @@ def to_decimal private - # rubocop:disable Style/WordArray def valid?(s) s.chars.all? { |char| ['0', '1'].include?(char) } end diff --git a/exercises/circular-buffer/circular_buffer_test.rb b/exercises/circular-buffer/circular_buffer_test.rb index 86bed08721..725daf591b 100755 --- a/exercises/circular-buffer/circular_buffer_test.rb +++ b/exercises/circular-buffer/circular_buffer_test.rb @@ -89,7 +89,6 @@ def test_forced_writes_to_non_full_buffer_should_behave_like_writes assert_raises(CircularBuffer::BufferEmptyException) { buffer.read } end - # rubocop:disable Metrics/MethodLength def test_alternate_read_and_write_into_buffer_overflow skip buffer = CircularBuffer.new(5) @@ -108,5 +107,4 @@ def test_alternate_read_and_write_into_buffer_overflow assert_equal 'B', buffer.read assert_raises(CircularBuffer::BufferEmptyException) { buffer.read } end - # rubocop:enable Metrics/MethodLength end diff --git a/exercises/clock/example.rb b/exercises/clock/example.rb index 579b7a8d02..fb26f26421 100644 --- a/exercises/clock/example.rb +++ b/exercises/clock/example.rb @@ -12,7 +12,6 @@ def initialize(hours=0, minutes=0) @internal = hours * 60 + minutes end - # rubocop:disable Style/OpMethod def +(hours=0, minutes) @internal += hours * 60 + minutes self @@ -21,7 +20,6 @@ def +(hours=0, minutes) def -(*args) self.+(*args.map(&:-@)) end - # rubocop:enable Style/OpMethod def ==(other) to_s == other.to_s diff --git a/exercises/connect/connect_test.rb b/exercises/connect/connect_test.rb index 82b65c5b41..a517fe948f 100755 --- a/exercises/connect/connect_test.rb +++ b/exercises/connect/connect_test.rb @@ -95,7 +95,6 @@ def test_x_wins_using_a_convoluted_path assert_equal 'X', game.winner, 'X wins using a convoluted path' end - # rubocop:disable MethodLength def test_x_wins_using_a_spiral_path skip board = [ diff --git a/exercises/custom-set/custom_set_test.rb b/exercises/custom-set/custom_set_test.rb index ba89640a1b..7a06d402a5 100755 --- a/exercises/custom-set/custom_set_test.rb +++ b/exercises/custom-set/custom_set_test.rb @@ -204,7 +204,6 @@ def test_intersection_of_two_sets_with_no_shared_elements_is_an_empty_set assert_equal expected, set2.intersection(set1) end - # rubocop:disable Metrics/LineLength def test_intersection_of_two_sets_with_shared_elements_is_a_set_of_the_shared_elements skip set1 = CustomSet.new [1, 2, 3, 4] diff --git a/exercises/diamond/diamond_test.rb b/exercises/diamond/diamond_test.rb index ab2a1ada2e..9649200075 100755 --- a/exercises/diamond/diamond_test.rb +++ b/exercises/diamond/diamond_test.rb @@ -1,4 +1,3 @@ -# rubocop:disable Metrics/MethodLength # !/usr/bin/env ruby gem 'minitest', '>= 5.0.0' require 'minitest/autorun' diff --git a/exercises/etl/etl_test.rb b/exercises/etl/etl_test.rb index a53c50caef..2a8b81d02c 100755 --- a/exercises/etl/etl_test.rb +++ b/exercises/etl/etl_test.rb @@ -32,7 +32,7 @@ def test_more_keys assert_equal expected, ETL.transform(old) end - def test_full_dataset # rubocop:disable Metrics/MethodLength + def test_full_dataset skip old = { 1 => %w(A E I O U L N R S T), diff --git a/exercises/house/house_test.rb b/exercises/house/house_test.rb index 42c35836fa..871724cc73 100755 --- a/exercises/house/house_test.rb +++ b/exercises/house/house_test.rb @@ -3,7 +3,6 @@ require 'minitest/autorun' require_relative 'house' -# rubocop:disable Metrics/MethodLength class HouseTest < Minitest::Test def test_rhyme expected = <<-RHYME diff --git a/exercises/largest-series-product/example.tt b/exercises/largest-series-product/example.tt index 4f923f201f..e3d54438c2 100644 --- a/exercises/largest-series-product/example.tt +++ b/exercises/largest-series-product/example.tt @@ -5,9 +5,6 @@ require_relative 'largest_series_product' # Test data version: # <%= sha1 %> -# Rubocop directives -# rubocop:disable Style/NumericLiterals -# rubocop:disable Metrics/LineLength # class Seriestest < Minitest::Test<% test_cases.each do |test_case| %> def <%= test_case.name %><% if test_case.skipped? %> diff --git a/exercises/largest-series-product/largest_series_product_test.rb b/exercises/largest-series-product/largest_series_product_test.rb index 2f0ac920ef..de1d9dff2e 100755 --- a/exercises/largest-series-product/largest_series_product_test.rb +++ b/exercises/largest-series-product/largest_series_product_test.rb @@ -6,10 +6,6 @@ # Test data version: # deb225e Implement canonical dataset for scrabble-score problem (#255) -# Rubocop directives -# rubocop:disable Style/NumericLiterals -# rubocop:disable Metrics/LineLength -# class Seriestest < Minitest::Test def test_can_find_the_largest_product_of_2_with_numbers_in_order assert_equal 72, Series.new('0123456789').largest_product(2) diff --git a/exercises/linked-list/linked_list_test.rb b/exercises/linked-list/linked_list_test.rb index baf8814b8c..1dbe46d07c 100755 --- a/exercises/linked-list/linked_list_test.rb +++ b/exercises/linked-list/linked_list_test.rb @@ -39,7 +39,7 @@ def test_unshift_pop assert_equal 20, deque.pop end - def test_example # rubocop:disable Metrics/MethodLength + def test_example skip deque = Deque.new deque.push(10) diff --git a/exercises/meetup/meetup_test.rb b/exercises/meetup/meetup_test.rb index 3595e1d3d5..304feb73f4 100755 --- a/exercises/meetup/meetup_test.rb +++ b/exercises/meetup/meetup_test.rb @@ -8,7 +8,6 @@ # and a method day(weekday, schedule) # where weekday is one of :monday, :tuesday, etc # and schedule is :first, :second, :third, :fourth, :last or :teenth. -# rubocop:disable Style/AlignParameters class MeetupTest < Minitest::Test def test_monteenth_of_may_2013 assert_equal Date.new(2013, 5, 13), diff --git a/exercises/nth-prime/example.tt b/exercises/nth-prime/example.tt index 822d6c5d49..8b4a62595f 100644 --- a/exercises/nth-prime/example.tt +++ b/exercises/nth-prime/example.tt @@ -5,8 +5,6 @@ require_relative 'nth_prime' # Test data version: # <%= sha1 %> -# Rubocop directives -# rubocop:disable Style/NumericLiterals # class NthPrimeTest < Minitest::Test<% test_cases.each do |test_case| %> def <%= test_case.name %><% if test_case.skipped? %> diff --git a/exercises/nth-prime/nth_prime_test.rb b/exercises/nth-prime/nth_prime_test.rb index 8ae9b56b62..f13f1ecc29 100755 --- a/exercises/nth-prime/nth_prime_test.rb +++ b/exercises/nth-prime/nth_prime_test.rb @@ -5,8 +5,6 @@ # Test data version: # bb79e10 -# Rubocop directives -# rubocop:disable Style/NumericLiterals # class NthPrimeTest < Minitest::Test def test_first_prime diff --git a/exercises/ocr-numbers/ocr_numbers_test.rb b/exercises/ocr-numbers/ocr_numbers_test.rb index cb093cc359..4211a1157c 100755 --- a/exercises/ocr-numbers/ocr_numbers_test.rb +++ b/exercises/ocr-numbers/ocr_numbers_test.rb @@ -4,7 +4,6 @@ require_relative 'ocr_numbers' class OCRTest < Minitest::Test - # rubocop:disable Style/TrailingWhitespace def test_recognize_zero text = <<-NUMBER.chomp _ @@ -169,7 +168,7 @@ def test_identify_1234567890 assert_equal '1234567890', OCR.new(text).convert end - def test_identify_123_456_789 # rubocop:disable Metrics/MethodLength + def test_identify_123_456_789 skip text = <<-NUMBER.chomp _ _ diff --git a/exercises/protein-translation/protein_translation_test.rb b/exercises/protein-translation/protein_translation_test.rb index 6810f43737..68209f551d 100755 --- a/exercises/protein-translation/protein_translation_test.rb +++ b/exercises/protein-translation/protein_translation_test.rb @@ -3,7 +3,6 @@ require 'minitest/autorun' require_relative 'protein_translation' -# rubocop:disable Style/MethodName class TranslationTest < Minitest::Test def test_AUG_translates_to_methionine assert_equal 'Methionine', Translation.of_codon('AUG') diff --git a/exercises/proverb/proverb_test.rb b/exercises/proverb/proverb_test.rb index 1414fead6f..992c1d1c41 100755 --- a/exercises/proverb/proverb_test.rb +++ b/exercises/proverb/proverb_test.rb @@ -38,7 +38,7 @@ def test_proverb_does_not_hard_code_the_rhyme_dictionary assert_equal expected, proverb.to_s end - def test_the_whole_proverb # rubocop:disable Metrics/MethodLength + def test_the_whole_proverb skip chain = %w(nail shoe horse rider message battle kingdom) proverb = Proverb.new(*chain) @@ -52,7 +52,6 @@ def test_the_whole_proverb # rubocop:disable Metrics/MethodLength assert_equal expected, proverb.to_s end - # rubocop:disable Metrics/MethodLength def test_an_optional_qualifier_in_the_final_consequence skip chain = %w(nail shoe horse rider message battle kingdom) @@ -66,7 +65,6 @@ def test_an_optional_qualifier_in_the_final_consequence 'And all for the want of a horseshoe nail.' assert_equal expected, proverb.to_s end - # rubocop:enable Metrics/MethodLength def test_proverb_is_same_each_time skip diff --git a/exercises/queen-attack/queen_attack_test.rb b/exercises/queen-attack/queen_attack_test.rb index 3bf71ed36b..2fe1c0d37c 100755 --- a/exercises/queen-attack/queen_attack_test.rb +++ b/exercises/queen-attack/queen_attack_test.rb @@ -34,7 +34,7 @@ def test_cannot_occupy_same_space end end - def test_string_representation # rubocop:disable Metrics/MethodLength + def test_string_representation skip queens = Queens.new(white: [2, 4], black: [6, 6]) board = <<-BOARD.chomp @@ -50,7 +50,7 @@ def test_string_representation # rubocop:disable Metrics/MethodLength assert_equal board, queens.to_s end - def test_another_string_representation # rubocop:disable Metrics/MethodLength + def test_another_string_representation skip queens = Queens.new(white: [7, 1], black: [0, 0]) board = <<-BOARD.chomp @@ -66,7 +66,6 @@ def test_another_string_representation # rubocop:disable Metrics/MethodLength assert_equal board, queens.to_s end - # rubocop:disable Metrics/MethodLength def test_yet_another_string_representation skip queens = Queens.new(white: [4, 3], black: [3, 4]) @@ -88,7 +87,6 @@ def test_cannot_attack queens = Queens.new(white: [2, 3], black: [4, 7]) assert !queens.attack? end - # rubocop:enable Metrics/MethodLength def test_can_attack_on_same_row skip diff --git a/exercises/robot-simulator/robot_simulator_test.rb b/exercises/robot-simulator/robot_simulator_test.rb index 6857793a93..8498ddec50 100755 --- a/exercises/robot-simulator/robot_simulator_test.rb +++ b/exercises/robot-simulator/robot_simulator_test.rb @@ -160,7 +160,7 @@ def test_instruct_robot assert_equal :west, robot.bearing end - def test_instruct_many_robots # rubocop:disable Metrics/MethodLength + def test_instruct_many_robots skip robot1 = Robot.new robot2 = Robot.new diff --git a/exercises/strain/strain_test.rb b/exercises/strain/strain_test.rb index 57dedad921..334f9d28e8 100755 --- a/exercises/strain/strain_test.rb +++ b/exercises/strain/strain_test.rb @@ -30,7 +30,7 @@ def test_keep_strings assert_equal %w(zebra zombies zelot), result end - def test_keep_arrays # rubocop:disable Metrics/MethodLength + def test_keep_arrays skip rows = [ [1, 2, 3], @@ -72,7 +72,7 @@ def test_discard_strings assert_equal %w(apple banana cherimoya), result end - def test_discard_arrays # rubocop:disable Metrics/MethodLength + def test_discard_arrays skip rows = [ [1, 2, 3],