From 8f6dc2612a5834042410961584962c6aa1c8841e Mon Sep 17 00:00:00 2001 From: Ryan Hartlage <2488333+ryanplusplus@users.noreply.github.com> Date: Sun, 3 May 2026 10:43:34 -0400 Subject: [PATCH 1/2] Add baffling-birthdays --- bin/generate-spec | 26 ++++ config.json | 8 ++ .../practice/anagram/.meta/spec_generator.lua | 10 +- 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.lua | 41 ++++++ .../.meta/spec_generator.lua | 64 +++++++++ .../baffling-birthdays/.meta/tests.toml | 61 +++++++++ .../baffling-birthdays/baffling-birthdays.lua | 12 ++ .../baffling-birthdays_spec.lua | 122 ++++++++++++++++++ .../practice/camicia/.meta/spec_generator.lua | 18 +-- .../practice/connect/.meta/spec_generator.lua | 10 +- .../.meta/spec_generator.lua | 12 +- .../dominoes/.meta/spec_generator.lua | 10 +- .../eliuds-eggs/.meta/spec_generator.lua | 12 +- .../flower-field/.meta/spec_generator.lua | 10 +- .../practice/forth/.meta/spec_generator.lua | 22 ++-- .../game-of-life/.meta/spec_generator.lua | 10 +- .../grade-school/.meta/spec_generator.lua | 21 +-- .../.meta/spec_generator.lua | 10 +- .../.meta/spec_generator.lua | 10 +- .../list-ops/.meta/spec_generator.lua | 12 +- .../practice/prism/.meta/spec_generator.lua | 14 +- .../.meta/spec_generator.lua | 10 +- .../rectangles/.meta/spec_generator.lua | 10 +- .../.meta/spec_generator.lua | 10 +- .../.meta/spec_generator.lua | 10 +- .../resistor-color/.meta/spec_generator.lua | 10 +- .../saddle-points/.meta/spec_generator.lua | 12 +- .../practice/series/.meta/spec_generator.lua | 10 +- .../.meta/spec_generator.lua | 10 +- .../twelve-days/.meta/spec_generator.lua | 10 +- .../.meta/spec_generator.lua | 10 +- .../word-search/.meta/spec_generator.lua | 16 +-- .../zebra-puzzle/.meta/spec_generator.lua | 12 +- 37 files changed, 474 insertions(+), 243 deletions(-) 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.lua create mode 100644 exercises/practice/baffling-birthdays/.meta/spec_generator.lua create mode 100644 exercises/practice/baffling-birthdays/.meta/tests.toml create mode 100644 exercises/practice/baffling-birthdays/baffling-birthdays.lua create mode 100644 exercises/practice/baffling-birthdays/baffling-birthdays_spec.lua diff --git a/bin/generate-spec b/bin/generate-spec index 909e5445..b23f6ba1 100755 --- a/bin/generate-spec +++ b/bin/generate-spec @@ -2,6 +2,32 @@ local json = require 'dkjson' +local utils = {} + +utils.map = function(t, f) + local mapped = {} + for i, v in ipairs(t) do + mapped[i] = f(v) + end + return mapped +end + +utils.stringify = function(x) + return ("'%s'"):format(x) +end + +utils.snake_case = function(str) + local s = str:gsub('%u', function(c) + return '_' .. c:lower() + end) + if s:sub(1, 1) == '_' then + s = s:sub(2) + end + return s +end + +package.loaded['utils'] = utils + local function read_file(path) local f = assert(io.open(path, 'r')) local contents = f:read('*a') diff --git a/config.json b/config.json index 4a0afe0a..9f4329fc 100644 --- a/config.json +++ b/config.json @@ -1481,6 +1481,14 @@ "exception_handling", "graphs" ] + }, + { + "slug": "baffling-birthdays", + "name": "Baffling Birthdays", + "uuid": "54dd8d8f-b58c-47d0-8c77-22dd5fcdf14f", + "practices": [], + "prerequisites": [], + "difficulty": 3 } ] }, diff --git a/exercises/practice/anagram/.meta/spec_generator.lua b/exercises/practice/anagram/.meta/spec_generator.lua index 33a13556..f815923f 100644 --- a/exercises/practice/anagram/.meta/spec_generator.lua +++ b/exercises/practice/anagram/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local function map(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_list(list) - return '{ ' .. table.concat(map(list, function(v) + return '{ ' .. table.concat(utils.map(list, function(v) return "'" .. v .. "'" end), ', ') .. ' }' end diff --git a/exercises/practice/baffling-birthdays/.busted b/exercises/practice/baffling-birthdays/.busted new file mode 100644 index 00000000..86b84e7c --- /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 00000000..a01ec867 --- /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 00000000..97dabd1e --- /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 00000000..5cf1160c --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "ryanplusplus" + ], + "files": { + "solution": [ + "baffling-birthdays.lua" + ], + "test": [ + "baffling-birthdays_spec.lua" + ], + "example": [ + ".meta/example.lua" + ] + }, + "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.lua b/exercises/practice/baffling-birthdays/.meta/example.lua new file mode 100644 index 00000000..bab1ea98 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/example.lua @@ -0,0 +1,41 @@ +local baffling_birthdays = {} + +baffling_birthdays.shared_birthday = function(birthdates) + local seen = {} + for _, birthdate in ipairs(birthdates) do + local without_year = birthdate:sub(6) + if seen[without_year] then + return true + end + seen[without_year] = true + end + return false +end + +baffling_birthdays.random_birthdates = function(count) + local birthdates = {} + + local non_leap_year = 2026 + local seconds_per_day = 24 * 60 * 60 + local start = os.time { year = non_leap_year, month = 1, day = 1, hour = 12 } + for i = 1, count do + local offset = math.random(0, 364) + local t = start + offset * seconds_per_day + table.insert(birthdates, os.date('%Y-%m-%d', t)) + end + + return birthdates +end + +baffling_birthdays.estimated_probability_of_shared_birthday = function(group_size) + local shared_count = 0 + for i = 1, 10000 do + local birthdates = baffling_birthdays.random_birthdates(group_size) + if baffling_birthdays.shared_birthday(birthdates) then + shared_count = shared_count + 1 + end + end + return shared_count / 100 +end + +return baffling_birthdays diff --git a/exercises/practice/baffling-birthdays/.meta/spec_generator.lua b/exercises/practice/baffling-birthdays/.meta/spec_generator.lua new file mode 100644 index 00000000..5ef22962 --- /dev/null +++ b/exercises/practice/baffling-birthdays/.meta/spec_generator.lua @@ -0,0 +1,64 @@ +local utils = require 'utils' + +return { + module_name = 'baffling_birthdays', + + generate_test = function(case) + if type(case.expected) == 'table' then + if case.expected.years and case.expected.years.leapYear ~= nil then + return [[ + local birthdates = baffling_birthdays.random_birthdates(1000) + for _, birthdate in ipairs(birthdates) do + local year = tonumber(birthdate:sub(1, 4)) + assert.is_false(year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0)) + end]] + elseif case.expected.months and case.expected.months.random ~= nil then + return [[ + local birthdates = baffling_birthdays.random_birthdates(1000) + local seen = {} + local unique_count = 0 + for _, birthdate in ipairs(birthdates) do + local month = tonumber(birthdate:sub(6, 7)) + if not seen[month] then + seen[month] = true + unique_count = unique_count + 1 + end + end + assert.are.equal(12, unique_count)]] + elseif case.expected.days and case.expected.days.random ~= nil then + return [[ + local birthdates = baffling_birthdays.random_birthdates(1000) + local seen = {} + local unique_count = 0 + for _, birthdate in ipairs(birthdates) do + local day = tonumber(birthdate:sub(9, 10)) + if not seen[day] then + seen[day] = true + unique_count = unique_count + 1 + end + end + assert.are.equal(31, unique_count)]] + else + error('Unhandled case') + end + elseif case.expected == 'length == groupsize' then + return [[ + for i = 1, 10 do + assert.are.equal(i, #baffling_birthdays.random_birthdates(i)) + end]] + elseif case.property == 'estimatedProbabilityOfSharedBirthday' then + local template = [[ + local actual = baffling_birthdays.%s(%s) + assert.are.near(%s, actual, 1)]] + + return template:format(utils.snake_case(case.property), case.input.groupSize, case.expected) + else + local template = [[ + local actual = baffling_birthdays.%s(%s) + assert.are.equal(%s, actual)]] + + local input = '{ ' .. table.concat(utils.map(case.input.birthdates, utils.stringify), ', ') .. ' }' + return template:format(utils.snake_case(case.property), input, case.expected) + end + end +} diff --git a/exercises/practice/baffling-birthdays/.meta/tests.toml b/exercises/practice/baffling-birthdays/.meta/tests.toml new file mode 100644 index 00000000..c76afb46 --- /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.lua b/exercises/practice/baffling-birthdays/baffling-birthdays.lua new file mode 100644 index 00000000..050599c0 --- /dev/null +++ b/exercises/practice/baffling-birthdays/baffling-birthdays.lua @@ -0,0 +1,12 @@ +local baffling_birthdays = {} + +baffling_birthdays.shared_birthday = function(birthdates) +end + +baffling_birthdays.random_birthdates = function(count) +end + +baffling_birthdays.estimated_probability_of_shared_birthday = function(group_size) +end + +return baffling_birthdays diff --git a/exercises/practice/baffling-birthdays/baffling-birthdays_spec.lua b/exercises/practice/baffling-birthdays/baffling-birthdays_spec.lua new file mode 100644 index 00000000..5247912d --- /dev/null +++ b/exercises/practice/baffling-birthdays/baffling-birthdays_spec.lua @@ -0,0 +1,122 @@ +local baffling_birthdays = require('baffling-birthdays') + +describe('baffling-birthdays', function() + describe('shared birthday', function() + it('one birthdate', function() + local actual = baffling_birthdays.shared_birthday({ '2000-01-01' }) + assert.are.equal(false, actual) + end) + + it('two birthdates with same year, month, and day', function() + local actual = baffling_birthdays.shared_birthday({ '2000-01-01', '2000-01-01' }) + assert.are.equal(true, actual) + end) + + it('two birthdates with same year and month, but different day', function() + local actual = baffling_birthdays.shared_birthday({ '2012-05-09', '2012-05-17' }) + assert.are.equal(false, actual) + end) + + it('two birthdates with same month and day, but different year', function() + local actual = baffling_birthdays.shared_birthday({ '1999-10-23', '1988-10-23' }) + assert.are.equal(true, actual) + end) + + it('two birthdates with same year, but different month and day', function() + local actual = baffling_birthdays.shared_birthday({ '2007-12-19', '2007-04-27' }) + assert.are.equal(false, actual) + end) + + it('two birthdates with different year, month, and day', function() + local actual = baffling_birthdays.shared_birthday({ '1997-08-04', '1963-11-23' }) + assert.are.equal(false, actual) + end) + + it('multiple birthdates without shared birthday', function() + local actual = baffling_birthdays.shared_birthday({ '1966-07-29', '1977-02-12', '2001-12-25', '1980-11-10' }) + assert.are.equal(false, actual) + end) + + it('multiple birthdates with one shared birthday', function() + local actual = baffling_birthdays.shared_birthday({ '1966-07-29', '1977-02-12', '2001-07-29', '1980-11-10' }) + assert.are.equal(true, actual) + end) + + it('multiple birthdates with more than one shared birthday', function() + local actual = baffling_birthdays.shared_birthday({ + '1966-07-29', + '1977-02-12', + '2001-12-25', + '1980-07-29', + '2019-02-12' + }) + assert.are.equal(true, actual) + end) + end) + + describe('random birthdates', function() + it('generate requested number of birthdates', function() + for i = 1, 10 do + assert.are.equal(i, #baffling_birthdays.random_birthdates(i)) + end + end) + + it('years are not leap years', function() + local birthdates = baffling_birthdays.random_birthdates(1000) + for _, birthdate in ipairs(birthdates) do + local year = tonumber(birthdate:sub(1, 4)) + assert.is_false(year % 4 == 0 and (year % 100 ~= 0 or year % 400 == 0)) + end + end) + + it('months are random', function() + local birthdates = baffling_birthdays.random_birthdates(1000) + local seen = {} + local unique_count = 0 + for _, birthdate in ipairs(birthdates) do + local month = tonumber(birthdate:sub(6, 7)) + if not seen[month] then + seen[month] = true + unique_count = unique_count + 1 + end + end + assert.are.equal(12, unique_count) + end) + + it('days are random', function() + local birthdates = baffling_birthdays.random_birthdates(1000) + local seen = {} + local unique_count = 0 + for _, birthdate in ipairs(birthdates) do + local day = tonumber(birthdate:sub(9, 10)) + if not seen[day] then + seen[day] = true + unique_count = unique_count + 1 + end + end + assert.are.equal(31, unique_count) + end) + end) + + describe('estimated probability of at least one shared birthday', function() + it('for one person', function() + local actual = baffling_birthdays.estimated_probability_of_shared_birthday(1) + assert.are.near(0.0, actual, 1) + end) + + it('among ten people', function() + local actual = baffling_birthdays.estimated_probability_of_shared_birthday(10) + assert.are.near(11.694818, actual, 1) + end) + + it('among twenty-three people', function() + local actual = baffling_birthdays.estimated_probability_of_shared_birthday(23) + assert.are.near(50.729723, actual, 1) + end) + + it('among seventy people', function() + local actual = baffling_birthdays.estimated_probability_of_shared_birthday(70) + assert.are.near(99.915958, actual, 1) + end) + end) +end) diff --git a/exercises/practice/camicia/.meta/spec_generator.lua b/exercises/practice/camicia/.meta/spec_generator.lua index 7328187a..b26440b3 100644 --- a/exercises/practice/camicia/.meta/spec_generator.lua +++ b/exercises/practice/camicia/.meta/spec_generator.lua @@ -1,25 +1,15 @@ +local utils = require 'utils' + return { module_name = 'camicia', generate_test = function(case) local lines = {} - local function snake_case(str) - local s = str:gsub('%u', function(c) - return '_' .. c:lower() - end) - if s:sub(1, 1) == '_' then - s = s:sub(2) - end - return s - end local function string_array(arr) if #arr == 0 then return "{}" end - local formatted = {} - for _, v in ipairs(arr) do - table.insert(formatted, string.format("'%s'", v)) - end + local formatted = utils.map(arr, utils.stringify) return string.format("{ %s }", table.concat(formatted, ", ")) end @@ -31,7 +21,7 @@ return { table.insert(lines, string.format("local expected = %s", expected)) - table.insert(lines, string.format("local result = camicia.%s(playerA, playerB)", snake_case(case.property))) + table.insert(lines, string.format("local result = camicia.%s(playerA, playerB)", utils.snake_case(case.property))) table.insert(lines, "assert.are.same(expected, result)") return table.concat(lines, "\n") diff --git a/exercises/practice/connect/.meta/spec_generator.lua b/exercises/practice/connect/.meta/spec_generator.lua index ca7f0e22..9d194595 100644 --- a/exercises/practice/connect/.meta/spec_generator.lua +++ b/exercises/practice/connect/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_board(board) - return table.concat(map(board, function(row) + return table.concat(utils.map(board, function(row) return "'" .. row .. "', --" end), '\n ') end diff --git a/exercises/practice/difference-of-squares/.meta/spec_generator.lua b/exercises/practice/difference-of-squares/.meta/spec_generator.lua index fd08d262..910b8732 100644 --- a/exercises/practice/difference-of-squares/.meta/spec_generator.lua +++ b/exercises/practice/difference-of-squares/.meta/spec_generator.lua @@ -1,12 +1,4 @@ -local function snake_case(str) - local s = str:gsub('%u', function(c) - return '_' .. c:lower() - end) - if s:sub(1, 1) == '_' then - s = s:sub(2) - end - return s -end +local utils = require 'utils' return { module_name = 'diff', @@ -14,6 +6,6 @@ return { generate_test = function(case) local template = [[ assert.equal(%s, diff.%s(%s))]] - return template:format(case.expected, snake_case(case.property), case.input.number) + return template:format(case.expected, utils.snake_case(case.property), case.input.number) end } diff --git a/exercises/practice/dominoes/.meta/spec_generator.lua b/exercises/practice/dominoes/.meta/spec_generator.lua index f9e0360f..b242c443 100644 --- a/exercises/practice/dominoes/.meta/spec_generator.lua +++ b/exercises/practice/dominoes/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_dominoes(dominoes) - return table.concat(map(dominoes, function(domino) + return table.concat(utils.map(dominoes, function(domino) return ('{ %s }'):format(table.concat(domino, ', ')) end), ', ') end diff --git a/exercises/practice/eliuds-eggs/.meta/spec_generator.lua b/exercises/practice/eliuds-eggs/.meta/spec_generator.lua index 698909cd..e22a4eef 100644 --- a/exercises/practice/eliuds-eggs/.meta/spec_generator.lua +++ b/exercises/practice/eliuds-eggs/.meta/spec_generator.lua @@ -1,12 +1,4 @@ -local function snake_case(str) - local s = str:gsub('%u', function(c) - return '_' .. c:lower() - end) - if s:sub(1, 1) == '_' then - s = s:sub(2) - end - return s -end +local utils = require 'utils' return { module_name = 'EliudsEggs', @@ -14,6 +6,6 @@ return { generate_test = function(case) local template = [[ assert.equal(%s, EliudsEggs.%s(%s))]] - return template:format(case.expected, snake_case(case.property), case.input.number) + return template:format(case.expected, utils.snake_case(case.property), case.input.number) end } diff --git a/exercises/practice/flower-field/.meta/spec_generator.lua b/exercises/practice/flower-field/.meta/spec_generator.lua index ba087f75..b43cd678 100644 --- a/exercises/practice/flower-field/.meta/spec_generator.lua +++ b/exercises/practice/flower-field/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_garden(garden) - return table.concat(map(garden, function(row) + return table.concat(utils.map(garden, function(row) return "'" .. row .. "', --" end), '\n ') end diff --git a/exercises/practice/forth/.meta/spec_generator.lua b/exercises/practice/forth/.meta/spec_generator.lua index 3169e52b..dc827d87 100644 --- a/exercises/practice/forth/.meta/spec_generator.lua +++ b/exercises/practice/forth/.meta/spec_generator.lua @@ -1,10 +1,4 @@ -local function map(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function stringify(x) return ("'%s'"):format(x) @@ -20,22 +14,22 @@ return { forth.evaluate({ %s }) end)]] - return template:format(table.concat(map(case.input.instructions, stringify), ' '), case.expected.error) + return template:format(table.concat(utils.map(case.input.instructions, stringify), ' '), case.expected.error) elseif case.input.instructions then local template = [[ assert.are.same({ %s }, forth.evaluate({ %s }))]] - return template:format(table.concat(map(case.expected, tostring), ', '), - table.concat(map(case.input.instructions, stringify), ', ')) + return template:format(table.concat(utils.map(case.expected, tostring), ', '), + table.concat(utils.map(case.input.instructions, stringify), ', ')) else local template = [[ assert.are.same({ %s }, forth.evaluate({ %s })) assert.are.same({ %s }, forth.evaluate({ %s }))]] - return template:format(table.concat(map(case.expected[1], tostring), ', '), - table.concat(map(case.input.instructionsFirst, stringify), ', '), - table.concat(map(case.expected[2], tostring), ', '), - table.concat(map(case.input.instructionsSecond, stringify), ', ')) + return template:format(table.concat(utils.map(case.expected[1], tostring), ', '), + table.concat(utils.map(case.input.instructionsFirst, stringify), ', '), + table.concat(utils.map(case.expected[2], tostring), ', '), + table.concat(utils.map(case.input.instructionsSecond, stringify), ', ')) end end } diff --git a/exercises/practice/game-of-life/.meta/spec_generator.lua b/exercises/practice/game-of-life/.meta/spec_generator.lua index 6ca9126a..b26cfad5 100644 --- a/exercises/practice/game-of-life/.meta/spec_generator.lua +++ b/exercises/practice/game-of-life/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_matrix(matrix) - return table.concat(map(matrix, function(row) + return table.concat(utils.map(matrix, function(row) return '{' .. table.concat(row, ', ') .. '}, --' end), '\n') end diff --git a/exercises/practice/grade-school/.meta/spec_generator.lua b/exercises/practice/grade-school/.meta/spec_generator.lua index ed1e930b..a2ac2c28 100644 --- a/exercises/practice/grade-school/.meta/spec_generator.lua +++ b/exercises/practice/grade-school/.meta/spec_generator.lua @@ -1,14 +1,4 @@ -local function map(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end - -local function stringify(x) - return ("'%s'"):format(x) -end +local utils = require 'utils' return { module_name = 'School', @@ -19,11 +9,12 @@ return { if case.property == 'grade' or case.property == 'roster' then for _, input in ipairs(case.input.students) do local student, grade = table.unpack(input) - table.insert(lines, ('school:add(%s, %d)'):format(stringify(student), grade)) + table.insert(lines, ('school:add(%s, %d)'):format(utils.stringify(student), grade)) end table.insert(lines, - ('assert.are.same({ %s }, school:%s(%s))'):format(table.concat(map(case.expected, stringify), ', '), - case.property, case.input.desiredGrade or '')) + ('assert.are.same({ %s }, school:%s(%s))'):format( + table.concat(utils.map(case.expected, utils.stringify), ', '), case.property, + case.input.desiredGrade or '')) elseif case.property == 'add' then for i, input in ipairs(case.input.students) do local student, grade = table.unpack(input) @@ -35,7 +26,7 @@ return { expected = case.expected end end - table.insert(lines, ('assert.is_%s(school:add(%s, %d))'):format(expected, stringify(student), grade)) + table.insert(lines, ('assert.is_%s(school:add(%s, %d))'):format(expected, utils.stringify(student), grade)) end else error("Unknown property: " .. case.property) diff --git a/exercises/practice/killer-sudoku-helper/.meta/spec_generator.lua b/exercises/practice/killer-sudoku-helper/.meta/spec_generator.lua index 26973e9e..efe196a9 100644 --- a/exercises/practice/killer-sudoku-helper/.meta/spec_generator.lua +++ b/exercises/practice/killer-sudoku-helper/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_solutions(solutions) - return table.concat(map(solutions, function(solution) + return table.concat(utils.map(solutions, function(solution) return '{' .. table.concat(solution, ', ') .. '}, --' end), '\n') end diff --git a/exercises/practice/kindergarten-garden/.meta/spec_generator.lua b/exercises/practice/kindergarten-garden/.meta/spec_generator.lua index 874d230d..70cd19f5 100644 --- a/exercises/practice/kindergarten-garden/.meta/spec_generator.lua +++ b/exercises/practice/kindergarten-garden/.meta/spec_generator.lua @@ -1,17 +1,11 @@ -local function map(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' return { module_name = 'Garden', generate_test = function(case) local diagram = case.input.diagram:gsub('\n', '\\n') - local expected = table.concat(map(case.expected, function(x) + local expected = table.concat(utils.map(case.expected, function(x) return ("'%s'"):format(x) end), ', ') diff --git a/exercises/practice/list-ops/.meta/spec_generator.lua b/exercises/practice/list-ops/.meta/spec_generator.lua index ac469847..e927ed0c 100644 --- a/exercises/practice/list-ops/.meta/spec_generator.lua +++ b/exercises/practice/list-ops/.meta/spec_generator.lua @@ -1,17 +1,11 @@ -local function map(t, f) - local result = {} - for i, v in ipairs(t) do - result[i] = f(v) - end - return result -end +local utils = require 'utils' local function render_list(list) if type(list) ~= 'table' then return list end - return '{' .. table.concat(map(list, render_list), ', ') .. '}' + return '{' .. table.concat(utils.map(list, render_list), ', ') .. '}' end local function render_function(s) @@ -31,7 +25,7 @@ return { assert.are.same(expected, actual)]] return template:format(render_list(case.expected), case.property, - render_list(table.concat(map(case.input.lists, render_list), ', '))) + render_list(table.concat(utils.map(case.input.lists, render_list), ', '))) elseif case.input.list1 and case.input.list2 then local template = [[ local expected = %s diff --git a/exercises/practice/prism/.meta/spec_generator.lua b/exercises/practice/prism/.meta/spec_generator.lua index 7aec3499..c2354827 100644 --- a/exercises/practice/prism/.meta/spec_generator.lua +++ b/exercises/practice/prism/.meta/spec_generator.lua @@ -1,19 +1,11 @@ +local utils = require 'utils' + return { module_name = 'prism', generate_test = function(case) local lines = {} - local function snake_case(str) - local s = str:gsub('%u', function(c) - return '_' .. c:lower() - end) - if s:sub(1, 1) == '_' then - s = s:sub(2) - end - return s - end - table.insert(lines, string.format("local start = { x = %s, y = %s, angle = %s }", case.input.start.x, case.input.start.y, case.input.start.angle)) @@ -37,7 +29,7 @@ return { end table.insert(lines, string.format("local expected = %s", expected)) - table.insert(lines, string.format("local result = prism.%s(start, prisms)", snake_case(case.property))) + table.insert(lines, string.format("local result = prism.%s(start, prisms)", utils.snake_case(case.property))) table.insert(lines, "assert.are.same(expected, result)") return table.concat(lines, "\n") diff --git a/exercises/practice/protein-translation/.meta/spec_generator.lua b/exercises/practice/protein-translation/.meta/spec_generator.lua index e06e3865..4150f582 100644 --- a/exercises/practice/protein-translation/.meta/spec_generator.lua +++ b/exercises/practice/protein-translation/.meta/spec_generator.lua @@ -1,10 +1,4 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function stringify(v) return ("'%s'"):format(tostring(v)) @@ -25,7 +19,7 @@ return { local template = [[ assert.are.same({ %s }, protein_translation.%s('%s'))]] - return template:format(table.concat(map(case.expected, stringify), ', '), case.property, case.input.strand) + return template:format(table.concat(utils.map(case.expected, stringify), ', '), case.property, case.input.strand) end end } diff --git a/exercises/practice/rectangles/.meta/spec_generator.lua b/exercises/practice/rectangles/.meta/spec_generator.lua index dea9b98a..00d7a128 100644 --- a/exercises/practice/rectangles/.meta/spec_generator.lua +++ b/exercises/practice/rectangles/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_strings(strings) - return table.concat(map(strings, function(row) + return table.concat(utils.map(strings, function(row) return "'" .. row .. "', --" end), '\n ') end diff --git a/exercises/practice/resistor-color-duo/.meta/spec_generator.lua b/exercises/practice/resistor-color-duo/.meta/spec_generator.lua index 6d0de7d3..eed6cb3d 100644 --- a/exercises/practice/resistor-color-duo/.meta/spec_generator.lua +++ b/exercises/practice/resistor-color-duo/.meta/spec_generator.lua @@ -1,10 +1,4 @@ -local function map(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function stringify(s) return "'" .. s .. "'" @@ -16,6 +10,6 @@ return { generate_test = function(case) local template = [[ assert.equal(%d, rcd.value({ %s }))]] - return template:format(case.expected, table.concat(map(case.input.colors, stringify), ', ')) + return template:format(case.expected, table.concat(utils.map(case.input.colors, stringify), ', ')) end } diff --git a/exercises/practice/resistor-color-trio/.meta/spec_generator.lua b/exercises/practice/resistor-color-trio/.meta/spec_generator.lua index b75bf626..54c3c79b 100644 --- a/exercises/practice/resistor-color-trio/.meta/spec_generator.lua +++ b/exercises/practice/resistor-color-trio/.meta/spec_generator.lua @@ -1,10 +1,4 @@ -local function map(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function stringify(s) return "'" .. s .. "'" @@ -20,7 +14,7 @@ return { assert.are.equal('%s', unit)]] return template:format(table.unpack({ case.property, - table.concat(map(case.input.colors, stringify), ', '), + table.concat(utils.map(case.input.colors, stringify), ', '), case.expected.value, case.expected.unit })) diff --git a/exercises/practice/resistor-color/.meta/spec_generator.lua b/exercises/practice/resistor-color/.meta/spec_generator.lua index 01b2d96e..5f08565e 100644 --- a/exercises/practice/resistor-color/.meta/spec_generator.lua +++ b/exercises/practice/resistor-color/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_table(t) - return table.concat(map(t, function(element) + return table.concat(utils.map(t, function(element) return "'" .. element .. "'" end), ', --\n') end diff --git a/exercises/practice/saddle-points/.meta/spec_generator.lua b/exercises/practice/saddle-points/.meta/spec_generator.lua index 3e3e7336..6eab07e4 100644 --- a/exercises/practice/saddle-points/.meta/spec_generator.lua +++ b/exercises/practice/saddle-points/.meta/spec_generator.lua @@ -1,19 +1,13 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_matrix(matrix) - return table.concat(map(matrix, function(row) + return table.concat(utils.map(matrix, function(row) return '{' .. table.concat(row, ', ') .. '}, --' end), '\n') end local function render_result(result) - return table.concat(map(result, function(point) + return table.concat(utils.map(result, function(point) return ('{ row = %s, column = %s }, --'):format(point.row, point.column) end), '\n') end diff --git a/exercises/practice/series/.meta/spec_generator.lua b/exercises/practice/series/.meta/spec_generator.lua index ca173ba7..66391c01 100644 --- a/exercises/practice/series/.meta/spec_generator.lua +++ b/exercises/practice/series/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_substrings(substrings) - return table.concat(map(substrings, function(substring) + return table.concat(utils.map(substrings, function(substring) return string.format('"%s"', substring) end), ', ') end diff --git a/exercises/practice/state-of-tic-tac-toe/.meta/spec_generator.lua b/exercises/practice/state-of-tic-tac-toe/.meta/spec_generator.lua index 0a4e0720..1496b3fd 100644 --- a/exercises/practice/state-of-tic-tac-toe/.meta/spec_generator.lua +++ b/exercises/practice/state-of-tic-tac-toe/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_board(board) - return table.concat(map(board, function(row) + return table.concat(utils.map(board, function(row) return '\'' .. row .. '\', --' end), '\n') end diff --git a/exercises/practice/twelve-days/.meta/spec_generator.lua b/exercises/practice/twelve-days/.meta/spec_generator.lua index 8088c862..3cf227e0 100644 --- a/exercises/practice/twelve-days/.meta/spec_generator.lua +++ b/exercises/practice/twelve-days/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_song(lines) - return table.concat(map(lines, function(line) + return table.concat(utils.map(lines, function(line) return '\'' .. line .. '\'' end), ',\n') end diff --git a/exercises/practice/variable-length-quantity/.meta/spec_generator.lua b/exercises/practice/variable-length-quantity/.meta/spec_generator.lua index 6595c50f..6258d121 100644 --- a/exercises/practice/variable-length-quantity/.meta/spec_generator.lua +++ b/exercises/practice/variable-length-quantity/.meta/spec_generator.lua @@ -1,13 +1,7 @@ -local map = function(t, f) - local mapped = {} - for i, v in ipairs(t) do - mapped[i] = f(v) - end - return mapped -end +local utils = require 'utils' local function render_integers(integers) - return table.concat(map(integers, function(integer) + return table.concat(utils.map(integers, function(integer) return string.format("0x%x", integer) end), ', ') end diff --git a/exercises/practice/word-search/.meta/spec_generator.lua b/exercises/practice/word-search/.meta/spec_generator.lua index 81959ffc..eddc26c2 100644 --- a/exercises/practice/word-search/.meta/spec_generator.lua +++ b/exercises/practice/word-search/.meta/spec_generator.lua @@ -1,14 +1,4 @@ -local function map(t, f) - local result = {} - for i, v in ipairs(t) do - result[i] = f(v) - end - return result -end - -local function stringify(x) - return ("'%s'"):format(x) -end +local utils = require 'utils' local function render_expected(expected) local key_value_pairs = {} @@ -40,8 +30,8 @@ return { local expected = %s assert.are.same(expected, WordSearch(grid).search(words))]] - local grid = table.concat(map(case.input.grid, stringify), ', --\n') - local words = table.concat(map(case.input.wordsToSearchFor, stringify), ', --\n') + local grid = table.concat(utils.map(case.input.grid, utils.stringify), ', --\n') + local words = table.concat(utils.map(case.input.wordsToSearchFor, utils.stringify), ', --\n') local expected = render_expected(case.expected) return template:format(grid, words, expected) diff --git a/exercises/practice/zebra-puzzle/.meta/spec_generator.lua b/exercises/practice/zebra-puzzle/.meta/spec_generator.lua index e3e3997a..a5e3fd54 100644 --- a/exercises/practice/zebra-puzzle/.meta/spec_generator.lua +++ b/exercises/practice/zebra-puzzle/.meta/spec_generator.lua @@ -1,12 +1,4 @@ -local function snake_case(str) - local s = str:gsub('%u', function(c) - return '_' .. c:lower() - end) - if s:sub(1, 1) == '_' then - s = s:sub(2) - end - return s -end +local utils = require 'utils' return { module_name = 'zebra_puzzle', @@ -14,6 +6,6 @@ return { generate_test = function(case) local template = [[ assert.equal('%s', zebra_puzzle.%s())]] - return template:format(case.expected, snake_case(case.property)) + return template:format(case.expected, utils.snake_case(case.property)) end } From 6e7a03f8cd383da883f16059d0bc1c4d23d5fc3b Mon Sep 17 00:00:00 2001 From: Ryan Hartlage <2488333+ryanplusplus@users.noreply.github.com> Date: Sun, 3 May 2026 19:01:48 -0400 Subject: [PATCH 2/2] Use a more capable stringify --- bin/generate-spec | 13 +++++++++---- .../practice/baffling-birthdays/.meta/example.lua | 2 +- .../practice/word-count/.meta/spec_generator.lua | 13 +++---------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/bin/generate-spec b/bin/generate-spec index b23f6ba1..ad8f13c9 100755 --- a/bin/generate-spec +++ b/bin/generate-spec @@ -12,8 +12,13 @@ utils.map = function(t, f) return mapped end -utils.stringify = function(x) - return ("'%s'"):format(x) +utils.stringify = function(s) + s = s:gsub('\n', '\\n') + if s:match("'") and not s:match('"') then + return '"' .. s .. '"' + else + return "'" .. s:gsub("'", "\\'") .. "'" + end end utils.snake_case = function(str) @@ -107,12 +112,12 @@ local function process(node) return table.concat(output, '\n') else local template = [[ - it('%s', function() + it(%s, function() %s end)]] return template:format( - node.description:lower():gsub('\'', '\\\''), + utils.stringify(node.description:lower()), spec_generator.generate_test(node) ) end diff --git a/exercises/practice/baffling-birthdays/.meta/example.lua b/exercises/practice/baffling-birthdays/.meta/example.lua index bab1ea98..b5d8df7a 100644 --- a/exercises/practice/baffling-birthdays/.meta/example.lua +++ b/exercises/practice/baffling-birthdays/.meta/example.lua @@ -17,7 +17,7 @@ baffling_birthdays.random_birthdates = function(count) local non_leap_year = 2026 local seconds_per_day = 24 * 60 * 60 - local start = os.time { year = non_leap_year, month = 1, day = 1, hour = 12 } + local start = os.time({ year = non_leap_year, month = 1, day = 1, hour = 12 }) for i = 1, count do local offset = math.random(0, 364) local t = start + offset * seconds_per_day diff --git a/exercises/practice/word-count/.meta/spec_generator.lua b/exercises/practice/word-count/.meta/spec_generator.lua index 89a6e8f7..70074656 100644 --- a/exercises/practice/word-count/.meta/spec_generator.lua +++ b/exercises/practice/word-count/.meta/spec_generator.lua @@ -1,16 +1,9 @@ -local function stringify(s) - s = s:gsub('\n', '\\n') - if s:match("'") and not s:match('"') then - return '"' .. s .. '"' - else - return "'" .. s:gsub("'", "\\'") .. "'" - end -end +local utils = require 'utils' local function format_hash(t) local key_value_pairs = {} for key in pairs(t) do - table.insert(key_value_pairs, "[" .. stringify(key) .. "] = " .. t[key]) + table.insert(key_value_pairs, "[" .. utils.stringify(key) .. "] = " .. t[key]) end table.sort(key_value_pairs) @@ -27,6 +20,6 @@ return { local expected = %s assert.are.same(expected, result)]] - return template:format(stringify(case.input.sentence), format_hash(case.expected)) + return template:format(utils.stringify(case.input.sentence), format_hash(case.expected)) end }