Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Alphametics exercise #105

Merged
merged 9 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,14 @@
"prerequisites": [],
"difficulty": 7
},
{
"slug": "alphametics",
"name": "Alphametics",
"uuid": "6bd98d4f-f95a-4fa2-981a-1684bd42334d",
"practices": [],
"prerequisites": [],
"difficulty": 8
},
{
"slug": "grep",
"name": "Grep",
Expand Down
29 changes: 29 additions & 0 deletions exercises/practice/alphametics/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -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
77 changes: 77 additions & 0 deletions exercises/practice/alphametics/.meta/Example.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
module [solve]

solve : Str -> Result (List (U8, U8)) _
solve = \problem ->
{ addends, sum } = parse? problem

# We can represent the equation as a dictionary of the letters mapped to their coefficients
# when we simplify the equation. For example, we can write AB + A + B == C as 11A + 2B + (-1)C == 0.
# That then becomes this dictionary: `Dict.fromList [('A', 11), ('B', 2), ('C', -1)]
equation =
List.walk addends (Dict.empty {}) \dict, term ->
insertTerm dict term 1
|> insertTerm sum -1

leadingDigits =
List.map addends \letters ->
List.first letters |> Result.withDefault 0
|> Set.fromList
|> Set.insert (List.first sum |> Result.withDefault 0)

findMatch = \assignments, remainingVars, remainingDigits ->
when remainingVars is
[] ->
totalVal =
List.walk assignments 0 \total, (letter, value) ->
Dict.get equation letter
|> Result.withDefault 0
|> Num.mul (Num.toI64 value)
|> Num.add total

if totalVal != 0 then
Err InvalidAssignment
else

Ok assignments

[letter, .. as rest] ->
findFirstOk remainingDigits \digit ->
if digit == 0 && Set.contains leadingDigits letter then
Err InvalidAssignment
else

# Each digit has to be unique, so once we use a digit we remove it from the pool
findMatch (List.append assignments (letter, digit)) rest (Set.remove remainingDigits digit)

digits = List.range { start: At 0, end: At 9 } |> Set.fromList
findMatch [] (Dict.keys equation) digits

# Apply a function to each element of a list until the function returns an Ok, then return that value
findFirstOk : Set a, (a -> Result b err) -> Result b [NotFound]
findFirstOk = \set, func ->
Set.walkUntil set (Err NotFound) \state, elem ->
when func elem is
Err _ -> Continue state
Ok val -> Break (Ok val)

# Update the equation with the values of a term
insertTerm : Dict U8 I64, List U8, I64 -> Dict U8 I64
insertTerm = \equation, letters, polarity ->
List.reverse letters
|> List.walkWithIndex equation \dict, letter, index ->
coeff =
Num.powInt 10 index
|> Num.toI64
|> Num.mul polarity
Dict.update dict letter \val ->
when val is
Missing -> Present coeff
Present c -> Present (c + coeff)

parse : Str -> Result { addends : List (List U8), sum : List U8 } _
parse = \problem ->
{ before, after } = Str.splitFirst? problem " == "
addends =
Str.split before " + "
|> List.map Str.toUtf8
Ok { addends, sum: Str.toUtf8 after }
17 changes: 17 additions & 0 deletions exercises/practice/alphametics/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"authors": [
"isaacvando"
],
"files": {
"solution": [
"Alphametics.roc"
],
"test": [
"alphametics-test.roc"
],
"example": [
".meta/Example.roc"
]
},
"blurb": "Given an alphametics puzzle, find the correct solution."
}
21 changes: 21 additions & 0 deletions exercises/practice/alphametics/.meta/template.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{%- import "generator_macros.j2" as macros with context -%}
{{ macros.canonical_ref() }}
{{ macros.header() }}

import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_camel }}]

{% for case in cases -%}
# {{ case["description"] }}
expect
result = {{ case["property"] | to_camel }} {{ case["input"]["puzzle"] | to_roc }}
{%- if case["expected"] %}
Result.withDefault result [] |> Set.fromList == Set.fromList [
{%- for letter, value in case["expected"].items() %}
('{{ letter }}', {{ value }}),
{%- endfor %}
]
{%- else %}
Result.isErr result
{%- endif %}

{% endfor %}
40 changes: 40 additions & 0 deletions exercises/practice/alphametics/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -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"
5 changes: 5 additions & 0 deletions exercises/practice/alphametics/Alphametics.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module [solve]

solve : Str -> Result (List (U8, U64)) _
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This annotation differs slightly from the one in .meta/Example.roc: it uses (U8, U64) instead of (U8, U8). I suppose you want to use the latter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks! I will update this

solve = \problem ->
crash "Please implement 'solve'"
137 changes: 137 additions & 0 deletions exercises/practice/alphametics/alphametics-test.roc
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# These tests are auto-generated with test data from:
# https://github.com/exercism/problem-specifications/tree/main/exercises/alphametics/canonical-data.json
# File last updated on 2024-09-22
app [main] {
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br",
}

main =
Task.ok {}

import Alphametics exposing [solve]

# puzzle with three letters
expect
result = solve "I + BB == ILL"
Result.withDefault result []
|> Set.fromList
== Set.fromList [
('I', 1),
('B', 9),
('L', 0),
]

# solution must have unique value for each letter
expect
result = solve "A == B"
Result.isErr result

# leading zero solution is invalid
expect
result = solve "ACA + DD == BD"
Result.isErr result

# puzzle with two digits final carry
expect
result = solve "A + A + A + A + A + A + A + A + A + A + A + B == BCC"
Result.withDefault result []
|> Set.fromList
== Set.fromList [
('A', 9),
('B', 1),
('C', 0),
]

# puzzle with four letters
expect
result = solve "AS + A == MOM"
Result.withDefault result []
|> Set.fromList
== Set.fromList [
('A', 9),
('S', 2),
('M', 1),
('O', 0),
]

# puzzle with six letters
expect
result = solve "NO + NO + TOO == LATE"
Result.withDefault result []
|> Set.fromList
== Set.fromList [
('N', 7),
('O', 4),
('T', 9),
('L', 1),
('A', 0),
('E', 2),
]

# puzzle with seven letters
expect
result = solve "HE + SEES + THE == LIGHT"
Result.withDefault result []
|> Set.fromList
== Set.fromList [
('E', 4),
('G', 2),
('H', 5),
('I', 0),
('L', 1),
('S', 9),
('T', 7),
]

# puzzle with eight letters
expect
result = solve "SEND + MORE == MONEY"
Result.withDefault result []
|> Set.fromList
== Set.fromList [
('S', 9),
('E', 5),
('N', 6),
('D', 7),
('M', 1),
('O', 0),
('R', 8),
('Y', 2),
]

# puzzle with ten letters
expect
result = solve "AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE"
Result.withDefault result []
|> Set.fromList
== Set.fromList [
('A', 5),
('D', 3),
('E', 4),
('F', 7),
('G', 8),
('N', 0),
('O', 2),
('R', 1),
('S', 6),
('T', 9),
]

# puzzle with ten letters and 199 addends
expect
result = 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"
Result.withDefault result []
|> Set.fromList
== Set.fromList [
('A', 1),
('E', 0),
('F', 5),
('H', 8),
('I', 7),
('L', 2),
('O', 6),
('R', 3),
('S', 4),
('T', 9),
]