diff --git a/config.json b/config.json index 31f5336061..18d28ca927 100644 --- a/config.json +++ b/config.json @@ -1899,6 +1899,14 @@ ], "difficulty": 5 }, + { + "slug": "baffling-birthdays", + "name": "Baffling Birthdays", + "uuid": "aa30fdf1-3904-4b5b-b801-62c239c1ba47", + "practices": [], + "prerequisites": [], + "difficulty": 6 + }, { "slug": "affine-cipher", "name": "Affine Cipher", diff --git a/exercises/practice/baffling-birthdays/.docs/instructions.md b/exercises/practice/baffling-birthdays/.docs/instructions.md new file mode 100644 index 0000000000..a01ec86796 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.docs/instructions.md @@ -0,0 +1,23 @@ +# Instructions + +Your task is to estimate the birthday paradox's probabilities. + +To do this, you need to: + +- Generate random birthdates. +- Check if a collection of randomly generated birthdates contains at least two with the same birthday. +- Estimate the probability that at least two people in a group share the same birthday for different group sizes. + +~~~~exercism/note +A birthdate includes the full date of birth (year, month, and day), whereas a birthday refers only to the month and day, which repeat each year. +Two birthdates with the same month and day correspond to the same birthday. +~~~~ + +~~~~exercism/caution +The birthday paradox assumes that: + +- There are 365 possible birthdays (no leap years). +- Each birthday is equally likely (uniform distribution). + +Your implementation must follow these assumptions. +~~~~ diff --git a/exercises/practice/baffling-birthdays/.docs/introduction.md b/exercises/practice/baffling-birthdays/.docs/introduction.md new file mode 100644 index 0000000000..97dabd1e6c --- /dev/null +++ b/exercises/practice/baffling-birthdays/.docs/introduction.md @@ -0,0 +1,25 @@ +# Introduction + +Fresh out of college, you're throwing a huge party to celebrate with friends and family. +Over 70 people have shown up, including your mildly eccentric Uncle Ted. + +In one of his usual antics, he bets you £100 that at least two people in the room share the same birthday. +That sounds ridiculous — there are many more possible birthdays than there are guests, so you confidently accept. + +To your astonishment, after collecting the birthdays of just 32 guests, you've already found two guests that share the same birthday. +Accepting your loss, you hand Uncle Ted his £100, but something feels off. + +The next day, curiosity gets the better of you. +A quick web search leads you to the [birthday paradox][birthday-problem], which reveals that with just 23 people, the probability of a shared birthday exceeds 50%. + +Ah. So _that's_ why Uncle Ted was so confident. + +Determined to turn the tables, you start looking up other paradoxes; next time, _you'll_ be the one making the bets. + +~~~~exercism/note +The birthday paradox is a [veridical paradox][veridical-paradox]: even though it feels wrong, it is actually true. + +[veridical-paradox]: https://en.wikipedia.org/wiki/Paradox#Quine's_classification +~~~~ + +[birthday-problem]: https://en.wikipedia.org/wiki/Birthday_problem diff --git a/exercises/practice/baffling-birthdays/.meta/config.json b/exercises/practice/baffling-birthdays/.meta/config.json new file mode 100644 index 0000000000..287518c56b --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "colinleach" + ], + "files": { + "solution": [ + "baffling_birthdays.py" + ], + "test": [ + "baffling_birthdays_test.py" + ], + "example": [ + ".meta/example.py" + ] + }, + "blurb": "Estimate the birthday paradox's probabilities.", + "source": "Erik Schierboom", + "source_url": "https://github.com/exercism/problem-specifications/pull/2539" +} diff --git a/exercises/practice/baffling-birthdays/.meta/example.py b/exercises/practice/baffling-birthdays/.meta/example.py new file mode 100644 index 0000000000..c1f179737b --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/example.py @@ -0,0 +1,29 @@ +from datetime import date, timedelta +from calendar import isleap +import random + + +def shared_birthday(birthdates): + if not birthdates: + return False + + if isinstance(birthdates[0], str): + birthdays = [(birthday.month, birthday.day) for birthday in + [date.fromisoformat(bdate) for bdate in birthdates]] + else: + birthdays = [(birthday.month, birthday.day) for birthday in birthdates] + + return len(birthdays) > len(set(birthdays)) + +def random_birthdates(groupsize): + return [random_birthdate() for _ in range(1, groupsize + 1)] + +def random_birthdate(): + rand_year = random.randrange(1900, date.today().year) + no_leaps = rand_year - 1 if isleap(rand_year) else rand_year + return date(no_leaps, 1, 1) + timedelta(days=random.randrange(0, 365)) + +def estimated_probability_of_shared_birthday(groupsize): + reps = 100 + are_shared = [shared_birthday(random_birthdates(groupsize)) for _ in range(1, reps)] + return sum(are_shared) / reps diff --git a/exercises/practice/baffling-birthdays/.meta/template.j2 b/exercises/practice/baffling-birthdays/.meta/template.j2 new file mode 100644 index 0000000000..c9c79038c9 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/template.j2 @@ -0,0 +1,51 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} + +{{ macros.header() }} + +from calendar import isleap + +class {{ exercise | camel_case }}Test(unittest.TestCase): +{% for case in cases -%} + +# {{ case["description"] }} + + {% if case["description"].startswith("shared") -%} + {% for subcase in case["cases"] %} + def test_{{ subcase["description"] | to_snake }}(self): + self.assertIs( + {{ subcase["property"] | to_snake }}({{ subcase["input"]["birthdates"] }}), + {{ subcase["expected"] }} + ) + + {% endfor %} + + {% elif case["description"].startswith("random") %} + {# non-deterministic results unsuitable for templating #} + + def test_random_birthdates_generate_requested_number_of_birthdates(self): + self.assertTrue(all(len(random_birthdates(groupsize)) == groupsize for groupsize in range(1, 20))) + + def test_random_birthdates_are_not_in_leap_years(self): + self.assertFalse(any([isleap(randyear.year) for randyear in random_birthdates(100)])) + + def test_random_birthdates_appear_random(self): + birthdates = random_birthdates(500) + months = set([bdate.month for bdate in birthdates]) + days = set([bdate.day for bdate in birthdates]) + self.assertTrue(len(months) >= 10) + self.assertTrue(len(days) >= 28) + + {% elif case["description"].startswith("estimated") %} + {% for subcase in case["cases"] -%} + def test_{{ subcase["description"] | to_snake }}(self): + {% set expected = subcase["expected"] / 100 -%} + {% set delta = 0.5 if 0.1 <= expected <= 0.9 else 0.1 %} + self.assertAlmostEqual( + estimated_probability_of_shared_birthday({{ subcase["input"]["groupSize"] }}), + {{ expected }}, + delta={{ delta }} + ) + {% endfor %} + {% endif %} +{% endfor %} diff --git a/exercises/practice/baffling-birthdays/.meta/tests.toml b/exercises/practice/baffling-birthdays/.meta/tests.toml new file mode 100644 index 0000000000..c76afb4667 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/tests.toml @@ -0,0 +1,61 @@ +# 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. + +[716dcc2b-8fe4-4fc9-8c48-cbe70d8e6b67] +description = "shared birthday -> one birthdate" + +[f7b3eb26-bcfc-4c1e-a2de-af07afc33f45] +description = "shared birthday -> two birthdates with same year, month, and day" + +[7193409a-6e16-4bcb-b4cc-9ffe55f79b25] +description = "shared birthday -> two birthdates with same year and month, but different day" + +[d04db648-121b-4b72-93e8-d7d2dced4495] +description = "shared birthday -> two birthdates with same month and day, but different year" + +[3c8bd0f0-14c6-4d4c-975a-4c636bfdc233] +description = "shared birthday -> two birthdates with same year, but different month and day" + +[df5daba6-0879-4480-883c-e855c99cdaa3] +description = "shared birthday -> two birthdates with different year, month, and day" + +[0c17b220-cbb9-4bd7-872f-373044c7b406] +description = "shared birthday -> multiple birthdates without shared birthday" + +[966d6b0b-5c0a-4b8c-bc2d-64939ada49f8] +description = "shared birthday -> multiple birthdates with one shared birthday" + +[b7937d28-403b-4500-acce-4d9fe3a9620d] +description = "shared birthday -> multiple birthdates with more than one shared birthday" + +[70b38cea-d234-4697-b146-7d130cd4ee12] +description = "random birthdates -> generate requested number of birthdates" + +[d9d5b7d3-5fea-4752-b9c1-3fcd176d1b03] +description = "random birthdates -> years are not leap years" + +[d1074327-f68c-4c8a-b0ff-e3730d0f0521] +description = "random birthdates -> months are random" + +[7df706b3-c3f5-471d-9563-23a4d0577940] +description = "random birthdates -> days are random" + +[89a462a4-4265-4912-9506-fb027913f221] +description = "estimated probability of at least one shared birthday -> for one person" + +[ec31c787-0ebb-4548-970c-5dcb4eadfb5f] +description = "estimated probability of at least one shared birthday -> among ten people" + +[b548afac-a451-46a3-9bb0-cb1f60c48e2f] +description = "estimated probability of at least one shared birthday -> among twenty-three people" + +[e43e6b9d-d77b-4f6c-a960-0fc0129a0bc5] +description = "estimated probability of at least one shared birthday -> among seventy people" diff --git a/exercises/practice/baffling-birthdays/baffling_birthdays.py b/exercises/practice/baffling-birthdays/baffling_birthdays.py new file mode 100644 index 0000000000..3996c109dc --- /dev/null +++ b/exercises/practice/baffling-birthdays/baffling_birthdays.py @@ -0,0 +1,8 @@ +def shared_birthday(birthdates): + pass + +def random_birthdates(groupsize): + pass + +def estimated_probability_of_shared_birthday(groupsize): + pass diff --git a/exercises/practice/baffling-birthdays/baffling_birthdays_test.py b/exercises/practice/baffling-birthdays/baffling_birthdays_test.py new file mode 100644 index 0000000000..784258e261 --- /dev/null +++ b/exercises/practice/baffling-birthdays/baffling_birthdays_test.py @@ -0,0 +1,103 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/baffling-birthdays/canonical-data.json +# File last updated on 2026-03-28 + +import unittest + +from baffling_birthdays import ( + estimated_probability_of_shared_birthday, + random_birthdates, + shared_birthday, +) + +from calendar import isleap + + +class BafflingBirthdaysTest(unittest.TestCase): + # shared birthday + + def test_one_birthdate(self): + self.assertIs(shared_birthday(["2000-01-01"]), False) + + def test_two_birthdates_with_same_year_month_and_day(self): + self.assertIs(shared_birthday(["2000-01-01", "2000-01-01"]), True) + + def test_two_birthdates_with_same_year_and_month_but_different_day(self): + self.assertIs(shared_birthday(["2012-05-09", "2012-05-17"]), False) + + def test_two_birthdates_with_same_month_and_day_but_different_year(self): + self.assertIs(shared_birthday(["1999-10-23", "1988-10-23"]), True) + + def test_two_birthdates_with_same_year_but_different_month_and_day(self): + self.assertIs(shared_birthday(["2007-12-19", "2007-04-27"]), False) + + def test_two_birthdates_with_different_year_month_and_day(self): + self.assertIs(shared_birthday(["1997-08-04", "1963-11-23"]), False) + + def test_multiple_birthdates_without_shared_birthday(self): + self.assertIs( + shared_birthday(["1966-07-29", "1977-02-12", "2001-12-25", "1980-11-10"]), + False, + ) + + def test_multiple_birthdates_with_one_shared_birthday(self): + self.assertIs( + shared_birthday(["1966-07-29", "1977-02-12", "2001-07-29", "1980-11-10"]), + True, + ) + + def test_multiple_birthdates_with_more_than_one_shared_birthday(self): + self.assertIs( + shared_birthday( + ["1966-07-29", "1977-02-12", "2001-12-25", "1980-07-29", "2019-02-12"] + ), + True, + ) + + # random birthdates + + def test_random_birthdates_generate_requested_number_of_birthdates(self): + self.assertTrue( + all( + len(random_birthdates(groupsize)) == groupsize + for groupsize in range(1, 20) + ) + ) + + def test_random_birthdates_are_not_in_leap_years(self): + self.assertFalse( + any([isleap(randyear.year) for randyear in random_birthdates(100)]) + ) + + def test_random_birthdates_appear_random(self): + birthdates = random_birthdates(500) + months = set([bdate.month for bdate in birthdates]) + days = set([bdate.day for bdate in birthdates]) + self.assertTrue(len(months) >= 10) + self.assertTrue(len(days) >= 28) + + # estimated probability of at least one shared birthday + + def test_for_one_person(self): + + self.assertAlmostEqual( + estimated_probability_of_shared_birthday(1), 0.0, delta=0.1 + ) + + def test_among_ten_people(self): + + self.assertAlmostEqual( + estimated_probability_of_shared_birthday(10), 0.11694818, delta=0.5 + ) + + def test_among_twenty_three_people(self): + + self.assertAlmostEqual( + estimated_probability_of_shared_birthday(23), 0.50729723, delta=0.5 + ) + + def test_among_seventy_people(self): + + self.assertAlmostEqual( + estimated_probability_of_shared_birthday(70), 0.99915958, delta=0.1 + )