Skip to content

Commit 8f43529

Browse files
authored
[#723] New practice exercise zebra-puzzle (#753)
* [#723] New practice exercise `zebra-puzzle` Add solution Add config file * Fix typos * Swap keyword-lists for maps * Fix formatting * Fix warnings
1 parent 13eef73 commit 8f43529

File tree

10 files changed

+322
-0
lines changed

10 files changed

+322
-0
lines changed

Diff for: config.json

+19
Original file line numberDiff line numberDiff line change
@@ -2379,6 +2379,25 @@
23792379
"pattern-matching"
23802380
],
23812381
"difficulty": 9
2382+
},
2383+
{
2384+
"slug": "zebra-puzzle",
2385+
"name": "Zebra Puzzle",
2386+
"uuid": "0a561b08-6e61-4415-a3f1-e7d411a4a0e2",
2387+
"prerequisites": [
2388+
"pattern-matching",
2389+
"case",
2390+
"cond",
2391+
"if",
2392+
"enum",
2393+
"maps",
2394+
"atoms"
2395+
],
2396+
"practices": [
2397+
"enum",
2398+
"cond"
2399+
],
2400+
"difficulty": 9
23822401
}
23832402
],
23842403
"foregone": [
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Description
2+
3+
Solve the zebra puzzle.
4+
5+
1. There are five houses.
6+
2. The Englishman lives in the red house.
7+
3. The Spaniard owns the dog.
8+
4. Coffee is drunk in the green house.
9+
5. The Ukrainian drinks tea.
10+
6. The green house is immediately to the right of the ivory house.
11+
7. The Old Gold smoker owns snails.
12+
8. Kools are smoked in the yellow house.
13+
9. Milk is drunk in the middle house.
14+
10. The Norwegian lives in the first house.
15+
11. The man who smokes Chesterfields lives in the house next to the man with the fox.
16+
12. Kools are smoked in the house next to the house where the horse is kept.
17+
13. The Lucky Strike smoker drinks orange juice.
18+
14. The Japanese smokes Parliaments.
19+
15. The Norwegian lives next to the blue house.
20+
21+
Each of the five houses is painted a different color, and their
22+
inhabitants are of different national extractions, own different pets,
23+
drink different beverages and smoke different brands of cigarettes.
24+
25+
Which of the residents drinks water?
26+
Who owns the zebra?
27+

Diff for: exercises/practice/zebra-puzzle/.formatter.exs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]

Diff for: exercises/practice/zebra-puzzle/.meta/config.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"blurb": "Solve the zebra puzzle.",
3+
"authors": ["jiegillet"],
4+
"contributors": [],
5+
"files": {
6+
"solution": ["lib/zebra_puzzle.ex"],
7+
"test": ["test/zebra_puzzle_test.exs"],
8+
"example": [".meta/example.ex"]
9+
},
10+
"source": "Wikipedia",
11+
"source_url": "https://en.wikipedia.org/wiki/Zebra_Puzzle"
12+
}

Diff for: exercises/practice/zebra-puzzle/.meta/example.ex

