diff --git a/config.json b/config.json index 194d6753..478c1d7b 100644 --- a/config.json +++ b/config.json @@ -426,6 +426,14 @@ "prerequisites": [], "difficulty": 8 }, + { + "slug": "say", + "name": "Say", + "uuid": "5b643efe-e63b-4598-95f8-a4cb319107de", + "practices": [], + "prerequisites": [], + "difficulty": 8 + }, { "slug": "scrabble-score", "name": "Scrabble Score", diff --git a/exercises/practice/say/.docs/instructions.md b/exercises/practice/say/.docs/instructions.md new file mode 100644 index 00000000..3251c519 --- /dev/null +++ b/exercises/practice/say/.docs/instructions.md @@ -0,0 +1,12 @@ +# Instructions + +Given a number, your task is to express it in English words exactly as your friend should say it out loud. +Yaʻqūb expects to use numbers from 0 up to 999,999,999,999. + +Examples: + +- 0 → zero +- 1 → one +- 12 → twelve +- 123 → one hundred twenty-three +- 1,234 → one thousand two hundred thirty-four diff --git a/exercises/practice/say/.docs/introduction.md b/exercises/practice/say/.docs/introduction.md new file mode 100644 index 00000000..abd22851 --- /dev/null +++ b/exercises/practice/say/.docs/introduction.md @@ -0,0 +1,6 @@ +# Introduction + +Your friend Yaʻqūb works the counter at the busiest deli in town, slicing, weighing, and wrapping orders for a never-ending line of hungry customers. +To keep things moving, each customer takes a numbered ticket when they arrive. + +When it’s time to call the next person, Yaʻqūb reads their number out loud, always in full English words to make sure everyone hears it clearly. diff --git a/exercises/practice/say/.meta/config.json b/exercises/practice/say/.meta/config.json new file mode 100644 index 00000000..65a8d47b --- /dev/null +++ b/exercises/practice/say/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "jimmytty" + ], + "files": { + "solution": [ + "say.sql" + ], + "test": [ + "say_test.sql" + ], + "example": [ + ".meta/example.sql" + ] + }, + "blurb": "Given a number from 0 to 999,999,999,999, spell out that number in English.", + "source": "A variation on the JavaRanch CattleDrive, Assignment 4", + "source_url": "https://web.archive.org/web/20240907035912/https://coderanch.com/wiki/718804" +} diff --git a/exercises/practice/say/.meta/example.sql b/exercises/practice/say/.meta/example.sql new file mode 100644 index 00000000..524edb65 --- /dev/null +++ b/exercises/practice/say/.meta/example.sql @@ -0,0 +1,147 @@ +UPDATE say SET result = 'zero' WHERE number = 0; + +UPDATE say + SET error = 'input out of range' + WHERE number < 0 + OR number >= 1e12 +; + +DROP TABLE IF EXISTS dict; +CREATE TEMPORARY TABLE dict ( + digits INTEGER PRIMARY KEY, + words TEXT NOT NULL +); +INSERT INTO dict (digits, words) +VALUES ( 0, 'zero' ), + ( 1, 'one' ), + ( 2, 'two' ), + ( 3, 'three' ), + ( 4, 'four' ), + ( 5, 'five' ), + ( 6, 'six' ), + ( 7, 'seven' ), + ( 8, 'eight' ), + ( 9, 'nine' ), + (10, 'ten' ), + (11, 'eleven' ), + (12, 'twelve' ), + (13, 'thirteen' ), + (14, 'fourteen' ), + (15, 'fifteen' ), + (16, 'sixteen' ), + (17, 'seventeen'), + (18, 'eighteen' ), + (19, 'nineteen' ), + (20, 'twenty' ), + (30, 'thirty' ), + (40, 'forty' ), + (50, 'fifty' ), + (60, 'sixty' ), + (70, 'seventy' ), + (80, 'eighty' ), + (90, 'ninety' ); +WITH dozens (jarray) AS ( + SELECT ( + WITH RECURSIVE rcte (base, unit, dozen, string) AS ( + VALUES (digits, 1, NULL, NULL) + UNION ALL + SELECT digits, + unit + 1, + base + unit, + PRINTF( + '%s-%s', + (SELECT words FROM dict WHERE digits = base), + (SELECT words FROM dict WHERE digits = unit)) + FROM rcte + WHERE unit < 10 + ) + SELECT JSON_GROUP_ARRAY(JSON_ARRAY(dozen, string)) + FROM rcte + WHERE dozen NOT NULL + ) + FROM dict + WHERE digits BETWEEN 20 AND 90 +) +INSERT INTO dict (digits, words) +SELECT JSON_EXTRACT(j.VALUE, '$[0]'), JSON_EXTRACT(j.VALUE, '$[1]') + FROM dozens, JSON_EACH(jarray) j; +INSERT INTO dict +SELECT value * 100, + (SELECT words FROM dict WHERE digits = VALUE) || ' hundred' + FROM GENERATE_SERIES(1, 9); + +DROP TABLE IF EXISTS positions; +CREATE TEMPORARY TABLE positions ( + pos INTEGER PRIMARY KEY, + words TEXT NOT NULL +); +INSERT INTO positions (pos, words) +VALUES ( 2, 'thousand' ), + ( 3, 'million' ), + ( 4, 'billion' ), + ( 5, 'trillion' ), + ( 6, 'quadrillion' ), + ( 7, 'quintillion' ), + ( 8, 'sextillion' ), + ( 9, 'septillion' ), + (10, 'octillion' ), + (11, 'nonillion' ), + (12, 'decillion' ), + (13, 'undecillion' ), + (14, 'duodecillion' ), + (15, 'tredecillion' ), + (16, 'quattuordecillion'), + (17, 'quindecillion' ), + (18, 'sexdecillion' ), + (19, 'septendecillion' ), + (20, 'octodecillion' ), + (21, 'novemdecillion' ), + (22, 'vigintillion' ); + +UPDATE say + SET result = ( + WITH + decompositor (num, thousands, thpos) AS ( + WITH RECURSIVE decompositor (num, thousands, thpos) AS ( + VALUES (number, NULL, 0) + UNION ALL + SELECT num / 1000, + JSON_ARRAY( num % 1000 - num % 100, (num % 1000) % 100 ), + thpos + 1 + FROM decompositor + WHERE num > 0 + ) + SELECT * FROM decompositor + ), + to_strings (thpos, hundred, dozen, pos_str) AS ( + SELECT + thpos, + ( + SELECT words + FROM dict + WHERE digits = NULLIF(JSON_EXTRACT(thousands, '$[0]'), 0) + ), + ( + SELECT words + FROM dict + WHERE digits = NULLIF(JSON_EXTRACT(thousands, '$[1]'), 0 ) + ), + IIF( + JSON_EXTRACT(thousands, '$[0]') + + JSON_EXTRACT(thousands, '$[1]') > 0, + (SELECT words FROM positions WHERE pos = thpos), + NULL + ) + FROM decompositor + WHERE thpos > 0 + ), + to_format (thpos, string) AS ( + SELECT thpos, TRIM(PRINTF('%s %s %s', hundred, dozen, pos_str)) + FROM to_strings + ORDER BY thpos DESC + ) + SELECT TRIM(GROUP_CONCAT(string, ' ')) FROM to_format + ) + WHERE error IS NULL + AND number > 0 +; diff --git a/exercises/practice/say/.meta/tests.toml b/exercises/practice/say/.meta/tests.toml new file mode 100644 index 00000000..a5532e9e --- /dev/null +++ b/exercises/practice/say/.meta/tests.toml @@ -0,0 +1,67 @@ +# 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. + +[5d22a120-ba0c-428c-bd25-8682235d83e8] +description = "zero" + +[9b5eed77-dbf6-439d-b920-3f7eb58928f6] +description = "one" + +[7c499be1-612e-4096-a5e1-43b2f719406d] +description = "fourteen" + +[f541dd8e-f070-4329-92b4-b7ce2fcf06b4] +description = "twenty" + +[d78601eb-4a84-4bfa-bf0e-665aeb8abe94] +description = "twenty-two" + +[f010d4ca-12c9-44e9-803a-27789841adb1] +description = "thirty" + +[738ce12d-ee5c-4dfb-ad26-534753a98327] +description = "ninety-nine" + +[e417d452-129e-4056-bd5b-6eb1df334dce] +description = "one hundred" + +[d6924f30-80ba-4597-acf6-ea3f16269da8] +description = "one hundred twenty-three" + +[2f061132-54bc-4fd4-b5df-0a3b778959b9] +description = "two hundred" + +[feed6627-5387-4d38-9692-87c0dbc55c33] +description = "nine hundred ninety-nine" + +[3d83da89-a372-46d3-b10d-de0c792432b3] +description = "one thousand" + +[865af898-1d5b-495f-8ff0-2f06d3c73709] +description = "one thousand two hundred thirty-four" + +[b6a3f442-266e-47a3-835d-7f8a35f6cf7f] +description = "one million" + +[2cea9303-e77e-4212-b8ff-c39f1978fc70] +description = "one million two thousand three hundred forty-five" + +[3e240eeb-f564-4b80-9421-db123f66a38f] +description = "one billion" + +[9a43fed1-c875-4710-8286-5065d73b8a9e] +description = "a big number" + +[49a6a17b-084e-423e-994d-a87c0ecc05ef] +description = "numbers below zero are out of range" + +[4d6492eb-5853-4d16-9d34-b0f61b261fd9] +description = "numbers above 999,999,999,999 are out of range" diff --git a/exercises/practice/say/create_fixture.sql b/exercises/practice/say/create_fixture.sql new file mode 100644 index 00000000..794ab413 --- /dev/null +++ b/exercises/practice/say/create_fixture.sql @@ -0,0 +1,11 @@ +DROP TABLE IF EXISTS say; +CREATE TABLE say ( + number INTEGER NOT NULL, + result TEXT, + error TEXT +); + +.mode csv +.import ./data.csv say + +UPDATE say SET result = NULL, error = NULL; diff --git a/exercises/practice/say/create_test_table.sql b/exercises/practice/say/create_test_table.sql new file mode 100644 index 00000000..ff655b5d --- /dev/null +++ b/exercises/practice/say/create_test_table.sql @@ -0,0 +1,38 @@ +DROP TABLE IF EXISTS tests; +CREATE TABLE IF NOT EXISTS tests ( + -- uuid and description are taken from the test.toml file + uuid TEXT PRIMARY KEY, + description TEXT NOT NULL, + -- The following section is needed by the online test-runner + status TEXT DEFAULT 'fail', + message TEXT, + output TEXT, + test_code TEXT, + task_id INTEGER DEFAULT NULL, + -- Here are columns for the actual tests + number INTEGER NOT NULL, + expected_result TEXT, + expected_error TEXT +); + +INSERT INTO tests (uuid, description, number, expected_result, expected_error) + VALUES + ('5d22a120-ba0c-428c-bd25-8682235d83e8', 'zero', 0, 'zero', NULL), + ('9b5eed77-dbf6-439d-b920-3f7eb58928f6', 'one', 1, 'one', NULL), + ('7c499be1-612e-4096-a5e1-43b2f719406d', 'fourteen', 14, 'fourteen', NULL), + ('f541dd8e-f070-4329-92b4-b7ce2fcf06b4', 'twenty', 20, 'twenty', NULL), + ('d78601eb-4a84-4bfa-bf0e-665aeb8abe94', 'twenty-two', 22, 'twenty-two', NULL), + ('f010d4ca-12c9-44e9-803a-27789841adb1', 'thirty', 30, 'thirty', NULL), + ('738ce12d-ee5c-4dfb-ad26-534753a98327', 'ninety-nine', 99, 'ninety-nine', NULL), + ('e417d452-129e-4056-bd5b-6eb1df334dce', 'one hundred', 100, 'one hundred', NULL), + ('d6924f30-80ba-4597-acf6-ea3f16269da8', 'one hundred twenty-three', 123, 'one hundred twenty-three', NULL), + ('2f061132-54bc-4fd4-b5df-0a3b778959b9', 'two hundred', 200, 'two hundred', NULL), + ('feed6627-5387-4d38-9692-87c0dbc55c33', 'nine hundred ninety-nine', 999, 'nine hundred ninety-nine', NULL), + ('3d83da89-a372-46d3-b10d-de0c792432b3', 'one thousand', 1000, 'one thousand', NULL), + ('865af898-1d5b-495f-8ff0-2f06d3c73709', 'one thousand two hundred thirty-four', 1234, 'one thousand two hundred thirty-four', NULL), + ('b6a3f442-266e-47a3-835d-7f8a35f6cf7f', 'one million', 1000000, 'one million', NULL), + ('2cea9303-e77e-4212-b8ff-c39f1978fc70', 'one million two thousand three hundred forty-five', 1002345, 'one million two thousand three hundred forty-five', NULL), + ('3e240eeb-f564-4b80-9421-db123f66a38f', 'one billion', 1000000000, 'one billion', NULL), + ('9a43fed1-c875-4710-8286-5065d73b8a9e', 'a big number', 987654321123, 'nine hundred eighty-seven billion six hundred fifty-four million three hundred twenty-one thousand one hundred twenty-three', NULL), + ('49a6a17b-084e-423e-994d-a87c0ecc05ef', 'numbers below zero are out of range', -1, NULL, 'input out of range'), + ('4d6492eb-5853-4d16-9d34-b0f61b261fd9', 'numbers above 999,999,999,999 are out of range', 1000000000000, NULL, 'input out of range'); diff --git a/exercises/practice/say/data.csv b/exercises/practice/say/data.csv new file mode 100644 index 00000000..4f3a5707 --- /dev/null +++ b/exercises/practice/say/data.csv @@ -0,0 +1,19 @@ +0,, +1,, +14,, +20,, +22,, +30,, +99,, +100,, +123,, +200,, +999,, +1000,, +1234,, +1000000,, +1002345,, +1000000000,, +987654321123,, +-1,, +1000000000000,, diff --git a/exercises/practice/say/say.sql b/exercises/practice/say/say.sql new file mode 100644 index 00000000..12aae8ee --- /dev/null +++ b/exercises/practice/say/say.sql @@ -0,0 +1,8 @@ +-- Schema: +-- CREATE TABLE say ( +-- number INTEGER NOT NULL, +-- result TEXT, +-- error TEXT +-- ); +-- +-- Task: update the say table and set the result or the error columns based on the number. diff --git a/exercises/practice/say/say_test.sql b/exercises/practice/say/say_test.sql new file mode 100644 index 00000000..7f837d7b --- /dev/null +++ b/exercises/practice/say/say_test.sql @@ -0,0 +1,48 @@ +-- Create database: +.read ./create_fixture.sql + +-- Read user student solution and save any output as markdown in user_output.md: +.mode markdown +.output user_output.md +.read ./say.sql +.output + +-- Create a clean testing environment: +.read ./create_test_table.sql + +-- Comparison of user input and the tests updates the status for each test: +UPDATE tests + SET status = 'pass' + FROM (SELECT number, result, error FROM say) AS actual + WHERE actual.number = tests.NUMBER + AND (actual.result = tests.expected_result + OR COALESCE(actual.result, tests.expected_result) ISNULL) + AND (actual.error = tests.expected_error + OR COALESCE(actual.error, tests.expected_error) ISNULL); + +-- Update message for failed tests to give helpful information: +UPDATE tests + SET message = ( + 'Result for "' || actual.NUMBER || '"' || ' is <' + || PRINTF('result=%s and error="%s"', + COALESCE(actual.result, 'NULL'), + COALESCE(actual.error, 'NULL')) + || '> but should be <' + || PRINTF('result=%s and error="%s"', + COALESCE(tests.expected_result, '"NULL"'), + COALESCE(tests.expected_error, 'NULL')) + || '>' +) +FROM (SELECT number, result, error FROM say) AS actual +WHERE (actual.number) = tests.number AND tests.status = 'fail'; + +-- Save results to ./output.json (needed by the online test-runner) +.mode json +.once './output.json' +SELECT description, status, message, output, test_code, task_id +FROM tests; + +-- Display test results in readable form for the student: +.mode table +SELECT description, status, message +FROM tests;