From 0f9ef25a3ce1bc55d2d44c9434d8bfb1c10fc13a Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Tue, 19 Mar 2024 16:53:44 +0100 Subject: [PATCH] Add `alphametics` exercise (#379) --- config.json | 50 +++++++++++++---- .../alphametics/.docs/instructions.md | 31 +++++++++++ .../alphametics/.meta/alphametics.example.pl | 55 +++++++++++++++++++ .../practice/alphametics/.meta/config.json | 17 ++++++ .../practice/alphametics/.meta/tests.toml | 40 ++++++++++++++ exercises/practice/alphametics/alphametics.pl | 1 + .../alphametics/alphametics_tests.plt | 55 +++++++++++++++++++ 7 files changed, 239 insertions(+), 10 deletions(-) create mode 100644 exercises/practice/alphametics/.docs/instructions.md create mode 100644 exercises/practice/alphametics/.meta/alphametics.example.pl create mode 100644 exercises/practice/alphametics/.meta/config.json create mode 100644 exercises/practice/alphametics/.meta/tests.toml create mode 100644 exercises/practice/alphametics/alphametics.pl create mode 100644 exercises/practice/alphametics/alphametics_tests.plt diff --git a/config.json b/config.json index 6a671622..51365ff3 100644 --- a/config.json +++ b/config.json @@ -19,10 +19,18 @@ "average_run_time": 1 }, "files": { - "solution": ["%{snake_slug}.pl"], - "test": ["%{snake_slug}_tests.plt"], - "example": [".meta/%{snake_slug}.example.pl"], - "exemplar": [".meta/%{snake_slug}.exemplar.pl"] + "solution": [ + "%{snake_slug}.pl" + ], + "test": [ + "%{snake_slug}_tests.plt" + ], + "example": [ + ".meta/%{snake_slug}.example.pl" + ], + "exemplar": [ + ".meta/%{snake_slug}.exemplar.pl" + ] }, "exercises": { "practice": [ @@ -65,7 +73,9 @@ "practices": [], "prerequisites": [], "difficulty": 1, - "topics": ["math"] + "topics": [ + "math" + ] }, { "slug": "rna-transcription", @@ -90,7 +100,9 @@ "practices": [], "prerequisites": [], "difficulty": 1, - "topics": ["math"] + "topics": [ + "math" + ] }, { "slug": "dominoes", @@ -107,7 +119,9 @@ "practices": [], "prerequisites": [], "difficulty": 1, - "topics": ["math"] + "topics": [ + "math" + ] }, { "slug": "grains", @@ -148,7 +162,11 @@ "practices": [], "prerequisites": [], "difficulty": 1, - "topics": ["binary_trees", "recursion", "tree_traversals"] + "topics": [ + "binary_trees", + "recursion", + "tree_traversals" + ] }, { "slug": "space-age", @@ -173,7 +191,9 @@ "practices": [], "prerequisites": [], "difficulty": 1, - "topics": ["math"] + "topics": [ + "math" + ] }, { "slug": "wordy", @@ -182,7 +202,9 @@ "practices": [], "prerequisites": [], "difficulty": 1, - "topics": ["parsing"] + "topics": [ + "parsing" + ] }, { "slug": "zebra-puzzle", @@ -663,6 +685,14 @@ "practices": [], "prerequisites": [], "difficulty": 1 + }, + { + "slug": "alphametics", + "name": "Alphametics", + "uuid": "9f430940-9e6f-4f80-9ae4-0ebcc91cf055", + "practices": [], + "prerequisites": [], + "difficulty": 1 } ], "foregone": [ diff --git a/exercises/practice/alphametics/.docs/instructions.md b/exercises/practice/alphametics/.docs/instructions.md new file mode 100644 index 00000000..649576ec --- /dev/null +++ b/exercises/practice/alphametics/.docs/instructions.md @@ -0,0 +1,31 @@ +# Instructions + +Write a function to solve alphametics puzzles. + +[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. + +Write a function to solve alphametics puzzles. + +[alphametics]: https://en.wikipedia.org/wiki/Alphametics diff --git a/exercises/practice/alphametics/.meta/alphametics.example.pl b/exercises/practice/alphametics/.meta/alphametics.example.pl new file mode 100644 index 00000000..031b4527 --- /dev/null +++ b/exercises/practice/alphametics/.meta/alphametics.example.pl @@ -0,0 +1,55 @@ +:- use_module(library(dcg/basics)). +:- use_module(library(clpfd)). +:- set_prolog_flag(double_quotes, chars). + +letter(Letter) --> [Letter], { char_type(Letter, upper) }. +operand([Letter|Letters]) --> letter(Letter), operand(Letters). +operand([Letter]) --> letter(Letter). + +operands([Operand|Operands]) --> operand(Operand), " + ", operands(Operands). +operands([Operand]) --> operand(Operand). + +equation(Left, Right) --> operands(Left), " == ", operands(Right). + +letter_map(Sign, Letters, Mapping) :- + reverse(Letters, ReversedLetters), + findall(L-C, (nth0(I, ReversedLetters, L), C is Sign * 10 ^ I), Mapping). + +map_merge(Mappings, Mapping) :- + setof(Key, Value^member(Key-Value, Mappings), Keys), + findall(Key-Sum, (member(Key, Keys), aggregate(sum(Value), member(Key-Value, Mappings), Sum)), Mapping). + +mapping(Left, Right, Mapping) :- + maplist(letter_map(1), Left, LeftMappings), + maplist(letter_map(-1), Right, RightMappings), + append(LeftMappings, RightMappings, NestedMappings), + flatten(NestedMappings, Mappings), + map_merge(Mappings, Mapping). + +zero_letters(Left, Right, ZeroLetters) :- + append(Left, Right, LeftAndRight), + findall(ZeroChar, member([ZeroChar|_], LeftAndRight), ALlZeroLetters), + sort(ALlZeroLetters, ZeroLetters). + +parse(Equation, Mapping, Letters, ZeroLetters) :- + string_chars(Equation, Chars), + phrase(equation(Left, Right), Chars), + mapping(Left, Right, Mapping), + zero_letters(Left, Right, ZeroLetters), + pairs_keys(Mapping, Letters). + +add_range(ZeroLetters, Letter, Digit) :- + (member(Letter, ZeroLetters) -> Min = 1; Min = 0), + Digit in Min..9. + +solve(Equation, Solution) :- + parse(Equation, Mapping, Letters, ZeroLetters), + length(Letters, Count), + length(Digits, Count), + all_different(Digits), + maplist(add_range(ZeroLetters), Letters, Digits), + pairs_values(Mapping, Multipliers), + scalar_product(Multipliers, Digits, #=, 0), + pairs_keys_values(Solution, Letters, Digits), + label(Digits), + !. diff --git a/exercises/practice/alphametics/.meta/config.json b/exercises/practice/alphametics/.meta/config.json new file mode 100644 index 00000000..f4165a0a --- /dev/null +++ b/exercises/practice/alphametics/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [ + "erikschierboom" + ], + "files": { + "solution": [ + "alphametics.pl" + ], + "test": [ + "alphametics_tests.plt" + ], + "example": [ + ".meta/alphametics.example.pl" + ] + }, + "blurb": "Write a function to solve alphametics puzzles." +} diff --git a/exercises/practice/alphametics/.meta/tests.toml b/exercises/practice/alphametics/.meta/tests.toml new file mode 100644 index 00000000..f599b3da --- /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.pl b/exercises/practice/alphametics/alphametics.pl new file mode 100644 index 00000000..07740d9b --- /dev/null +++ b/exercises/practice/alphametics/alphametics.pl @@ -0,0 +1 @@ +solve(Equation, Solution). diff --git a/exercises/practice/alphametics/alphametics_tests.plt b/exercises/practice/alphametics/alphametics_tests.plt new file mode 100644 index 00000000..c439d38f --- /dev/null +++ b/exercises/practice/alphametics/alphametics_tests.plt @@ -0,0 +1,55 @@ +pending :- + current_prolog_flag(argv, ['--all'|_]). +pending :- + write('\nA TEST_IS PENDING!\n'), + fail. + +:- begin_tests(alphametics). + + test(puzzle_with_three_letters, condition(true)) :- + solve("I + BB == ILL", Solution), + sort(Solution, SortedSolution), + SortedSolution == ['B'-9, 'I'-1, 'L'-0]. + + test(solution_must_have_unique_value_for_each_letter, [fail, condition(pending)]) :- + solve("A == B", _). + + test(leading_zero_solution_is_invalid, [fail, condition(pending)]) :- + solve("ACA + DD == BD", _). + + test(puzzle_with_two_digits_final_carry, condition(pending)) :- + solve("A + A + A + A + A + A + A + A + A + A + A + B == BCC", Solution), + sort(Solution, SortedSolution), + SortedSolution == ['A'-9, 'B'-1, 'C'-0]. + + test(puzzle_with_four_letters, condition(pending)) :- + solve("AS + A == MOM", Solution), + sort(Solution, SortedSolution), + SortedSolution == ['A'-9, 'M'-1, 'O'-0, 'S'-2]. + + test(puzzle_with_six_letters, condition(pending)) :- + solve("NO + NO + TOO == LATE", Solution), + sort(Solution, SortedSolution), + SortedSolution == ['A'-0, 'E'-2, 'L'-1, 'N'-7, 'O'-4, 'T'-9]. + + test(puzzle_with_seven_letters, condition(pending)) :- + solve("HE + SEES + THE == LIGHT", Solution), + sort(Solution, SortedSolution), + SortedSolution == ['E'-4, 'G'-2, 'H'-5, 'I'-0, 'L'-1, 'S'-9, 'T'-7]. + + test(puzzle_with_eight_letters, condition(pending)) :- + solve("SEND + MORE == MONEY", Solution), + sort(Solution, SortedSolution), + SortedSolution == ['D'-7, 'E'-5, 'M'-1, 'N'-6, 'O'-0, 'R'-8, 'S'-9, 'Y'-2]. + + test(puzzle_with_ten_letters, condition(pending)) :- + solve("AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE", Solution), + sort(Solution, SortedSolution), + SortedSolution == ['A'-5, 'D'-3, 'E'-4, 'F'-7, 'G'-8, 'N'-0, 'O'-2, 'R'-1, 'S'-6, 'T'-9]. + + test(puzzle_with_ten_letters_and_199_addends, condition(pending)) :- + solve("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", Solution), + sort(Solution, SortedSolution), + SortedSolution == ['A'-1, 'E'-0, 'F'-5, 'H'-8, 'I'-7, 'L'-2, 'O'-6, 'R'-3, 'S'-4, 'T'-9]. + +:- end_tests(alphametics).