Skip to content

Commit 588342d

Browse files
committed
[New practice exercise] Baffling Birthdays
1 parent 5fe8d41 commit 588342d

File tree

9 files changed

+327
-0
lines changed

9 files changed

+327
-0
lines changed

config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1899,6 +1899,14 @@
18991899
],
19001900
"difficulty": 5
19011901
},
1902+
{
1903+
"slug": "baffling-birthdays",
1904+
"name": "Baffling Birthdays",
1905+
"uuid": "aa30fdf1-3904-4b5b-b801-62c239c1ba47",
1906+
"practices": [],
1907+
"prerequisites": [],
1908+
"difficulty": 6
1909+
},
19021910
{
19031911
"slug": "affine-cipher",
19041912
"name": "Affine Cipher",
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Instructions
2+
3+
Your task is to estimate the birthday paradox's probabilities.
4+
5+
To do this, you need to:
6+
7+
- Generate random birthdates.
8+
- Check if a collection of randomly generated birthdates contains at least two with the same birthday.
9+
- Estimate the probability that at least two people in a group share the same birthday for different group sizes.
10+
11+
~~~~exercism/note
12+
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.
13+
Two birthdates with the same month and day correspond to the same birthday.
14+
~~~~
15+
16+
~~~~exercism/caution
17+
The birthday paradox assumes that:
18+
19+
- There are 365 possible birthdays (no leap years).
20+
- Each birthday is equally likely (uniform distribution).
21+
22+
Your implementation must follow these assumptions.
23+
~~~~
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Introduction
2+
3+
Fresh out of college, you're throwing a huge party to celebrate with friends and family.
4+
Over 70 people have shown up, including your mildly eccentric Uncle Ted.
5+
6+
In one of his usual antics, he bets you £100 that at least two people in the room share the same birthday.
7+
That sounds ridiculous — there are many more possible birthdays than there are guests, so you confidently accept.
8+
9+
To your astonishment, after collecting the birthdays of just 32 guests, you've already found two guests that share the same birthday.
10+
Accepting your loss, you hand Uncle Ted his £100, but something feels off.
11+
12+
The next day, curiosity gets the better of you.
13+
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%.
14+
15+
Ah. So _that's_ why Uncle Ted was so confident.
16+
17+
Determined to turn the tables, you start looking up other paradoxes; next time, _you'll_ be the one making the bets.
18+
19+
~~~~exercism/note
20+
The birthday paradox is a [veridical paradox][veridical-paradox]: even though it feels wrong, it is actually true.
21+
22+
[veridical-paradox]: https://en.wikipedia.org/wiki/Paradox#Quine's_classification
23+
~~~~
24+
25+
[birthday-problem]: https://en.wikipedia.org/wiki/Birthday_problem
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"authors": [
3+
"colinleach"
4+
],
5+
"files": {
6+
"solution": [
7+
"baffling_birthdays.py"
8+
],
9+
"test": [
10+
"baffling_birthdays_test.py"
11+
],
12+
"example": [
13+
".meta/example.py"
14+
]
15+
},
16+
"blurb": "Estimate the birthday paradox's probabilities.",
17+
"source": "Erik Schierboom",
18+
"source_url": "https://github.com/exercism/problem-specifications/pull/2539"
19+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from datetime import date, timedelta
2+
from calendar import isleap
3+
import random
4+
5+
6+
def shared_birthday(birthdates):
7+
if not birthdates:
8+
return False
9+
10+
if isinstance(birthdates[0], str):
11+
birthdays = [(birthday.month, birthday.day) for birthday in
12+
[date.fromisoformat(bdate) for bdate in birthdates]]
13+
else:
14+
birthdays = [(birthday.month, birthday.day) for birthday in birthdates]
15+
16+
return len(birthdays) > len(set(birthdays))
17+
18+
def random_birthdates(groupsize):
19+
return [random_birthdate() for _ in range(1, groupsize + 1)]
20+
21+
def random_birthdate():
22+
rand_year = random.randrange(1900, date.today().year)
23+
no_leaps = rand_year - 1 if isleap(rand_year) else rand_year
24+
return date(no_leaps, 1, 1) + timedelta(days=random.randrange(0, 365))
25+
26+
def estimated_probability_of_shared_birthday(groupsize):
27+
reps = 100
28+
are_shared = [shared_birthday(random_birthdates(groupsize)) for _ in range(1, reps)]
29+
return sum(are_shared) / reps
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{%- import "generator_macros.j2" as macros with context -%}
2+
{{ macros.canonical_ref() }}
3+
4+
{{ macros.header() }}
5+
6+
from calendar import isleap
7+
8+
class {{ exercise | camel_case }}Test(unittest.TestCase):
9+
{% for case in cases -%}
10+
11+
# {{ case["description"] }}
12+
13+
{% if case["description"].startswith("shared") -%}
14+
{% for subcase in case["cases"] %}
15+
def test_{{ subcase["description"] | to_snake }}(self):
16+
self.assertIs(
17+
{{ subcase["property"] | to_snake }}({{ subcase["input"]["birthdates"] }}),
18+
{{ subcase["expected"] }}
19+
)
20+
21+
{% endfor %}
22+
23+
{% elif case["description"].startswith("random") %}
24+
{# non-deterministic results unsuitable for templating #}
25+
26+
def test_random_birthdates_generate_requested_number_of_birthdates(self):
27+
self.assertTrue(all(len(random_birthdates(groupsize)) == groupsize for groupsize in range(1, 20)))
28+
29+
def test_random_birthdates_are_not_in_leap_years(self):
30+
self.assertFalse(any([isleap(randyear.year) for randyear in random_birthdates(100)]))
31+
32+
def test_random_birthdates_appear_random(self):
33+
birthdates = random_birthdates(500)
34+
months = set([bdate.month for bdate in birthdates])
35+
days = set([bdate.day for bdate in birthdates])
36+
self.assertTrue(len(months) >= 10)
37+
self.assertTrue(len(days) >= 28)
38+
39+
{% elif case["description"].startswith("estimated") %}
40+
{% for subcase in case["cases"] -%}
41+
def test_{{ subcase["description"] | to_snake }}(self):
42+
{% set expected = subcase["expected"] / 100 -%}
43+
{% set delta = 0.5 if 0.1 <= expected <= 0.9 else 0.1 %}
44+
self.assertAlmostEqual(
45+
estimated_probability_of_shared_birthday({{ subcase["input"]["groupSize"] }}),
46+
{{ expected }},
47+
delta={{ delta }}
48+
)
49+
{% endfor %}
50+
{% endif %}
51+
{% endfor %}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
[716dcc2b-8fe4-4fc9-8c48-cbe70d8e6b67]
13+
description = "shared birthday -> one birthdate"
14+
15+
[f7b3eb26-bcfc-4c1e-a2de-af07afc33f45]
16+
description = "shared birthday -> two birthdates with same year, month, and day"
17+
18+
[7193409a-6e16-4bcb-b4cc-9ffe55f79b25]
19+
description = "shared birthday -> two birthdates with same year and month, but different day"
20+
21+
[d04db648-121b-4b72-93e8-d7d2dced4495]
22+
description = "shared birthday -> two birthdates with same month and day, but different year"
23+
24+
[3c8bd0f0-14c6-4d4c-975a-4c636bfdc233]
25+
description = "shared birthday -> two birthdates with same year, but different month and day"
26+
27+
[df5daba6-0879-4480-883c-e855c99cdaa3]
28+
description = "shared birthday -> two birthdates with different year, month, and day"
29+
30+
[0c17b220-cbb9-4bd7-872f-373044c7b406]
31+
description = "shared birthday -> multiple birthdates without shared birthday"
32+
33+
[966d6b0b-5c0a-4b8c-bc2d-64939ada49f8]
34+
description = "shared birthday -> multiple birthdates with one shared birthday"
35+
36+
[b7937d28-403b-4500-acce-4d9fe3a9620d]
37+
description = "shared birthday -> multiple birthdates with more than one shared birthday"
38+
39+
[70b38cea-d234-4697-b146-7d130cd4ee12]
40+
description = "random birthdates -> generate requested number of birthdates"
41+
42+
[d9d5b7d3-5fea-4752-b9c1-3fcd176d1b03]
43+
description = "random birthdates -> years are not leap years"
44+
45+
[d1074327-f68c-4c8a-b0ff-e3730d0f0521]
46+
description = "random birthdates -> months are random"
47+
48+
[7df706b3-c3f5-471d-9563-23a4d0577940]
49+
description = "random birthdates -> days are random"
50+
51+
[89a462a4-4265-4912-9506-fb027913f221]
52+
description = "estimated probability of at least one shared birthday -> for one person"
53+
54+
[ec31c787-0ebb-4548-970c-5dcb4eadfb5f]
55+
description = "estimated probability of at least one shared birthday -> among ten people"
56+
57+
[b548afac-a451-46a3-9bb0-cb1f60c48e2f]
58+
description = "estimated probability of at least one shared birthday -> among twenty-three people"
59+
60+
[e43e6b9d-d77b-4f6c-a960-0fc0129a0bc5]
61+
description = "estimated probability of at least one shared birthday -> among seventy people"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def shared_birthday(birthdates):
2+
pass
3+
4+
def random_birthdates(groupsize):
5+
pass
6+
7+
def estimated_probability_of_shared_birthday(groupsize):
8+
pass
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# These tests are auto-generated with test data from:
2+
# https://github.com/exercism/problem-specifications/tree/main/exercises/baffling-birthdays/canonical-data.json
3+
# File last updated on 2026-03-28
4+
5+
import unittest
6+
7+
from baffling_birthdays import (
8+
estimated_probability_of_shared_birthday,
9+
random_birthdates,
10+
shared_birthday,
11+
)
12+
13+
from calendar import isleap
14+
15+
16+
class BafflingBirthdaysTest(unittest.TestCase):
17+
# shared birthday
18+
19+
def test_one_birthdate(self):
20+
self.assertIs(shared_birthday(["2000-01-01"]), False)
21+
22+
def test_two_birthdates_with_same_year_month_and_day(self):
23+
self.assertIs(shared_birthday(["2000-01-01", "2000-01-01"]), True)
24+
25+
def test_two_birthdates_with_same_year_and_month_but_different_day(self):
26+
self.assertIs(shared_birthday(["2012-05-09", "2012-05-17"]), False)
27+
28+
def test_two_birthdates_with_same_month_and_day_but_different_year(self):
29+
self.assertIs(shared_birthday(["1999-10-23", "1988-10-23"]), True)
30+
31+
def test_two_birthdates_with_same_year_but_different_month_and_day(self):
32+
self.assertIs(shared_birthday(["2007-12-19", "2007-04-27"]), False)
33+
34+
def test_two_birthdates_with_different_year_month_and_day(self):
35+
self.assertIs(shared_birthday(["1997-08-04", "1963-11-23"]), False)
36+
37+
def test_multiple_birthdates_without_shared_birthday(self):
38+
self.assertIs(
39+
shared_birthday(["1966-07-29", "1977-02-12", "2001-12-25", "1980-11-10"]),
40+
False,
41+
)
42+
43+
def test_multiple_birthdates_with_one_shared_birthday(self):
44+
self.assertIs(
45+
shared_birthday(["1966-07-29", "1977-02-12", "2001-07-29", "1980-11-10"]),
46+
True,
47+
)
48+
49+
def test_multiple_birthdates_with_more_than_one_shared_birthday(self):
50+
self.assertIs(
51+
shared_birthday(
52+
["1966-07-29", "1977-02-12", "2001-12-25", "1980-07-29", "2019-02-12"]
53+
),
54+
True,
55+
)
56+
57+
# random birthdates
58+
59+
def test_random_birthdates_generate_requested_number_of_birthdates(self):
60+
self.assertTrue(
61+
all(
62+
len(random_birthdates(groupsize)) == groupsize
63+
for groupsize in range(1, 20)
64+
)
65+
)
66+
67+
def test_random_birthdates_are_not_in_leap_years(self):
68+
self.assertFalse(
69+
any([isleap(randyear.year) for randyear in random_birthdates(100)])
70+
)
71+
72+
def test_random_birthdates_appear_random(self):
73+
birthdates = random_birthdates(500)
74+
months = set([bdate.month for bdate in birthdates])
75+
days = set([bdate.day for bdate in birthdates])
76+
self.assertTrue(len(months) >= 10)
77+
self.assertTrue(len(days) >= 28)
78+
79+
# estimated probability of at least one shared birthday
80+
81+
def test_for_one_person(self):
82+
83+
self.assertAlmostEqual(
84+
estimated_probability_of_shared_birthday(1), 0.0, delta=0.1
85+
)
86+
87+
def test_among_ten_people(self):
88+
89+
self.assertAlmostEqual(
90+
estimated_probability_of_shared_birthday(10), 0.11694818, delta=0.5
91+
)
92+
93+
def test_among_twenty_three_people(self):
94+
95+
self.assertAlmostEqual(
96+
estimated_probability_of_shared_birthday(23), 0.50729723, delta=0.5
97+
)
98+
99+
def test_among_seventy_people(self):
100+
101+
self.assertAlmostEqual(
102+
estimated_probability_of_shared_birthday(70), 0.99915958, delta=0.1
103+
)

0 commit comments

Comments
 (0)