Skip to content
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

[alphametics] Warn about long running tests in the common test data. #469

Merged
merged 7 commits into from
Nov 5, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
93 changes: 51 additions & 42 deletions exercises/alphametics/alphametics_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,72 @@
require 'minitest/autorun'
require_relative 'alphametics'

# Test data version:
# 8d8589f
# Test data version: c1cb73f
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
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
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
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_puzzle_with_four_letters
skip
input = 'AS + A == MOM'
expected = { 'A' => 9, 'M' => 1, 'O' => 0, 'S' => 2 }
assert_equal expected, Alphametics.solve(input)
end

def test_solve_puzzle_with_four_words
def test_puzzle_with_six_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 = 'NO + NO + TOO == LATE'
expected = { 'A' => 0, 'E' => 2, 'L' => 1, 'N' => 7,
'O' => 4, 'T' => 9 }
assert_equal expected, Alphametics.solve(input)
end

# This test has been commented out due its long runtime.
# def test_solve_puzzle_with_many_words
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

# These tests have been commented out due their long runtime. If you are
# interested in optimsing your solution for speed these are a good tests to
# try.
#
# 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

# def test_puzzle_with_ten_letters
# 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)
# 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
Expand All @@ -81,6 +90,6 @@ def test_solve_puzzle_with_four_words

def test_bookkeeping
skip
assert_equal 3, BookKeeping::VERSION
assert_equal 4, BookKeeping::VERSION
end
end
7 changes: 5 additions & 2 deletions exercises/alphametics/example.rb
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
module BookKeeping
VERSION = 3
VERSION = 4
end

class Alphametics
class << self

def solve(puzzle)
letters = Hash.new(0)
puzzle.scan(/[a-zA-Z]/) { |w| letters[w] += 1 }
Expand All @@ -11,7 +13,7 @@ def solve(puzzle)
return letters_values if valid?(puzzle, letters_values)
end

nil
{}
end

private
Expand All @@ -30,6 +32,7 @@ def valid?(puzzle, letters_values)
equation = puzzle.gsub(/[a-zA-Z]/, letters_values)
Equation.new(equation).valid?
end
end
end

class Equation
Expand Down
33 changes: 27 additions & 6 deletions exercises/alphametics/example.tt
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,37 @@ 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| %>
def <%= test_case.test_name %>
<%= test_case.skipped %>
expect = <%= test_case.expect %>
actual = <%= test_case.work_load %>
assert_equal(expect, actual)
input = <%= test_case.input %>
expected = <%= test_case.expect %>
<%= test_case.workload %>
end
<% end %>
# These tests have been commented out due their long runtime. If you are
# interested in optimsing your solution for speed these are a good tests to
# try.
#
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The 'long' tests are ignored by the test case generator but are hardcoded here for reference.
It would also be possible (but more complicated) to generate them (I think I forgot that's what I started out trying to do.)

# 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

# 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

<%= IO.read(XRUBY_LIB + '/bookkeeping.md') %>
def test_bookkeeping
skip
Expand Down
47 changes: 33 additions & 14 deletions lib/alphametics_cases.rb
Original file line number Diff line number Diff line change
@@ -1,35 +1,54 @@
class AlphameticsCase < OpenStruct
PAIRS_PER_LINE = 4
SPACE = ->(num) { ' ' * num }

def test_name
"test_#{description.tr(' ', '_')}"
end

def work_load
"Alphametics.new.solve('#{puzzle}')"
def skipped
index.zero? ? '# skip' : 'skip'
end

def input
"'#{puzzle}'"
end

def expect
return 'nil' if expected.nil?
expected_values
expected.nil? ? {} : expected_values
end

def workload
'assert_equal expected, Alphametics.solve(input)'
end

private

def expected_values
"{\n" << expected.each_slice(PAIRS_PER_LINE).map do |pairs|
'%s'.prepend(SPACE[6]) %
pairs.map { |k, v| "'#{k}' => #{v}" }.join(', ')
end.join(",\n") << "\n }"
"{ #{indent(expected_values_as_lines,17)} }"
end

def skipped
index.zero? ? '# skip' : 'skip'
def expected_values_as_lines
lines = expected_values_as_strings.each_slice(4).map { |line| line.join(', ') }
add_trailing_comma(lines)
end

def expected_values_as_strings
expected.sort.map { |(key,value)| "'#{key}' => #{value}" }
end

def add_trailing_comma(lines)
lines[0...-1].map { |line| "#{line}," }.push(lines.last)
end

def indent(string, spaces)
string.join("\n" + ' ' * spaces)
end
end

AlphameticsCases = proc do |data|
JSON.parse(data)['solve']['cases'].map.with_index do |row, i|
testcases = JSON.parse(data)['solve']['cases'].map.with_index do |row, i|
row = row.merge('index' => i)
AlphameticsCase.new(row)
end

# The example algorithm takes a long time to solve these.
testcases.reject { |testcase| (testcase.expected||{}).size > 7 }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Test cases are ignored based on how many letters there are in the expected output.
Things started slowing down on my computer after 7 so that's where I drew the line.

end