From 77fa10abe6230f50d21f8273d09fbcf00befdb49 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Wed, 22 Apr 2026 09:37:31 -0400 Subject: [PATCH] Add baffling-birthdays --- config.json | 8 ++ exercises/practice/baffling-birthdays/.busted | 5 + .../baffling-birthdays/.docs/instructions.md | 23 ++++ .../baffling-birthdays/.docs/introduction.md | 25 ++++ .../baffling-birthdays/.meta/config.json | 19 +++ .../baffling-birthdays/.meta/example.moon | 31 +++++ .../.meta/spec_generator.moon | 85 ++++++++++++++ .../baffling-birthdays/.meta/tests.toml | 61 ++++++++++ .../baffling_birthdays.moon | 13 +++ .../baffling_birthdays_spec.moon | 108 ++++++++++++++++++ 10 files changed, 378 insertions(+) create mode 100644 exercises/practice/baffling-birthdays/.busted create mode 100644 exercises/practice/baffling-birthdays/.docs/instructions.md create mode 100644 exercises/practice/baffling-birthdays/.docs/introduction.md create mode 100644 exercises/practice/baffling-birthdays/.meta/config.json create mode 100644 exercises/practice/baffling-birthdays/.meta/example.moon create mode 100644 exercises/practice/baffling-birthdays/.meta/spec_generator.moon create mode 100644 exercises/practice/baffling-birthdays/.meta/tests.toml create mode 100644 exercises/practice/baffling-birthdays/baffling_birthdays.moon create mode 100644 exercises/practice/baffling-birthdays/baffling_birthdays_spec.moon diff --git a/config.json b/config.json index a3a2a02..c7665a1 100644 --- a/config.json +++ b/config.json @@ -650,6 +650,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "baffling-birthdays", + "name": "Baffling Birthdays", + "uuid": "68cc53af-905b-40c9-8456-6bffdf4452a1", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "binary-search-tree", "name": "Binary Search Tree", diff --git a/exercises/practice/baffling-birthdays/.busted b/exercises/practice/baffling-birthdays/.busted new file mode 100644 index 0000000..86b84e7 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} diff --git a/exercises/practice/baffling-birthdays/.docs/instructions.md b/exercises/practice/baffling-birthdays/.docs/instructions.md new file mode 100644 index 0000000..a01ec86 --- /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 0000000..97dabd1 --- /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 0000000..6d6ef3c --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "baffling_birthdays.moon" + ], + "test": [ + "baffling_birthdays_spec.moon" + ], + "example": [ + ".meta/example.moon" + ] + }, + "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.moon b/exercises/practice/baffling-birthdays/.meta/example.moon new file mode 100644 index 0000000..e140555 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/example.moon @@ -0,0 +1,31 @@ +YearDates = [os.date('%m-%d', os.time {year: 2001, month: 1, day: d}) for d = 1, 365] + +isLeapYear = (y) -> y % 4 == 0 and (y % 100 != 0 or y % 400 == 0) + +randomYear = -> + year = 1900 + math.random 126 + isLeapYear(year) and randomYear! or year + +randomBirthdate = -> "#{randomYear!}-#{YearDates[math.random #YearDates]}" + +sharedBirthday = (birthdates) -> + if #birthdates > 1 + dates = {} + for date in *[bd\sub 6 for bd in *birthdates] + dates[date] = (dates[date] or 0) + 1 + return true if dates[date] > 1 + false + +randomBirthdates = (n) -> + [randomBirthdate! for _ = 1, n] + +estimatedProbabilityOfSharedBirthday = (n) -> + iterations = 10000 + shared = 0 + if n > 1 + for i = 1, iterations + if sharedBirthday randomBirthdates n + shared += 1 + 100.0 * shared / iterations + +{ :sharedBirthday, :randomBirthdates, :estimatedProbabilityOfSharedBirthday } diff --git a/exercises/practice/baffling-birthdays/.meta/spec_generator.moon b/exercises/practice/baffling-birthdays/.meta/spec_generator.moon new file mode 100644 index 0000000..6e3afc0 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/spec_generator.moon @@ -0,0 +1,85 @@ +tablex = require 'pl.tablex' + +int_list = (list) -> "{#{table.concat list, ', '}}" + +string_list = (list) -> + "{#{table.concat [quote word for word in *list], ', '}}" + +{ + module_name: 'BafflingBirthdays', + + generate_test: (case, level) -> + local lines + switch case.property + when 'sharedBirthday' + lines = { + "birthdates = #{string_list case.input.birthdates}", + "assert.is.#{case.expected} BafflingBirthdays.#{case.property} birthdates", + } + when 'estimatedProbabilityOfSharedBirthday' + lines = { + "result = BafflingBirthdays.#{case.property} #{case.input.groupSize}", + "assert.is.approx_equal #{case.expected}, result" + } + when 'randomBirthdates' + switch case.description + when "generate requested number of birthdates" + lines = { + "result = true", + "for n = 1, 100", + " birthdates = BafflingBirthdays.randomBirthdates n", + " result = result and (#birthdates == n)", + "assert.is.true result" + } + when "years are not leap years" + lines = { + "result = true", + "for birthdate in *BafflingBirthdays.randomBirthdates(100)", + " year = birthdate\\sub 1, 4" + " result = result and not isLeapYear tonumber(year)", + "assert.is.true result" + } + when "months are random" + lines = { + "months = tablex.new 12, 0", + "for birthdate in *BafflingBirthdays.randomBirthdates(100)", + " month = tonumber birthdate\\sub 6, 7" + " assert.is.true 1 <= month and month <= 12", + " months[month] += 1", + "notSeen = [month for month,count in pairs months when count == 0]", + "assert.is.equal 0, #notSeen" + } + when "days are random" + lines = { + "days = tablex.new 31, 0", + "for birthdate in *BafflingBirthdays.randomBirthdates(300)", + " day = tonumber birthdate\\sub 9, 10" + " assert.is.true 1 <= day and day <= 31", + " days[day] += 1", + "notSeen = [day for day,count in pairs days when count == 0]", + "assert.is.equal 0, #notSeen" + } + table.concat [indent line, level for line in *lines], '\n' + + + test_helpers: [[ + -- ---------------------------------------- + -- https://lunarmodules.github.io/Penlight/libraries/pl.tablex.html + tablex = require 'pl.tablex' + + -- + epsilon = 0.5 + is_close_to = (state, arguments) -> + {a, b} = arguments + math.abs(a - b) <= epsilon + + say = require 'say' + say\set 'assertion.approx_equal.positive', "Expected %s and %s to be within #{epsilon}" + say\set 'assertion.approx_equal.negative', "Expected %s and %s not to be within #{epsilon}" + assert\register 'assertion', 'approx_equal', is_close_to, 'assertion.approx_equal.positive', 'assertion.approx_equal.negative' + + -- + isLeapYear = (year) -> year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + -- ---------------------------------------- +]] +} diff --git a/exercises/practice/baffling-birthdays/.meta/tests.toml b/exercises/practice/baffling-birthdays/.meta/tests.toml new file mode 100644 index 0000000..c76afb4 --- /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.moon b/exercises/practice/baffling-birthdays/baffling_birthdays.moon new file mode 100644 index 0000000..e482e72 --- /dev/null +++ b/exercises/practice/baffling-birthdays/baffling_birthdays.moon @@ -0,0 +1,13 @@ +{ + -- is there at least one shared birthday among the given birthdates? + sharedBirthday: (birthdates) -> + error 'Implement me' + + -- generate a list of n random birthdates + randomBirthdates: (n) -> + error 'Implement me' + + -- determine the probability that there is a shared birthday amongst n people + estimatedProbabilityOfSharedBirthday: (n) -> + error 'Implement me' +} diff --git a/exercises/practice/baffling-birthdays/baffling_birthdays_spec.moon b/exercises/practice/baffling-birthdays/baffling_birthdays_spec.moon new file mode 100644 index 0000000..08f6539 --- /dev/null +++ b/exercises/practice/baffling-birthdays/baffling_birthdays_spec.moon @@ -0,0 +1,108 @@ +BafflingBirthdays = require 'baffling_birthdays' + +describe 'baffling-birthdays', -> + -- ---------------------------------------- + -- https://lunarmodules.github.io/Penlight/libraries/pl.tablex.html + tablex = require 'pl.tablex' + + -- + epsilon = 0.5 + is_close_to = (state, arguments) -> + {a, b} = arguments + math.abs(a - b) <= epsilon + + say = require 'say' + say\set 'assertion.approx_equal.positive', "Expected %s and %s to be within #{epsilon}" + say\set 'assertion.approx_equal.negative', "Expected %s and %s not to be within #{epsilon}" + assert\register 'assertion', 'approx_equal', is_close_to, 'assertion.approx_equal.positive', 'assertion.approx_equal.negative' + + -- + isLeapYear = (year) -> year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + -- ---------------------------------------- + + describe 'shared birthday', -> + it 'one birthdate', -> + birthdates = {'2000-01-01'} + assert.is.false BafflingBirthdays.sharedBirthday birthdates + + pending 'two birthdates with same year, month, and day', -> + birthdates = {'2000-01-01', '2000-01-01'} + assert.is.true BafflingBirthdays.sharedBirthday birthdates + + pending 'two birthdates with same year and month, but different day', -> + birthdates = {'2012-05-09', '2012-05-17'} + assert.is.false BafflingBirthdays.sharedBirthday birthdates + + pending 'two birthdates with same month and day, but different year', -> + birthdates = {'1999-10-23', '1988-10-23'} + assert.is.true BafflingBirthdays.sharedBirthday birthdates + + pending 'two birthdates with same year, but different month and day', -> + birthdates = {'2007-12-19', '2007-04-27'} + assert.is.false BafflingBirthdays.sharedBirthday birthdates + + pending 'two birthdates with different year, month, and day', -> + birthdates = {'1997-08-04', '1963-11-23'} + assert.is.false BafflingBirthdays.sharedBirthday birthdates + + pending 'multiple birthdates without shared birthday', -> + birthdates = {'1966-07-29', '1977-02-12', '2001-12-25', '1980-11-10'} + assert.is.false BafflingBirthdays.sharedBirthday birthdates + + pending 'multiple birthdates with one shared birthday', -> + birthdates = {'1966-07-29', '1977-02-12', '2001-07-29', '1980-11-10'} + assert.is.true BafflingBirthdays.sharedBirthday birthdates + + pending 'multiple birthdates with more than one shared birthday', -> + birthdates = {'1966-07-29', '1977-02-12', '2001-12-25', '1980-07-29', '2019-02-12'} + assert.is.true BafflingBirthdays.sharedBirthday birthdates + + describe 'random birthdates', -> + pending 'generate requested number of birthdates', -> + result = true + for n = 1, 100 + birthdates = BafflingBirthdays.randomBirthdates n + result = result and (#birthdates == n) + assert.is.true result + + pending 'years are not leap years', -> + result = true + for birthdate in *BafflingBirthdays.randomBirthdates(100) + year = birthdate\sub 1, 4 + result = result and not isLeapYear tonumber(year) + assert.is.true result + + pending 'months are random', -> + months = tablex.new 12, 0 + for birthdate in *BafflingBirthdays.randomBirthdates(100) + month = tonumber birthdate\sub 6, 7 + assert.is.true 1 <= month and month <= 12 + months[month] += 1 + notSeen = [month for month,count in pairs months when count == 0] + assert.is.equal 0, #notSeen + + pending 'days are random', -> + days = tablex.new 31, 0 + for birthdate in *BafflingBirthdays.randomBirthdates(300) + day = tonumber birthdate\sub 9, 10 + assert.is.true 1 <= day and day <= 31 + days[day] += 1 + notSeen = [day for day,count in pairs days when count == 0] + assert.is.equal 0, #notSeen + + describe 'estimated probability of at least one shared birthday', -> + pending 'for one person', -> + result = BafflingBirthdays.estimatedProbabilityOfSharedBirthday 1 + assert.is.approx_equal 0.0, result + + pending 'among ten people', -> + result = BafflingBirthdays.estimatedProbabilityOfSharedBirthday 10 + assert.is.approx_equal 11.694818, result + + pending 'among twenty-three people', -> + result = BafflingBirthdays.estimatedProbabilityOfSharedBirthday 23 + assert.is.approx_equal 50.729723, result + + pending 'among seventy people', -> + result = BafflingBirthdays.estimatedProbabilityOfSharedBirthday 70 + assert.is.approx_equal 99.915958, result