Skip to content
Open
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
10 changes: 10 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1791,6 +1791,16 @@
],
"difficulty": 5
},
{
"slug": "relative-distance",
"name": "Relative Distance",
"uuid": "d590865c-ef30-424a-8cfb-7f31f04dee1b",
"practices": [],
"prerequisites": [
"lists"
],
"difficulty": 5
},
{
"slug": "dot-dsl",
"name": "DOT DSL",
Expand Down
28 changes: 28 additions & 0 deletions exercises/practice/relative-distance/.docs/instructions.append.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Instructions append

## Class-based solution

The tests for this exercise expect your solution to be implemented as a `RelativeDistance` class in Python.
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.
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.

If you are unfamiliar with classes in Python, here is a brief overview of how to implement the `RelativeDistance` class:

A class is a blueprint for creating objects, bundling attributes (data) and methods (functionality) together.
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.
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.
Then you can add your logic to the `degree_of_separation` method to calculate the degree of separation between `person_a` and `person_b`.

## Exception messages

Sometimes it is necessary to [raise an exception](https://docs.python.org/3/tutorial/errors.html#raising-exceptions).
When you do this, you should always include a **meaningful error message** to indicate what the source of the error is.
This makes your code more readable and helps significantly with debugging.
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.

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.
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.
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.
The tests will only pass if you both `raise` the expected `exception` type and include the expected message with it.

Please check the tests and their expected results carefully.
39 changes: 39 additions & 0 deletions exercises/practice/relative-distance/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Instructions

Your task is to determine the degree of separation between two individuals in a family tree.
This is similar to the pop culture idea that every Hollywood actor is [within six degrees of Kevin Bacon][six-bacons].

- You will be given an input, with all parent names and their children.
- Each name is unique, a child _can_ have one or two parents.
- The degree of separation is defined as the shortest number of connections from one person to another.
- If two individuals are not connected, return a value that represents "no known relationship."
Please see the test cases for the actual implementation.

## Example

Given the following family tree:

```text
┌──────────┐ ┌──────────┐ ┌───────────┐
│ Helena │ │ Erdős ├─────┤ Shusaku │
└───┬───┬──┘ └─────┬────┘ └────┬──────┘
┌───┘ └───────┐ └───────┬───────┘
┌─────┴────┐ ┌────┴───┐ ┌─────┴────┐
│ Isla ├─────┤ Tariq │ │ Kevin │
└────┬─────┘ └────┬───┘ └──────────┘
│ │
┌────┴────┐ ┌────┴───┐
│ Uma │ │ Morphy │
└─────────┘ └────────┘
```

The degree of separation between Tariq and Uma is 2 (Tariq → Isla → Uma).
There's no known relationship between Isla and Kevin, as there is no connection in the given data.
The degree of separation between Uma and Isla is 1.

~~~~exercism/note
Isla and Tariq are siblings and have a separation of 1.
Similarly, this implementation would report a separation of 2 from you to your father's brother.
~~~~

[six-bacons]: https://en.m.wikipedia.org/wiki/Six_Degrees_of_Kevin_Bacon
12 changes: 12 additions & 0 deletions exercises/practice/relative-distance/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Introduction

You've been hired to develop **Noble Knots**, the hottest new dating app for nobility!
With centuries of royal intermarriage, things have gotten… _complicated_.
To avoid any _oops-we're-twins_ situations, your job is to build a system that checks how closely two people are related.

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.
Your algorithm will determine the **degree of separation** between two individuals in the royal family tree.

Will your app help crown a perfect match?

[islendiga-app]: https://web.archive.org/web/20250816223614/http://www.islendingaapp.is/information-in-english/
41 changes: 41 additions & 0 deletions exercises/practice/relative-distance/.meta/additional_tests.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"cases": [
{
"description": "person A not in tree",
"property": "degreeOfSeparation",
"input": {
"family_tree": {
"Priya": ["Rami"]
},
"person_a": "Kaito",
"person_b": "Priya"
},
"expected": {"error": "Person A not in family tree."}
},
{
"description": "person B not in tree",
"property": "degreeOfSeparation",
"input": {
"family_tree": {
"Priya": ["Rami"]
},
"person_a": "Priya",
"person_b": "Kaito"
},
"expected": {"error": "Person B not in family tree."}
},
{
"description": "no connection between individuals",
"property": "degreeOfSeparation",
"input": {
"family_tree": {
"Priya": ["Rami"],
"Kaito": ["Elif"]
},
"person_a": "Priya",
"person_b": "Kaito"
},
"expected": {"error": "No connection between person A and person B."}
}
]
}
19 changes: 19 additions & 0 deletions exercises/practice/relative-distance/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"authors": [
"BNAndras"
],
"files": {
"solution": [
"relative_distance.py"
],
"test": [
"relative_distance_test.py"
],
"example": [
".meta/example.py"
]
},
"blurb": "Given a family tree, calculate the degree of separation.",
"source": "vaeng",
"source_url": "https://github.com/exercism/problem-specifications/pull/2537"
}
36 changes: 36 additions & 0 deletions exercises/practice/relative-distance/.meta/example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import collections
import itertools

