Skip to content

Commit

Permalink
dominoes: Add generator and example (#484)
Browse files Browse the repository at this point in the history
dominoes: Add generator and example

Note that this exercise appears to differ from other xruby exercises in
the following significant manner:

Verification that the student function under test has produced a correct
answer is NOT done by comparing the observed output to an expected
output (or even a list of expected outputs), because the set of
acceptable outputs is quite large even for a small input set.

Thus, verification is done by running the observed output against a
function that verifies various properties about the output.

https://github.com/exercism/x-common/blob/master/exercises/dominoes/canonical-data.json
explains the approach taken.
  • Loading branch information
petertseng authored and Insti committed Nov 21, 2016
1 parent 6e14659 commit 1940663
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 0 deletions.
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

# 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
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)
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

0 comments on commit 1940663

Please sign in to comment.