From 5e61776527c197aed5f06f2e1f4da894a32c70b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Wed, 12 Aug 2026 22:23:12 +1200 Subject: [PATCH 1/6] Format code and add 8 new exercices: bottle-song, dnd-character, grade-school, line-up, resistor-color-trio, save-the-cow, simple-cipher, and twelve-days --- .../bottle-song/.docs/instructions.md | 57 ++++ .../practice/bottle-song/.meta/Example.roc | 71 +++++ .../practice/bottle-song/.meta/config.json | 17 ++ .../practice/bottle-song/.meta/template.j2 | 30 ++ .../practice/bottle-song/.meta/tests.toml | 31 +++ exercises/practice/bottle-song/BottleSong.roc | 6 + .../practice/bottle-song/bottle-song-test.roc | 142 ++++++++++ .../circular-buffer/circular-buffer-test.roc | 42 +-- .../dnd-character/.docs/instructions.md | 32 +++ .../dnd-character/.docs/introduction.md | 10 + .../practice/dnd-character/.meta/Example.roc | 55 ++++ .../practice/dnd-character/.meta/config.json | 17 ++ .../practice/dnd-character/.meta/template.j2 | 52 ++++ .../practice/dnd-character/.meta/tests.toml | 73 +++++ .../practice/dnd-character/DndCharacter.roc | 27 ++ .../dnd-character/dnd-character-test.roc | 131 +++++++++ .../grade-school/.docs/instructions.md | 21 ++ .../practice/grade-school/.meta/Example.roc | 70 +++++ .../practice/grade-school/.meta/config.json | 16 ++ .../practice/grade-school/.meta/template.j2 | 50 ++++ .../practice/grade-school/.meta/tests.toml | 86 ++++++ .../practice/grade-school/GradeSchool.roc | 29 ++ .../grade-school/grade-school-test.roc | 260 ++++++++++++++++++ .../practice/line-up/.docs/instructions.md | 19 ++ .../practice/line-up/.docs/introduction.md | 7 + exercises/practice/line-up/.meta/Example.roc | 19 ++ exercises/practice/line-up/.meta/config.json | 17 ++ exercises/practice/line-up/.meta/template.j2 | 30 ++ exercises/practice/line-up/.meta/tests.toml | 85 ++++++ exercises/practice/line-up/LineUp.roc | 6 + exercises/practice/line-up/line-up-test.roc | 136 +++++++++ .../resistor-color-trio/.docs/instructions.md | 56 ++++ .../resistor-color-trio/.meta/Example.roc | 63 +++++ .../resistor-color-trio/.meta/config.json | 17 ++ .../resistor-color-trio/.meta/template.j2 | 16 ++ .../resistor-color-trio/.meta/tests.toml | 40 +++ .../resistor-color-trio/ResistorColorTrio.roc | 21 ++ .../resistor-color-trio-test.roc | 70 +++++ exercises/practice/rest-api/.meta/Example.roc | 4 +- exercises/practice/rest-api/rest-api-test.roc | 36 +-- .../run-length-encoding/.meta/Example.roc | 6 +- .../run-length-encoding-test.roc | 2 +- .../practice/saddle-points/.meta/Example.roc | 2 +- .../save-the-cow/.docs/instructions.md | 7 + .../save-the-cow/.docs/introduction.md | 4 + .../practice/save-the-cow/.meta/Example.roc | 55 ++++ .../practice/save-the-cow/.meta/config.json | 15 + .../practice/save-the-cow/.meta/template.j2 | 28 ++ .../practice/save-the-cow/.meta/tests.toml | 40 +++ .../practice/save-the-cow/SaveTheCow.roc | 6 + .../save-the-cow/save-the-cow-test.roc | 70 +++++ .../simple-cipher/.docs/instructions.md | 40 +++ .../practice/simple-cipher/.meta/Example.roc | 50 ++++ .../practice/simple-cipher/.meta/config.json | 17 ++ .../practice/simple-cipher/.meta/template.j2 | 123 +++++++++ .../practice/simple-cipher/.meta/tests.toml | 46 ++++ .../practice/simple-cipher/SimpleCipher.roc | 23 ++ .../simple-cipher/simple-cipher-test.roc | 130 +++++++++ .../simple-linked-list-test.roc | 38 +-- .../twelve-days/.docs/instructions.md | 36 +++ .../practice/twelve-days/.meta/Example.roc | 72 +++++ .../practice/twelve-days/.meta/config.json | 17 ++ .../practice/twelve-days/.meta/template.j2 | 30 ++ .../practice/twelve-days/.meta/tests.toml | 55 ++++ exercises/practice/twelve-days/TwelveDays.roc | 6 + .../practice/twelve-days/twelve-days-test.roc | 108 ++++++++ 66 files changed, 2928 insertions(+), 65 deletions(-) create mode 100644 exercises/practice/bottle-song/.docs/instructions.md create mode 100644 exercises/practice/bottle-song/.meta/Example.roc create mode 100644 exercises/practice/bottle-song/.meta/config.json create mode 100644 exercises/practice/bottle-song/.meta/template.j2 create mode 100644 exercises/practice/bottle-song/.meta/tests.toml create mode 100644 exercises/practice/bottle-song/BottleSong.roc create mode 100644 exercises/practice/bottle-song/bottle-song-test.roc create mode 100644 exercises/practice/dnd-character/.docs/instructions.md create mode 100644 exercises/practice/dnd-character/.docs/introduction.md create mode 100644 exercises/practice/dnd-character/.meta/Example.roc create mode 100644 exercises/practice/dnd-character/.meta/config.json create mode 100644 exercises/practice/dnd-character/.meta/template.j2 create mode 100644 exercises/practice/dnd-character/.meta/tests.toml create mode 100644 exercises/practice/dnd-character/DndCharacter.roc create mode 100644 exercises/practice/dnd-character/dnd-character-test.roc create mode 100644 exercises/practice/grade-school/.docs/instructions.md create mode 100644 exercises/practice/grade-school/.meta/Example.roc create mode 100644 exercises/practice/grade-school/.meta/config.json create mode 100644 exercises/practice/grade-school/.meta/template.j2 create mode 100644 exercises/practice/grade-school/.meta/tests.toml create mode 100644 exercises/practice/grade-school/GradeSchool.roc create mode 100644 exercises/practice/grade-school/grade-school-test.roc create mode 100644 exercises/practice/line-up/.docs/instructions.md create mode 100644 exercises/practice/line-up/.docs/introduction.md create mode 100644 exercises/practice/line-up/.meta/Example.roc create mode 100644 exercises/practice/line-up/.meta/config.json create mode 100644 exercises/practice/line-up/.meta/template.j2 create mode 100644 exercises/practice/line-up/.meta/tests.toml create mode 100644 exercises/practice/line-up/LineUp.roc create mode 100644 exercises/practice/line-up/line-up-test.roc create mode 100644 exercises/practice/resistor-color-trio/.docs/instructions.md create mode 100644 exercises/practice/resistor-color-trio/.meta/Example.roc create mode 100644 exercises/practice/resistor-color-trio/.meta/config.json create mode 100644 exercises/practice/resistor-color-trio/.meta/template.j2 create mode 100644 exercises/practice/resistor-color-trio/.meta/tests.toml create mode 100644 exercises/practice/resistor-color-trio/ResistorColorTrio.roc create mode 100644 exercises/practice/resistor-color-trio/resistor-color-trio-test.roc create mode 100644 exercises/practice/save-the-cow/.docs/instructions.md create mode 100644 exercises/practice/save-the-cow/.docs/introduction.md create mode 100644 exercises/practice/save-the-cow/.meta/Example.roc create mode 100644 exercises/practice/save-the-cow/.meta/config.json create mode 100644 exercises/practice/save-the-cow/.meta/template.j2 create mode 100644 exercises/practice/save-the-cow/.meta/tests.toml create mode 100644 exercises/practice/save-the-cow/SaveTheCow.roc create mode 100644 exercises/practice/save-the-cow/save-the-cow-test.roc create mode 100644 exercises/practice/simple-cipher/.docs/instructions.md create mode 100644 exercises/practice/simple-cipher/.meta/Example.roc create mode 100644 exercises/practice/simple-cipher/.meta/config.json create mode 100644 exercises/practice/simple-cipher/.meta/template.j2 create mode 100644 exercises/practice/simple-cipher/.meta/tests.toml create mode 100644 exercises/practice/simple-cipher/SimpleCipher.roc create mode 100644 exercises/practice/simple-cipher/simple-cipher-test.roc create mode 100644 exercises/practice/twelve-days/.docs/instructions.md create mode 100644 exercises/practice/twelve-days/.meta/Example.roc create mode 100644 exercises/practice/twelve-days/.meta/config.json create mode 100644 exercises/practice/twelve-days/.meta/template.j2 create mode 100644 exercises/practice/twelve-days/.meta/tests.toml create mode 100644 exercises/practice/twelve-days/TwelveDays.roc create mode 100644 exercises/practice/twelve-days/twelve-days-test.roc diff --git a/exercises/practice/bottle-song/.docs/instructions.md b/exercises/practice/bottle-song/.docs/instructions.md new file mode 100644 index 00000000..febdfc86 --- /dev/null +++ b/exercises/practice/bottle-song/.docs/instructions.md @@ -0,0 +1,57 @@ +# Instructions + +Recite the lyrics to that popular children's repetitive song: Ten Green Bottles. + +Note that not all verses are identical. + +```text +Ten green bottles hanging on the wall, +Ten green bottles hanging on the wall, +And if one green bottle should accidentally fall, +There'll be nine green bottles hanging on the wall. + +Nine green bottles hanging on the wall, +Nine green bottles hanging on the wall, +And if one green bottle should accidentally fall, +There'll be eight green bottles hanging on the wall. + +Eight green bottles hanging on the wall, +Eight green bottles hanging on the wall, +And if one green bottle should accidentally fall, +There'll be seven green bottles hanging on the wall. + +Seven green bottles hanging on the wall, +Seven green bottles hanging on the wall, +And if one green bottle should accidentally fall, +There'll be six green bottles hanging on the wall. + +Six green bottles hanging on the wall, +Six green bottles hanging on the wall, +And if one green bottle should accidentally fall, +There'll be five green bottles hanging on the wall. + +Five green bottles hanging on the wall, +Five green bottles hanging on the wall, +And if one green bottle should accidentally fall, +There'll be four green bottles hanging on the wall. + +Four green bottles hanging on the wall, +Four green bottles hanging on the wall, +And if one green bottle should accidentally fall, +There'll be three green bottles hanging on the wall. + +Three green bottles hanging on the wall, +Three green bottles hanging on the wall, +And if one green bottle should accidentally fall, +There'll be two green bottles hanging on the wall. + +Two green bottles hanging on the wall, +Two green bottles hanging on the wall, +And if one green bottle should accidentally fall, +There'll be one green bottle hanging on the wall. + +One green bottle hanging on the wall, +One green bottle hanging on the wall, +And if one green bottle should accidentally fall, +There'll be no green bottles hanging on the wall. +``` diff --git a/exercises/practice/bottle-song/.meta/Example.roc b/exercises/practice/bottle-song/.meta/Example.roc new file mode 100644 index 00000000..e97c298e --- /dev/null +++ b/exercises/practice/bottle-song/.meta/Example.roc @@ -0,0 +1,71 @@ +BottleSong :: {}.{ + recite : U8, U8 -> Try(Str, [InvalidVerseNumber, ..]) + recite = |starting_number, number_of_verses| { + if starting_number == 0 or number_of_verses == 0 or starting_number < number_of_verses { + Err(InvalidVerseNumber) + } else { + ((starting_number - number_of_verses + 1)..=starting_number) + .rev() + |> List.from_iter + .map_try(verse)? + |> Str.join_with("\n\n") + |> Ok + } + } +} + +verse : U8 -> Try(Str, [InvalidVerseNumber, ..]) +verse = |number| { + \\${describe_bottles(number, Uppercase)?} hanging on the wall, + \\${describe_bottles(number, Uppercase)?} hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be ${describe_bottles(number - 1, Lowercase)?} hanging on the wall. + |> Ok +} + +describe_bottles : U8, [Uppercase, Lowercase] -> Try(Str, [InvalidVerseNumber, ..]) +describe_bottles = |number, case| { + if number > 10 { + return Err(InvalidVerseNumber) + } + number_str_upper : Str + number_str_upper = match number { + 0 => "No" + 1 => "One" + 2 => "Two" + 3 => "Three" + 4 => "Four" + 5 => "Five" + 6 => "Six" + 7 => "Seven" + 8 => "Eight" + 9 => "Nine" + 10 => "Ten" + _ => crash "Unreachable" + } + + number_str : Str + number_str = match case { + Uppercase => number_str_upper + Lowercase => number_str_upper |> lowercase_first_letter + } + + maybe_s : Str + maybe_s = if number != 1 { + "s" + } else { + "" + } + + Ok("${number_str} green bottle${maybe_s}") +} + +lowercase_first_letter : Str -> Str +lowercase_first_letter = |str| { + match str.to_utf8() { + [first, .. as rest] if first >= 'A' and first <= 'Z' => Str.from_utf8(rest.prepend(first - 'A' + 'a')) ?? { + crash "Unreachable" + } + _ => str + } +} diff --git a/exercises/practice/bottle-song/.meta/config.json b/exercises/practice/bottle-song/.meta/config.json new file mode 100644 index 00000000..5b506c95 --- /dev/null +++ b/exercises/practice/bottle-song/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [], + "files": { + "solution": [ + "BottleSong.roc" + ], + "test": [ + "bottle-song-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Produce the lyrics to the popular children's repetitive song: Ten Green Bottles.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Ten_Green_Bottles" +} diff --git a/exercises/practice/bottle-song/.meta/template.j2 b/exercises/practice/bottle-song/.meta/template.j2 new file mode 100644 index 00000000..5a4f401b --- /dev/null +++ b/exercises/practice/bottle-song/.meta/template.j2 @@ -0,0 +1,30 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +import {{ exercise | to_pascal }} exposing [{{ properties | map("to_snake") | join(", ") }}] + +{% macro render_cases(cases) %} +{%- for case in cases %} +{%- if "cases" in case %} +{{ render_cases(case["cases"]) }} +{%- else %} +# {{ case["description"] }} +expect { + result = {{ case["property"] | to_snake }}( + {%- for key, value in case["input"].items() -%} + {{ value | to_roc }}{% if not loop.last %}, {% endif %} + {%- endfor -%} + )? +{%- if case["expected"] is mapping and "error" in case["expected"] %} + result.is_err() +{%- else %} + result == {{ case["expected"] | join('\n') | to_roc_multiline_string | indent(8) }} +{%- endif %} +} +{%- endif %} +{%- endfor %} +{% endmacro %} + +{{ render_cases(cases) }} +{{ macros.footer() }} diff --git a/exercises/practice/bottle-song/.meta/tests.toml b/exercises/practice/bottle-song/.meta/tests.toml new file mode 100644 index 00000000..1f6e40a3 --- /dev/null +++ b/exercises/practice/bottle-song/.meta/tests.toml @@ -0,0 +1,31 @@ +# 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. + +[d4ccf8fc-01dc-48c0-a201-4fbeb30f2d03] +description = "verse -> single verse -> first generic verse" + +[0f0aded3-472a-4c64-b842-18d4f1f5f030] +description = "verse -> single verse -> last generic verse" + +[f61f3c97-131f-459e-b40a-7428f3ed99d9] +description = "verse -> single verse -> verse with 2 bottles" + +[05eadba9-5dbd-401e-a7e8-d17cc9baa8e0] +description = "verse -> single verse -> verse with 1 bottle" + +[a4a28170-83d6-4dc1-bd8b-319b6abb6a80] +description = "lyrics -> multiple verses -> first two verses" + +[3185d438-c5ac-4ce6-bcd3-02c9ff1ed8db] +description = "lyrics -> multiple verses -> last three verses" + +[28c1584a-0e51-4b65-9ae2-fbc0bf4bbb28] +description = "lyrics -> multiple verses -> all verses" diff --git a/exercises/practice/bottle-song/BottleSong.roc b/exercises/practice/bottle-song/BottleSong.roc new file mode 100644 index 00000000..6beb74e2 --- /dev/null +++ b/exercises/practice/bottle-song/BottleSong.roc @@ -0,0 +1,6 @@ +BottleSong :: {}.{ + recite : U8, U8 -> Try(Str, _) + recite = |starting_number, number_of_verses| { + crash "Please implement the 'recite' function" + } +} diff --git a/exercises/practice/bottle-song/bottle-song-test.roc b/exercises/practice/bottle-song/bottle-song-test.roc new file mode 100644 index 00000000..2ba7f99c --- /dev/null +++ b/exercises/practice/bottle-song/bottle-song-test.roc @@ -0,0 +1,142 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/bottle-song/canonical-data.json +# File last updated on 2026-08-10 + +import BottleSong exposing [recite] + +# first generic verse +expect { + result = recite(10, 1)? + result == + \\Ten green bottles hanging on the wall, + \\Ten green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be nine green bottles hanging on the wall. + +} +# last generic verse +expect { + result = recite(3, 1)? + result == + \\Three green bottles hanging on the wall, + \\Three green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be two green bottles hanging on the wall. + +} +# verse with 2 bottles +expect { + result = recite(2, 1)? + result == + \\Two green bottles hanging on the wall, + \\Two green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be one green bottle hanging on the wall. + +} +# verse with 1 bottle +expect { + result = recite(1, 1)? + result == + \\One green bottle hanging on the wall, + \\One green bottle hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be no green bottles hanging on the wall. + +} + +# first two verses +expect { + result = recite(10, 2)? + result == + \\Ten green bottles hanging on the wall, + \\Ten green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be nine green bottles hanging on the wall. + \\ + \\Nine green bottles hanging on the wall, + \\Nine green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be eight green bottles hanging on the wall. + +} +# last three verses +expect { + result = recite(3, 3)? + result == + \\Three green bottles hanging on the wall, + \\Three green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be two green bottles hanging on the wall. + \\ + \\Two green bottles hanging on the wall, + \\Two green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be one green bottle hanging on the wall. + \\ + \\One green bottle hanging on the wall, + \\One green bottle hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be no green bottles hanging on the wall. + +} +# all verses +expect { + result = recite(10, 10)? + result == + \\Ten green bottles hanging on the wall, + \\Ten green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be nine green bottles hanging on the wall. + \\ + \\Nine green bottles hanging on the wall, + \\Nine green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be eight green bottles hanging on the wall. + \\ + \\Eight green bottles hanging on the wall, + \\Eight green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be seven green bottles hanging on the wall. + \\ + \\Seven green bottles hanging on the wall, + \\Seven green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be six green bottles hanging on the wall. + \\ + \\Six green bottles hanging on the wall, + \\Six green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be five green bottles hanging on the wall. + \\ + \\Five green bottles hanging on the wall, + \\Five green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be four green bottles hanging on the wall. + \\ + \\Four green bottles hanging on the wall, + \\Four green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be three green bottles hanging on the wall. + \\ + \\Three green bottles hanging on the wall, + \\Three green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be two green bottles hanging on the wall. + \\ + \\Two green bottles hanging on the wall, + \\Two green bottles hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be one green bottle hanging on the wall. + \\ + \\One green bottle hanging on the wall, + \\One green bottle hanging on the wall, + \\And if one green bottle should accidentally fall, + \\There'll be no green bottles hanging on the wall. + +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/circular-buffer/circular-buffer-test.roc b/exercises/practice/circular-buffer/circular-buffer-test.roc index 335a53e9..473c9c1d 100644 --- a/exercises/practice/circular-buffer/circular-buffer-test.roc +++ b/exercises/practice/circular-buffer/circular-buffer-test.roc @@ -22,7 +22,7 @@ expect { _result = CircularBuffer.create({ capacity: 1 }) .write(1)? .read()? - -> expect_value(1) + |> expect_value(1) Bool.True } @@ -32,7 +32,7 @@ expect { result = CircularBuffer.create({ capacity: 1 }) .write(1)? .read()? - -> expect_value(1) + |> expect_value(1) .read() result == Err(BufferEmpty) @@ -44,9 +44,9 @@ expect { .write(1)? .write(2)? .read()? - -> expect_value(1) + |> expect_value(1) .read()? - -> expect_value(2) + |> expect_value(2) Bool.True } @@ -65,10 +65,10 @@ expect { _result = CircularBuffer.create({ capacity: 1 }) .write(1)? .read()? - -> expect_value(1) + |> expect_value(1) .write(2)? .read()? - -> expect_value(2) + |> expect_value(2) Bool.True } @@ -79,12 +79,12 @@ expect { .write(1)? .write(2)? .read()? - -> expect_value(1) + |> expect_value(1) .write(3)? .read()? - -> expect_value(2) + |> expect_value(2) .read()? - -> expect_value(3) + |> expect_value(3) Bool.True } @@ -106,7 +106,7 @@ expect { .clear() .write(2)? .read()? - -> expect_value(2) + |> expect_value(2) Bool.True } @@ -117,7 +117,7 @@ expect { .clear() .write(1)? .read()? - -> expect_value(1) + |> expect_value(1) Bool.True } @@ -128,9 +128,9 @@ expect { .write(1)? .overwrite(2) .read()? - -> expect_value(1) + |> expect_value(1) .read()? - -> expect_value(2) + |> expect_value(2) Bool.True } @@ -142,9 +142,9 @@ expect { .write(2)? .overwrite(3) .read()? - -> expect_value(2) + |> expect_value(2) .read()? - -> expect_value(3) + |> expect_value(3) Bool.True } @@ -156,15 +156,15 @@ expect { .write(2)? .write(3)? .read()? - -> expect_value(1) + |> expect_value(1) .write(4)? .overwrite(5) .read()? - -> expect_value(3) + |> expect_value(3) .read()? - -> expect_value(4) + |> expect_value(4) .read()? - -> expect_value(5) + |> expect_value(5) Bool.True } @@ -178,9 +178,9 @@ expect { .overwrite(3) .overwrite(4) .read()? - -> expect_value(3) + |> expect_value(3) .read()? - -> expect_value(4) + |> expect_value(4) .read() result == Err(BufferEmpty) diff --git a/exercises/practice/dnd-character/.docs/instructions.md b/exercises/practice/dnd-character/.docs/instructions.md new file mode 100644 index 00000000..e14e7949 --- /dev/null +++ b/exercises/practice/dnd-character/.docs/instructions.md @@ -0,0 +1,32 @@ +# Instructions + +For a game of [Dungeons & Dragons][dnd], each player starts by generating a character they can play with. +This character has, among other things, six abilities; strength, dexterity, constitution, intelligence, wisdom and charisma. +These six abilities have scores that are determined randomly. +You do this by rolling four 6-sided dice and recording the sum of the largest three dice. +You do this six times, once for each ability. + +Your character's initial hitpoints are 10 + your character's constitution modifier. +You find your character's constitution modifier by subtracting 10 from your character's constitution, divide by 2 and round down. + +Write a random character generator that follows the above rules. + +For example, the six throws of four dice may look like: + +- 5, 3, 1, 6: You discard the 1 and sum 5 + 3 + 6 = 14, which you assign to strength. +- 3, 2, 5, 3: You discard the 2 and sum 3 + 5 + 3 = 11, which you assign to dexterity. +- 1, 1, 1, 1: You discard the 1 and sum 1 + 1 + 1 = 3, which you assign to constitution. +- 2, 1, 6, 6: You discard the 1 and sum 2 + 6 + 6 = 14, which you assign to intelligence. +- 3, 5, 3, 4: You discard the 3 and sum 5 + 3 + 4 = 12, which you assign to wisdom. +- 6, 6, 6, 6: You discard the 6 and sum 6 + 6 + 6 = 18, which you assign to charisma. + +Because constitution is 3, the constitution modifier is -4 and the hitpoints are 6. + +~~~~exercism/note +Most programming languages feature (pseudo-)random generators, but few programming languages are designed to roll dice. +One such language is [Troll][troll]. + +[troll]: https://di.ku.dk/Ansatte/?pure=da%2Fpublications%2Ftroll-a-language-for-specifying-dicerolls(84a45ff0-068b-11df-825d-000ea68e967b)%2Fexport.html +~~~~ + +[dnd]: https://en.wikipedia.org/wiki/Dungeons_%26_Dragons diff --git a/exercises/practice/dnd-character/.docs/introduction.md b/exercises/practice/dnd-character/.docs/introduction.md new file mode 100644 index 00000000..5301f618 --- /dev/null +++ b/exercises/practice/dnd-character/.docs/introduction.md @@ -0,0 +1,10 @@ +# Introduction + +After weeks of anticipation, you and your friends get together for your very first game of [Dungeons & Dragons][dnd] (D&D). +Since this is the first session of the game, each player has to generate a character to play with. +The character's abilities are determined by rolling 6-sided dice, but where _are_ the dice? +With a shock, you realize that your friends are waiting for _you_ to produce the dice; after all it was your idea to play D&D! +Panicking, you realize you forgot to bring the dice, which would mean no D&D game. +As you have some basic coding skills, you quickly come up with a solution: you'll write a program to simulate dice rolls. + +[dnd]: https://en.wikipedia.org/wiki/Dungeons_%26_Dragons diff --git a/exercises/practice/dnd-character/.meta/Example.roc b/exercises/practice/dnd-character/.meta/Example.roc new file mode 100644 index 00000000..aed1fc83 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/Example.roc @@ -0,0 +1,55 @@ +## This exercise uses the https://github.com/kili-ilo/roc-random library +import random.Random + +DndCharacter := { + strength : U8, + dexterity : U8, + constitution : U8, + intelligence : U8, + wisdom : U8, + charisma : U8, + hitpoints : U8, +}.{ + modifier : U8 -> I8 + modifier = |constitution| { + ( + ( + constitution.to_i8_try() ?? { + crash "Unreachable" + } + ) - 10 + ).div_floor_by(2) + } + + ability : Random.Generator(U8) + ability = { + dice_gen = Random.list(Random.bounded_u8(1, 6), 4) + sum_without_min = |dice| dice.sum() - ( + dice.min() ?? { + crash "Unreachable: cannot be empty, as there are always 4 dice" + } + ) + Random.map(dice_gen, sum_without_min) + } + + generate : Random.Generator(DndCharacter) + generate = { + { + strength: ability, + dexterity: ability, + constitution: ability, + intelligence: ability, + wisdom: ability, + charisma: ability, + hitpoints: Random.static(0), + }.Random + |> Random.map( + |character| { + { + ..character, + hitpoints: (10.I8 + modifier(character.constitution)).to_u8_try() ?? 1, + } + }, + ) + } +} diff --git a/exercises/practice/dnd-character/.meta/config.json b/exercises/practice/dnd-character/.meta/config.json new file mode 100644 index 00000000..abf3762e --- /dev/null +++ b/exercises/practice/dnd-character/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [], + "files": { + "solution": [ + "DndCharacter.roc" + ], + "test": [ + "dnd-character-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Randomly generate Dungeons & Dragons characters.", + "source": "Simon Shine, Erik Schierboom", + "source_url": "https://github.com/exercism/problem-specifications/issues/616#issuecomment-437358945" +} diff --git a/exercises/practice/dnd-character/.meta/template.j2 b/exercises/practice/dnd-character/.meta/template.j2 new file mode 100644 index 00000000..91a5bd11 --- /dev/null +++ b/exercises/practice/dnd-character/.meta/template.j2 @@ -0,0 +1,52 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header(imports=["random"]) }} + +import DndCharacter +import random.Random + +{% macro render_cases(cases) %} +{%- for case in cases %} +{%- if "cases" in case %} +{{ render_cases(case["cases"]) }} +{%- else %} +# {{ case["description"] }} +expect { + {%- if case["property"] == "modifier" %} + result = DndCharacter.{{ case["property"] | to_snake }}( + {%- for key, value in case["input"].items() -%} + {{ value | to_roc }}{% if not loop.last %}, {% endif %} + {%- endfor -%} + ) + result == {{ case["expected"] | to_roc }} + {%- elif case["property"] == "ability" %} + random_state = Random.seed(0) + { value: score, state: _updated_random_state } = DndCharacter.ability(random_state) + {%- if case["expected"] == "score >= 3 && score <= 18" %} + score >= 3 and score <= 18 + {%- else %} + ERROR: TEST NOT IMPLEMENTED + {%- endif %} + {%- elif case["property"] == "character" %} + random_state = Random.seed(0) + { value: character, state: _updated_random_state } = DndCharacter.generate(random_state) + {%- if case["expected"] == { "strength": "strength >= 3 && strength <= 18", "dexterity": "dexterity >= 3 && dexterity <= 18", "constitution": "constitution >= 3 && constitution <= 18", "intelligence": "intelligence >= 3 && intelligence <= 18", "wisdom": "wisdom >= 3 && wisdom <= 18", "charisma": "charisma >= 3 && charisma <= 18", "hitpoints": "hitpoints == 10 + modifier(constitution)" } %} + character.strength >= 3 and character.strength <= 18 + and character.dexterity >= 3 and character.dexterity <= 18 + and character.constitution >= 3 and character.constitution <= 18 + and character.intelligence >= 3 and character.intelligence <= 18 + and character.wisdom >= 3 and character.wisdom <= 18 + and character.charisma >= 3 and character.charisma <= 18 + and character.hitpoints == (10.I8 + DndCharacter.modifier(character.constitution)).to_u8_try() ?? 1 # minimum HP = 1 + {%- else %} + ERROR: TEST NOT IMPLEMENTED + {%- endif %} + {%- endif %} +} +{%- endif %} + +{% endfor %} +{% endmacro %} + +{{ render_cases(cases) }} +{{ macros.footer() }} diff --git a/exercises/practice/dnd-character/.meta/tests.toml b/exercises/practice/dnd-character/.meta/tests.toml new file mode 100644 index 00000000..44b3c20e --- /dev/null +++ b/exercises/practice/dnd-character/.meta/tests.toml @@ -0,0 +1,73 @@ +# 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. + +[1e9ae1dc-35bd-43ba-aa08-e4b94c20fa37] +description = "ability modifier -> ability modifier for score 3 is -4" + +[cc9bb24e-56b8-4e9e-989d-a0d1a29ebb9c] +description = "ability modifier -> ability modifier for score 4 is -3" + +[5b519fcd-6946-41ee-91fe-34b4f9808326] +description = "ability modifier -> ability modifier for score 5 is -3" + +[dc2913bd-6d7a-402e-b1e2-6d568b1cbe21] +description = "ability modifier -> ability modifier for score 6 is -2" + +[099440f5-0d66-4b1a-8a10-8f3a03cc499f] +description = "ability modifier -> ability modifier for score 7 is -2" + +[cfda6e5c-3489-42f0-b22b-4acb47084df0] +description = "ability modifier -> ability modifier for score 8 is -1" + +[c70f0507-fa7e-4228-8463-858bfbba1754] +description = "ability modifier -> ability modifier for score 9 is -1" + +[6f4e6c88-1cd9-46a0-92b8-db4a99b372f7] +description = "ability modifier -> ability modifier for score 10 is 0" + +[e00d9e5c-63c8-413f-879d-cd9be9697097] +description = "ability modifier -> ability modifier for score 11 is 0" + +[eea06f3c-8de0-45e7-9d9d-b8cab4179715] +description = "ability modifier -> ability modifier for score 12 is +1" + +[9c51f6be-db72-4af7-92ac-b293a02c0dcd] +description = "ability modifier -> ability modifier for score 13 is +1" + +[94053a5d-53b6-4efc-b669-a8b5098f7762] +description = "ability modifier -> ability modifier for score 14 is +2" + +[8c33e7ca-3f9f-4820-8ab3-65f2c9e2f0e2] +description = "ability modifier -> ability modifier for score 15 is +2" + +[c3ec871e-1791-44d0-b3cc-77e5fb4cd33d] +description = "ability modifier -> ability modifier for score 16 is +3" + +[3d053cee-2888-4616-b9fd-602a3b1efff4] +description = "ability modifier -> ability modifier for score 17 is +3" + +[bafd997a-e852-4e56-9f65-14b60261faee] +description = "ability modifier -> ability modifier for score 18 is +4" + +[4f28f19c-2e47-4453-a46a-c0d365259c14] +description = "random ability is within range" + +[385d7e72-864f-4e88-8279-81a7d75b04ad] +description = "random character is valid" + +[2ca77b9b-c099-46c3-a02c-0d0f68ffa0fe] +description = "each ability is only calculated once" +include = false + +[dca2b2ec-f729-4551-84b9-078876bb4808] +description = "each ability is only calculated once" +reimplements = "2ca77b9b-c099-46c3-a02c-0d0f68ffa0fe" +include = false diff --git a/exercises/practice/dnd-character/DndCharacter.roc b/exercises/practice/dnd-character/DndCharacter.roc new file mode 100644 index 00000000..c4b06b99 --- /dev/null +++ b/exercises/practice/dnd-character/DndCharacter.roc @@ -0,0 +1,27 @@ +## This exercise uses the https://github.com/kili-ilo/roc-random library +import random.Random + +DndCharacter := { + strength : U8, + dexterity : U8, + constitution : U8, + intelligence : U8, + wisdom : U8, + charisma : U8, + hitpoints : U8, +}.{ + modifier : U8 -> I8 + modifier = |constitution| { + crash "Please implement the 'modifier' function" + } + + ability : Random.Generator(U8) + ability = { + crash "Please implement the 'ability' function" + } + + generate : Random.Generator(DndCharacter) + generate = { + crash "Please implement the 'generate' function" + } +} diff --git a/exercises/practice/dnd-character/dnd-character-test.roc b/exercises/practice/dnd-character/dnd-character-test.roc new file mode 100644 index 00000000..8befcb19 --- /dev/null +++ b/exercises/practice/dnd-character/dnd-character-test.roc @@ -0,0 +1,131 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/dnd-character/canonical-data.json +# File last updated on 2026-08-10 +app [main!] { + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.21.0/4rAQg8kUYZ3Vksr4qMQHpaFYNiHSn9GgS7gVxghd1XYV.tar.zst", + random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.0/CwDEAmyUMCsqW6dh4pxYnp7suUZAj5b5gpZuh7udtyE7.tar.zst", +} + +import DndCharacter +import random.Random + +# ability modifier for score 3 is -4 +expect { + result = DndCharacter.modifier(3) + result == -4 +} + +# ability modifier for score 4 is -3 +expect { + result = DndCharacter.modifier(4) + result == -3 +} + +# ability modifier for score 5 is -3 +expect { + result = DndCharacter.modifier(5) + result == -3 +} + +# ability modifier for score 6 is -2 +expect { + result = DndCharacter.modifier(6) + result == -2 +} + +# ability modifier for score 7 is -2 +expect { + result = DndCharacter.modifier(7) + result == -2 +} + +# ability modifier for score 8 is -1 +expect { + result = DndCharacter.modifier(8) + result == -1 +} + +# ability modifier for score 9 is -1 +expect { + result = DndCharacter.modifier(9) + result == -1 +} + +# ability modifier for score 10 is 0 +expect { + result = DndCharacter.modifier(10) + result == 0 +} + +# ability modifier for score 11 is 0 +expect { + result = DndCharacter.modifier(11) + result == 0 +} + +# ability modifier for score 12 is +1 +expect { + result = DndCharacter.modifier(12) + result == 1 +} + +# ability modifier for score 13 is +1 +expect { + result = DndCharacter.modifier(13) + result == 1 +} + +# ability modifier for score 14 is +2 +expect { + result = DndCharacter.modifier(14) + result == 2 +} + +# ability modifier for score 15 is +2 +expect { + result = DndCharacter.modifier(15) + result == 2 +} + +# ability modifier for score 16 is +3 +expect { + result = DndCharacter.modifier(16) + result == 3 +} + +# ability modifier for score 17 is +3 +expect { + result = DndCharacter.modifier(17) + result == 3 +} + +# ability modifier for score 18 is +4 +expect { + result = DndCharacter.modifier(18) + result == 4 +} + +# random ability is within range +expect { + random_state = Random.seed(0) + { value: score, state: _updated_random_state } = DndCharacter.ability(random_state) + score >= 3 and score <= 18 +} + +# random character is valid +expect { + random_state = Random.seed(0) + { value: character, state: _updated_random_state } = DndCharacter.generate(random_state) + character.strength >= 3 and character.strength <= 18 + and character.dexterity >= 3 and character.dexterity <= 18 + and character.constitution >= 3 and character.constitution <= 18 + and character.intelligence >= 3 and character.intelligence <= 18 + and character.wisdom >= 3 and character.wisdom <= 18 + and character.charisma >= 3 and character.charisma <= 18 + and character.hitpoints == (10.I8 + DndCharacter.modifier(character.constitution)).to_u8_try() ?? 1 # minimum HP = 1 +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/grade-school/.docs/instructions.md b/exercises/practice/grade-school/.docs/instructions.md new file mode 100644 index 00000000..3cb1b5d5 --- /dev/null +++ b/exercises/practice/grade-school/.docs/instructions.md @@ -0,0 +1,21 @@ +# Instructions + +Given students' names along with the grade they are in, create a roster for the school. + +In the end, you should be able to: + +- Add a student's name to the roster for a grade: + - "Add Jim to grade 2." + - "OK." +- Get a list of all students enrolled in a grade: + - "Which students are in grade 2?" + - "We've only got Jim right now." +- Get a sorted list of all students in all grades. + Grades should be sorted as 1, 2, 3, etc., and students within a grade should be sorted alphabetically by name. + - "Who is enrolled in school right now?" + - "Let me think. + We have Anna, Barb, and Charlie in grade 1, Alex, Peter, and Zoe in grade 2, and Jim in grade 5. + So the answer is: Anna, Barb, Charlie, Alex, Peter, Zoe, and Jim." + +Note that all our students only have one name (it's a small town, what do you want?), and each student cannot be added more than once to a grade or the roster. +If a test attempts to add the same student more than once, your implementation should indicate that this is incorrect. diff --git a/exercises/practice/grade-school/.meta/Example.roc b/exercises/practice/grade-school/.meta/Example.roc new file mode 100644 index 00000000..c5505ef7 --- /dev/null +++ b/exercises/practice/grade-school/.meta/Example.roc @@ -0,0 +1,70 @@ +GradeSchool :: { + students : List(GradeSchool.Student), +}.{ + Student : { name : Str, grade : U8 } + + empty : GradeSchool + empty = { + { students: [] } + } + + add : GradeSchool, List(Student) -> { results : List([Accepted, Rejected]), updated_school : GradeSchool } + add = |grade_school, new_student_grade_pairs| { + initial_state = { results: [], updated_school: grade_school } + new_student_grade_pairs.fold(initial_state, add_student) + } + + roster : GradeSchool -> List(Str) + roster = |grade_school| { + grade_school.students + .sort_with(compare_students) + .map(|student| student.name) + } + + grade : GradeSchool, U8 -> List(Str) + grade = |grade_school, grade| { + grade_school.students + .keep_if(|student| student.grade == grade) + .map(|student| student.name) + .sort_with(compare_strings) + } +} + +add_student : { results : List([Accepted, Rejected]), updated_school : GradeSchool }, Student -> { results : List([Accepted, Rejected]), updated_school : GradeSchool } +add_student = |state, student| { + if state.updated_school.roster().contains(student.name) { + { updated_school: state.updated_school, results: state.results.append(Rejected) } + } else { + students = state.updated_school.students.append(student) + { updated_school: GradeSchool.{ students }, results: state.results.append(Accepted) } + } +} + +compare_strings : Str, Str -> [LT, EQ, GT] +compare_strings = |string1, string2| { + b1 = string1.to_utf8() + b2 = string2.to_utf8() + result = + b1.map2(b2, |c1, c2| c1.compare(c2)) + .fold_try( + Ok(EQ), + |_state, cmp| { + match cmp { + EQ => Ok(EQ) + res => Err(res) + } + }, + ) + match result { + Ok(_cmp) => b1.len().compare(b2.len()) + Err(res) => res + } +} + +compare_students = |student1, student2| { + compare_grades = student1.grade.compare(student2.grade) + match compare_grades { + LT | GT => compare_grades + EQ => compare_strings(student1.name, student2.name) + } +} diff --git a/exercises/practice/grade-school/.meta/config.json b/exercises/practice/grade-school/.meta/config.json new file mode 100644 index 00000000..fc6cbd1e --- /dev/null +++ b/exercises/practice/grade-school/.meta/config.json @@ -0,0 +1,16 @@ +{ + "authors": [], + "files": { + "solution": [ + "GradeSchool.roc" + ], + "test": [ + "grade-school-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Given students' names along with the grade that they are in, create a roster for the school.", + "source": "A pairing session with Phil Battos at gSchool" +} diff --git a/exercises/practice/grade-school/.meta/template.j2 b/exercises/practice/grade-school/.meta/template.j2 new file mode 100644 index 00000000..197af9f0 --- /dev/null +++ b/exercises/practice/grade-school/.meta/template.j2 @@ -0,0 +1,50 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +import {{ exercise | to_pascal }} exposing [{{ properties | map("to_snake") | join(", ") }}] + +{%- macro student_grade(name, grade) -%} +{ name: {{ name | to_roc }}, grade: {{ grade }} } +{%- endmacro %} + +{% macro render_cases(cases) %} +{%- for case in cases %} +{%- if "cases" in case %} +{{ render_cases(case["cases"]) }} +{%- else %} +# {{ case["description"] }} +expect { + {%- if case["input"]["students"] %} + new_school = GradeSchool.empty + { results{% if case["property"] != "add" %}: _{% endif %}, updated_school: {% if case["property"] == "add" %}_{% else %}grade_school{% endif %} } = new_school.add([ + {%- for name, grade in case["input"]["students"] -%} + {{ student_grade(name, grade) }}, + {%- endfor -%} + ]) + {%- else %} + grade_school = GradeSchool.empty + {%- endif %} + {%- if case["property"] == "roster" %} + result = grade_school.roster() + result == {{ case["expected"] | to_roc }} + {%- elif case["property"] == "grade" %} + result = grade_school.grade({{ case["input"]["desiredGrade"] }}) + result == {{ case["expected"] | to_roc }} + {%- elif case["property"] == "add" %} + results == [ + {%- for is_ok in case["expected"] -%} + {% if is_ok %}Accepted{% else %}Rejected{% endif %}, + {%- endfor -%} + ] + {%- else %} + ERROR: TEST NOT IMPLEMENTED + {%- endif %} +} + +{% endif %} +{%- endfor %} +{% endmacro %} + +{{ render_cases(cases) }} +{{ macros.footer() }} diff --git a/exercises/practice/grade-school/.meta/tests.toml b/exercises/practice/grade-school/.meta/tests.toml new file mode 100644 index 00000000..50c9e2e5 --- /dev/null +++ b/exercises/practice/grade-school/.meta/tests.toml @@ -0,0 +1,86 @@ +# 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. + +[a3f0fb58-f240-4723-8ddc-e644666b85cc] +description = "Roster is empty when no student is added" + +[9337267f-7793-4b90-9b4a-8e3978408824] +description = "Add a student" + +[6d0a30e4-1b4e-472e-8e20-c41702125667] +description = "Student is added to the roster" + +[73c3ca75-0c16-40d7-82f5-ed8fe17a8e4a] +description = "Adding multiple students in the same grade in the roster" + +[233be705-dd58-4968-889d-fb3c7954c9cc] +description = "Multiple students in the same grade are added to the roster" + +[87c871c1-6bde-4413-9c44-73d59a259d83] +description = "Cannot add student to same grade in the roster more than once" + +[c125dab7-2a53-492f-a99a-56ad511940d8] +description = "A student can't be in two different grades" +include = false + +[a0c7b9b8-0e89-47f8-8b4a-c50f885e79d1] +description = "A student can only be added to the same grade in the roster once" +include = false +reimplements = "c125dab7-2a53-492f-a99a-56ad511940d8" + +[d7982c4f-1602-49f6-a651-620f2614243a] +description = "Student not added to same grade in the roster more than once" +reimplements = "a0c7b9b8-0e89-47f8-8b4a-c50f885e79d1" + +[e70d5d8f-43a9-41fd-94a4-1ea0fa338056] +description = "Adding students in multiple grades" + +[75a51579-d1d7-407c-a2f8-2166e984e8ab] +description = "Students in multiple grades are added to the roster" + +[7df542f1-57ce-433c-b249-ff77028ec479] +description = "Cannot add same student to multiple grades in the roster" + +[6a03b61e-1211-4783-a3cc-fc7f773fba3f] +description = "A student cannot be added to more than one grade in the sorted roster" +include = false +reimplements = "c125dab7-2a53-492f-a99a-56ad511940d8" + +[c7ec1c5e-9ab7-4d3b-be5c-29f2f7a237c5] +description = "Student not added to multiple grades in the roster" +reimplements = "6a03b61e-1211-4783-a3cc-fc7f773fba3f" + +[d9af4f19-1ba1-48e7-94d0-dabda4e5aba6] +description = "Students are sorted by grades in the roster" + +[d9fb5bea-f5aa-4524-9d61-c158d8906807] +description = "Students are sorted by name in the roster" + +[180a8ff9-5b94-43fc-9db1-d46b4a8c93b6] +description = "Students are sorted by grades and then by name in the roster" + +[5e67aa3c-a3c6-4407-a183-d8fe59cd1630] +description = "Grade is empty if no students in the roster" + +[1e0cf06b-26e0-4526-af2d-a2e2df6a51d6] +description = "Grade is empty if no students in that grade" + +[2bfc697c-adf2-4b65-8d0f-c46e085f796e] +description = "Student not added to same grade more than once" + +[66c8e141-68ab-4a04-a15a-c28bc07fe6b9] +description = "Student not added to multiple grades" + +[c9c1fc2f-42e0-4d2c-b361-99271f03eda7] +description = "Student not added to other grade for multiple grades" + +[1bfbcef1-e4a3-49e8-8d22-f6f9f386187e] +description = "Students are sorted by name in a grade" diff --git a/exercises/practice/grade-school/GradeSchool.roc b/exercises/practice/grade-school/GradeSchool.roc new file mode 100644 index 00000000..c9ccc4e7 --- /dev/null +++ b/exercises/practice/grade-school/GradeSchool.roc @@ -0,0 +1,29 @@ +GradeSchool :: { + # TODO: change this opaque type however you need + todo1 : U64, + todo2 : U64, + todo3 : U64, + # etc. +}.{ + Student : { name : Str, grade : U8 } + + empty : GradeSchool + empty = { + crash "Please implement the 'empty' definition" + } + + add : GradeSchool, List(Student) -> { results : List([Accepted, Rejected]), updated_school : GradeSchool } + add = |grade_school, new_student_grade_pairs| { + crash "Please implement the 'add' function" + } + + roster : GradeSchool -> List(Str) + roster = |grade_school| { + crash "Please implement the 'roster' function" + } + + grade : GradeSchool, U8 -> List(Str) + grade = |grade_school, grade| { + crash "Please implement the 'grade' function" + } +} diff --git a/exercises/practice/grade-school/grade-school-test.roc b/exercises/practice/grade-school/grade-school-test.roc new file mode 100644 index 00000000..df324b2a --- /dev/null +++ b/exercises/practice/grade-school/grade-school-test.roc @@ -0,0 +1,260 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/grade-school/canonical-data.json +# File last updated on 2026-08-10 + +import GradeSchool exposing [add, grade, roster] + +# Roster is empty when no student is added +expect { + grade_school = GradeSchool.empty + result = grade_school.roster() + result == [] +} + +# Add a student +expect { + new_school = GradeSchool.empty + { results, updated_school: _ } = new_school.add([ + { name: "Aimee", grade: 2 }, + ]) + results == [ + Accepted, + ] +} + +# Student is added to the roster +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Aimee", grade: 2 }, + ]) + result = grade_school.roster() + result == ["Aimee"] +} + +# Adding multiple students in the same grade in the roster +expect { + new_school = GradeSchool.empty + { results, updated_school: _ } = new_school.add([ + { name: "Blair", grade: 2 }, + { name: "James", grade: 2 }, + { name: "Paul", grade: 2 }, + ]) + results == [ + Accepted, + Accepted, + Accepted, + ] +} + +# Multiple students in the same grade are added to the roster +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Blair", grade: 2 }, + { name: "James", grade: 2 }, + { name: "Paul", grade: 2 }, + ]) + result = grade_school.roster() + result == ["Blair", "James", "Paul"] +} + +# Cannot add student to same grade in the roster more than once +expect { + new_school = GradeSchool.empty + { results, updated_school: _ } = new_school.add([ + { name: "Blair", grade: 2 }, + { name: "James", grade: 2 }, + { name: "James", grade: 2 }, + { name: "Paul", grade: 2 }, + ]) + results == [ + Accepted, + Accepted, + Rejected, + Accepted, + ] +} + +# Student not added to same grade in the roster more than once +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Blair", grade: 2 }, + { name: "James", grade: 2 }, + { name: "James", grade: 2 }, + { name: "Paul", grade: 2 }, + ]) + result = grade_school.roster() + result == ["Blair", "James", "Paul"] +} + +# Adding students in multiple grades +expect { + new_school = GradeSchool.empty + { results, updated_school: _ } = new_school.add([ + { name: "Chelsea", grade: 3 }, + { name: "Logan", grade: 7 }, + ]) + results == [ + Accepted, + Accepted, + ] +} + +# Students in multiple grades are added to the roster +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Chelsea", grade: 3 }, + { name: "Logan", grade: 7 }, + ]) + result = grade_school.roster() + result == ["Chelsea", "Logan"] +} + +# Cannot add same student to multiple grades in the roster +expect { + new_school = GradeSchool.empty + { results, updated_school: _ } = new_school.add([ + { name: "Blair", grade: 2 }, + { name: "James", grade: 2 }, + { name: "James", grade: 3 }, + { name: "Paul", grade: 3 }, + ]) + results == [ + Accepted, + Accepted, + Rejected, + Accepted, + ] +} + +# Student not added to multiple grades in the roster +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Blair", grade: 2 }, + { name: "James", grade: 2 }, + { name: "James", grade: 3 }, + { name: "Paul", grade: 3 }, + ]) + result = grade_school.roster() + result == ["Blair", "James", "Paul"] +} + +# Students are sorted by grades in the roster +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Jim", grade: 3 }, + { name: "Peter", grade: 2 }, + { name: "Anna", grade: 1 }, + ]) + result = grade_school.roster() + result == ["Anna", "Peter", "Jim"] +} + +# Students are sorted by name in the roster +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Peter", grade: 2 }, + { name: "Zoe", grade: 2 }, + { name: "Alex", grade: 2 }, + ]) + result = grade_school.roster() + result == ["Alex", "Peter", "Zoe"] +} + +# Students are sorted by grades and then by name in the roster +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Peter", grade: 2 }, + { name: "Anna", grade: 1 }, + { name: "Barb", grade: 1 }, + { name: "Zoe", grade: 2 }, + { name: "Alex", grade: 2 }, + { name: "Jim", grade: 3 }, + { name: "Charlie", grade: 1 }, + ]) + result = grade_school.roster() + result == ["Anna", "Barb", "Charlie", "Alex", "Peter", "Zoe", "Jim"] +} + +# Grade is empty if no students in the roster +expect { + grade_school = GradeSchool.empty + result = grade_school.grade(1) + result == [] +} + +# Grade is empty if no students in that grade +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Peter", grade: 2 }, + { name: "Zoe", grade: 2 }, + { name: "Alex", grade: 2 }, + { name: "Jim", grade: 3 }, + ]) + result = grade_school.grade(1) + result == [] +} + +# Student not added to same grade more than once +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Blair", grade: 2 }, + { name: "James", grade: 2 }, + { name: "James", grade: 2 }, + { name: "Paul", grade: 2 }, + ]) + result = grade_school.grade(2) + result == ["Blair", "James", "Paul"] +} + +# Student not added to multiple grades +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Blair", grade: 2 }, + { name: "James", grade: 2 }, + { name: "James", grade: 3 }, + { name: "Paul", grade: 3 }, + ]) + result = grade_school.grade(2) + result == ["Blair", "James"] +} + +# Student not added to other grade for multiple grades +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Blair", grade: 2 }, + { name: "James", grade: 2 }, + { name: "James", grade: 3 }, + { name: "Paul", grade: 3 }, + ]) + result = grade_school.grade(3) + result == ["Paul"] +} + +# Students are sorted by name in a grade +expect { + new_school = GradeSchool.empty + { results: _, updated_school: grade_school } = new_school.add([ + { name: "Franklin", grade: 5 }, + { name: "Bradley", grade: 5 }, + { name: "Jeff", grade: 1 }, + ]) + result = grade_school.grade(5) + result == ["Bradley", "Franklin"] +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/line-up/.docs/instructions.md b/exercises/practice/line-up/.docs/instructions.md new file mode 100644 index 00000000..9e686ecb --- /dev/null +++ b/exercises/practice/line-up/.docs/instructions.md @@ -0,0 +1,19 @@ +# Instructions + +Given a name and a number, your task is to produce a sentence using that name and that number as an [ordinal numeral][ordinal-numeral]. +Yaʻqūb expects to use numbers from 1 up to 999. + +Rules: + +- Numbers ending in 1 (unless ending in 11) → `"st"` +- Numbers ending in 2 (unless ending in 12) → `"nd"` +- Numbers ending in 3 (unless ending in 13) → `"rd"` +- All other numbers → `"th"` + +Examples: + +- `"Mary", 1` → `"Mary, you are the 1st customer we serve today. Thank you!"` +- `"John", 12` → `"John, you are the 12th customer we serve today. Thank you!"` +- `"Dahir", 162` → `"Dahir, you are the 162nd customer we serve today. Thank you!"` + +[ordinal-numeral]: https://en.wikipedia.org/wiki/Ordinal_numeral diff --git a/exercises/practice/line-up/.docs/introduction.md b/exercises/practice/line-up/.docs/introduction.md new file mode 100644 index 00000000..ea07268a --- /dev/null +++ b/exercises/practice/line-up/.docs/introduction.md @@ -0,0 +1,7 @@ +# Introduction + +Your friend Yaʻqūb works the counter at a deli in town, slicing, weighing, and wrapping orders for a line of hungry customers that gets longer every day. +Waiting customers are starting to lose track of who is next, so he wants numbered tickets they can use to track the order in which they arrive. + +To make the customers feel special, he does not want the ticket to have only a number on it. +They shall get a proper English sentence with their name and number on it. diff --git a/exercises/practice/line-up/.meta/Example.roc b/exercises/practice/line-up/.meta/Example.roc new file mode 100644 index 00000000..f000efdd --- /dev/null +++ b/exercises/practice/line-up/.meta/Example.roc @@ -0,0 +1,19 @@ +LineUp :: {}.{ + format : Str, U64 -> Str + format = |name, rank| { + "${name}, you are the ${rank.to_str()}${ending(rank)} customer we serve today. Thank you!" + } +} + +ending : U64 -> Str +ending = |rank| { + match rank % 100 { + 11 | 12 | 13 => "th" + _ => match rank % 10 { + 1 => "st" + 2 => "nd" + 3 => "rd" + _ => "th" + } + } +} diff --git a/exercises/practice/line-up/.meta/config.json b/exercises/practice/line-up/.meta/config.json new file mode 100644 index 00000000..dfe35d13 --- /dev/null +++ b/exercises/practice/line-up/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [], + "files": { + "solution": [ + "LineUp.roc" + ], + "test": [ + "line-up-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Help lining up customers at Yaʻqūb's Deli.", + "source": "mk-mxp, based on previous work from Exercism contributors codedge and neenjaw", + "source_url": "https://forum.exercism.org/t/new-exercise-ordinal-numbers/19147" +} diff --git a/exercises/practice/line-up/.meta/template.j2 b/exercises/practice/line-up/.meta/template.j2 new file mode 100644 index 00000000..6b70fda0 --- /dev/null +++ b/exercises/practice/line-up/.meta/template.j2 @@ -0,0 +1,30 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +import {{ exercise | to_pascal }} exposing [{{ properties | map("to_snake") | join(", ") }}] + +{% macro render_cases(cases) %} +{%- for case in cases %} +{%- if "cases" in case %} +{{ render_cases(case["cases"]) }} +{%- else %} +# {{ case["description"] }} +expect { + result = {{ case["property"] | to_snake }}( + {%- for key, value in case["input"].items() -%} + {{ value | to_roc }}{% if not loop.last %}, {% endif %} + {%- endfor -%} + ) +{%- if case["expected"] is mapping and "error" in case["expected"] %} + result.is_err() +{%- else %} + result == {{ case["expected"] | to_roc }} +{%- endif %} +} +{%- endif %} +{%- endfor %} +{% endmacro %} + +{{ render_cases(cases) }} +{{ macros.footer() }} diff --git a/exercises/practice/line-up/.meta/tests.toml b/exercises/practice/line-up/.meta/tests.toml new file mode 100644 index 00000000..dd66fc15 --- /dev/null +++ b/exercises/practice/line-up/.meta/tests.toml @@ -0,0 +1,85 @@ +# 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. + +[7760d1b8-4864-4db4-953b-0fa7c047dbc0] +description = "format smallest non-exceptional ordinal numeral 4" + +[e8b7c715-6baa-4f7b-8fb3-2fa48044ab7a] +description = "format greatest single digit non-exceptional ordinal numeral 9" + +[f370aae9-7ae7-4247-90ce-e8ff8c6934df] +description = "format non-exceptional ordinal numeral 5" + +[37f10dea-42a2-49de-bb92-0b690b677908] +description = "format non-exceptional ordinal numeral 6" + +[d8dfb9a2-3a1f-4fee-9dae-01af3600054e] +description = "format non-exceptional ordinal numeral 7" + +[505ec372-1803-42b1-9377-6934890fd055] +description = "format non-exceptional ordinal numeral 8" + +[8267072d-be1f-4f70-b34a-76b7557a47b9] +description = "format exceptional ordinal numeral 1" + +[4d8753cb-0364-4b29-84b8-4374a4fa2e3f] +description = "format exceptional ordinal numeral 2" + +[8d44c223-3a7e-4f48-a0ca-78e67bf98aa7] +description = "format exceptional ordinal numeral 3" + +[6c4f6c88-b306-4f40-bc78-97cdd583c21a] +description = "format smallest two digit non-exceptional ordinal numeral 10" + +[e257a43f-d2b1-457a-97df-25f0923fc62a] +description = "format non-exceptional ordinal numeral 11" + +[bb1db695-4d64-457f-81b8-4f5a2107e3f4] +description = "format non-exceptional ordinal numeral 12" + +[60a3187c-9403-4835-97de-4f10ebfd63e2] +description = "format non-exceptional ordinal numeral 13" + +[2bdcebc5-c029-4874-b6cc-e9bec80d603a] +description = "format exceptional ordinal numeral 21" + +[a98e2e22-ab41-4557-a7c2-efedc19c16da] +description = "format exceptional ordinal numeral 22 ending in nd even though it is a multiple of 11" + +[ab45d2fb-e0ee-4016-b605-76917584db0a] +description = "format exceptional ordinal numeral 33 ending in rd even though it is a multiple of 11" + +[c9243603-9f17-45b3-9a41-db9ebdbf08e1] +description = "format exceptional ordinal numeral 52 ending in nd even though it is a multiple of 13" + +[74ee2317-0295-49d2-baf0-d56bcefa14e3] +description = "format exceptional ordinal numeral 62" + +[3f6c408c-4331-42b6-bb6c-3ad0823e568a] +description = "format non-exceptional ordinal numeral 72 ending in nd even though it is a multiple of 12" + +[8db52cd9-9689-413f-a812-6c36fcfd0d07] +description = "format exceptional ordinal numeral 91 ending in st even though it is a multiple of 13" + +[b37c332d-7f68-40e3-8503-e43cbd67a0c4] +description = "format exceptional ordinal numeral 100" + +[0375f250-ce92-4195-9555-00e28ccc4d99] +description = "format exceptional ordinal numeral 101" + +[0d8a4974-9a8a-45a4-aca7-a9fb473c9836] +description = "format non-exceptional ordinal numeral 112" + +[06b62efe-199e-4ce7-970d-4bf73945713f] +description = "format exceptional ordinal numeral 123" + +[6792c54e-59a7-4faf-839a-c4bb61014229] +description = "format large number 972 ending in nd even though it is a multiple of 12" diff --git a/exercises/practice/line-up/LineUp.roc b/exercises/practice/line-up/LineUp.roc new file mode 100644 index 00000000..a1b29ca7 --- /dev/null +++ b/exercises/practice/line-up/LineUp.roc @@ -0,0 +1,6 @@ +LineUp :: {}.{ + format : Str, U64 -> Str + format = |name, rank| { + crash "Please implement the 'format' function" + } +} diff --git a/exercises/practice/line-up/line-up-test.roc b/exercises/practice/line-up/line-up-test.roc new file mode 100644 index 00000000..23959860 --- /dev/null +++ b/exercises/practice/line-up/line-up-test.roc @@ -0,0 +1,136 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/line-up/canonical-data.json +# File last updated on 2026-08-10 + +import LineUp exposing [format] + +# format smallest non-exceptional ordinal numeral 4 +expect { + result = format("Gianna", 4) + result == "Gianna, you are the 4th customer we serve today. Thank you!" +} +# format greatest single digit non-exceptional ordinal numeral 9 +expect { + result = format("Maarten", 9) + result == "Maarten, you are the 9th customer we serve today. Thank you!" +} +# format non-exceptional ordinal numeral 5 +expect { + result = format("Petronila", 5) + result == "Petronila, you are the 5th customer we serve today. Thank you!" +} +# format non-exceptional ordinal numeral 6 +expect { + result = format("Attakullakulla", 6) + result == "Attakullakulla, you are the 6th customer we serve today. Thank you!" +} +# format non-exceptional ordinal numeral 7 +expect { + result = format("Kate", 7) + result == "Kate, you are the 7th customer we serve today. Thank you!" +} +# format non-exceptional ordinal numeral 8 +expect { + result = format("Maximiliano", 8) + result == "Maximiliano, you are the 8th customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 1 +expect { + result = format("Mary", 1) + result == "Mary, you are the 1st customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 2 +expect { + result = format("Haruto", 2) + result == "Haruto, you are the 2nd customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 3 +expect { + result = format("Henriette", 3) + result == "Henriette, you are the 3rd customer we serve today. Thank you!" +} +# format smallest two digit non-exceptional ordinal numeral 10 +expect { + result = format("Alvarez", 10) + result == "Alvarez, you are the 10th customer we serve today. Thank you!" +} +# format non-exceptional ordinal numeral 11 +expect { + result = format("Jacqueline", 11) + result == "Jacqueline, you are the 11th customer we serve today. Thank you!" +} +# format non-exceptional ordinal numeral 12 +expect { + result = format("Juan", 12) + result == "Juan, you are the 12th customer we serve today. Thank you!" +} +# format non-exceptional ordinal numeral 13 +expect { + result = format("Patricia", 13) + result == "Patricia, you are the 13th customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 21 +expect { + result = format("Washi", 21) + result == "Washi, you are the 21st customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 22 ending in nd even though it is a multiple of 11 +expect { + result = format("Ingrid", 22) + result == "Ingrid, you are the 22nd customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 33 ending in rd even though it is a multiple of 11 +expect { + result = format("Mario", 33) + result == "Mario, you are the 33rd customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 52 ending in nd even though it is a multiple of 13 +expect { + result = format("Quentin", 52) + result == "Quentin, you are the 52nd customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 62 +expect { + result = format("Nayra", 62) + result == "Nayra, you are the 62nd customer we serve today. Thank you!" +} +# format non-exceptional ordinal numeral 72 ending in nd even though it is a multiple of 12 +expect { + result = format("Ugo", 72) + result == "Ugo, you are the 72nd customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 91 ending in st even though it is a multiple of 13 +expect { + result = format("Boris", 91) + result == "Boris, you are the 91st customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 100 +expect { + result = format("John", 100) + result == "John, you are the 100th customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 101 +expect { + result = format("Zeinab", 101) + result == "Zeinab, you are the 101st customer we serve today. Thank you!" +} +# format non-exceptional ordinal numeral 112 +expect { + result = format("Knud", 112) + result == "Knud, you are the 112th customer we serve today. Thank you!" +} +# format exceptional ordinal numeral 123 +expect { + result = format("Yma", 123) + result == "Yma, you are the 123rd customer we serve today. Thank you!" +} +# format large number 972 ending in nd even though it is a multiple of 12 +expect { + result = format("Elias", 972) + result == "Elias, you are the 972nd customer we serve today. Thank you!" +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/resistor-color-trio/.docs/instructions.md b/exercises/practice/resistor-color-trio/.docs/instructions.md new file mode 100644 index 00000000..1ac5cf5e --- /dev/null +++ b/exercises/practice/resistor-color-trio/.docs/instructions.md @@ -0,0 +1,56 @@ +# Instructions + +If you want to build something using a Raspberry Pi, you'll probably use _resistors_. +For this exercise, you need to know only three things about them: + +- Each resistor has a resistance value. +- Resistors are small - so small in fact that if you printed the resistance value on them, it would be hard to read. + To get around this problem, manufacturers print color-coded bands onto the resistors to denote their resistance values. +- Each band acts as a digit of a number. + For example, if they printed a brown band (value 1) followed by a green band (value 5), it would translate to the number 15. + In this exercise, you are going to create a helpful program so that you don't have to remember the values of the bands. + The program will take 3 colors as input, and outputs the correct value, in ohms. + The color bands are encoded as follows: + +- black: 0 +- brown: 1 +- red: 2 +- orange: 3 +- yellow: 4 +- green: 5 +- blue: 6 +- violet: 7 +- grey: 8 +- white: 9 + +In Resistor Color Duo you decoded the first two colors. +For instance: orange-orange got the main value `33`. +The third color stands for how many zeros need to be added to the main value. +The main value plus the zeros gives us a value in ohms. +For the exercise it doesn't matter what ohms really are. +For example: + +- orange-orange-black would be 33 and no zeros, which becomes 33 ohms. +- orange-orange-red would be 33 and 2 zeros, which becomes 3300 ohms. +- orange-orange-orange would be 33 and 3 zeros, which becomes 33000 ohms. + +(If Math is your thing, you may want to think of the zeros as exponents of 10. +If Math is not your thing, go with the zeros. +It really is the same thing, just in plain English instead of Math lingo.) + +This exercise is about translating the colors into a label: + +> "... ohms" + +So an input of `"orange", "orange", "black"` should return: + +> "33 ohms" + +When we get to larger resistors, a [metric prefix][metric-prefix] is used to indicate a larger magnitude of ohms, such as "kiloohms". +That is similar to saying "2 kilometers" instead of "2000 meters", or "2 kilograms" for "2000 grams". + +For example, an input of `"orange", "orange", "orange"` should return: + +> "33 kiloohms" + +[metric-prefix]: https://en.wikipedia.org/wiki/Metric_prefix diff --git a/exercises/practice/resistor-color-trio/.meta/Example.roc b/exercises/practice/resistor-color-trio/.meta/Example.roc new file mode 100644 index 00000000..eda3466e --- /dev/null +++ b/exercises/practice/resistor-color-trio/.meta/Example.roc @@ -0,0 +1,63 @@ +ResistorColorTrio :: {}.{ + Color : [ + Black, + Brown, + Red, + Orange, + Yellow, + Green, + Blue, + Violet, + Grey, + White, + ] + + Label : [Ohms(U16), Kiloohms(U16), Megaohms(U16), Gigaohms(U16)] + + label : Color, Color, Color -> Label + label = |first, second, third| { + if (first, second) == (Black, Black) return Ohms(0) + value = if second == Black { + get_code(first) + } else { + 10 * get_code(first) + get_code(second) + } + pow = get_code(third) + if second == Black { + 1 + } else { + 0 + } + match pow { + 0 => Ohms(value) + 1 => Ohms(value * 10) + 2 => Ohms(value * 100) + 3 => Kiloohms(value) + 4 => Kiloohms(value * 10) + 5 => Kiloohms(value * 100) + 6 => Megaohms(value) + 7 => Megaohms(value * 10) + 8 => Megaohms(value * 100) + 9 => Gigaohms(value) + 10 => Gigaohms(value * 10) + _ => { + crash "Unreachable" + } + } + } +} + +get_code : Color -> U16 +get_code = |color| { + match color { + Black => 0 + Brown => 1 + Red => 2 + Orange => 3 + Yellow => 4 + Green => 5 + Blue => 6 + Violet => 7 + Grey => 8 + White => 9 + } +} diff --git a/exercises/practice/resistor-color-trio/.meta/config.json b/exercises/practice/resistor-color-trio/.meta/config.json new file mode 100644 index 00000000..d56cbc49 --- /dev/null +++ b/exercises/practice/resistor-color-trio/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [], + "files": { + "solution": [ + "ResistorColorTrio.roc" + ], + "test": [ + "resistor-color-trio-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Convert color codes, as used on resistors, to a human-readable label.", + "source": "Maud de Vries, Erik Schierboom", + "source_url": "https://github.com/exercism/problem-specifications/issues/1549" +} diff --git a/exercises/practice/resistor-color-trio/.meta/template.j2 b/exercises/practice/resistor-color-trio/.meta/template.j2 new file mode 100644 index 00000000..b9da9605 --- /dev/null +++ b/exercises/practice/resistor-color-trio/.meta/template.j2 @@ -0,0 +1,16 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake }}] + +{% for case in cases -%} +# {{ case["description"] }} +expect { + result = {{ case["property"] | to_snake }}({{ case["input"]["colors"][0] | to_pascal }}, {{ case["input"]["colors"][1] | to_pascal }}, {{ case["input"]["colors"][2] | to_pascal }}) + result == {{ case["expected"]["unit"] | to_pascal }}({{ case["expected"]["value"] | to_roc }}) +} + +{% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/resistor-color-trio/.meta/tests.toml b/exercises/practice/resistor-color-trio/.meta/tests.toml new file mode 100644 index 00000000..b7d45fa5 --- /dev/null +++ b/exercises/practice/resistor-color-trio/.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. + +[d6863355-15b7-40bb-abe0-bfb1a25512ed] +description = "Orange and orange and black" + +[1224a3a9-8c8e-4032-843a-5224e04647d6] +description = "Blue and grey and brown" + +[b8bda7dc-6b95-4539-abb2-2ad51d66a207] +description = "Red and black and red" + +[5b1e74bc-d838-4eda-bbb3-eaba988e733b] +description = "Green and brown and orange" + +[f5d37ef9-1919-4719-a90d-a33c5a6934c9] +description = "Yellow and violet and yellow" + +[5f6404a7-5bb3-4283-877d-3d39bcc33854] +description = "Blue and violet and blue" + +[7d3a6ab8-e40e-46c3-98b1-91639fff2344] +description = "Minimum possible value" + +[ca0aa0ac-3825-42de-9f07-dac68cc580fd] +description = "Maximum possible value" + +[0061a76c-903a-4714-8ce2-f26ce23b0e09] +description = "First two colors make an invalid octal number" + +[30872c92-f567-4b69-a105-8455611c10c4] +description = "Ignore extra colors" diff --git a/exercises/practice/resistor-color-trio/ResistorColorTrio.roc b/exercises/practice/resistor-color-trio/ResistorColorTrio.roc new file mode 100644 index 00000000..7df7650e --- /dev/null +++ b/exercises/practice/resistor-color-trio/ResistorColorTrio.roc @@ -0,0 +1,21 @@ +ResistorColorTrio :: {}.{ + Color : [ + Black, + Brown, + Red, + Orange, + Yellow, + Green, + Blue, + Violet, + Grey, + White, + ] + + Label : [Ohms(U16), Kiloohms(U16), Megaohms(U16), Gigaohms(U16)] + + label : Color, Color, Color -> Label + label = |first, second, third| { + crash "Please implement the 'label' function" + } +} diff --git a/exercises/practice/resistor-color-trio/resistor-color-trio-test.roc b/exercises/practice/resistor-color-trio/resistor-color-trio-test.roc new file mode 100644 index 00000000..df640f14 --- /dev/null +++ b/exercises/practice/resistor-color-trio/resistor-color-trio-test.roc @@ -0,0 +1,70 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/resistor-color-trio/canonical-data.json +# File last updated on 2026-08-09 + +import ResistorColorTrio exposing [label] + +# Orange and orange and black +expect { + result = label(Orange, Orange, Black) + result == Ohms(33) +} + +# Blue and grey and brown +expect { + result = label(Blue, Grey, Brown) + result == Ohms(680) +} + +# Red and black and red +expect { + result = label(Red, Black, Red) + result == Kiloohms(2) +} + +# Green and brown and orange +expect { + result = label(Green, Brown, Orange) + result == Kiloohms(51) +} + +# Yellow and violet and yellow +expect { + result = label(Yellow, Violet, Yellow) + result == Kiloohms(470) +} + +# Blue and violet and blue +expect { + result = label(Blue, Violet, Blue) + result == Megaohms(67) +} + +# Minimum possible value +expect { + result = label(Black, Black, Black) + result == Ohms(0) +} + +# Maximum possible value +expect { + result = label(White, White, White) + result == Gigaohms(99) +} + +# First two colors make an invalid octal number +expect { + result = label(Black, Grey, Black) + result == Ohms(8) +} + +# Ignore extra colors +expect { + result = label(Blue, Green, Yellow) + result == Kiloohms(650) +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/rest-api/.meta/Example.roc b/exercises/practice/rest-api/.meta/Example.roc index aab15f00..0332fd8c 100644 --- a/exercises/practice/rest-api/.meta/Example.roc +++ b/exercises/practice/rest-api/.meta/Example.roc @@ -91,8 +91,8 @@ add_user = |_database, payload| { add_loan : RestApi.Database, Str -> Try(Str, [NotFound, InvalidJson]) add_loan = |database, payload| { loan = parse_json_loan(payload)? - lender = database -> get_user(loan.lender)? - borrower = database -> get_user(loan.borrower)? + lender = database |> get_user(loan.lender)? + borrower = database |> get_user(loan.borrower)? updated_lender = lender |> update_lender(borrower.name, loan.amount) updated_borrower = borrower |> update_lender(lender.name, -loan.amount) Ok(users_to_json([updated_lender, updated_borrower])) diff --git a/exercises/practice/rest-api/rest-api-test.roc b/exercises/practice/rest-api/rest-api-test.roc index 4c53e32d..a693df41 100644 --- a/exercises/practice/rest-api/rest-api-test.roc +++ b/exercises/practice/rest-api/rest-api-test.roc @@ -12,18 +12,18 @@ import RestApi exposing [get, post] expect { database = { users: [] } result_json = database - -> get({ url: "/users", payload: "" })? + |> get({ url: "/users", payload: "" })? expected_json = "{\"users\": []}" - result_json -> is_equivalent_to(expected_json)? + result_json |> is_equivalent_to(expected_json)? } # add user expect { database = { users: [] } result_json = database - -> post({ url: "/add", payload: "{\"user\": \"Adam\"}" })? + |> post({ url: "/add", payload: "{\"user\": \"Adam\"}" })? expected_json = "{\"balance\": 0.0, \"name\": \"Adam\", \"owed_by\": {}, \"owes\": {}}" - result_json -> is_equivalent_to(expected_json)? + result_json |> is_equivalent_to(expected_json)? } # get single user @@ -45,9 +45,9 @@ expect { ], } result_json = database - -> get({ url: "/users", payload: "{\"users\": [\"Bob\"]}" })? + |> get({ url: "/users", payload: "{\"users\": [\"Bob\"]}" })? expected_json = "{\"users\": [{\"balance\": 0.0, \"name\": \"Bob\", \"owed_by\": {}, \"owes\": {}}]}" - result_json -> is_equivalent_to(expected_json)? + result_json |> is_equivalent_to(expected_json)? } ## @@ -73,9 +73,9 @@ expect { ], } result_json = database - -> post({ url: "/iou", payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}" })? + |> post({ url: "/iou", payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}" })? expected_json = "{\"users\": [{\"balance\": 3.0, \"name\": \"Adam\", \"owed_by\": {\"Bob\": 3.0}, \"owes\": {}}, {\"balance\": -3.0, \"name\": \"Bob\", \"owed_by\": {}, \"owes\": {\"Adam\": 3.0}}]}" - result_json -> is_equivalent_to(expected_json)? + result_json |> is_equivalent_to(expected_json)? } # borrower has negative balance @@ -107,9 +107,9 @@ expect { ], } result_json = database - -> post({ url: "/iou", payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}" })? + |> post({ url: "/iou", payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}" })? expected_json = "{\"users\": [{\"balance\": 3.0, \"name\": \"Adam\", \"owed_by\": {\"Bob\": 3.0}, \"owes\": {}}, {\"balance\": -6.0, \"name\": \"Bob\", \"owed_by\": {}, \"owes\": {\"Adam\": 3.0, \"Chuck\": 3.0}}]}" - result_json -> is_equivalent_to(expected_json)? + result_json |> is_equivalent_to(expected_json)? } # lender has negative balance @@ -141,9 +141,9 @@ expect { ], } result_json = database - -> post({ url: "/iou", payload: "{\"amount\": 3.0, \"borrower\": \"Adam\", \"lender\": \"Bob\"}" })? + |> post({ url: "/iou", payload: "{\"amount\": 3.0, \"borrower\": \"Adam\", \"lender\": \"Bob\"}" })? expected_json = "{\"users\": [{\"balance\": -3.0, \"name\": \"Adam\", \"owed_by\": {}, \"owes\": {\"Bob\": 3.0}}, {\"balance\": 0.0, \"name\": \"Bob\", \"owed_by\": {\"Adam\": 3.0}, \"owes\": {\"Chuck\": 3.0}}]}" - result_json -> is_equivalent_to(expected_json)? + result_json |> is_equivalent_to(expected_json)? } # lender owes borrower @@ -169,9 +169,9 @@ expect { ], } result_json = database - -> post({ url: "/iou", payload: "{\"amount\": 2.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}" })? + |> post({ url: "/iou", payload: "{\"amount\": 2.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}" })? expected_json = "{\"users\": [{\"balance\": -1.0, \"name\": \"Adam\", \"owed_by\": {}, \"owes\": {\"Bob\": 1.0}}, {\"balance\": 1.0, \"name\": \"Bob\", \"owed_by\": {\"Adam\": 1.0}, \"owes\": {}}]}" - result_json -> is_equivalent_to(expected_json)? + result_json |> is_equivalent_to(expected_json)? } # lender owes borrower less than new loan @@ -197,9 +197,9 @@ expect { ], } result_json = database - -> post({ url: "/iou", payload: "{\"amount\": 4.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}" })? + |> post({ url: "/iou", payload: "{\"amount\": 4.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}" })? expected_json = "{\"users\": [{\"balance\": 1.0, \"name\": \"Adam\", \"owed_by\": {\"Bob\": 1.0}, \"owes\": {}}, {\"balance\": -1.0, \"name\": \"Bob\", \"owed_by\": {}, \"owes\": {\"Adam\": 1.0}}]}" - result_json -> is_equivalent_to(expected_json)? + result_json |> is_equivalent_to(expected_json)? } # lender owes borrower same as new loan @@ -225,9 +225,9 @@ expect { ], } result_json = database - -> post({ url: "/iou", payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}" })? + |> post({ url: "/iou", payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}" })? expected_json = "{\"users\": [{\"balance\": 0.0, \"name\": \"Adam\", \"owed_by\": {}, \"owes\": {}}, {\"balance\": 0.0, \"name\": \"Bob\", \"owed_by\": {}, \"owes\": {}}]}" - result_json -> is_equivalent_to(expected_json)? + result_json |> is_equivalent_to(expected_json)? } is_equivalent_to = |result_json, expected_json| { diff --git a/exercises/practice/run-length-encoding/.meta/Example.roc b/exercises/practice/run-length-encoding/.meta/Example.roc index d46ff31c..6bf6f5bd 100644 --- a/exercises/practice/run-length-encoding/.meta/Example.roc +++ b/exercises/practice/run-length-encoding/.meta/Example.roc @@ -36,7 +36,7 @@ RunLengthEncoding :: {}.{ state_to_str = |state| state.chars |> Str.from_utf8 string .to_utf8() - -> fold_try( + |> fold_try( { chars: [], digits: [] }, |state, char| { if char >= '0' and char <= '9' { @@ -46,8 +46,8 @@ RunLengthEncoding :: {}.{ chars = state.chars.append(char) Ok({ ..state, chars }) } else { - count_str = state.digits -> Str.from_utf8? - count = count_str -> U64.from_str? + count_str = (state.digits |> Str.from_utf8)? + count = (count_str |> U64.from_str)? chars = state.chars.concat(char |> List.repeat(count)) Ok({ chars, digits: [] }) } diff --git a/exercises/practice/run-length-encoding/run-length-encoding-test.roc b/exercises/practice/run-length-encoding/run-length-encoding-test.roc index 3ed06e93..5fb37301 100644 --- a/exercises/practice/run-length-encoding/run-length-encoding-test.roc +++ b/exercises/practice/run-length-encoding/run-length-encoding-test.roc @@ -115,7 +115,7 @@ expect { # encode followed by decode gives original string expect { string = "zzz ZZ zZ" - encoded = string -> encode()? + encoded = string |> encode()? result = encoded |> decode result == Ok(string) } diff --git a/exercises/practice/saddle-points/.meta/Example.roc b/exercises/practice/saddle-points/.meta/Example.roc index 5c7f37ec..d9a28f0e 100644 --- a/exercises/practice/saddle-points/.meta/Example.roc +++ b/exercises/practice/saddle-points/.meta/Example.roc @@ -19,7 +19,7 @@ SaddlePoints :: {}.{ [] }, ) - .join() + .join() }, ) .join() diff --git a/exercises/practice/save-the-cow/.docs/instructions.md b/exercises/practice/save-the-cow/.docs/instructions.md new file mode 100644 index 00000000..1ec98a43 --- /dev/null +++ b/exercises/practice/save-the-cow/.docs/instructions.md @@ -0,0 +1,7 @@ +# Instructions + +Implement the logic for a word-guessing game. + +A player tries to solve a secret word by guessing individual letters. +They win if they reveal all the letters in the secret word. +They lose if they make ten incorrect guesses before revealing the word. diff --git a/exercises/practice/save-the-cow/.docs/introduction.md b/exercises/practice/save-the-cow/.docs/introduction.md new file mode 100644 index 00000000..c87a7fa9 --- /dev/null +++ b/exercises/practice/save-the-cow/.docs/introduction.md @@ -0,0 +1,4 @@ +# Introduction + +Bessie the cow has wandered onto an alien spaceship. +Guess the secret door code to bring her home before the ship blasts off. diff --git a/exercises/practice/save-the-cow/.meta/Example.roc b/exercises/practice/save-the-cow/.meta/Example.roc new file mode 100644 index 00000000..a8481299 --- /dev/null +++ b/exercises/practice/save-the-cow/.meta/Example.roc @@ -0,0 +1,55 @@ +SaveTheCow :: {}.{ + guess : Str, List(U8) -> Try({ outcome : [Win, Ongoing, Lose], masked_word : Str, remaining_failures : U8 }, [GameOver, ..]) + guess = |word, guesses| { + word_chars = word.to_utf8() + initial_state = { + outcome: Ongoing, + masked_word: "_".repeat(word.to_utf8().len()), + remaining_failures: 9, + } + guesses.fold_try( + initial_state, + |state, char| { + if state.outcome != Ongoing { + Err(GameOver) + } else { + masked_chars = state.masked_word.to_utf8() + masked_word = + if masked_chars.contains(char) or !word_chars.contains(char) { + state.masked_word + } else { + masked_chars.map2( + word_chars, + |m, c| { + if m == '_' and c == char { + c + } else { + m + } + }, + ) + |> Str.from_utf8 ?? { + crash "Unreachable assuming the word is ASCII-only" + } + } + if masked_word == state.masked_word { + remaining_failures = state.remaining_failures.minus_saturated(1) + outcome = if state.remaining_failures == 0 { + Lose + } else { + Ongoing + } + Ok({ outcome, masked_word, remaining_failures }) + } else { + outcome = if word == masked_word { + Win + } else { + Ongoing + } + Ok({ ..state, outcome, masked_word }) + } + } + }, + ) + } +} diff --git a/exercises/practice/save-the-cow/.meta/config.json b/exercises/practice/save-the-cow/.meta/config.json new file mode 100644 index 00000000..755dbe2c --- /dev/null +++ b/exercises/practice/save-the-cow/.meta/config.json @@ -0,0 +1,15 @@ +{ + "authors": [], + "files": { + "solution": [ + "SaveTheCow.roc" + ], + "test": [ + "save-the-cow-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Implement a word-guessing game." +} diff --git a/exercises/practice/save-the-cow/.meta/template.j2 b/exercises/practice/save-the-cow/.meta/template.j2 new file mode 100644 index 00000000..0bc3a7dc --- /dev/null +++ b/exercises/practice/save-the-cow/.meta/template.j2 @@ -0,0 +1,28 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +import {{ exercise | to_pascal }} exposing [{{ properties | map("to_snake") | join(", ") }}] + +{% macro render_cases(cases) %} +{%- for case in cases %} +{%- if "cases" in case %} +{{ render_cases(case["cases"]) }} +{%- else %} +# {{ case["description"] }} +expect { + result = {{ case["property"] | to_snake }}({{ case["input"]["word"] | to_roc }}, [ + {%- for ch in case["input"]["guesses"] %}'{{ ch }}'{% if not loop.last %}, {% endif %}{% endfor -%} + ]) +{%- if case["expected"] is mapping and "error" in case["expected"] %} + result.is_err() +{%- else %} + result == Ok({ outcome : {{ case["expected"]["state"] }}, masked_word : {{ case["expected"]["maskedWord"] | to_roc }}, remaining_failures : {{ case["expected"]["remainingFailures"] }} }) +{%- endif %} +} +{% endif %} +{%- endfor %} +{% endmacro %} + +{{ render_cases(cases) }} +{{ macros.footer() }} diff --git a/exercises/practice/save-the-cow/.meta/tests.toml b/exercises/practice/save-the-cow/.meta/tests.toml new file mode 100644 index 00000000..eca72c42 --- /dev/null +++ b/exercises/practice/save-the-cow/.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. + +[71d340f9-fc29-4826-872e-ad7d0b83dd98] +description = "Initially 9 failures are allowed and no letters are guessed" + +[76759c24-8f1a-4fc8-9ffd-d6a1ff0cf03b] +description = "After 10 failures the game is over" + +[d6f2e202-7857-46fb-b709-43a7f3f3b2de] +description = "Losing with several correct guesses" + +[71bc0cda-2032-4637-80c8-fc8771124c08] +description = "Feeding a correct letter removes underscores" + +[5b568a1c-867d-418f-97a8-7b6f8a7ca0a2] +description = "Feeding a correct letter twice counts as a failure" + +[3d40f15b-0271-4c5d-b1a4-3e1f66ff221f] +description = "Guessing a repeated letter reveals all instances" + +[11a86435-e401-4250-a26e-3b0d8c4049ad] +description = "Getting all the letters right makes for a win" + +[b3d81876-84ee-45bb-b531-baa1b05b4709] +description = "Winning on the last guess is still a win" + +[cf204398-5e9f-402f-8bff-42cb7cbbbda9] +description = "Guessing after a lose is error" + +[c2ec5b3d-4923-4a0e-a485-6aa6e78c7ece] +description = "Guessing after a win is error" diff --git a/exercises/practice/save-the-cow/SaveTheCow.roc b/exercises/practice/save-the-cow/SaveTheCow.roc new file mode 100644 index 00000000..5e7d8b29 --- /dev/null +++ b/exercises/practice/save-the-cow/SaveTheCow.roc @@ -0,0 +1,6 @@ +SaveTheCow :: {}.{ + guess : Str, List(U8) -> Try({ outcome : [Win, Ongoing, Lose], masked_word : Str, remaining_failures : U8 }, [GameOver, ..]) + guess = |word, guesses| { + crash "Please implement the 'guess' function" + } +} diff --git a/exercises/practice/save-the-cow/save-the-cow-test.roc b/exercises/practice/save-the-cow/save-the-cow-test.roc new file mode 100644 index 00000000..615ab89a --- /dev/null +++ b/exercises/practice/save-the-cow/save-the-cow-test.roc @@ -0,0 +1,70 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/save-the-cow/canonical-data.json +# File last updated on 2026-08-10 + +import SaveTheCow exposing [guess] + +# Initially 9 failures are allowed and no letters are guessed +expect { + result = guess("loot", []) + result == Ok({ outcome: Ongoing, masked_word: "____", remaining_failures: 9 }) +} + +# After 10 failures the game is over +expect { + result = guess("loot", ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']) + result == Ok({ outcome: Lose, masked_word: "____", remaining_failures: 0 }) +} + +# Losing with several correct guesses +expect { + result = guess("loot", ['t', 'o', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']) + result == Ok({ outcome: Lose, masked_word: "_oot", remaining_failures: 0 }) +} + +# Feeding a correct letter removes underscores +expect { + result = guess("loot", ['t']) + result == Ok({ outcome: Ongoing, masked_word: "___t", remaining_failures: 9 }) +} + +# Feeding a correct letter twice counts as a failure +expect { + result = guess("loot", ['t', 't']) + result == Ok({ outcome: Ongoing, masked_word: "___t", remaining_failures: 8 }) +} + +# Guessing a repeated letter reveals all instances +expect { + result = guess("loot", ['t', 't', 'o']) + result == Ok({ outcome: Ongoing, masked_word: "_oot", remaining_failures: 8 }) +} + +# Getting all the letters right makes for a win +expect { + result = guess("loot", ['t', 't', 'o', 'l']) + result == Ok({ outcome: Win, masked_word: "loot", remaining_failures: 8 }) +} + +# Winning on the last guess is still a win +expect { + result = guess("loot", ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 't', 'o', 'l']) + result == Ok({ outcome: Win, masked_word: "loot", remaining_failures: 0 }) +} + +# Guessing after a lose is error +expect { + result = guess("loot", ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k']) + result.is_err() +} + +# Guessing after a win is error +expect { + result = guess("loot", ['t', 'o', 'l', 'l']) + result.is_err() +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/simple-cipher/.docs/instructions.md b/exercises/practice/simple-cipher/.docs/instructions.md new file mode 100644 index 00000000..afd0b57d --- /dev/null +++ b/exercises/practice/simple-cipher/.docs/instructions.md @@ -0,0 +1,40 @@ +# Instructions + +Create an implementation of the [Vigenère cipher][wiki]. +The Vigenère cipher is a simple substitution cipher. + +## Cipher terminology + +A cipher is an algorithm used to encrypt, or encode, a string. +The unencrypted string is called the _plaintext_ and the encrypted string is called the _ciphertext_. +Converting plaintext to ciphertext is called _encoding_ while the reverse is called _decoding_. + +In a _substitution cipher_, each plaintext letter is replaced with a ciphertext letter which is computed with the help of a _key_. +(Note, it is possible for replacement letter to be the same as the original letter.) + +## Encoding details + +In this cipher, the key is a series of lowercase letters, such as `"abcd"`. +Each letter of the plaintext is _shifted_ or _rotated_ by a distance based on a corresponding letter in the key. +An `"a"` in the key means a shift of 0 (that is, no shift). +A `"b"` in the key means a shift of 1. +A `"c"` in the key means a shift of 2, and so on. + +The first letter of the plaintext uses the first letter of the key, the second letter of the plaintext uses the second letter of the key and so on. +If you run out of letters in the key before you run out of letters in the plaintext, start over from the start of the key again. + +If the key only contains one letter, such as `"dddddd"`, then all letters of the plaintext are shifted by the same amount (three in this example), which would make this the same as a rotational cipher or shift cipher (sometimes called a Caesar cipher). +For example, the plaintext `"iamapandabear"` would become `"ldpdsdqgdehdu"`. + +If the key only contains the letter `"a"` (one or more times), the shift distance is zero and the ciphertext is the same as the plaintext. + +Usually the key is more complicated than that, though! +If the key is `"abcd"` then letters of the plaintext would be shifted by a distance of 0, 1, 2, and 3. +If the plaintext is `"hello"`, we need 5 shifts so the key would wrap around, giving shift distances of 0, 1, 2, 3, and 0. +Applying those shifts to the letters of `"hello"` we get `"hfnoo"`. + +## Random keys + +If no key is provided, generate a key which consists of at least 100 random lowercase letters from the Latin alphabet. + +[wiki]: https://en.wikipedia.org/wiki/Vigen%C3%A8re_cipher diff --git a/exercises/practice/simple-cipher/.meta/Example.roc b/exercises/practice/simple-cipher/.meta/Example.roc new file mode 100644 index 00000000..5da7f9e0 --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/Example.roc @@ -0,0 +1,50 @@ +import random.Random + +SimpleCipher := { key : Str }.{ + create : { key : Str } -> SimpleCipher + create = |{ key }| { + { key } + } + + create_random : { random_state : Random.State, key_length : U64 } -> { cipher : SimpleCipher, random_state : Random.State } + create_random = |{ random_state, key_length }| { + key_gen = + Random.bounded_u8('a', 'z') + |> Random.list(key_length) + |> Random.map( + |chars| Str.from_utf8(chars) ?? { + crash "Unreachable: only a to z are possible" + }, + ) + { value, state } = Random.step(random_state, key_gen) + { cipher: { key: value }, random_state: state } + } + + encode : SimpleCipher, Str -> Str + encode = |cipher, plaintext| { + transform(cipher, plaintext, |rank, offset| (rank + offset) % 26) + } + + decode : SimpleCipher, Str -> Str + decode = |cipher, ciphertext| { + transform(cipher, ciphertext, |rank, offset| ((rank + 26) - offset) % 26) + } +} + +transform = |cipher, str, compute_new_rank| { + key_chars = cipher.key.to_utf8() + str + .to_utf8() + .map_with_index( + |char, index| { + key_index = index % key_chars.len() + key_char = key_chars.get(key_index) ?? { + crash "Unreachable: key_index cannot be out of bounds" + } + compute_new_rank(char - 'a', key_char - 'a') + 'a' + }, + ) + |> Str.from_utf8 ?? { + crash "Unreachable: only a to z are possible" + } +} diff --git a/exercises/practice/simple-cipher/.meta/config.json b/exercises/practice/simple-cipher/.meta/config.json new file mode 100644 index 00000000..a0654dd6 --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [], + "files": { + "solution": [ + "SimpleCipher.roc" + ], + "test": [ + "simple-cipher-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Implement the Vigenère cipher, a simple substitution cipher.", + "source": "Substitution Cipher at Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Substitution_cipher" +} diff --git a/exercises/practice/simple-cipher/.meta/template.j2 b/exercises/practice/simple-cipher/.meta/template.j2 new file mode 100644 index 00000000..e7eadc09 --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/template.j2 @@ -0,0 +1,123 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header(imports=["random"]) }} + +import SimpleCipher +import random.Random + +## +# Random key cipher +## + +# Can encode +expect { + { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) + plaintext = "aaaaaaaaaa" + ciphertext = cipher.encode(plaintext) + expected = cipher.key |> substring(0, plaintext.count_utf8_bytes())? + ciphertext == expected +} + +# Can decode +expect { + { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) + expected = "aaaaaaaaaa" + ciphertext = cipher.key |> substring(0, expected.count_utf8_bytes())? + decoded = cipher.decode(ciphertext) + decoded == expected +} + +# Is reversible. I.e., if you apply decode to an encoded result, you must see the original plaintext +expect { + { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) + plaintext = "abcdefghij" + ciphertext = cipher.encode(plaintext) + decoded = cipher.decode(ciphertext) + decoded == plaintext +} + +# Key is made only of lowercase letters +expect { + { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) + cipher.key |> all_lowercase_letters +} + +## +# Substitution cipher +## + +# Can encode +expect { + cipher = SimpleCipher.create({ key: "abcdefghij" }) + plaintext = "aaaaaaaaaa" + ciphertext = cipher.encode(plaintext) + ciphertext == "abcdefghij" +} + +# Can decode +expect { + cipher = SimpleCipher.create({ key: "abcdefghij" }) + plaintext = "abcdefghij" + ciphertext = cipher.decode(plaintext) + ciphertext == "aaaaaaaaaa" +} + +# Is reversible. I.e., if you apply decode to an encoded result, you must see the original plaintext +expect { + cipher = SimpleCipher.create({ key: "abcdefghij" }) + plaintext = "abcdefghij" + ciphertext = cipher.encode(plaintext) + decoded = cipher.decode(ciphertext) + decoded == plaintext +} + +# Can double shift encode +expect { + cipher = SimpleCipher.create({ key: "iamapandabear" }) + plaintext = "iamapandabear" + ciphertext = cipher.encode(plaintext) + ciphertext == "qayaeaagaciai" +} + +# Can wrap on encode +expect { + cipher = SimpleCipher.create({ key: "abcdefghij" }) + plaintext = "zzzzzzzzzz" + ciphertext = cipher.encode(plaintext) + ciphertext == "zabcdefghi" +} + +# Can wrap on decode +expect { + cipher = SimpleCipher.create({ key: "abcdefghij" }) + ciphertext = "zabcdefghi" + decoded = cipher.decode(ciphertext) + decoded == "zzzzzzzzzz" +} + +# Can encode messages longer than the key +expect { + cipher = SimpleCipher.create({ key: "abc" }) + plaintext = "iamapandabear" + ciphertext = cipher.encode(plaintext) + ciphertext == "iboaqcnecbfcr" +} + +# Can decode messages longer than the key +expect { + cipher = SimpleCipher.create({ key: "abc" }) + ciphertext = "iboaqcnecbfcr" + decoded = cipher.decode(ciphertext) + decoded == "iamapandabear" +} + +substring = |str, start, end| { + len = str.count_utf8_bytes() + str.drop_last_bytes(len - end)?.drop_first_bytes(start) +} + +all_lowercase_letters = |str| { + str.to_utf8().all(|c| c >= 'a' and c < 'z') # only characters a to z +} + +{{ macros.footer() }} diff --git a/exercises/practice/simple-cipher/.meta/tests.toml b/exercises/practice/simple-cipher/.meta/tests.toml new file mode 100644 index 00000000..77e6571e --- /dev/null +++ b/exercises/practice/simple-cipher/.meta/tests.toml @@ -0,0 +1,46 @@ +# 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. + +[b8bdfbe1-bea3-41bb-a999-b41403f2b15d] +description = "Random key cipher -> Can encode" + +[3dff7f36-75db-46b4-ab70-644b3f38b81c] +description = "Random key cipher -> Can decode" + +[8143c684-6df6-46ba-bd1f-dea8fcb5d265] +description = "Random key cipher -> Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method" + +[defc0050-e87d-4840-85e4-51a1ab9dd6aa] +description = "Random key cipher -> Key is made only of lowercase letters" + +[565e5158-5b3b-41dd-b99d-33b9f413c39f] +description = "Substitution cipher -> Can encode" + +[d44e4f6a-b8af-4e90-9d08-fd407e31e67b] +description = "Substitution cipher -> Can decode" + +[70a16473-7339-43df-902d-93408c69e9d1] +description = "Substitution cipher -> Is reversible. I.e., if you apply decode in a encoded result, you must see the same plaintext encode parameter as a result of the decode method" + +[69a1458b-92a6-433a-a02d-7beac3ea91f9] +description = "Substitution cipher -> Can double shift encode" + +[21d207c1-98de-40aa-994f-86197ae230fb] +description = "Substitution cipher -> Can wrap on encode" + +[a3d7a4d7-24a9-4de6-bdc4-a6614ced0cb3] +description = "Substitution cipher -> Can wrap on decode" + +[e31c9b8c-8eb6-45c9-a4b5-8344a36b9641] +description = "Substitution cipher -> Can encode messages longer than the key" + +[93cfaae0-17da-4627-9a04-d6d1e1be52e3] +description = "Substitution cipher -> Can decode messages longer than the key" diff --git a/exercises/practice/simple-cipher/SimpleCipher.roc b/exercises/practice/simple-cipher/SimpleCipher.roc new file mode 100644 index 00000000..f97dc6a5 --- /dev/null +++ b/exercises/practice/simple-cipher/SimpleCipher.roc @@ -0,0 +1,23 @@ +import random.Random + +SimpleCipher := { key : Str }.{ + create : { key : Str } -> SimpleCipher + create = |{ key }| { + crash "Please implement the 'create' function" + } + + create_random : { random_state : Random.State, key_length : U64 } -> { cipher : SimpleCipher, random_state : Random.State } + create_random = |{ random_state, key_length }| { + crash "Please implement the 'create_random' function" + } + + encode : SimpleCipher, Str -> Str + encode = |cipher, plaintext| { + crash "Please implement the 'encode' function" + } + + decode : SimpleCipher, Str -> Str + decode = |cipher, ciphertext| { + crash "Please implement the 'decode' function" + } +} diff --git a/exercises/practice/simple-cipher/simple-cipher-test.roc b/exercises/practice/simple-cipher/simple-cipher-test.roc new file mode 100644 index 00000000..1f456e37 --- /dev/null +++ b/exercises/practice/simple-cipher/simple-cipher-test.roc @@ -0,0 +1,130 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/simple-cipher/canonical-data.json +# File last updated on 2026-08-12 +app [main!] { + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.21.0/4rAQg8kUYZ3Vksr4qMQHpaFYNiHSn9GgS7gVxghd1XYV.tar.zst", + random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.0/CwDEAmyUMCsqW6dh4pxYnp7suUZAj5b5gpZuh7udtyE7.tar.zst", +} + +import SimpleCipher +import random.Random + +## +# Random key cipher +## + +# Can encode +expect { + { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) + plaintext = "aaaaaaaaaa" + ciphertext = cipher.encode(plaintext) + expected = cipher.key |> substring(0, plaintext.count_utf8_bytes())? + ciphertext == expected +} + +# Can decode +expect { + { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) + expected = "aaaaaaaaaa" + ciphertext = cipher.key |> substring(0, expected.count_utf8_bytes())? + decoded = cipher.decode(ciphertext) + decoded == expected +} + +# Is reversible. I.e., if you apply decode to an encoded result, you must see the original plaintext +expect { + { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) + plaintext = "abcdefghij" + ciphertext = cipher.encode(plaintext) + decoded = cipher.decode(ciphertext) + decoded == plaintext +} + +# Key is made only of lowercase letters +expect { + { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) + cipher.key |> all_lowercase_letters +} + +## +# Substitution cipher +## + +# Can encode +expect { + cipher = SimpleCipher.create({ key: "abcdefghij" }) + plaintext = "aaaaaaaaaa" + ciphertext = cipher.encode(plaintext) + ciphertext == "abcdefghij" +} + +# Can decode +expect { + cipher = SimpleCipher.create({ key: "abcdefghij" }) + plaintext = "abcdefghij" + ciphertext = cipher.decode(plaintext) + ciphertext == "aaaaaaaaaa" +} + +# Is reversible. I.e., if you apply decode to an encoded result, you must see the original plaintext +expect { + cipher = SimpleCipher.create({ key: "abcdefghij" }) + plaintext = "abcdefghij" + ciphertext = cipher.encode(plaintext) + decoded = cipher.decode(ciphertext) + decoded == plaintext +} + +# Can double shift encode +expect { + cipher = SimpleCipher.create({ key: "iamapandabear" }) + plaintext = "iamapandabear" + ciphertext = cipher.encode(plaintext) + ciphertext == "qayaeaagaciai" +} + +# Can wrap on encode +expect { + cipher = SimpleCipher.create({ key: "abcdefghij" }) + plaintext = "zzzzzzzzzz" + ciphertext = cipher.encode(plaintext) + ciphertext == "zabcdefghi" +} + +# Can wrap on decode +expect { + cipher = SimpleCipher.create({ key: "abcdefghij" }) + ciphertext = "zabcdefghi" + decoded = cipher.decode(ciphertext) + decoded == "zzzzzzzzzz" +} + +# Can encode messages longer than the key +expect { + cipher = SimpleCipher.create({ key: "abc" }) + plaintext = "iamapandabear" + ciphertext = cipher.encode(plaintext) + ciphertext == "iboaqcnecbfcr" +} + +# Can decode messages longer than the key +expect { + cipher = SimpleCipher.create({ key: "abc" }) + ciphertext = "iboaqcnecbfcr" + decoded = cipher.decode(ciphertext) + decoded == "iamapandabear" +} + +substring = |str, start, end| { + len = str.count_utf8_bytes() + str.drop_last_bytes(len - end)?.drop_first_bytes(start) +} + +all_lowercase_letters = |str| { + str.to_utf8().all(|c| c >= 'a' and c < 'z') # only characters a to z +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/simple-linked-list/simple-linked-list-test.roc b/exercises/practice/simple-linked-list/simple-linked-list-test.roc index 7c31028c..66761747 100644 --- a/exercises/practice/simple-linked-list/simple-linked-list-test.roc +++ b/exercises/practice/simple-linked-list/simple-linked-list-test.roc @@ -73,7 +73,7 @@ expect { result = SimpleLinkedList.from_list([1, 2]) .pop()? - -> expect_value(2)? + |> expect_value(2)? .pop()? .value @@ -84,12 +84,12 @@ expect { expect { result = SimpleLinkedList.from_list([1, 2]) - -> expect_len(2)? + |> expect_len(2)? .pop()? - -> expect_value(2)? - -> expect_len(1)? + |> expect_value(2)? + |> expect_len(1)? .pop()? - -> expect_value(1)? + |> expect_value(1)? .len() result == 0 @@ -116,13 +116,13 @@ expect { .push(1) .push(2) .pop()? - -> expect_value(2)? + |> expect_value(2)? .push(3) - -> expect_len(2)? + |> expect_len(2)? .pop()? - -> expect_value(3)? + |> expect_value(3)? .pop()? - -> expect_value(1)? + |> expect_value(1)? .len() result == 0 @@ -163,7 +163,7 @@ expect { expect { result = SimpleLinkedList.from_list([1, 2]) - -> expect_peek(2)? + |> expect_peek(2)? .len() result == 2 @@ -175,10 +175,10 @@ expect { SimpleLinkedList.from_list([]) .push(1) .push(2) - -> expect_peek(2)? + |> expect_peek(2)? .pop()? - -> expect_value(2)? - -> expect_peek(1)? + |> expect_value(2)? + |> expect_peek(1)? .push(3) .peek()? @@ -215,7 +215,7 @@ expect { .push(2) .push(3) .pop()? - -> expect_value(3)? + |> expect_value(3)? .push(4) .to_list() @@ -251,11 +251,11 @@ expect { result = SimpleLinkedList.from_list([1, 2, 3]) .reverse() - -> expect_len(3)? + |> expect_len(3)? .pop()? - -> expect_value(1)? + |> expect_value(1)? .pop()? - -> expect_value(2)? + |> expect_value(2)? .pop()? .value @@ -269,9 +269,9 @@ expect { .reverse() .reverse() .pop()? - -> expect_value(3)? + |> expect_value(3)? .pop()? - -> expect_value(2)? + |> expect_value(2)? .pop()? .value diff --git a/exercises/practice/twelve-days/.docs/instructions.md b/exercises/practice/twelve-days/.docs/instructions.md new file mode 100644 index 00000000..83bb6e19 --- /dev/null +++ b/exercises/practice/twelve-days/.docs/instructions.md @@ -0,0 +1,36 @@ +# Instructions + +Your task in this exercise is to write code that returns the lyrics of the song: "The Twelve Days of Christmas." + +"The Twelve Days of Christmas" is a common English Christmas carol. +Each subsequent verse of the song builds on the previous verse. + +The lyrics your code returns should _exactly_ match the full song text shown below. + +## Lyrics + +```text +On the first day of Christmas my true love gave to me: a Partridge in a Pear Tree. + +On the second day of Christmas my true love gave to me: two Turtle Doves, and a Partridge in a Pear Tree. + +On the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +On the fourth day of Christmas my true love gave to me: four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +On the fifth day of Christmas my true love gave to me: five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +On the sixth day of Christmas my true love gave to me: six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +On the seventh day of Christmas my true love gave to me: seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +On the eighth day of Christmas my true love gave to me: eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +On the ninth day of Christmas my true love gave to me: nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +On the tenth day of Christmas my true love gave to me: ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +On the eleventh day of Christmas my true love gave to me: eleven Pipers Piping, ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +On the twelfth day of Christmas my true love gave to me: twelve Drummers Drumming, eleven Pipers Piping, ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. +``` diff --git a/exercises/practice/twelve-days/.meta/Example.roc b/exercises/practice/twelve-days/.meta/Example.roc new file mode 100644 index 00000000..c1aa2fd0 --- /dev/null +++ b/exercises/practice/twelve-days/.meta/Example.roc @@ -0,0 +1,72 @@ +TwelveDays :: {}.{ + recite : U8, U8 -> Try(Str, [InvalidVerseNumber, ..]) + recite = |first_verse, last_verse| { + if first_verse == 0 or last_verse == 0 or first_verse > last_verse { + Err(InvalidVerseNumber) + } else { + (first_verse..=last_verse) + |> List.from_iter + .map_try(verse)? + |> Str.join_with("\n") + |> Ok + } + } +} + +verse : U8 -> Try(Str, [InvalidVerseNumber, ..]) +verse = |day| { + Ok("On the ${to_ordinal(day)?} day of Christmas my true love gave to me: ${presents(day)?}.") +} + +present : U8 -> Try(Str, [InvalidVerseNumber, ..]) +present = |number| { + match number { + 1 => Ok("a Partridge in a Pear Tree") + 2 => Ok("two Turtle Doves") + 3 => Ok("three French Hens") + 4 => Ok("four Calling Birds") + 5 => Ok("five Gold Rings") + 6 => Ok("six Geese-a-Laying") + 7 => Ok("seven Swans-a-Swimming") + 8 => Ok("eight Maids-a-Milking") + 9 => Ok("nine Ladies Dancing") + 10 => Ok("ten Lords-a-Leaping") + 11 => Ok("eleven Pipers Piping") + 12 => Ok("twelve Drummers Drumming") + _ => Err(InvalidVerseNumber) + } +} + +presents : U8 -> Try(Str, [InvalidVerseNumber, ..]) +presents = |day| { + (1..=day) + .rev() + |> List.from_iter + .map_try(present)? + |> Str.join_with(", ") + |> (|str| if day > 1 { + str.replace_first(", a", ", and a") + } else { + str + }) + |> Ok +} + +to_ordinal : U8 -> Try(Str, [InvalidVerseNumber, ..]) +to_ordinal = |day| { + match day { + 1 => Ok("first") + 2 => Ok("second") + 3 => Ok("third") + 4 => Ok("fourth") + 5 => Ok("fifth") + 6 => Ok("sixth") + 7 => Ok("seventh") + 8 => Ok("eighth") + 9 => Ok("ninth") + 10 => Ok("tenth") + 11 => Ok("eleventh") + 12 => Ok("twelfth") + _ => Err(InvalidVerseNumber) + } +} diff --git a/exercises/practice/twelve-days/.meta/config.json b/exercises/practice/twelve-days/.meta/config.json new file mode 100644 index 00000000..1900ba0c --- /dev/null +++ b/exercises/practice/twelve-days/.meta/config.json @@ -0,0 +1,17 @@ +{ + "authors": [], + "files": { + "solution": [ + "TwelveDays.roc" + ], + "test": [ + "twelve-days-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "blurb": "Output the lyrics to 'The Twelve Days of Christmas'.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/The_Twelve_Days_of_Christmas_(song)" +} diff --git a/exercises/practice/twelve-days/.meta/template.j2 b/exercises/practice/twelve-days/.meta/template.j2 new file mode 100644 index 00000000..5a4f401b --- /dev/null +++ b/exercises/practice/twelve-days/.meta/template.j2 @@ -0,0 +1,30 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +import {{ exercise | to_pascal }} exposing [{{ properties | map("to_snake") | join(", ") }}] + +{% macro render_cases(cases) %} +{%- for case in cases %} +{%- if "cases" in case %} +{{ render_cases(case["cases"]) }} +{%- else %} +# {{ case["description"] }} +expect { + result = {{ case["property"] | to_snake }}( + {%- for key, value in case["input"].items() -%} + {{ value | to_roc }}{% if not loop.last %}, {% endif %} + {%- endfor -%} + )? +{%- if case["expected"] is mapping and "error" in case["expected"] %} + result.is_err() +{%- else %} + result == {{ case["expected"] | join('\n') | to_roc_multiline_string | indent(8) }} +{%- endif %} +} +{%- endif %} +{%- endfor %} +{% endmacro %} + +{{ render_cases(cases) }} +{{ macros.footer() }} diff --git a/exercises/practice/twelve-days/.meta/tests.toml b/exercises/practice/twelve-days/.meta/tests.toml new file mode 100644 index 00000000..01fbc03f --- /dev/null +++ b/exercises/practice/twelve-days/.meta/tests.toml @@ -0,0 +1,55 @@ +# 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. + +[c0b5a5e6-c89d-49b1-a6b2-9f523bff33f7] +description = "verse -> first day a partridge in a pear tree" + +[1c64508a-df3d-420a-b8e1-fe408847854a] +description = "verse -> second day two turtle doves" + +[a919e09c-75b2-4e64-bb23-de4a692060a8] +description = "verse -> third day three french hens" + +[9bed8631-ec60-4894-a3bb-4f0ec9fbe68d] +description = "verse -> fourth day four calling birds" + +[cf1024f0-73b6-4545-be57-e9cea565289a] +description = "verse -> fifth day five gold rings" + +[50bd3393-868a-4f24-a618-68df3d02ff04] +description = "verse -> sixth day six geese-a-laying" + +[8f29638c-9bf1-4680-94be-e8b84e4ade83] +description = "verse -> seventh day seven swans-a-swimming" + +[7038d6e1-e377-47ad-8c37-10670a05bc05] +description = "verse -> eighth day eight maids-a-milking" + +[37a800a6-7a56-4352-8d72-0f51eb37cfe8] +description = "verse -> ninth day nine ladies dancing" + +[10b158aa-49ff-4b2d-afc3-13af9133510d] +description = "verse -> tenth day ten lords-a-leaping" + +[08d7d453-f2ba-478d-8df0-d39ea6a4f457] +description = "verse -> eleventh day eleven pipers piping" + +[0620fea7-1704-4e48-b557-c05bf43967f0] +description = "verse -> twelfth day twelve drummers drumming" + +[da8b9013-b1e8-49df-b6ef-ddec0219e398] +description = "lyrics -> recites first three verses of the song" + +[c095af0d-3137-4653-ad32-bfb899eda24c] +description = "lyrics -> recites three verses from the middle of the song" + +[20921bc9-cc52-4627-80b3-198cbbfcf9b7] +description = "lyrics -> recites the whole song" diff --git a/exercises/practice/twelve-days/TwelveDays.roc b/exercises/practice/twelve-days/TwelveDays.roc new file mode 100644 index 00000000..1de464b8 --- /dev/null +++ b/exercises/practice/twelve-days/TwelveDays.roc @@ -0,0 +1,6 @@ +TwelveDays :: {}.{ + recite : U8, U8 -> Try(Str, _) + recite = |first_verse, last_verse| { + crash "Please implement the 'recite' function" + } +} diff --git a/exercises/practice/twelve-days/twelve-days-test.roc b/exercises/practice/twelve-days/twelve-days-test.roc new file mode 100644 index 00000000..7d6b52f3 --- /dev/null +++ b/exercises/practice/twelve-days/twelve-days-test.roc @@ -0,0 +1,108 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/twelve-days/canonical-data.json +# File last updated on 2026-08-10 + +import TwelveDays exposing [recite] + +# first day a partridge in a pear tree +expect { + result = recite(1, 1)? + result == "On the first day of Christmas my true love gave to me: a Partridge in a Pear Tree." +} +# second day two turtle doves +expect { + result = recite(2, 2)? + result == "On the second day of Christmas my true love gave to me: two Turtle Doves, and a Partridge in a Pear Tree." +} +# third day three french hens +expect { + result = recite(3, 3)? + result == "On the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree." +} +# fourth day four calling birds +expect { + result = recite(4, 4)? + result == "On the fourth day of Christmas my true love gave to me: four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree." +} +# fifth day five gold rings +expect { + result = recite(5, 5)? + result == "On the fifth day of Christmas my true love gave to me: five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree." +} +# sixth day six geese-a-laying +expect { + result = recite(6, 6)? + result == "On the sixth day of Christmas my true love gave to me: six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree." +} +# seventh day seven swans-a-swimming +expect { + result = recite(7, 7)? + result == "On the seventh day of Christmas my true love gave to me: seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree." +} +# eighth day eight maids-a-milking +expect { + result = recite(8, 8)? + result == "On the eighth day of Christmas my true love gave to me: eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree." +} +# ninth day nine ladies dancing +expect { + result = recite(9, 9)? + result == "On the ninth day of Christmas my true love gave to me: nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree." +} +# tenth day ten lords-a-leaping +expect { + result = recite(10, 10)? + result == "On the tenth day of Christmas my true love gave to me: ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree." +} +# eleventh day eleven pipers piping +expect { + result = recite(11, 11)? + result == "On the eleventh day of Christmas my true love gave to me: eleven Pipers Piping, ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree." +} +# twelfth day twelve drummers drumming +expect { + result = recite(12, 12)? + result == "On the twelfth day of Christmas my true love gave to me: twelve Drummers Drumming, eleven Pipers Piping, ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree." +} + +# recites first three verses of the song +expect { + result = recite(1, 3)? + result == + \\On the first day of Christmas my true love gave to me: a Partridge in a Pear Tree. + \\On the second day of Christmas my true love gave to me: two Turtle Doves, and a Partridge in a Pear Tree. + \\On the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +} +# recites three verses from the middle of the song +expect { + result = recite(4, 6)? + result == + \\On the fourth day of Christmas my true love gave to me: four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the fifth day of Christmas my true love gave to me: five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the sixth day of Christmas my true love gave to me: six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +} +# recites the whole song +expect { + result = recite(1, 12)? + result == + \\On the first day of Christmas my true love gave to me: a Partridge in a Pear Tree. + \\On the second day of Christmas my true love gave to me: two Turtle Doves, and a Partridge in a Pear Tree. + \\On the third day of Christmas my true love gave to me: three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the fourth day of Christmas my true love gave to me: four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the fifth day of Christmas my true love gave to me: five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the sixth day of Christmas my true love gave to me: six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the seventh day of Christmas my true love gave to me: seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the eighth day of Christmas my true love gave to me: eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the ninth day of Christmas my true love gave to me: nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the tenth day of Christmas my true love gave to me: ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the eleventh day of Christmas my true love gave to me: eleven Pipers Piping, ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + \\On the twelfth day of Christmas my true love gave to me: twelve Drummers Drumming, eleven Pipers Piping, ten Lords-a-Leaping, nine Ladies Dancing, eight Maids-a-Milking, seven Swans-a-Swimming, six Geese-a-Laying, five Gold Rings, four Calling Birds, three French Hens, two Turtle Doves, and a Partridge in a Pear Tree. + +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} From f9e79312280e4506b3988bc1894ae085d5b49b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Wed, 12 Aug 2026 22:37:07 +1200 Subject: [PATCH 2/6] Update metadata --- config.json | 64 +++++++++++++++++++ .../nucleotide-count/.meta/config.json | 2 +- exercises/practice/pov/.meta/config.json | 2 +- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index c224acbe..20e72174 100644 --- a/config.json +++ b/config.json @@ -55,6 +55,14 @@ "prerequisites": [], "difficulty": 1 }, + { + "slug": "line-up", + "name": "Line Up", + "uuid": "f7ab5511-8436-43b3-a804-6dc2d625e3e4", + "practices": [], + "prerequisites": [], + "difficulty": 1 + }, { "slug": "raindrops", "name": "Raindrops", @@ -97,6 +105,14 @@ "difficulty": 2, "status": "deprecated" }, + { + "slug": "bottle-song", + "name": "Bottle Song", + "uuid": "ec751487-6801-4a8e-a4e3-513419aef901", + "practices": [], + "prerequisites": [], + "difficulty": 2 + }, { "slug": "collatz-conjecture", "name": "Collatz Conjecture", @@ -203,6 +219,14 @@ "prerequisites": [], "difficulty": 2 }, + { + "slug": "resistor-color-trio", + "name": "Resistor Color Trio", + "uuid": "e7e4af93-ace3-4ff1-8309-6d6bbaa41368", + "practices": [], + "prerequisites": [], + "difficulty": 2 + }, { "slug": "rna-transcription", "name": "RNA Transcription", @@ -283,6 +307,14 @@ "prerequisites": [], "difficulty": 2 }, + { + "slug": "twelve-days", + "name": "Twelve Days", + "uuid": "4f14f4f5-ea9f-4604-b773-a1e0e750357e", + "practices": [], + "prerequisites": [], + "difficulty": 2 + }, { "slug": "allergies", "name": "Allergies", @@ -483,6 +515,14 @@ "prerequisites": [], "difficulty": 3 }, + { + "slug": "save-the-cow", + "name": "Save the Cow", + "uuid": "f1aa17d2-dca6-4b1b-8e02-4c62dc322f59", + "practices": [], + "prerequisites": [], + "difficulty": 3 + }, { "slug": "say", "name": "Say", @@ -603,6 +643,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "grade-school", + "name": "Grade School", + "uuid": "17c90f99-9cb8-4856-b89e-3732ef5d7575", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "house", "name": "House", @@ -651,6 +699,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "simple-cipher", + "name": "Simple Cipher", + "uuid": "0396ed11-d589-4165-8ce7-62ecc2a66721", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "variable-length-quantity", "name": "Variable Length Quantity", @@ -675,6 +731,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "dnd-character", + "name": "D&D Character", + "uuid": "972316c8-0f5d-4d32-b461-4240ba40924c", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "error-handling", "name": "Error Handling", diff --git a/exercises/practice/nucleotide-count/.meta/config.json b/exercises/practice/nucleotide-count/.meta/config.json index dbefb939..b05e8809 100644 --- a/exercises/practice/nucleotide-count/.meta/config.json +++ b/exercises/practice/nucleotide-count/.meta/config.json @@ -14,6 +14,6 @@ ] }, "blurb": "Given a DNA string, compute how many times each nucleotide occurs in the string.", - "source": "The Calculating DNA Nucleotides_problem at Rosalind", + "source": "The Counting DNA Nucleotides problem at Rosalind", "source_url": "https://rosalind.info/problems/dna/" } diff --git a/exercises/practice/pov/.meta/config.json b/exercises/practice/pov/.meta/config.json index a1463bae..78416da2 100644 --- a/exercises/practice/pov/.meta/config.json +++ b/exercises/practice/pov/.meta/config.json @@ -15,5 +15,5 @@ }, "blurb": "Reparent a graph on a selected node.", "source": "Adaptation of exercise from 4clojure", - "source_url": "https://www.4clojure.com/" + "source_url": "https://github.com/oxalorg/4ever-clojure" } From 5e425a8352d041362c6a5c064736dcc6cf6b24a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Wed, 12 Aug 2026 22:41:00 +1200 Subject: [PATCH 3/6] Remove simple-cipher's template.j2 which didn't do anything useful --- .../practice/simple-cipher/.meta/template.j2 | 123 ------------------ .../simple-cipher/simple-cipher-test.roc | 2 - 2 files changed, 125 deletions(-) delete mode 100644 exercises/practice/simple-cipher/.meta/template.j2 diff --git a/exercises/practice/simple-cipher/.meta/template.j2 b/exercises/practice/simple-cipher/.meta/template.j2 deleted file mode 100644 index e7eadc09..00000000 --- a/exercises/practice/simple-cipher/.meta/template.j2 +++ /dev/null @@ -1,123 +0,0 @@ -{%- import "generator_macros.j2" as macros with context -%} -{{ macros.canonical_ref() }} -{{ macros.header(imports=["random"]) }} - -import SimpleCipher -import random.Random - -## -# Random key cipher -## - -# Can encode -expect { - { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) - plaintext = "aaaaaaaaaa" - ciphertext = cipher.encode(plaintext) - expected = cipher.key |> substring(0, plaintext.count_utf8_bytes())? - ciphertext == expected -} - -# Can decode -expect { - { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) - expected = "aaaaaaaaaa" - ciphertext = cipher.key |> substring(0, expected.count_utf8_bytes())? - decoded = cipher.decode(ciphertext) - decoded == expected -} - -# Is reversible. I.e., if you apply decode to an encoded result, you must see the original plaintext -expect { - { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) - plaintext = "abcdefghij" - ciphertext = cipher.encode(plaintext) - decoded = cipher.decode(ciphertext) - decoded == plaintext -} - -# Key is made only of lowercase letters -expect { - { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) - cipher.key |> all_lowercase_letters -} - -## -# Substitution cipher -## - -# Can encode -expect { - cipher = SimpleCipher.create({ key: "abcdefghij" }) - plaintext = "aaaaaaaaaa" - ciphertext = cipher.encode(plaintext) - ciphertext == "abcdefghij" -} - -# Can decode -expect { - cipher = SimpleCipher.create({ key: "abcdefghij" }) - plaintext = "abcdefghij" - ciphertext = cipher.decode(plaintext) - ciphertext == "aaaaaaaaaa" -} - -# Is reversible. I.e., if you apply decode to an encoded result, you must see the original plaintext -expect { - cipher = SimpleCipher.create({ key: "abcdefghij" }) - plaintext = "abcdefghij" - ciphertext = cipher.encode(plaintext) - decoded = cipher.decode(ciphertext) - decoded == plaintext -} - -# Can double shift encode -expect { - cipher = SimpleCipher.create({ key: "iamapandabear" }) - plaintext = "iamapandabear" - ciphertext = cipher.encode(plaintext) - ciphertext == "qayaeaagaciai" -} - -# Can wrap on encode -expect { - cipher = SimpleCipher.create({ key: "abcdefghij" }) - plaintext = "zzzzzzzzzz" - ciphertext = cipher.encode(plaintext) - ciphertext == "zabcdefghi" -} - -# Can wrap on decode -expect { - cipher = SimpleCipher.create({ key: "abcdefghij" }) - ciphertext = "zabcdefghi" - decoded = cipher.decode(ciphertext) - decoded == "zzzzzzzzzz" -} - -# Can encode messages longer than the key -expect { - cipher = SimpleCipher.create({ key: "abc" }) - plaintext = "iamapandabear" - ciphertext = cipher.encode(plaintext) - ciphertext == "iboaqcnecbfcr" -} - -# Can decode messages longer than the key -expect { - cipher = SimpleCipher.create({ key: "abc" }) - ciphertext = "iboaqcnecbfcr" - decoded = cipher.decode(ciphertext) - decoded == "iamapandabear" -} - -substring = |str, start, end| { - len = str.count_utf8_bytes() - str.drop_last_bytes(len - end)?.drop_first_bytes(start) -} - -all_lowercase_letters = |str| { - str.to_utf8().all(|c| c >= 'a' and c < 'z') # only characters a to z -} - -{{ macros.footer() }} diff --git a/exercises/practice/simple-cipher/simple-cipher-test.roc b/exercises/practice/simple-cipher/simple-cipher-test.roc index 1f456e37..5bac77e5 100644 --- a/exercises/practice/simple-cipher/simple-cipher-test.roc +++ b/exercises/practice/simple-cipher/simple-cipher-test.roc @@ -1,5 +1,3 @@ -# These tests are auto-generated with test data from: -# https://github.com/exercism/problem-specifications/tree/main/exercises/simple-cipher/canonical-data.json # File last updated on 2026-08-12 app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.21.0/4rAQg8kUYZ3Vksr4qMQHpaFYNiHSn9GgS7gVxghd1XYV.tar.zst", From a6cb3c361b9598615e5ca82337ca6df098a53121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Fri, 14 Aug 2026 11:30:10 +1200 Subject: [PATCH 4/6] Work around recent compiler regression --- config/generator_macros.j2 | 2 +- exercises/practice/bottle-song/.meta/Example.roc | 2 +- exercises/practice/dnd-character/dnd-character-test.roc | 2 +- exercises/practice/robot-name/robot-name-test.roc | 2 +- exercises/practice/simple-cipher/simple-cipher-test.roc | 6 +++--- exercises/practice/twelve-days/.meta/Example.roc | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/config/generator_macros.j2 b/config/generator_macros.j2 index 1a4b2877..c2699972 100644 --- a/config/generator_macros.j2 +++ b/config/generator_macros.j2 @@ -28,7 +28,7 @@ app [main!] { {%- elif name == "parser" -%} parser: "https://github.com/lukewilliamboswell/roc-parser/releases/download/1.0.2/FrnJ4RGDKpQyoDyESNoBwFNviY4ZGbMVLnUjW9tvSRjk.tar.zst", {%- elif name == "random" -%} - random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.0/CwDEAmyUMCsqW6dh4pxYnp7suUZAj5b5gpZuh7udtyE7.tar.zst", + random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.1/4y2ZUECKuLohLfnxRmukt3wHCBMDYwkKDc2jLSmYz8NM.tar.zst", {%- endif -%} {%- endfor %} } diff --git a/exercises/practice/bottle-song/.meta/Example.roc b/exercises/practice/bottle-song/.meta/Example.roc index e97c298e..9a2d428a 100644 --- a/exercises/practice/bottle-song/.meta/Example.roc +++ b/exercises/practice/bottle-song/.meta/Example.roc @@ -5,8 +5,8 @@ BottleSong :: {}.{ Err(InvalidVerseNumber) } else { ((starting_number - number_of_verses + 1)..=starting_number) - .rev() |> List.from_iter + .rev() .map_try(verse)? |> Str.join_with("\n\n") |> Ok diff --git a/exercises/practice/dnd-character/dnd-character-test.roc b/exercises/practice/dnd-character/dnd-character-test.roc index 8befcb19..20d3e5dc 100644 --- a/exercises/practice/dnd-character/dnd-character-test.roc +++ b/exercises/practice/dnd-character/dnd-character-test.roc @@ -3,7 +3,7 @@ # File last updated on 2026-08-10 app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.21.0/4rAQg8kUYZ3Vksr4qMQHpaFYNiHSn9GgS7gVxghd1XYV.tar.zst", - random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.0/CwDEAmyUMCsqW6dh4pxYnp7suUZAj5b5gpZuh7udtyE7.tar.zst", + random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.1/4y2ZUECKuLohLfnxRmukt3wHCBMDYwkKDc2jLSmYz8NM.tar.zst", } import DndCharacter diff --git a/exercises/practice/robot-name/robot-name-test.roc b/exercises/practice/robot-name/robot-name-test.roc index a25026e6..1b2920a9 100644 --- a/exercises/practice/robot-name/robot-name-test.roc +++ b/exercises/practice/robot-name/robot-name-test.roc @@ -1,6 +1,6 @@ app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.21.0/4rAQg8kUYZ3Vksr4qMQHpaFYNiHSn9GgS7gVxghd1XYV.tar.zst", - random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.0/CwDEAmyUMCsqW6dh4pxYnp7suUZAj5b5gpZuh7udtyE7.tar.zst", + random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.1/4y2ZUECKuLohLfnxRmukt3wHCBMDYwkKDc2jLSmYz8NM.tar.zst", } import RobotName exposing [Factory, Robot] diff --git a/exercises/practice/simple-cipher/simple-cipher-test.roc b/exercises/practice/simple-cipher/simple-cipher-test.roc index 5bac77e5..cf5ab7c8 100644 --- a/exercises/practice/simple-cipher/simple-cipher-test.roc +++ b/exercises/practice/simple-cipher/simple-cipher-test.roc @@ -1,7 +1,7 @@ # File last updated on 2026-08-12 app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.21.0/4rAQg8kUYZ3Vksr4qMQHpaFYNiHSn9GgS7gVxghd1XYV.tar.zst", - random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.0/CwDEAmyUMCsqW6dh4pxYnp7suUZAj5b5gpZuh7udtyE7.tar.zst", + random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.1/4y2ZUECKuLohLfnxRmukt3wHCBMDYwkKDc2jLSmYz8NM.tar.zst", } import SimpleCipher @@ -13,7 +13,7 @@ import random.Random # Can encode expect { - { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) + cipher = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }).cipher plaintext = "aaaaaaaaaa" ciphertext = cipher.encode(plaintext) expected = cipher.key |> substring(0, plaintext.count_utf8_bytes())? @@ -22,7 +22,7 @@ expect { # Can decode expect { - { cipher, random_state: _ } = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }) + cipher = SimpleCipher.create_random({ random_state: Random.seed(0), key_length: 100 }).cipher expected = "aaaaaaaaaa" ciphertext = cipher.key |> substring(0, expected.count_utf8_bytes())? decoded = cipher.decode(ciphertext) diff --git a/exercises/practice/twelve-days/.meta/Example.roc b/exercises/practice/twelve-days/.meta/Example.roc index c1aa2fd0..2adbc1f7 100644 --- a/exercises/practice/twelve-days/.meta/Example.roc +++ b/exercises/practice/twelve-days/.meta/Example.roc @@ -40,8 +40,8 @@ present = |number| { presents : U8 -> Try(Str, [InvalidVerseNumber, ..]) presents = |day| { (1..=day) - .rev() |> List.from_iter + .rev() .map_try(present)? |> Str.join_with(", ") |> (|str| if day > 1 { From d0287e52767216e3c9b3dae1e686c39fe4344957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Wed, 19 Aug 2026 09:39:46 +1200 Subject: [PATCH 5/6] Update new exercises to latest Roc version and latest libraries --- exercises/practice/bottle-song/.meta/Example.roc | 1 + exercises/practice/bottle-song/bottle-song-test.roc | 2 +- exercises/practice/dnd-character/dnd-character-test.roc | 6 +++--- exercises/practice/grade-school/grade-school-test.roc | 2 +- exercises/practice/line-up/line-up-test.roc | 2 +- .../resistor-color-trio/resistor-color-trio-test.roc | 2 +- exercises/practice/save-the-cow/save-the-cow-test.roc | 2 +- exercises/practice/simple-cipher/simple-cipher-test.roc | 2 +- exercises/practice/twelve-days/.meta/Example.roc | 2 ++ exercises/practice/twelve-days/twelve-days-test.roc | 2 +- 10 files changed, 13 insertions(+), 10 deletions(-) diff --git a/exercises/practice/bottle-song/.meta/Example.roc b/exercises/practice/bottle-song/.meta/Example.roc index 9a2d428a..ef25daa0 100644 --- a/exercises/practice/bottle-song/.meta/Example.roc +++ b/exercises/practice/bottle-song/.meta/Example.roc @@ -5,6 +5,7 @@ BottleSong :: {}.{ Err(InvalidVerseNumber) } else { ((starting_number - number_of_verses + 1)..=starting_number) + .iter() |> List.from_iter .rev() .map_try(verse)? diff --git a/exercises/practice/bottle-song/bottle-song-test.roc b/exercises/practice/bottle-song/bottle-song-test.roc index 2ba7f99c..f5e4bfb3 100644 --- a/exercises/practice/bottle-song/bottle-song-test.roc +++ b/exercises/practice/bottle-song/bottle-song-test.roc @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/bottle-song/canonical-data.json -# File last updated on 2026-08-10 +# File last updated on 2026-08-18 import BottleSong exposing [recite] diff --git a/exercises/practice/dnd-character/dnd-character-test.roc b/exercises/practice/dnd-character/dnd-character-test.roc index 20d3e5dc..4c1b4731 100644 --- a/exercises/practice/dnd-character/dnd-character-test.roc +++ b/exercises/practice/dnd-character/dnd-character-test.roc @@ -1,9 +1,9 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/dnd-character/canonical-data.json -# File last updated on 2026-08-10 +# File last updated on 2026-08-18 app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.21.0/4rAQg8kUYZ3Vksr4qMQHpaFYNiHSn9GgS7gVxghd1XYV.tar.zst", - random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.1/4y2ZUECKuLohLfnxRmukt3wHCBMDYwkKDc2jLSmYz8NM.tar.zst", + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.22.0/F1JVZPYfWP71s8vk6tHcV1Qx1Ef6CZkwswGoCn8VHZmL.tar.zst", + random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.2/2ZXLX8WRqrosGu1V3VL5aXqgtfTRvJmjFPx8a26ecVmc.tar.zst", } import DndCharacter diff --git a/exercises/practice/grade-school/grade-school-test.roc b/exercises/practice/grade-school/grade-school-test.roc index df324b2a..347b5bc1 100644 --- a/exercises/practice/grade-school/grade-school-test.roc +++ b/exercises/practice/grade-school/grade-school-test.roc @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/grade-school/canonical-data.json -# File last updated on 2026-08-10 +# File last updated on 2026-08-18 import GradeSchool exposing [add, grade, roster] diff --git a/exercises/practice/line-up/line-up-test.roc b/exercises/practice/line-up/line-up-test.roc index 23959860..f12713c3 100644 --- a/exercises/practice/line-up/line-up-test.roc +++ b/exercises/practice/line-up/line-up-test.roc @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/line-up/canonical-data.json -# File last updated on 2026-08-10 +# File last updated on 2026-08-18 import LineUp exposing [format] diff --git a/exercises/practice/resistor-color-trio/resistor-color-trio-test.roc b/exercises/practice/resistor-color-trio/resistor-color-trio-test.roc index df640f14..3076acef 100644 --- a/exercises/practice/resistor-color-trio/resistor-color-trio-test.roc +++ b/exercises/practice/resistor-color-trio/resistor-color-trio-test.roc @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/resistor-color-trio/canonical-data.json -# File last updated on 2026-08-09 +# File last updated on 2026-08-18 import ResistorColorTrio exposing [label] diff --git a/exercises/practice/save-the-cow/save-the-cow-test.roc b/exercises/practice/save-the-cow/save-the-cow-test.roc index 615ab89a..42113f6a 100644 --- a/exercises/practice/save-the-cow/save-the-cow-test.roc +++ b/exercises/practice/save-the-cow/save-the-cow-test.roc @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/save-the-cow/canonical-data.json -# File last updated on 2026-08-10 +# File last updated on 2026-08-18 import SaveTheCow exposing [guess] diff --git a/exercises/practice/simple-cipher/simple-cipher-test.roc b/exercises/practice/simple-cipher/simple-cipher-test.roc index cf5ab7c8..ab6c8530 100644 --- a/exercises/practice/simple-cipher/simple-cipher-test.roc +++ b/exercises/practice/simple-cipher/simple-cipher-test.roc @@ -1,7 +1,7 @@ # File last updated on 2026-08-12 app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.21.0/4rAQg8kUYZ3Vksr4qMQHpaFYNiHSn9GgS7gVxghd1XYV.tar.zst", - random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.1/4y2ZUECKuLohLfnxRmukt3wHCBMDYwkKDc2jLSmYz8NM.tar.zst", + random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.2/2ZXLX8WRqrosGu1V3VL5aXqgtfTRvJmjFPx8a26ecVmc.tar.zst", } import SimpleCipher diff --git a/exercises/practice/twelve-days/.meta/Example.roc b/exercises/practice/twelve-days/.meta/Example.roc index 2adbc1f7..95c7ff4f 100644 --- a/exercises/practice/twelve-days/.meta/Example.roc +++ b/exercises/practice/twelve-days/.meta/Example.roc @@ -5,6 +5,7 @@ TwelveDays :: {}.{ Err(InvalidVerseNumber) } else { (first_verse..=last_verse) + .iter() |> List.from_iter .map_try(verse)? |> Str.join_with("\n") @@ -40,6 +41,7 @@ present = |number| { presents : U8 -> Try(Str, [InvalidVerseNumber, ..]) presents = |day| { (1..=day) + .iter() |> List.from_iter .rev() .map_try(present)? diff --git a/exercises/practice/twelve-days/twelve-days-test.roc b/exercises/practice/twelve-days/twelve-days-test.roc index 7d6b52f3..499a9a0e 100644 --- a/exercises/practice/twelve-days/twelve-days-test.roc +++ b/exercises/practice/twelve-days/twelve-days-test.roc @@ -1,6 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/twelve-days/canonical-data.json -# File last updated on 2026-08-10 +# File last updated on 2026-08-18 import TwelveDays exposing [recite] From 5727af79cb4113b0a8ae2333be3669cad5c21c05 Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Wed, 19 Aug 2026 17:17:47 +0200 Subject: [PATCH 6/6] one more basic-cli update --- exercises/practice/simple-cipher/simple-cipher-test.roc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/simple-cipher/simple-cipher-test.roc b/exercises/practice/simple-cipher/simple-cipher-test.roc index ab6c8530..e4ef8f2b 100644 --- a/exercises/practice/simple-cipher/simple-cipher-test.roc +++ b/exercises/practice/simple-cipher/simple-cipher-test.roc @@ -1,6 +1,6 @@ # File last updated on 2026-08-12 app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.21.0/4rAQg8kUYZ3Vksr4qMQHpaFYNiHSn9GgS7gVxghd1XYV.tar.zst", + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.22.0/F1JVZPYfWP71s8vk6tHcV1Qx1Ef6CZkwswGoCn8VHZmL.tar.zst", random: "https://github.com/kili-ilo/roc-random/releases/download/0.9.2/2ZXLX8WRqrosGu1V3VL5aXqgtfTRvJmjFPx8a26ecVmc.tar.zst", }