class RelativeDistance:
def __init__(self, family_tree):
self.neighbors = collections.defaultdict(set)
for parent, children in family_tree.items():
for child in children:
self.neighbors[parent].add(child)
self.neighbors[child].add(parent)

for child1, child2 in itertools.combinations(children, 2):
self.neighbors[child1].add(child2)
self.neighbors[child2].add(child1)

def degree_of_separation(self, person_a, person_b):
if person_a not in self.neighbors:
raise ValueError("Person A not in family tree.")
if person_b not in self.neighbors:
raise ValueError("Person B not in family tree.")

queue = collections.deque([(person_a, 0)])
visited = {person_a}

while queue:
current, degree = queue.popleft()

if current == person_b:
return degree

for neighbor in self.neighbors.get(current, []):
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, degree + 1))

raise ValueError("No connection between person A and person B.")
44 changes: 44 additions & 0 deletions exercises/practice/relative-distance/.meta/template.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{%- import "generator_macros.j2" as macros with context -%}
{{ macros.canonical_ref() }}

{{ macros.header(imports=['RelativeDistance']) }}

class {{ exercise | camel_case }}Test(unittest.TestCase):
{% for case in cases -%}
def test_{{ case["description"] | to_snake }}(self):
family_tree = {
{%- for person, relatives in case["input"]["familyTree"].items() %}
"{{ person }}": {{ relatives }},
{%- endfor %}
}
{%- if case["expected"] is none %}
with self.assertRaises(ValueError):
RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["personA"] }}", "{{ case["input"]["personB"] }}")
{%- else %}
self.assertEqual(
RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["personA"] }}", "{{ case["input"]["personB"] }}"),
{{ case["expected"] }}
)
{%- endif %}
{% endfor -%}

# Additional track-specfic tests
{% for case in additional_cases -%}
def test_{{ case["description"] | to_snake }}(self):
family_tree = {
{%- for person, relatives in case["input"]["family_tree"].items() %}
"{{ person }}": {{ relatives }},
{%- endfor %}
}
{%- if case["expected"]["error"] %}
with self.assertRaises(ValueError) as err:
RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["person_a"] }}", "{{ case["input"]["person_b"] }}")
self.assertEqual(type(err.exception), ValueError)
self.assertEqual(err.exception.args[0], "{{ case["expected"]["error"] }}")
{%- else %}
self.assertEqual(
RelativeDistance(family_tree).degree_of_separation("{{ case["input"]["person_a"] }}", "{{ case["input"]["person_b"] }}"),
{{ case["expected"] }}
)
{%- endif %}
{% endfor -%}
33 changes: 33 additions & 0 deletions exercises/practice/relative-distance/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# This is an auto-generated file.
#
# Regenerating this file via `configlet sync` will:
# - Recreate every `description` key/value pair
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
# - Preserve any other key/value pair
#
# As user-added comments (using the # character) will be removed when this file
# is regenerated, comments can be added via a `comment` key.

[4a1ded74-5d32-47fb-8ae5-321f51d06b5b]
description = "Direct parent-child relation"

[30d17269-83e9-4f82-a0d7-8ef9656d8dce]
description = "Sibling relationship"

[8dffa27d-a8ab-496d-80b3-2f21c77648b5]
description = "Two degrees of separation, grandchild"

[34e56ec1-d528-4a42-908e-020a4606ee60]
description = "Unrelated individuals"
comment = "skipped in favor of test-specific tests"
include = false

[93ffe989-bad2-48c4-878f-3acb1ce2611b]
description = "Complex graph, cousins"

[2cc2e76b-013a-433c-9486-1dbe29bf06e5]
description = "Complex graph, no shortcut, far removed nephew"

[46c9fbcb-e464-455f-a718-049ea3c7400a]
description = "Complex graph, some shortcuts, cross-down and cross-up, cousins several times removed, with unrelated family tree"
6 changes: 6 additions & 0 deletions exercises/practice/relative-distance/relative_distance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class RelativeDistance:
def __init__(self, family_tree):
pass

def degree_of_separation(self, person_a, person_b):
pass
Loading
Loading