+192
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
defmodule ZebraPuzzle do
2+
@nationalities ~w(englishman norwegian ukrainian japanese spaniard)a
3+
@colors ~w(red green ivory yellow blue)a
4+
@drinks ~w(coffee tea milk orange_juice water)a
5+
@pets ~w(dog snails fox horse zebra)a
6+
@cigarettes ~w{old_gold kool chesterfield lucky_strike parliament}a
7+
8+
@doc """
9+
Determine who drinks the water
10+
"""
11+
@spec drinks_water() :: atom
12+
def drinks_water() do
13+
[%{nationality: nationality}] =
14+
solve_puzzle()
15+
|> Enum.filter(fn %{drink: drink} -> drink == :water end)
16+
17+
nationality
18+
end
19+
20+
@doc """
21+
Determine who owns the zebra
22+
"""
23+
@spec owns_zebra() :: atom
24+
def owns_zebra() do
25+
[%{nationality: nationality}] =
26+
solve_puzzle()
27+
|> Enum.filter(fn %{pet: pet} -> pet == :zebra end)
28+
29+
nationality
30+
end
31+
32+
def solve_puzzle() do
33+
#
34+
# Step 0: Consider all possible combinations of values
35+
#
36+
possibilities =
37+
Enum.flat_map(1..5, fn order ->
38+
Enum.flat_map(@colors, fn color ->
39+
Enum.flat_map(@drinks, fn drink ->
40+
Enum.flat_map(@nationalities, fn nationality ->
41+
Enum.flat_map(@cigarettes, fn cigarette ->
42+
Enum.map(@pets, fn pet ->
43+
%{
44+
order: order,
45+
color: color,
46+
drink: drink,
47+
nationality: nationality,
48+
cigarette: cigarette,
49+
pet: pet
50+
}
51+
end)
52+
end)
53+
end)
54+
end)
55+
end)
56+
end)
57+
58+
#
59+
# Step 1: Add the direct constraints and filter possibilities
60+
#
61+
possibilities
62+
# The Englishman lives in the red house.
63+
|> filter_direct(:color, :red, :nationality, :englishman)
64+
# The Spaniard owns the dog.
65+
|> filter_direct(:nationality, :spaniard, :pet, :dog)
66+
# Coffee is drunk in the green house.
67+
|> filter_direct(:drink, :coffee, :color, :green)
68+
# The Ukrainian drinks tea.
69+
|> filter_direct(:drink, :tea, :nationality, :ukrainian)
70+
# The Old Gold smoker owns snails.
71+
|> filter_direct(:cigarette, :old_gold, :pet, :snails)
72+
# Kools are smoked in the yellow house.
73+
|> filter_direct(:cigarette, :kool, :color, :yellow)
74+
# Milk is drunk in the middle house.
75+
|> filter_direct(:drink, :milk, :order, 3)
76+
# The Norwegian lives in the first house.
77+
|> filter_direct(:nationality, :norwegian, :order, 1)
78+
# The Lucky Strike smoker drinks orange juice.
79+
|> filter_direct(:cigarette, :lucky_strike, :drink, :orange_juice)
80+
# The Japanese smokes Parliaments.
81+
|> filter_direct(:cigarette, :parliament, :nationality, :japanese)
82+
#
83+
# Step 2: Add indirect constraints (relations with neighbors)
84+
#
85+
|> filter_by_neighbors
86+
#
87+
# Step 3: Check if some values happen to be possibly in only one house,
88+
# add those constraints, filter and back to step 2 until all is solved
89+
#
90+
|> filter_by_unique_relations
91+
end
92+
93+
def filter_direct(list, field_1, value_1, field_2, value_2) do
94+
Enum.filter(list, fn element ->
95+
cond do
96+
element[field_1] == value_1 and element[field_2] == value_2 -> true
97+
element[field_1] == value_1 -> false
98+
element[field_2] == value_2 -> false
99+
true -> true
100+
end
101+
end)
102+
end
103+
104+
def filter_by_neighbors(list) do
105+
next_to = fn n -> [n - 1, n + 1] end
106+
107+
filtered_list =
108+
list
109+
# The green house is immediately to the right of the ivory house.
110+
|> filter_indirect(:color, :green, fn n -> [n - 1] end, :color, :ivory, fn n -> [n + 1] end)
111+
# The man who smokes Chesterfields lives in the house next to the man with the fox.
112+
|> filter_indirect(:cigarette, :chesterfield, next_to, :pet, :fox, next_to)
113+
# Kools are smoked in the house next to the house where the horse is kept.
114+
|> filter_indirect(:cigarette, :kool, next_to, :pet, :horse, next_to)
115+
# The Norwegian lives next to the blue house.
116+
|> filter_indirect(:nationality, :norwegian, next_to, :color, :blue, next_to)
117+
118+
# later filters may influence earlier ones, so we loop until there is no change
119+
if length(filtered_list) == length(list) do
120+
list
121+
else
122+
filter_by_neighbors(filtered_list)
123+
end
124+
end
125+
126+
def filter_indirect(list, field_1, value_1, order_1_to_2, field_2, value_2, order_2_to_1) do
127+
# Get all possible neighbor houses of possibilities with field_1: value_1
128+
# Ex: find all possible house numbers that neighbor a green house
129+
orders_2 = get_orders(list, field_1, value_1, order_1_to_2)
130+
# Only keep possibilities with field_2: value_2 in that neighborhood
131+
list2 = filter_neighbors(list, field_2, value_2, orders_2)
132+
133+
# Same from the other perspective
134+
orders_1 = get_orders(list2, field_2, value_2, order_2_to_1)
135+
filter_neighbors(list2, field_1, value_1, orders_1)
136+
end
137+
138+
def get_orders(list, field, value, to_other_order) do
139+
list
140+
|> Enum.filter(&(&1[field] == value))
141+
|> Enum.map(fn %{order: order} -> to_other_order.(order) end)
142+
|> Enum.concat()
143+
|> Enum.uniq()
144+
|> Enum.filter(fn order -> 1 <= order and order <= 5 end)
145+
end
146+
147+
def filter_neighbors(list, field, value, orders) do
148+
Enum.filter(list, fn element ->
149+
cond do
150+
element[field] == value and element.order in orders -> true
151+
element[field] == value -> false
152+
length(orders) == 1 and element.order == hd(orders) -> false
153+
true -> true
154+
end
155+
end)
156+
end
157+
158+
def filter_by_unique_relations(list) do
159+
# Some values happen to exist only in one particular house number
160+
filter_parameters =
161+
list
162+
|> Enum.reduce(%{}, fn house, all ->
163+
Map.update(all, house[:order], values_to_set(house), fn previous ->
164+
Map.merge(previous, house, fn _field, val_1, val_2 -> MapSet.put(val_1, val_2) end)
165+
end)
166+
end)
167+
|> Enum.map(fn {order, house} ->
168+
house
169+
|> Enum.filter(fn {field, value} -> field != :order and MapSet.size(value) == 1 end)
170+
|> Enum.map(fn {field, value} -> {order, field, value |> MapSet.to_list() |> hd} end)
171+
end)
172+
|> Enum.concat()
173+
174+
# Add those values as constraints and filter
175+
filtered_list =
176+
filter_parameters
177+
|> Enum.reduce(list, fn {order, f, v}, lst -> filter_direct(lst, :order, order, f, v) end)
178+
# Run the neighbors filter again
179+
|> filter_by_neighbors
180+
181+
# Loop until no more change (final solution)
182+
if length(filtered_list) == length(list) do
183+
filtered_list
184+
else
185+
filter_by_unique_relations(filtered_list)
186+
end
187+
end
188+
189+
def values_to_set(map) do
190+
Map.new(map, fn {field, value} -> {field, MapSet.new([value])} end)
191+
end
192+
end

