Skip to content

Commit eb4b767

Browse files
authored
Add relative-distance (#4079)
* Add `relative-distance` * Apply code review
1 parent fcba46f commit eb4b767

File tree

11 files changed

+515
-0
lines changed

11 files changed

+515
-0
lines changed

config.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1821,6 +1821,17 @@
18211821
],
18221822
"difficulty": 5
18231823
},
1824+
{
1825+
"slug": "relative-distance",
1826+
"name": "Relative Distance",
1827+
"uuid": "d590865c-ef30-424a-8cfb-7f31f04dee1b",
1828+
"practices": [],
1829+
"prerequisites": [
1830+
"lists",
1831+
"dicts"
1832+
],
1833+
"difficulty": 5
1834+
},
18241835
{
18251836
"slug": "dot-dsl",
18261837
"name": "DOT DSL",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Instructions append
2+
3+
## Class-based solution
4+
5+
The tests for this exercise expect your solution to be implemented as a `RelativeDistance` class in Python.
6+
Your `RelativeDistance` class should be initialized using `family_tree`, a dictionary where the keys are individuals and the values are lists of that individual's children.
7+
You will also need to implement a `degree_of_separation` method which will return the degree of separation between `person_a` and `person_b` who are individuals in the family tree.
8+
9+
If you are unfamiliar with classes in Python, here is a brief overview of how to implement the `RelativeDistance` class:
10+
11+
A class is a blueprint for creating objects, bundling attributes (data) and methods (functionality) together.
12+
In this exercise, you are given stubbed implementations for the `__init__` special method used to create an instance of the `RelativeDistance` class as well as the `degree_of_separation` method.
13+
To access the `family_tree` data from within the `degree_of_separation` method, you will need to first assign it within the `__init__` method to an appropriate attribute on `self`, which represents the current instance of the `RelativeDistance` class.
14+
Then you can add your logic to the `degree_of_separation` method to calculate the degree of separation between `person_a` and `person_b`.
15+
16+
## Exception messages
17+
18+
Sometimes it is necessary to [raise an exception](https://docs.python.org/3/tutorial/errors.html#raising-exceptions).
19+
When you do this, you should always include a **meaningful error message** to indicate what the source of the error is.
20+
This makes your code more readable and helps significantly with debugging.
21+
For situations where you know that the error source will be a certain type, you can choose to raise one of the [built in error types](https://docs.python.org/3/library/exceptions.html#base-classes), but should still include a meaningful message.
22+
23+
This particular exercise requires that you use the [raise statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement) to "throw" multiple `ValueError`s.
24+
In the first scenario, you will need to raise a `ValueError` when either one or both of the people passed to the `RelativeDistance.degree_of_separation` method are not present in the family tree.
25+
If both people are present in the family tree, you will need to raise a `ValueError` when there is no valid connection between them as defined by the rules.
26+
The tests will only pass if you both `raise` the expected `exception` type and include the expected message with it.
27+
28+
Please check the tests and their expected results carefully.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Instructions
2+
3+
Your task is to determine the degree of separation between two individuals in a family tree.
4+
This is similar to the pop culture idea that every Hollywood actor is [within six degrees of Kevin Bacon][six-bacons].
5+
6+
- You will be given an input, with all parent names and their children.
7+
- Each name is unique, a child _can_ have one or two parents.
8+
- The degree of separation is defined as the shortest number of connections from one person to another.
9+
- If two individuals are not connected, return a value that represents "no known relationship."
10+
Please see the test cases for the actual implementation.
11+
12+
## Example
13+
14+
Given the following family tree:
15+
16+
```text
17+
┌──────────┐ ┌──────────┐ ┌───────────┐
18+
│ Helena │ │ Erdős ├─────┤ Shusaku │
19+
└───┬───┬──┘ └─────┬────┘ └────┬──────┘
20+
┌───┘ └───────┐ └───────┬───────┘
21+
┌─────┴────┐ ┌────┴───┐ ┌─────┴────┐
22+
│ Isla ├─────┤ Tariq │ │ Kevin │
23+
└────┬─────┘ └────┬───┘ └──────────┘
24+
│ │
25+
┌────┴────┐ ┌────┴───┐
26+
│ Uma │ │ Morphy │
27+
└─────────┘ └────────┘
28+
```
29+
30+
The degree of separation between Tariq and Uma is 2 (Tariq → Isla → Uma).
31+
There's no known relationship between Isla and Kevin, as there is no connection in the given data.
32+
The degree of separation between Uma and Isla is 1.
33+
34+
~~~~exercism/note
35+
Isla and Tariq are siblings and have a separation of 1.
36+
Similarly, this implementation would report a separation of 2 from you to your father's brother.
37+
~~~~
38+
39+
[six-bacons]: https://en.m.wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Introduction
2+
3+
You've been hired to develop **Noble Knots**, the hottest new dating app for nobility!
4+
With centuries of royal intermarriage, things have gotten… _complicated_.
5+
To avoid any _oops-we're-twins_ situations, your job is to build a system that checks how closely two people are related.
6+
7+
Noble Knots is inspired by Iceland's "[Islendinga-App][islendiga-app]," which is backed up by a database that traces all known family connections between Icelanders from the time of the settlement of Iceland.
8+
Your algorithm will determine the **degree of separation** between two individuals in the royal family tree.
9+
10+
Will your app help crown a perfect match?
11+
12+
[islendiga-app]: https://web.archive.org/web/20250816223614/http://www.islendingaapp.is/information-in-english/
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"cases": [
3+
{
4+
"description": "person A not in tree",
5+
"property": "degreeOfSeparation",
6+
"input": {
7+
"family_tree": {
8+
"Priya": ["Rami"]
9+
},
10+
"person_a": "Kaito",
11+
"person_b": "Priya"
12+
},
13+
"expected": {"error": "Person A not in family tree."}
14+
},
15+
{
16+
"description": "person B not in tree",
17+
"property": "degreeOfSeparation",
18+
"input": {
19+
"family_tree": {
20+
"Priya": ["Rami"]
21+
},
22+
"person_a": "Priya",
23+
"person_b": "Kaito"
24+
},
25+
"expected": {"error": "Person B not in family tree."}
26+
},
27+
{
28+
"description": "no connection between individuals",
29+
"property": "degreeOfSeparation",
30+
"input": {
31+
"family_tree": {
32+
"Priya": ["Rami"],
33+
"Kaito": ["Elif"]
34+
},
35+
"person_a": "Priya",
36+
"person_b": "Kaito"
37+
},
38+
"expected": {"error": "No connection between person A and person B."}
39+
}
40+
]
41+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"authors": [
3+
"BNAndras"
4+
],
5+
"files": {
6+
"solution": [
7+
"relative_distance.py"
8+
],
9+
"test": [
10+
"relative_distance_test.py"
11+
],
12+
"example": [
13+
".meta/example.py"
14+
]
15+
},
16+
"blurb": "Given a family tree, calculate the degree of separation.",
17+
"source": "vaeng",
18+
"source_url": "https://github.com/exercism/problem-specifications/pull/2537"
19+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import collections
2+
import itertools
3+
4+
class RelativeDistance:
5+
def __init__(self, family_tree):
6+
self.neighbors = collections.defaultdict(set)
7+
for parent, children in family_tree.items():
8+
for child in children:
9+
self.neighbors[parent].add(child)
10+
self.neighbors[child].add(parent)
11+
12+
for child1, child2 in itertools.combinations(children, 2):
13+
self.neighbors[child1].add(child2)
14+
self.neighbors[child2].add(child1)
15+
16+
def degree_of_separation(self, person_a, person_b):
17+
if person_a not in self.neighbors:
18+
raise ValueError("Person A not in family tree.")
19+
if person_b not in self.neighbors:
20+
raise ValueError("Person B not in family tree.")
21+
22+
queue = collections.deque([(person_a, 0)])
23+
visited = {person_a}
24+
25+
while queue:
26+
current, degree = queue.popleft()
27+
28+
if current == person_b:
29+
return degree
30+
31+
for neighbor in self.neighbors.get(current, []):
32+
if neighbor not in visited:
33+
visited.add(neighbor)
34+
queue.append((neighbor, degree + 1))
35+
36+
raise ValueError("No connection between person A and person B.")
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{%- import "generator_macros.j2" as macros with context -%}
2+
{{ macros.canonical_ref() }}
3+
4+
{{ macros.header(imports=['RelativeDistance']) }}
5+
6+
class {{ exercise | camel_case }}Test(unittest.TestCase):
7+
{% for case in cases -%}
8+
def test_{{ case["description"] | to_snake }}(self):
9+
family_tree = {
10+
{%- for person, relatives in case["input"]["familyTree"].items() %}
11+
"{{ person }}": {{ relatives }},
12+
{%- endfor %}
13+
}
14+
{%- if case["expected"] is none %}
15+
with self.assertRaises(ValueError):
16+
RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["personA"] }}", "{{ case["input"]["personB"] }}")
17+
{%- else %}
18+
self.assertEqual(
19+
RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["personA"] }}", "{{ case["input"]["personB"] }}"),
20+
{{ case["expected"] }}
21+
)
22+
{%- endif %}
23+
{% endfor -%}
24+
25+
# Additional track-specific tests
26+
{% for case in additional_cases -%}
27+
def test_{{ case["description"] | to_snake }}(self):
28+
family_tree = {
29+
{%- for person, relatives in case["input"]["family_tree"].items() %}
30+
"{{ person }}": {{ relatives }},
31+
{%- endfor %}
32+
}
33+
{%- if case["expected"]["error"] %}
34+
with self.assertRaises(ValueError) as err:
35+
RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["person_a"] }}", "{{ case["input"]["person_b"] }}")
36+
self.assertEqual(type(err.exception), ValueError)
37+
self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}")
38+
{%- else %}
39+
self.assertEqual(
40+
RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["person_a"] }}", "{{ case["input"]["person_b"] }}"),
41+
{{ case["expected"] }}
42+
)
43+
{%- endif %}
44+
{% endfor -%}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[4a1ded74-5d32-47fb-8ae5-321f51d06b5b]
13+
description = "Direct parent-child relation"
14+
15+
[30d17269-83e9-4f82-a0d7-8ef9656d8dce]
16+
description = "Sibling relationship"
17+
18+
[8dffa27d-a8ab-496d-80b3-2f21c77648b5]
19+
description = "Two degrees of separation, grandchild"
20+
21+
[34e56ec1-d528-4a42-908e-020a4606ee60]
22+
description = "Unrelated individuals"
23+
comment = "skipped in favor of test-specific tests"
24+
include = false
25+
26+
[93ffe989-bad2-48c4-878f-3acb1ce2611b]
27+
description = "Complex graph, cousins"
28+
29+
[2cc2e76b-013a-433c-9486-1dbe29bf06e5]
30+
description = "Complex graph, no shortcut, far removed nephew"
31+
32+
[46c9fbcb-e464-455f-a718-049ea3c7400a]
33+
description = "Complex graph, some shortcuts, cross-down and cross-up, cousins several times removed, with unrelated family tree"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class RelativeDistance:
2+
def __init__(self, family_tree):
3+
pass
4+
5+
def degree_of_separation(self, person_a, person_b):
6+
pass

0 commit comments

Comments
 (0)