diff --git a/config.json b/config.json index 2bbf7486ad..91ef7ded1a 100644 --- a/config.json +++ b/config.json @@ -1791,6 +1791,17 @@ ], "difficulty": 5 }, + { + "slug": "relative-distance", + "name": "Relative Distance", + "uuid": "d590865c-ef30-424a-8cfb-7f31f04dee1b", + "practices": [], + "prerequisites": [ + "lists", + "dicts" + ], + "difficulty": 5 + }, { "slug": "dot-dsl", "name": "DOT DSL", diff --git a/exercises/practice/relative-distance/.docs/instructions.append.md b/exercises/practice/relative-distance/.docs/instructions.append.md new file mode 100644 index 0000000000..241ff7d310 --- /dev/null +++ b/exercises/practice/relative-distance/.docs/instructions.append.md @@ -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. diff --git a/exercises/practice/relative-distance/.docs/instructions.md b/exercises/practice/relative-distance/.docs/instructions.md new file mode 100644 index 0000000000..9046aee7c8 --- /dev/null +++ b/exercises/practice/relative-distance/.docs/instructions.md @@ -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 diff --git a/exercises/practice/relative-distance/.docs/introduction.md b/exercises/practice/relative-distance/.docs/introduction.md new file mode 100644 index 0000000000..34073b40ac --- /dev/null +++ b/exercises/practice/relative-distance/.docs/introduction.md @@ -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/ diff --git a/exercises/practice/relative-distance/.meta/additional_tests.json b/exercises/practice/relative-distance/.meta/additional_tests.json new file mode 100644 index 0000000000..0b42263ce8 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/additional_tests.json @@ -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."} + } + ] +} diff --git a/exercises/practice/relative-distance/.meta/config.json b/exercises/practice/relative-distance/.meta/config.json new file mode 100644 index 0000000000..97abf1e033 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/config.json @@ -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" +} diff --git a/exercises/practice/relative-distance/.meta/example.py b/exercises/practice/relative-distance/.meta/example.py new file mode 100644 index 0000000000..2de98263b7 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/example.py @@ -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.") diff --git a/exercises/practice/relative-distance/.meta/template.j2 b/exercises/practice/relative-distance/.meta/template.j2 new file mode 100644 index 0000000000..99d8ed42e7 --- /dev/null +++ b/exercises/practice/relative-distance/.meta/template.j2 @@ -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-specific 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 -%} diff --git a/exercises/practice/relative-distance/.meta/tests.toml b/exercises/practice/relative-distance/.meta/tests.toml new file mode 100644 index 0000000000..25560343fa --- /dev/null +++ b/exercises/practice/relative-distance/.meta/tests.toml @@ -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" diff --git a/exercises/practice/relative-distance/relative_distance.py b/exercises/practice/relative-distance/relative_distance.py new file mode 100644 index 0000000000..21f09f2f74 --- /dev/null +++ b/exercises/practice/relative-distance/relative_distance.py @@ -0,0 +1,6 @@ +class RelativeDistance: + def __init__(self, family_tree): + pass + + def degree_of_separation(self, person_a, person_b): + pass diff --git a/exercises/practice/relative-distance/relative_distance_test.py b/exercises/practice/relative-distance/relative_distance_test.py new file mode 100644 index 0000000000..bedc24f873 --- /dev/null +++ b/exercises/practice/relative-distance/relative_distance_test.py @@ -0,0 +1,246 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/relative-distance/canonical-data.json +# File last updated on 2026-01-30 + +import unittest + +from relative_distance import ( + RelativeDistance, +) + + +class RelativeDistanceTest(unittest.TestCase): + def test_direct_parent_child_relation(self): + family_tree = { + "Vera": ["Tomoko"], + "Tomoko": ["Aditi"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Vera", "Tomoko"), 1 + ) + + def test_sibling_relationship(self): + family_tree = { + "Dalia": ["Olga", "Yassin"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Olga", "Yassin"), 1 + ) + + def test_two_degrees_of_separation_grandchild(self): + family_tree = { + "Khadija": ["Mateo"], + "Mateo": ["Rami"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Khadija", "Rami"), 2 + ) + + def test_complex_graph_cousins(self): + family_tree = { + "Aiko": ["Bao", "Carlos"], + "Bao": ["Dalia", "Elias"], + "Carlos": ["Fatima", "Gustavo"], + "Dalia": ["Hassan", "Isla"], + "Elias": ["Javier"], + "Fatima": ["Khadija", "Liam"], + "Gustavo": ["Mina"], + "Hassan": ["Noah", "Olga"], + "Isla": ["Pedro"], + "Javier": ["Quynh", "Ravi"], + "Khadija": ["Sofia"], + "Liam": ["Tariq", "Uma"], + "Mina": ["Viktor", "Wang"], + "Noah": ["Xiomara"], + "Olga": ["Yuki"], + "Pedro": ["Zane", "Aditi"], + "Quynh": ["Boris"], + "Ravi": ["Celine"], + "Sofia": ["Diego", "Elif"], + "Tariq": ["Farah"], + "Uma": ["Giorgio"], + "Viktor": ["Hana", "Ian"], + "Wang": ["Jing"], + "Xiomara": ["Kaito"], + "Yuki": ["Leila"], + "Zane": ["Mateo"], + "Aditi": ["Nia"], + "Boris": ["Oscar"], + "Celine": ["Priya"], + "Diego": ["Qi"], + "Elif": ["Rami"], + "Farah": ["Sven"], + "Giorgio": ["Tomoko"], + "Hana": ["Umar"], + "Ian": ["Vera"], + "Jing": ["Wyatt"], + "Kaito": ["Xia"], + "Leila": ["Yassin"], + "Mateo": ["Zara"], + "Nia": ["Antonio"], + "Oscar": ["Bianca"], + "Priya": ["Cai"], + "Qi": ["Dimitri"], + "Rami": ["Ewa"], + "Sven": ["Fabio"], + "Tomoko": ["Gabriela"], + "Umar": ["Helena"], + "Vera": ["Igor"], + "Wyatt": ["Jun"], + "Xia": ["Kim"], + "Yassin": ["Lucia"], + "Zara": ["Mohammed"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Dimitri", "Fabio"), 9 + ) + + def test_complex_graph_no_shortcut_far_removed_nephew(self): + family_tree = { + "Aiko": ["Bao", "Carlos"], + "Bao": ["Dalia", "Elias"], + "Carlos": ["Fatima", "Gustavo"], + "Dalia": ["Hassan", "Isla"], + "Elias": ["Javier"], + "Fatima": ["Khadija", "Liam"], + "Gustavo": ["Mina"], + "Hassan": ["Noah", "Olga"], + "Isla": ["Pedro"], + "Javier": ["Quynh", "Ravi"], + "Khadija": ["Sofia"], + "Liam": ["Tariq", "Uma"], + "Mina": ["Viktor", "Wang"], + "Noah": ["Xiomara"], + "Olga": ["Yuki"], + "Pedro": ["Zane", "Aditi"], + "Quynh": ["Boris"], + "Ravi": ["Celine"], + "Sofia": ["Diego", "Elif"], + "Tariq": ["Farah"], + "Uma": ["Giorgio"], + "Viktor": ["Hana", "Ian"], + "Wang": ["Jing"], + "Xiomara": ["Kaito"], + "Yuki": ["Leila"], + "Zane": ["Mateo"], + "Aditi": ["Nia"], + "Boris": ["Oscar"], + "Celine": ["Priya"], + "Diego": ["Qi"], + "Elif": ["Rami"], + "Farah": ["Sven"], + "Giorgio": ["Tomoko"], + "Hana": ["Umar"], + "Ian": ["Vera"], + "Jing": ["Wyatt"], + "Kaito": ["Xia"], + "Leila": ["Yassin"], + "Mateo": ["Zara"], + "Nia": ["Antonio"], + "Oscar": ["Bianca"], + "Priya": ["Cai"], + "Qi": ["Dimitri"], + "Rami": ["Ewa"], + "Sven": ["Fabio"], + "Tomoko": ["Gabriela"], + "Umar": ["Helena"], + "Vera": ["Igor"], + "Wyatt": ["Jun"], + "Xia": ["Kim"], + "Yassin": ["Lucia"], + "Zara": ["Mohammed"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Lucia", "Jun"), 14 + ) + + def test_complex_graph_some_shortcuts_cross_down_and_cross_up_cousins_several_times_removed_with_unrelated_family_tree( + self, + ): + family_tree = { + "Aiko": ["Bao", "Carlos"], + "Bao": ["Dalia"], + "Carlos": ["Fatima", "Gustavo"], + "Dalia": ["Hassan", "Isla"], + "Fatima": ["Khadija", "Liam"], + "Gustavo": ["Mina"], + "Hassan": ["Noah", "Olga"], + "Isla": ["Pedro"], + "Javier": ["Quynh", "Ravi"], + "Khadija": ["Sofia"], + "Liam": ["Tariq", "Uma"], + "Mina": ["Viktor", "Wang"], + "Noah": ["Xiomara"], + "Olga": ["Yuki"], + "Pedro": ["Zane", "Aditi"], + "Quynh": ["Boris"], + "Ravi": ["Celine"], + "Sofia": ["Diego", "Elif"], + "Tariq": ["Farah"], + "Uma": ["Giorgio"], + "Viktor": ["Hana", "Ian"], + "Wang": ["Jing"], + "Xiomara": ["Kaito"], + "Yuki": ["Leila"], + "Zane": ["Mateo"], + "Aditi": ["Nia"], + "Boris": ["Oscar"], + "Celine": ["Priya"], + "Diego": ["Qi"], + "Elif": ["Rami"], + "Farah": ["Sven"], + "Giorgio": ["Tomoko"], + "Hana": ["Umar"], + "Ian": ["Vera"], + "Jing": ["Wyatt"], + "Kaito": ["Xia"], + "Leila": ["Yassin"], + "Mateo": ["Zara"], + "Nia": ["Antonio"], + "Oscar": ["Bianca"], + "Priya": ["Cai"], + "Qi": ["Dimitri"], + "Rami": ["Ewa"], + "Sven": ["Fabio"], + "Tomoko": ["Gabriela"], + "Umar": ["Helena"], + "Vera": ["Igor"], + "Wyatt": ["Jun"], + "Xia": ["Kim"], + "Yassin": ["Lucia"], + "Zara": ["Mohammed"], + } + self.assertEqual( + RelativeDistance(family_tree).degree_of_separation("Wyatt", "Xia"), 12 + ) + + # Additional track-specific tests + def test_person_a_not_in_tree(self): + family_tree = { + "Priya": ["Rami"], + } + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("Kaito", "Priya") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Person A not in family tree.") + + def test_person_b_not_in_tree(self): + family_tree = { + "Priya": ["Rami"], + } + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("Priya", "Kaito") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual(err.exception.args[0], "Person B not in family tree.") + + def test_no_connection_between_individuals(self): + family_tree = { + "Priya": ["Rami"], + "Kaito": ["Elif"], + } + with self.assertRaises(ValueError) as err: + RelativeDistance(family_tree).degree_of_separation("Priya", "Kaito") + self.assertEqual(type(err.exception), ValueError) + self.assertEqual( + err.exception.args[0], "No connection between person A and person B." + )