diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b7d516..4da0a0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -129,7 +129,7 @@ It's a module that returns a table. - [`dnd-character`][dnd-character] -- `assert.between value, min, max` - [`space-age`][space-age] -- `assert.approx_equal #{case.expected}, result` -- [`word-count`][word-count] -- `assert.has.same_kv result, expected` +- [`alphametics`][alphametics] -- `assert.has.same_kv result, expected` #### Helper functions for formatting test cases @@ -246,9 +246,9 @@ Here, the value `4` was chosen to reflect the max depth of the expected value: [generate-spec-exported]: ./bin/generate-spec#L51 [test-helpers]: ./lib/test_helpers.moon [space-age]: ./exercises/practice/space-age/.meta/spec_generator.moon -[word-count]: ./exercises/practice/word-count/.meta/spec_generator.moon +[alphametics]: ./exercises/practice/alphametics/.meta/spec_generator.moon [dnd-character]: ./exercises/practice/dnd-character/.meta/spec_generator.moon [gigasecond]: ./exercises/practice/gigasecond/.meta/spec_generator.moon [simple-linked-list]: ./exercises/practice/simple-linked-list/.meta/spec_generator.moon [custom-set]: ./exercises/practice/custom-set/.meta/spec_generator.moon#L42 -[robot-name]: ./exercises/practice/robot-name/robot_name_spec.moon#L59 \ No newline at end of file +[robot-name]: ./exercises/practice/robot-name/robot_name_spec.moon#L59 diff --git a/config.json b/config.json index 10dfc4c..51df31e 100644 --- a/config.json +++ b/config.json @@ -882,6 +882,14 @@ "prerequisites": [], "difficulty": 7 }, + { + "slug": "alphametics", + "name": "Alphametics", + "uuid": "11e3eb84-464f-49ce-8f4c-983c552cc748", + "practices": [], + "prerequisites": [], + "difficulty": 8 + }, { "slug": "forth", "name": "Forth", diff --git a/exercises/practice/alphametics/.busted b/exercises/practice/alphametics/.busted new file mode 100644 index 0000000..86b84e7 --- /dev/null +++ b/exercises/practice/alphametics/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} diff --git a/exercises/practice/alphametics/.docs/instructions.md b/exercises/practice/alphametics/.docs/instructions.md new file mode 100644 index 0000000..ef2cbb4 --- /dev/null +++ b/exercises/practice/alphametics/.docs/instructions.md @@ -0,0 +1,29 @@ +# Instructions + +Given an alphametics puzzle, find the correct solution. + +[Alphametics][alphametics] is a puzzle where letters in words are replaced with numbers. + +For example `SEND + MORE = MONEY`: + +```text + S E N D + M O R E + +----------- +M O N E Y +``` + +Replacing these with valid numbers gives: + +```text + 9 5 6 7 + 1 0 8 5 + +----------- +1 0 6 5 2 +``` + +This is correct because every letter is replaced by a different number and the words, translated into numbers, then make a valid sum. + +Each letter must represent a different digit, and the leading digit of a multi-digit number must not be zero. + +[alphametics]: https://en.wikipedia.org/wiki/Alphametics diff --git a/exercises/practice/alphametics/.meta/config.json b/exercises/practice/alphametics/.meta/config.json new file mode 100644 index 0000000..f26d13f --- /dev/null +++ b/exercises/practice/alphametics/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "alphametics.moon" + ], + "test": [ + "alphametics_spec.moon" + ], + "example": [ + ".meta/example.moon" + ] + }, + "blurb": "Given an alphametics puzzle, find the correct solution." +} diff --git a/exercises/practice/alphametics/.meta/example.moon b/exercises/practice/alphametics/.meta/example.moon new file mode 100644 index 0000000..ff5061c --- /dev/null +++ b/exercises/practice/alphametics/.meta/example.moon @@ -0,0 +1,85 @@ +-- Returns a _map_ where the keys are initial letters of words in the puzzle. +-- These letters cannot be mapped to digit zero. +getLeadingDigits = (str) -> + {c, true for c in str\gmatch '%f[%u].'} + +-- Returns an ordered list of the last letters of each word in the puzzle. +getLastDigits = (str) -> + [word\sub(-1) for word in str\gmatch '%u+'] + +-- Returns a list of the letters in the puzzle, prioritizing: +-- a) last letters +-- b) most frequently occurring letters +extractUniqueLetters = (str, lastLetters) -> + alreadySeen, rest = {}, {} + for c in *lastLetters + alreadySeen[c] = (alreadySeen[c] or 0) + 1 + for c in str\gmatch '%u' + if not alreadySeen[c] + rest[c] = (rest[c] or 0) + 1 + + orderedKeys = (freq) -> + keys = [k for k, _ in pairs freq] + table.sort keys, (a, b) -> freq[a] > freq[b] + keys + + all = {} + all[#all + 1] = c for c in *orderedKeys(alreadySeen) + all[#all + 1] = c for c in *orderedKeys(rest) + all + +-- Does the mapping solve the puzzle? +isValid = (map, str) -> + eqn = str\gsub '%a', (c) -> map[c] + numbers = [tonumber num for num in eqn\gmatch '%d+'] + sum = 0 + sum += numbers[i] for i = 1, #numbers - 1 + sum == numbers[#numbers] + +-- Do the numbers in the last column add up? +-- * Return nil if not all the letters have been mapped +-- * Otherwise return true or false +isLastColumnValid = (map, lastLetters) -> + return nil if not map[lastLetters[#lastLetters]] + sum = 0 + for i = 1, #lastLetters - 1 + return nil if not map[lastLetters[i]] + sum += map[lastLetters[i]] + (sum % 10) == map[lastLetters[#lastLetters]] + +-- Does a key-value table contain a given falue +containsValue = (t, value) -> + for key, val in pairs t + if val == value + return true + false + +-- -------------------------------------------------------------------------- +solveAlphametics = (equation) -> + leadingLetters = getLeadingDigits equation + lastLetters = getLastDigits equation + variables = extractUniqueLetters equation, lastLetters + + backtrack = (assignment, index) -> + if index > #variables + return if isValid(assignment, equation) then assignment else nil + + currentVar = variables[index] + start = if leadingLetters[currentVar] then 1 else 0 + + for digit = start, 9 + if not containsValue assignment, digit + assignment[currentVar] = digit + constraint = isLastColumnValid assignment, lastLetters + if constraint == nil or constraint == true + result = backtrack assignment, index + 1 + return result if result + assignment[currentVar] = nil + + nil -- no solution found + + backtrack {}, 1 + +{ + solve: solveAlphametics +} diff --git a/exercises/practice/alphametics/.meta/spec_generator.moon b/exercises/practice/alphametics/.meta/spec_generator.moon new file mode 100644 index 0000000..a6b996f --- /dev/null +++ b/exercises/practice/alphametics/.meta/spec_generator.moon @@ -0,0 +1,39 @@ +import kv_table from require 'test_helpers' + +{ + module_imports: {'solve'}, + + generate_test: (case, level) -> + lines = if is_json_null case.expected + { + "puzzle = #{quote case.input.puzzle}", + "assert.is.falsy solve puzzle" + } + else + { + "puzzle = #{quote case.input.puzzle}", + "result = solve puzzle", + "expected = #{kv_table case.expected, level}", + "assert.is.same_kv result, expected" + } + table.concat [indent line, level for line in *lines], '\n' + + test_helpers: [[ + -- ---------------------------------------------------------- + same_kv = (state, arguments) -> + actual = arguments[1] + return false if type(actual) != 'table' + expected = arguments[2] + size = (t) -> #[k for k, _ in pairs t] + return false if size(expected) != size(actual) + for k, v in pairs expected + return false if actual[k] != v + true + + say = require 'say' + say\set 'assertion.same_kv.positive', 'Actual result\n%s\ndoes not have the same keys and values as expected\n%s' + say\set 'assertion.same_kv.negative', 'Actual result\n%s\nwas not supposed to be the same as expected\n%s' + assert\register 'assertion', 'same_kv', same_kv, 'assertion.same_kv.positive', 'assertion.same_kv.negative' + -- ---------------------------------------------------------- +]] +} diff --git a/exercises/practice/alphametics/.meta/tests.toml b/exercises/practice/alphametics/.meta/tests.toml new file mode 100644 index 0000000..f599b3d --- /dev/null +++ b/exercises/practice/alphametics/.meta/tests.toml @@ -0,0 +1,40 @@ +# 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. + +[e0c08b07-9028-4d5f-91e1-d178fead8e1a] +description = "puzzle with three letters" + +[a504ee41-cb92-4ec2-9f11-c37e95ab3f25] +description = "solution must have unique value for each letter" + +[4e3b81d2-be7b-4c5c-9a80-cd72bc6d465a] +description = "leading zero solution is invalid" + +[8a3e3168-d1ee-4df7-94c7-b9c54845ac3a] +description = "puzzle with two digits final carry" + +[a9630645-15bd-48b6-a61e-d85c4021cc09] +description = "puzzle with four letters" + +[3d905a86-5a52-4e4e-bf80-8951535791bd] +description = "puzzle with six letters" + +[4febca56-e7b7-4789-97b9-530d09ba95f0] +description = "puzzle with seven letters" + +[12125a75-7284-4f9a-a5fa-191471e0d44f] +description = "puzzle with eight letters" + +[fb05955f-38dc-477a-a0b6-5ef78969fffa] +description = "puzzle with ten letters" + +[9a101e81-9216-472b-b458-b513a7adacf7] +description = "puzzle with ten letters and 199 addends" diff --git a/exercises/practice/alphametics/alphametics.moon b/exercises/practice/alphametics/alphametics.moon new file mode 100644 index 0000000..620e45b --- /dev/null +++ b/exercises/practice/alphametics/alphametics.moon @@ -0,0 +1,4 @@ +{ + solve: (puzzle) -> + error 'Implement me' +} diff --git a/exercises/practice/alphametics/alphametics_spec.moon b/exercises/practice/alphametics/alphametics_spec.moon new file mode 100644 index 0000000..9bdf2ea --- /dev/null +++ b/exercises/practice/alphametics/alphametics_spec.moon @@ -0,0 +1,134 @@ +import solve from require 'alphametics' + +describe 'alphametics', -> + -- ---------------------------------------------------------- + same_kv = (state, arguments) -> + actual = arguments[1] + return false if type(actual) != 'table' + expected = arguments[2] + size = (t) -> #[k for k, _ in pairs t] + return false if size(expected) != size(actual) + for k, v in pairs expected + return false if actual[k] != v + true + + say = require 'say' + say\set 'assertion.same_kv.positive', 'Actual result\n%s\ndoes not have the same keys and values as expected\n%s' + say\set 'assertion.same_kv.negative', 'Actual result\n%s\nwas not supposed to be the same as expected\n%s' + assert\register 'assertion', 'same_kv', same_kv, 'assertion.same_kv.positive', 'assertion.same_kv.negative' + -- ---------------------------------------------------------- + + it 'puzzle with three letters', -> + puzzle = 'I + BB == ILL' + result = solve puzzle + expected = { + I: 1, + L: 0, + B: 9, + } + assert.is.same_kv result, expected + + pending 'solution must have unique value for each letter', -> + puzzle = 'A == B' + assert.is.falsy solve puzzle + + pending 'leading zero solution is invalid', -> + puzzle = 'ACA + DD == BD' + assert.is.falsy solve puzzle + + pending 'puzzle with two digits final carry', -> + puzzle = 'A + A + A + A + A + A + A + A + A + A + A + B == BCC' + result = solve puzzle + expected = { + A: 9, + C: 0, + B: 1, + } + assert.is.same_kv result, expected + + pending 'puzzle with four letters', -> + puzzle = 'AS + A == MOM' + result = solve puzzle + expected = { + A: 9, + O: 0, + S: 2, + M: 1, + } + assert.is.same_kv result, expected + + pending 'puzzle with six letters', -> + puzzle = 'NO + NO + TOO == LATE' + result = solve puzzle + expected = { + E: 2, + L: 1, + A: 0, + T: 9, + O: 4, + N: 7, + } + assert.is.same_kv result, expected + + pending 'puzzle with seven letters', -> + puzzle = 'HE + SEES + THE == LIGHT' + result = solve puzzle + expected = { + E: 4, + L: 1, + S: 9, + I: 0, + H: 5, + G: 2, + T: 7, + } + assert.is.same_kv result, expected + + pending 'puzzle with eight letters', -> + puzzle = 'SEND + MORE == MONEY' + result = solve puzzle + expected = { + E: 5, + D: 7, + S: 9, + R: 8, + Y: 2, + M: 1, + O: 0, + N: 6, + } + assert.is.same_kv result, expected + + pending 'puzzle with ten letters', -> + puzzle = 'AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE' + result = solve puzzle + expected = { + A: 5, + O: 2, + N: 0, + E: 4, + D: 3, + S: 6, + R: 1, + T: 9, + G: 8, + F: 7, + } + assert.is.same_kv result, expected + + pending 'puzzle with ten letters and 199 addends', -> + puzzle = 'THIS + A + FIRE + THEREFORE + FOR + ALL + HISTORIES + I + TELL + A + TALE + THAT + FALSIFIES + ITS + TITLE + TIS + A + LIE + THE + TALE + OF + THE + LAST + FIRE + HORSES + LATE + AFTER + THE + FIRST + FATHERS + FORESEE + THE + HORRORS + THE + LAST + FREE + TROLL + TERRIFIES + THE + HORSES + OF + FIRE + THE + TROLL + RESTS + AT + THE + HOLE + OF + LOSSES + IT + IS + THERE + THAT + SHE + STORES + ROLES + OF + LEATHERS + AFTER + SHE + SATISFIES + HER + HATE + OFF + THOSE + FEARS + A + TASTE + RISES + AS + SHE + HEARS + THE + LEAST + FAR + HORSE + THOSE + FAST + HORSES + THAT + FIRST + HEAR + THE + TROLL + FLEE + OFF + TO + THE + FOREST + THE + HORSES + THAT + ALERTS + RAISE + THE + STARES + OF + THE + OTHERS + AS + THE + TROLL + ASSAILS + AT + THE + TOTAL + SHIFT + HER + TEETH + TEAR + HOOF + OFF + TORSO + AS + THE + LAST + HORSE + FORFEITS + ITS + LIFE + THE + FIRST + FATHERS + HEAR + OF + THE + HORRORS + THEIR + FEARS + THAT + THE + FIRES + FOR + THEIR + FEASTS + ARREST + AS + THE + FIRST + FATHERS + RESETTLE + THE + LAST + OF + THE + FIRE + HORSES + THE + LAST + TROLL + HARASSES + THE + FOREST + HEART + FREE + AT + LAST + OF + THE + LAST + TROLL + ALL + OFFER + THEIR + FIRE + HEAT + TO + THE + ASSISTERS + FAR + OFF + THE + TROLL + FASTS + ITS + LIFE + SHORTER + AS + STARS + RISE + THE + HORSES + REST + SAFE + AFTER + ALL + SHARE + HOT + FISH + AS + THEIR + AFFILIATES + TAILOR + A + ROOFS + FOR + THEIR + SAFE == FORTRESSES' + result = solve puzzle + expected = { + L: 2, + A: 1, + O: 6, + E: 0, + T: 9, + S: 4, + R: 3, + I: 7, + H: 8, + F: 5, + } + assert.is.same_kv result, expected