Skip to content

dominoes: Add generator and example #484

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

Merged
merged 14 commits into from
Nov 21, 2016
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
6 changes: 6 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,12 @@
"difficulty": 1,
"topics": [
]
},
{
"slug": "dominoes",
"difficulty": 1,
"topics": [
]
}
],
"deprecated": [
Expand Down
1 change: 1 addition & 0 deletions exercises/dominoes/.version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
149 changes: 149 additions & 0 deletions exercises/dominoes/dominoes_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#!/usr/bin/env ruby
gem 'minitest', '>= 5.0.0'
require 'minitest/autorun'
require_relative 'dominoes'

# Test data version: 82eb00d
class DominoesTest < Minitest::Test
def test_empty_input_empty_output
# skip
input_dominoes = []
output_chain = Dominoes.chain(input_dominoes)
assert_correct_chain(input_dominoes, output_chain)
end

def test_singleton_input_singleton_output
skip
input_dominoes = [[1, 1]]
output_chain = Dominoes.chain(input_dominoes)
assert_correct_chain(input_dominoes, output_chain)
end

def test_singleton_that_can_not_be_chained
skip
input_dominoes = [[1, 2]]
output_chain = Dominoes.chain(input_dominoes)
refute_correct_chain(input_dominoes, output_chain)
end

def test_three_elements
skip
input_dominoes = [[1, 2], [3, 1], [2, 3]]
output_chain = Dominoes.chain(input_dominoes)
assert_correct_chain(input_dominoes, output_chain)
end

def test_can_reverse_dominoes
skip
input_dominoes = [[1, 2], [1, 3], [2, 3]]
output_chain = Dominoes.chain(input_dominoes)
assert_correct_chain(input_dominoes, output_chain)
end

def test_can_not_be_chained
skip
input_dominoes = [[1, 2], [4, 1], [2, 3]]
output_chain = Dominoes.chain(input_dominoes)
refute_correct_chain(input_dominoes, output_chain)
end

def test_disconnected_simple
skip
input_dominoes = [[1, 1], [2, 2]]
output_chain = Dominoes.chain(input_dominoes)
refute_correct_chain(input_dominoes, output_chain)
end

def test_disconnected_double_loop
skip
input_dominoes = [[1, 2], [2, 1], [3, 4], [4, 3]]
output_chain = Dominoes.chain(input_dominoes)
refute_correct_chain(input_dominoes, output_chain)
end

def test_disconnected_single_isolated
skip
input_dominoes = [[1, 2], [2, 3], [3, 1], [4, 4]]
output_chain = Dominoes.chain(input_dominoes)
refute_correct_chain(input_dominoes, output_chain)
end

def test_need_backtrack
skip
input_dominoes = [[1, 2], [2, 3], [3, 1], [2, 4], [2, 4]]
output_chain = Dominoes.chain(input_dominoes)
assert_correct_chain(input_dominoes, output_chain)
end

def test_separate_loops
skip
input_dominoes = [[1, 2], [2, 3], [3, 1], [1, 1], [2, 2], [3, 3]]
output_chain = Dominoes.chain(input_dominoes)
assert_correct_chain(input_dominoes, output_chain)
end

def test_ten_elements
skip
input_dominoes = [[1, 2], [5, 3], [3, 1], [1, 2], [2, 4], [1, 6], [2, 3], [3, 4], [5, 6]]
output_chain = Dominoes.chain(input_dominoes)
assert_correct_chain(input_dominoes, output_chain)
end

# Problems in exercism evolve over time, as we find better ways to ask
# questions.
# The version number refers to the version of the problem you solved,
# not your solution.
#
# Define a constant named VERSION inside of the top level BookKeeping
# module, which may be placed near the end of your file.
#
# In your file, it will look like this:
#
# module BookKeeping
# VERSION = 1 # Where the version number matches the one in the test.
# end
#
# 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 1, BookKeeping::VERSION
end

Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be worth inserting the comment about why we can't just use input/output tests in between the bookkeeping and the custom asserts?

Copy link
Member Author

Choose a reason for hiding this comment

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

Seems reasonable. Added.

# It's infeasible to use example-based tests for this exercise,
# because the list of acceptable answers for a given input can be quite large.
# Instead, we verify certain properties of a correct chain.

def assert_correct_chain(input_dominoes, output_chain)
refute_nil output_chain, "There should be a chain for #{input_dominoes}"
assert_same_dominoes(input_dominoes, output_chain)
return if output_chain.empty?
assert_consecutive_dominoes_match(output_chain)
assert_dominoes_at_end_match(output_chain)
end

def assert_same_dominoes(input_dominoes, output_chain)
input_normal = input_dominoes.map(&:sort).sort
output_normal = output_chain.map(&:sort).sort
assert_equal input_normal, output_normal,
'Dominoes used in the output must be the same as the ones given in the input'
end

def assert_consecutive_dominoes_match(chain)
chain.each_cons(2).with_index { |(d1, d2), i|
assert_equal d1.last, d2.first,
"In chain #{chain}, right end of domino #{i} (#{d1}) and left end of domino #{i + 1} (#{d2}) must match"
}
end

def assert_dominoes_at_end_match(chain)
first_domino = chain.first
last_domino = chain.last
assert_equal first_domino.first, last_domino.last,
"In chain #{chain}, left end of first domino (#{first_domino}) and right end of last domino (#{last_domino}) must match"
end

def refute_correct_chain(input_dominoes, output_chain)
assert_nil output_chain, "There should be no chain for #{input_dominoes}"
end
end
37 changes: 37 additions & 0 deletions exercises/dominoes/example.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module Dominoes
def self.chain(dominoes)
return dominoes if dominoes.empty?

first = dominoes.first

subchain = try_subchain(dominoes.drop(1), *first)
subchain && [first] + subchain
end

def self.try_subchain(dominoes, chain_left, chain_right)
return chain_left == chain_right ? [] : nil if dominoes.empty?

dominoes.each_with_index { |domino, i|
other_dominoes = dominoes.take(i) + dominoes.drop(i + 1)
# Try adding the domino either flipped or unflipped.
[domino, domino.reverse].each { |candidate|
domino_left, domino_right = candidate
if domino_left == chain_right
if (subchain = try_subchain(other_dominoes, chain_left, domino_right))
return [candidate] + subchain
end
end
}
}

# Found no suitable chain.
# Note that for "no chain" we have to use nil instead of [].
# This is because [] is the valid answer for `Dominoes.chain([])`.
# If we used [] for "no chain", then the meaning of [] is ambiguous.
nil
Copy link
Member Author

@petertseng petertseng Nov 8, 2016

Choose a reason for hiding this comment

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

unfortunately, it's going to have to be nil and not [].

Why? Because for the input Dominoes.chain([]), [] is the valid answer. Therefore, returning [] when you have no chain will mean that the answer [] is ambiguous.

Copy link
Contributor

Choose a reason for hiding this comment

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

It might be useful to add this in a comment in the example.rb

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure. I'll note that that comment was partially targeting #469 (comment) to explain why in this case it cannot be that this method always returns the same object - it has to sometimes return nil instead of list. It's useful to have the comment somewhere, so example it will be. Putting it in the test file didn't seem right since it wasn't relevant to all students.

end
end

module BookKeeping
VERSION = 1
end
57 changes: 57 additions & 0 deletions exercises/dominoes/example.tt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env ruby
gem 'minitest', '>= 5.0.0'
require 'minitest/autorun'
require_relative 'dominoes'

# Test data version: <%= sha1 %>
class DominoesTest < Minitest::Test
<% test_cases.each do |test_case| %>
def <%= test_case.test_name %>
<%= test_case.skipped %>
<%= test_case.workload %>
end

<% end %>
<%= IO.read(XRUBY_LIB + '/bookkeeping.md') %>
def test_bookkeeping
skip
assert_equal <%= version.next %>, BookKeeping::VERSION
end

# It's infeasible to use example-based tests for this exercise,
# because the list of acceptable answers for a given input can be quite large.
# Instead, we verify certain properties of a correct chain.

def assert_correct_chain(input_dominoes, output_chain)
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you move this to the bottom of the class please.

In real life I'd put it at the top, but here it would be good if the first thing the student saw was the first test they need to implement.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah that's probably better. I imagine the student might wonder "what's assert_correct_chain", but hopefully they can just search the file!

refute_nil output_chain, "There should be a chain for #{input_dominoes}"
assert_same_dominoes(input_dominoes, output_chain)
return if output_chain.empty?
assert_consecutive_dominoes_match(output_chain)
assert_dominoes_at_end_match(output_chain)
end

def assert_same_dominoes(input_dominoes, output_chain)
input_normal = input_dominoes.map(&:sort).sort
output_normal = output_chain.map(&:sort).sort
assert_equal input_normal, output_normal,
'Dominoes used in the output must be the same as the ones given in the input'
end

def assert_consecutive_dominoes_match(chain)
chain.each_cons(2).with_index { |(d1, d2), i|
assert_equal d1.last, d2.first,
"In chain #{chain}, right end of domino #{i} (#{d1}) and left end of domino #{i + 1} (#{d2}) must match"
}
end

def assert_dominoes_at_end_match(chain)
first_domino = chain.first
last_domino = chain.last
assert_equal first_domino.first, last_domino.last,
"In chain #{chain}, left end of first domino (#{first_domino}) and right end of last domino (#{last_domino}) must match"
end

def refute_correct_chain(input_dominoes, output_chain)
assert_nil output_chain, "There should be no chain for #{input_dominoes}"
end
end
23 changes: 23 additions & 0 deletions lib/dominoes_cases.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class DominoesCase < OpenStruct
def test_name
'test_%s' % description.gsub("can't", 'can not').gsub(/[= -]+/, '_')
end

def workload
<<-WL.chomp
input_dominoes = #{input}
output_chain = Dominoes.chain(input_dominoes)
#{can_chain ? 'assert' : 'refute' }_correct_chain(input_dominoes, output_chain)
WL
end

def skipped
index.zero? ? '# skip' : 'skip'
end
end

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