Diff for: exercises/practice/zebra-puzzle/.meta/tests.toml

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# This is an auto-generated file. Regular comments will be removed when this
2+
# file is regenerated. Regenerating will not touch any manually added keys,
3+
# so comments can be added in a "comment" key.
4+
5+
[16efb4e4-8ad7-4d5e-ba96-e5537b66fd42]
6+
description = "resident who drinks water"
7+
8+
[084d5b8b-24e2-40e6-b008-c800da8cd257]
9+
description = "resident who owns zebra"

Diff for: exercises/practice/zebra-puzzle/lib/zebra_puzzle.ex

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
defmodule ZebraPuzzle do
2+
@doc """
3+
Determine who drinks the water
4+
"""
5+
@spec drinks_water() :: atom
6+
def drinks_water() do
7+
end
8+
9+
@doc """
10+
Determine who owns the zebra
11+
"""
12+
@spec owns_zebra() :: atom
13+
def owns_zebra() do
14+
end
15+
end

Diff for: exercises/practice/zebra-puzzle/mix.exs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule ZebraPuzzle.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :zebra_puzzle,
7+
version: "0.1.0",
8+
# elixir: "~> 1.8",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger]
18+
]
19+
end
20+
21+
# Run "mix help deps" to learn about dependencies.
22+
defp deps do
23+
[
24+
# {:dep_from_hexpm, "~> 0.3.0"},
25+
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
26+
]
27+
end
28+
end

Diff for: exercises/practice/zebra-puzzle/test/test_helper.exs

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ExUnit.start()
2+
ExUnit.configure(exclude: :pending, trace: true)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
defmodule ZebraPuzzleTest do
2+
import ZebraPuzzle
3+
use ExUnit.Case
4+
5+
# @tag :pending
6+
test "resident who drinks water" do
7+
assert ZebraPuzzle.drinks_water() == :norwegian
8+
end
9+
10+
@tag :pending
11+
test "resident who owns zebra" do
12+
assert ZebraPuzzle.owns_zebra() == :japanese
13+
end
14+
end

0 commit comments

Comments
 (0)