diff --git a/bin/add-exercise b/bin/add-exercise index b813d0ab..dd464f8a 100755 --- a/bin/add-exercise +++ b/bin/add-exercise @@ -88,10 +88,11 @@ cat << END_TEST > "exercises/practice/${slug}/${slug}-test.roc" END_TEST cat << END_STUB > "exercises/practice/${slug}/${slug_pascal}.roc" -module [${slug_camel}] - -${slug_camel} = |my_arg| - crash "Please implement the '${slug_camel}' function" +${slug_pascal} :: {}.{ + ${slug_camel} = |my_arg| { + crash "Please implement the '${slug_camel}' function" + } +} END_STUB cp "exercises/practice/${slug}/${slug_pascal}.roc" "exercises/practice/${slug}/.meta/Example.roc" @@ -107,7 +108,7 @@ Your next steps are: - Create the example solution in 'exercises/practice/${slug}/.meta/Example.roc' - Verify the example solution by running 'bin/verify-exercises ${slug}' - Edit the stub solution in 'exercises/practice/${slug}/${slug_pascal}.roc' -- Format all your Roc code by running 'roc format' on each .roc file +- Format all your Roc code by running 'roc fmt' on each .roc file - Update the 'difficulty' value for the exercise's entry in the 'config.json' file - Validate CI using 'bin/configlet lint' and 'bin/configlet fmt' NEXT_STEPS diff --git a/bin/generate_tests.py b/bin/generate_tests.py index 25680fef..65835acc 100755 --- a/bin/generate_tests.py +++ b/bin/generate_tests.py @@ -157,9 +157,9 @@ def to_roc_multiline_string(lines: Union[str, List[str]]) -> str: elif len(lines) == 1: return to_roc_string(lines[0]) else: - return "\n".join( - ["", '"""'] + [escape_roc_string_content(line) for line in lines] + ['"""'] - ).replace("$(", "\\$(") + return "\n" + "\n".join( + [r'\\' + escape_roc_string_content(line) for line in lines] + ).replace("${", r"\${") + "\n" def to_roc_tuple(values: Any): @@ -177,7 +177,7 @@ def to_roc_record(obj: Dict[str, Any]): def to_roc_bool(value: bool): - return "Bool.true" if value else "Bool.false" + return "Bool.True" if value else "Bool.False" def to_roc_list(values: Any): @@ -187,7 +187,7 @@ def to_roc_list(values: Any): def to_roc_float(value: Union[int, float]): value = float(value) - return f"{value!r}f64".replace("+", "") + return f"{value!r}.F64".replace("+", "") def to_roc(value: Any) -> str: @@ -377,9 +377,10 @@ def load_additional_tests(exercise: Path) -> List[TypeJSON]: def format_file(path: Path) -> NoReturn: """ - Runs roc format on file at path + Runs roc fmt on file at path """ - subprocess.check_call(["roc", "format", path]) + subprocess.check_call(["roc", "fmt", path]) + pass def drop_timestamp(lines): @@ -567,6 +568,8 @@ def generate( env.tests["error_case"] = error_case result = True for exercise in sorted(Path("exercises/practice").glob(exercise_glob)): + if not exercise.is_dir(): + continue if not generate_exercise(env, spec_path, exercise, check): result = False if stop_on_failure: diff --git a/bin/install-roc b/bin/install-roc index 378b6c18..69c628ee 100755 --- a/bin/install-roc +++ b/bin/install-roc @@ -5,12 +5,11 @@ function download_alpha_release() { local install_dir="${1}" - local install_file="${install_dir}/roc.tar.gz" - local release_url="https://github.com/roc-lang/roc/releases/download/alpha4-rolling/roc-linux_x86_64-alpha4-rolling.tar.gz" + rm -rf "${install_dir}" mkdir -p "${install_dir}" - wget -q -O ${install_file} ${release_url} - tar -xzf "${install_file}" -C "${install_dir}" --strip-components 1 - rm -f "${install_file}" + cd "${install_dir}" + wget --secure-protocol=TLSv1_2 -qO- https://roc-lang.org/install_roc.sh | ROC_CONTINUE_IF_STALE=y ROC_ADD_TO_PATH=n sh + ln -s roc_nightly*/roc roc } function update_shell { diff --git a/config/generator_macros.j2 b/config/generator_macros.j2 index 977c431b..08570e1d 100644 --- a/config/generator_macros.j2 +++ b/config/generator_macros.j2 @@ -17,27 +17,29 @@ {% macro header(imports=[], ignore=[]) -%} -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" {%- if imports -%} +app [main!] { + pf: platform "https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/download/0.9/8GdFEvQYS3TeAZxKvTzCLVdQiomweGtXcdZkXNDEeABq.tar.zst" {%- for name in imports -%}, {% if name == "unicode" -%} - unicode: "https://github.com/roc-lang/unicode/releases/download/0.3.0/9KKFsA4CdOz0JIOL7iBSI_2jGIXQ6TsFBXgd086idpY.tar.br" + unicode: "https://github.com/roc-lang/unicode/..." # TODO: update when a zig-compatible release is available {%- elif name == "isodate" -%} - isodate: "https://github.com/imclerran/roc-isodate/releases/download/v0.6.2/73w_H-aSJNcWqtXvMG4JQw_HoaApMBLnE92XD4OcVGU.tar.br" + isodate: "https://github.com/imclerran/roc-isodate/..." # TODO: update when a zig-compatible release is available {%- elif name == "json" -%} - json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.13.0/RqendgZw5e1RsQa3kFhgtnMP8efWoqGRsAvubx4-zus.tar.br" + json: "https://github.com/lukewilliamboswell/roc-json/..." # TODO: update when a zig-compatible release is available {%- elif name == "parser" -%} - parser: "https://github.com/lukewilliamboswell/roc-parser/releases/download/0.10.0/6eZYaXkrakq9fJ4oUc0VfdxU1Fap2iTuAN18q9OgQss.tar.br" + parser: "https://github.com/lukewilliamboswell/roc-parser/..." # TODO: update when a zig-compatible release is available {%- endif -%} -{%- endfor -%} -{%- endif %} +{%- endfor %} } import pf.Stdout +{%- endif %} +{%- endmacro %} -main! = |_args| - Stdout.line!("") +{% macro footer() -%} -{%- endmacro %} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { Ok({}) } +{%- endmacro %} diff --git a/docs/SNIPPET.txt b/docs/SNIPPET.txt index 6dd0a7c3..b48912cf 100644 --- a/docs/SNIPPET.txt +++ b/docs/SNIPPET.txt @@ -1,14 +1,14 @@ -app [main!] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" } - -import pf.Stdout - -factorial = |number| - when number is - 1 -> 1 - n -> n * factorial (n - 1) - -expect factorial 5 == 1 * 2 * 3 * 4 * 5 - -main! = |_args| - result = factorial 20 |> Num.to_str - Stdout.line! "factorial 20 = ${result}" +factorial = |number| { + match number { + 1 => 1 + n => n * factorial(n - 1) + } +} + +expect factorial(5) == 1 * 2 * 3 * 4 * 5 + +main! = |_args| { + result = factorial(20) + echo!("factorial 20 = ${result.to_str()}") + Ok({}) +} diff --git a/docs/TESTS.md b/docs/TESTS.md index 9c0f85ab..33ffd995 100644 --- a/docs/TESTS.md +++ b/docs/TESTS.md @@ -22,7 +22,7 @@ roc test hello-world-test.roc If you've solved the exercise, you should see 0 failed tests, for example: ``` -0 failed and 1 passed in 583 ms. +All (5) tests passed in 123.4 ms. ``` However, if your code has any errors, they will look like this: @@ -36,7 +36,7 @@ This expectation failed: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -1 failed and 0 passed in 1264 ms. +1 failed and 0 passed in 123.4 ms. ``` This should help you fix your code. Once your code works, you can submit it using the `exercism submit` command (see `HELP.md` in the exercise directory for more details). diff --git a/exercises/practice/accumulate/.meta/Example.roc b/exercises/practice/accumulate/.meta/Example.roc index f62ad429..0043c1b6 100644 --- a/exercises/practice/accumulate/.meta/Example.roc +++ b/exercises/practice/accumulate/.meta/Example.roc @@ -1,12 +1,12 @@ -module [accumulate] - -accumulate : List a, (a -> b) -> List b -accumulate = |list, func| - help = |output, input| - when input is - [] -> output - [first, .. as rest] -> - List.append(output, func(first)) - |> help(rest) - - help([], list) +Accumulate :: {}.{ + accumulate : List(a), (a -> b) -> List(b) + accumulate = |list, func| { + help = |output, input| { + match input { + [] => output + [first, .. as rest] => output.append(func(first))->help(rest) + } + } + help([], list) + } +} diff --git a/exercises/practice/accumulate/.meta/template.j2 b/exercises/practice/accumulate/.meta/template.j2 index 3b652ad1..e26e848e 100644 --- a/exercises/practice/accumulate/.meta/template.j2 +++ b/exercises/practice/accumulate/.meta/template.j2 @@ -5,36 +5,43 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake }}] {% set accumulators = { - "(x) => x * x": "|x|\n x * x", + "(x) => x * x": "|x| { x * x }", "(x) => upcase(x)": "to_upper", - "(x) => reverse(x)": "reverse", - "(x) => accumulate([\"1\", \"2\", \"3\"], (y) => x + y)": "|x|\n accumulate([\"1\", \"2\", \"3\"], |y| Str.concat(x, y))" + "(x) => reverse(x)": "str_reverse", + "(x) => accumulate([\"1\", \"2\", \"3\"], (y) => x + y)": "|x| {\n accumulate([\"1\", \"2\", \"3\"], |y| { Str.concat(x, y) })\n }" } -%} {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["list"] | to_roc }}, {{ accumulators[case["input"]["accumulator"]] }}) result == {{ case["expected"] | to_roc }} +} -{% endfor %} +{% endfor -%} -reverse : Str -> Str -reverse = |str| - Str.to_utf8(str) - |> List.reverse - |> Str.from_utf8 - |> Result.with_default("") + +to_upper_char : U8 -> U8 +to_upper_char = |byte| { + if 'a' <= byte and byte <= 'z' { + byte - 'a' + 'A' + } else { + byte + } +} to_upper : Str -> Str -to_upper = |str| - Str.to_utf8(str) - |> List.map( - |byte| - if 'a' <= byte && byte <= 'z' then - byte - 'a' + 'A' - else - byte - ) - |> Str.from_utf8 - |> Result.with_default("") +to_upper = |str| { + str.to_utf8().map(to_upper_char)->Str.from_utf8() ?? "" +} + +str_reverse : Str -> Str +str_reverse = |str| { + str + .to_utf8() + .rev() + ->Str.from_utf8() + ?? "" +} + +{{ macros.footer() }} \ No newline at end of file diff --git a/exercises/practice/accumulate/Accumulate.roc b/exercises/practice/accumulate/Accumulate.roc index c06a2698..98251076 100644 --- a/exercises/practice/accumulate/Accumulate.roc +++ b/exercises/practice/accumulate/Accumulate.roc @@ -1,5 +1,6 @@ -module [accumulate] - -accumulate : List a, (a -> b) -> List b -accumulate = |list, func| - crash("Please implement 'accumulate'") +Accumulate :: {}.{ + accumulate : List(a), (a -> b) -> List(b) + accumulate = |list, func| { + crash "Please implement the 'accumulate' function" + } +} diff --git a/exercises/practice/accumulate/accumulate-test.roc b/exercises/practice/accumulate/accumulate-test.roc index c5ccace4..915fc8fc 100644 --- a/exercises/practice/accumulate/accumulate-test.roc +++ b/exercises/practice/accumulate/accumulate-test.roc @@ -1,70 +1,83 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/accumulate/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Accumulate exposing [accumulate] # accumulate empty -expect - result = accumulate( - [], - |x| - x * x, - ) - result == [] +expect { + result = accumulate( + [], + |x| { + x * x + }, + ) + result == [] +} # accumulate squares -expect - result = accumulate( - [1, 2, 3], - |x| - x * x, - ) - result == [1, 4, 9] +expect { + result = accumulate( + [1, 2, 3], + |x| { + x * x + }, + ) + result == [1, 4, 9] +} # accumulate upcases -expect - result = accumulate(["Hello", "world"], to_upper) - result == ["HELLO", "WORLD"] +expect { + result = accumulate(["Hello", "world"], to_upper) + result == ["HELLO", "WORLD"] +} # accumulate reversed strings -expect - result = accumulate(["the", "quick", "brown", "fox", "etc"], reverse) - result == ["eht", "kciuq", "nworb", "xof", "cte"] +expect { + result = accumulate(["the", "quick", "brown", "fox", "etc"], str_reverse) + result == ["eht", "kciuq", "nworb", "xof", "cte"] +} # accumulate recursively -expect - result = accumulate( - ["a", "b", "c"], - |x| - accumulate(["1", "2", "3"], |y| Str.concat(x, y)), - ) - result == [["a1", "a2", "a3"], ["b1", "b2", "b3"], ["c1", "c2", "c3"]] +expect { + result = accumulate( + ["a", "b", "c"], + |x| { + accumulate( + ["1", "2", "3"], + |y| { + Str.concat(x, y) + }, + ) + }, + ) + result == [["a1", "a2", "a3"], ["b1", "b2", "b3"], ["c1", "c2", "c3"]] +} -reverse : Str -> Str -reverse = |str| - Str.to_utf8(str) - |> List.reverse - |> Str.from_utf8 - |> Result.with_default("") +to_upper_char : U8 -> U8 +to_upper_char = |byte| { + if 'a' <= byte and byte <= 'z' { + byte - 'a' + 'A' + } else { + byte + } +} to_upper : Str -> Str -to_upper = |str| - Str.to_utf8(str) - |> List.map( - |byte| - if 'a' <= byte and byte <= 'z' then - byte - 'a' + 'A' - else - byte, - ) - |> Str.from_utf8 - |> Result.with_default("") +to_upper = |str| { + str.to_utf8().map(to_upper_char)->Str.from_utf8() ?? "" +} + +str_reverse : Str -> Str +str_reverse = |str| { + str + .to_utf8() + .rev() + ->Str.from_utf8() + ?? "" +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/acronym/.meta/Example.roc b/exercises/practice/acronym/.meta/Example.roc index 4f5eff11..0cf187c9 100644 --- a/exercises/practice/acronym/.meta/Example.roc +++ b/exercises/practice/acronym/.meta/Example.roc @@ -1,36 +1,44 @@ -module [abbreviate] +Acronym :: {}.{ + abbreviate : Str -> Str + abbreviate = |text| { + bytes = text.to_utf8() -abbreviate : Str -> Str -abbreviate = |text| - bytes = Str.to_utf8(text) + { acronym, ready_for_letter: _ } = bytes.fold( + { acronym: [], ready_for_letter: Bool.True }, + |state, byte| { + if state.ready_for_letter and is_letter(byte) { + { acronym: state.acronym.append(byte), ready_for_letter: Bool.False } + } else if byte == ' ' or byte == '-' { + { acronym: state.acronym, ready_for_letter: Bool.True } + } else { + state + } + }, + ) - { acronym } = List.walk( - bytes, - { acronym: [], ready_for_letter: Bool.true }, - |state, byte| - if state.ready_for_letter and is_letter(byte) then - { acronym: List.append(state.acronym, byte), ready_for_letter: Bool.false } - else if byte == ' ' or byte == '-' then - { acronym: state.acronym, ready_for_letter: Bool.true } - else - state, - ) + capitalized = acronym.map(capitalize) - capitalized = List.map(acronym, capitalize) - - when Str.from_utf8(capitalized) is - Err(_) -> crash("There was an error converting the bytes to a Str! This should never happen.") - Ok(str) -> str + match capitalized->Str.from_utf8() { + Err(_) => { + crash "There was an error converting the bytes to a Str! This should never happen." + } + Ok(str) => str + } + } +} is_letter : U8 -> Bool -is_letter = |byte| - ('a' <= byte and byte <= 'z') - or - ('A' <= byte and byte <= 'Z') +is_letter = |byte| { + ('a' <= byte and byte <= 'z') + or + ('A' <= byte and byte <= 'Z') +} capitalize : U8 -> U8 -capitalize = |byte| - if 'a' <= byte and byte <= 'z' then - byte - 'a' + 'A' - else - byte +capitalize = |byte| { + if 'a' <= byte and byte <= 'z' { + byte - 'a' + 'A' + } else { + byte + } +} diff --git a/exercises/practice/acronym/.meta/template.j2 b/exercises/practice/acronym/.meta/template.j2 index c49cadb6..3e36675e 100644 --- a/exercises/practice/acronym/.meta/template.j2 +++ b/exercises/practice/acronym/.meta/template.j2 @@ -2,12 +2,15 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [abbreviate] +import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake }}] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["phrase"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/acronym/Acronym.roc b/exercises/practice/acronym/Acronym.roc index 61f027e6..97fb4167 100644 --- a/exercises/practice/acronym/Acronym.roc +++ b/exercises/practice/acronym/Acronym.roc @@ -1,5 +1,6 @@ -module [abbreviate] - -abbreviate : Str -> Str -abbreviate = |text| - crash("Please implement the 'abbreviate' function") +Acronym :: {}.{ + abbreviate : Str -> Str + abbreviate = |text| { + crash "Please implement the 'abbreviate' function" + } +} diff --git a/exercises/practice/acronym/acronym-test.roc b/exercises/practice/acronym/acronym-test.roc index bd677916..66b60b56 100644 --- a/exercises/practice/acronym/acronym-test.roc +++ b/exercises/practice/acronym/acronym-test.roc @@ -1,59 +1,64 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/acronym/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Acronym exposing [abbreviate] # basic -expect - result = abbreviate("Portable Network Graphics") - result == "PNG" +expect { + result = abbreviate("Portable Network Graphics") + result == "PNG" +} # lowercase words -expect - result = abbreviate("Ruby on Rails") - result == "ROR" +expect { + result = abbreviate("Ruby on Rails") + result == "ROR" +} # punctuation -expect - result = abbreviate("First In, First Out") - result == "FIFO" +expect { + result = abbreviate("First In, First Out") + result == "FIFO" +} # all caps word -expect - result = abbreviate("GNU Image Manipulation Program") - result == "GIMP" +expect { + result = abbreviate("GNU Image Manipulation Program") + result == "GIMP" +} # punctuation without whitespace -expect - result = abbreviate("Complementary metal-oxide semiconductor") - result == "CMOS" +expect { + result = abbreviate("Complementary metal-oxide semiconductor") + result == "CMOS" +} # very long abbreviation -expect - result = abbreviate("Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me") - result == "ROTFLSHTMDCOALM" +expect { + result = abbreviate("Rolling On The Floor Laughing So Hard That My Dogs Came Over And Licked Me") + result == "ROTFLSHTMDCOALM" +} # consecutive delimiters -expect - result = abbreviate("Something - I made up from thin air") - result == "SIMUFTA" +expect { + result = abbreviate("Something - I made up from thin air") + result == "SIMUFTA" +} # apostrophes -expect - result = abbreviate("Halley's Comet") - result == "HC" +expect { + result = abbreviate("Halley's Comet") + result == "HC" +} # underscore emphasis -expect - result = abbreviate("The Road _Not_ Taken") - result == "TRNT" +expect { + result = abbreviate("The Road _Not_ Taken") + result == "TRNT" +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/affine-cipher/.meta/Example.roc b/exercises/practice/affine-cipher/.meta/Example.roc index 55b4efa4..3000f7c9 100644 --- a/exercises/practice/affine-cipher/.meta/Example.roc +++ b/exercises/practice/affine-cipher/.meta/Example.roc @@ -1,79 +1,185 @@ -module [encode, decode] +AffineCipher :: { a : U64, b : U64, encode_map : List(U8), decode_map : List(U8) }.{ + alphabet_size : U64 + alphabet_size = 26 -alphabet_size : U64 -alphabet_size = 26 + group_length : U64 + group_length = 5 -group_length : U64 -group_length = 5 + new : { a : U64, b : U64 } -> Try(AffineCipher, [InvalidKey]) + new = |{ a, b }| { + encode_map : List(U8) + encode_map = + (0..collect() -encode : Str, { a : U64, b : U64 } -> Result Str [InvalidKey, BadUtf8 _ ] -encode = |phrase, key| - alphabet = encoded_alphabet(key)? - phrase - |> Str.to_utf8 - |> List.join_map( - |char| - if char >= '0' and char <= '9' then - [char] - else - char_lower = if char >= 'A' and char <= 'Z' then char - 'A' + 'a' else char - if char_lower >= 'a' and char_lower <= 'z' then - index = char_lower - 'a' |> Num.to_u64 - when alphabet |> List.get(index) is - Ok(encoded_char) -> [encoded_char] - Err(OutOfBounds) -> crash("Unreachable: index cannot be out of bounds here") - else - [], - ) - |> List.chunks_of(group_length) - |> List.intersperse([' ']) - |> List.join - |> Str.from_utf8 + if Set.from_list(encode_map).len() < encode_map.len() { + Err(InvalidKey) + } else { + decode_map : List(U8) + decode_map = + encode_map + .map_with_index( + |encoded, decoded_index| { encoded, decoded_index }, + ) + .sort_with( + |{ encoded: encoded1, decoded_index: _ }, { encoded: encoded2, decoded_index: _ }| { + if encoded1 < encoded2 { + LT + } else if encoded1 > encoded2 { + GT + } else { + EQ + } + }, + ) + .map( + |pair| { + ( + pair.decoded_index.to_u8_try() ?? { + crash "Unreachable" + }, + ) + 'a' + }, + ) -encoded_alphabet : { a : U64, b : U64 } -> Result (List U8) [InvalidKey] -encoded_alphabet = |{ a, b }| - encoded = - List.range({ start: At('a'), end: At('z') }) - |> List.map( - |char| - num = (char - 'a') |> Num.to_u64 - index = (a * num + b) % alphabet_size - 'a' + Num.to_u8(index), - ) - if (encoded |> Set.from_list |> Set.len) < alphabet_size then - Err(InvalidKey) - else - Ok(encoded) + Ok({ a, b, encode_map, decode_map }) + } + } -decoded_alphabet : { a : U64, b : U64 } -> Result (List U8) [InvalidKey] -decoded_alphabet = |key| - encoded_alphabet(key)? - |> List.map_with_index(|encoded, decoded_index| { encoded, decoded_index }) - |> List.sort_with( - |{ encoded: encoded1 }, { encoded: encoded2 }| - Num.compare(encoded1, encoded2), - ) - |> List.map(|pair| Num.to_u8(pair.decoded_index) + 'a') - |> Ok + encode : AffineCipher, Str -> Str + encode = |affine_cipher, phrase| { + maybe_result = phrase + .to_utf8() + ->join_map( + |char| { + if char >= '0' and char <= '9' { + [char] + } else { + char_lower = if char >= 'A' and char <= 'Z' { + char - 'A' + 'a' + } else { + char + } + if char_lower >= 'a' and char_lower <= 'z' { + index = U8.to_u64(char_lower) - 'a' + match affine_cipher.encode_map.get(index) { + Ok(encoded_char) => [encoded_char] + Err(OutOfBounds) => { + crash "Unreachable: index cannot be out of bounds here" + } + } + } else { + [] + } + } + }, + ) + ->chunks_of(group_length) + ->intersperse([' ']) + ->join() + ->Str.from_utf8() + match maybe_result { + Ok(result) => result + Err(_) => { + crash "Unreachable: ASCII characters are always valid UTF-8" + } + } + } -decode : Str, { a : U64, b : U64 } -> Result Str [InvalidKey, BadUtf8 _, InvalidCharacter] -decode = |phrase, key| - alphabet = decoded_alphabet(key)? - phrase - |> Str.to_utf8 - |> List.map_try( - |char| - if char == ' ' then - Ok([]) - else if char >= '0' and char <= '9' then - Ok([char]) - else if char >= 'a' and char <= 'z' then - index = char - 'a' |> Num.to_u64 - when alphabet |> List.get(index) is - Ok(decoded_char) -> Ok([decoded_char]) - Err(OutOfBounds) -> crash("Unreachable: index cannot be out of bounds here") - else - Err(InvalidCharacter), - )? - |> List.join - |> Str.from_utf8 + decode : AffineCipher, Str -> Try(Str, [BadUtf8(_), InvalidCharacter]) + decode = |affine_cipher, phrase| { + phrase + .to_utf8() + ->map_try( + |char| { + if char == ' ' { + Ok([]) + } else if char >= '0' and char <= '9' { + Ok([char]) + } else if char >= 'a' and char <= 'z' { + index = U8.to_u64(char) - 'a' + match affine_cipher.decode_map.get(index) { + Ok(decoded_char) => Ok([decoded_char]) + Err(OutOfBounds) => { + crash "Unreachable: index cannot be out of bounds here" + } + } + } else { + Err(InvalidCharacter) + } + }, + )? + ->join() + ->Str.from_utf8() + } +} + +# The following functions should soon be available in Roc's builtins +chunks_of = |iter, size| { + var $state = [] + var $chunk = [] + for item in iter { + $chunk = $chunk.append(item) + if $chunk.len() == size { + $state = $state.append($chunk) + $chunk = [] + } + } + if $chunk.len() > 0 { + $state = $state.append($chunk) + } + $state +} + +collect = |iter| { + var $state = [] + for item in iter { + $state = $state.append(item) + } + $state +} + +intersperse = |list, sep| { + match list { + [] | [_] => list + [first, .. as rest] => [first, sep].concat(intersperse(rest, sep)) + } +} + +join = |iter| { + var $state = [] + for sublist in iter { + for item in sublist { + $state = $state.append(item) + } + } + $state +} + +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} + +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/affine-cipher/.meta/template.j2 b/exercises/practice/affine-cipher/.meta/template.j2 index 764b52b8..c4985b1f 100644 --- a/exercises/practice/affine-cipher/.meta/template.j2 +++ b/exercises/practice/affine-cipher/.meta/template.j2 @@ -2,7 +2,7 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [encode, decode] +import {{ exercise | to_pascal }} {% for supercase in cases %} ## @@ -11,16 +11,25 @@ import {{ exercise | to_pascal }} exposing [encode, decode] {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect - phrase = {{ case["input"]["phrase"] | to_roc }} - key = {a: {{ case["input"]["key"]["a"] }}, b: {{ case["input"]["key"]["b"] }}} - result = {{ case["property"] | to_snake }}(phrase, key) +expect { {%- if case["expected"]["error"] %} - result |> Result.is_err + affine_cipher = AffineCipher.new({a: {{ case["input"]["key"]["a"] }}, b: {{ case["input"]["key"]["b"] }}}) + affine_cipher.is_err() + # AffineCipher could not be created, so cannot encode or decode {%- else %} + phrase = {{ case["input"]["phrase"] | to_roc }} + affine_cipher = AffineCipher.new({a: {{ case["input"]["key"]["a"] }}, b: {{ case["input"]["key"]["b"] }}})? + result = affine_cipher.{{ case["property"] | to_snake }}(phrase) + {%- if case["property"] == "encode" %} + expected = {{ case["expected"] | to_roc }} + {%- else -%} expected = Ok({{ case["expected"] | to_roc }}) + {%- endif -%} result == expected {%- endif %} +} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/affine-cipher/AffineCipher.roc b/exercises/practice/affine-cipher/AffineCipher.roc index 1d0dfd78..6c5c6aea 100644 --- a/exercises/practice/affine-cipher/AffineCipher.roc +++ b/exercises/practice/affine-cipher/AffineCipher.roc @@ -1,9 +1,22 @@ -module [encode, decode] +AffineCipher :: { a : U64, b : U64 }.{ + alphabet_size : U64 + alphabet_size = 26 -encode : Str, { a : U64, b : U64 } -> Result Str _ -encode = |phrase, key| - crash("Please implement the 'encode' function") + group_length : U64 + group_length = 5 -decode : Str, { a : U64, b : U64 } -> Result Str _ -decode = |phrase, key| - crash("Please implement the 'decode' function") + new : { a : U64, b : U64 } -> Try(AffineCipher, _) + new = |key| { + crash "Please implement the 'new' method" + } + + encode : Str, AffineCipher -> Try(Str, _) + encode = |affine_cipher, phrase| { + crash "Please implement the 'encode' method" + } + + decode : Str, AffineCipher -> Try(Str, _) + decode = |affine_cipher, phrase| { + crash "Please implement the 'decode' method" + } +} diff --git a/exercises/practice/affine-cipher/affine-cipher-test.roc b/exercises/practice/affine-cipher/affine-cipher-test.roc index de2cd894..0a6d57f3 100644 --- a/exercises/practice/affine-cipher/affine-cipher-test.roc +++ b/exercises/practice/affine-cipher/affine-cipher-test.roc @@ -1,148 +1,158 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/affine-cipher/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 -import AffineCipher exposing [encode, decode] +import AffineCipher ## ## encode ## # encode yes -expect - phrase = "yes" - key = { a: 5, b: 7 } - result = encode(phrase, key) - expected = Ok("xbt") - result == expected +expect { + phrase = "yes" + affine_cipher = AffineCipher.new({ a: 5, b: 7 })? + result = affine_cipher.encode(phrase) + expected = "xbt" + result == expected +} # encode no -expect - phrase = "no" - key = { a: 15, b: 18 } - result = encode(phrase, key) - expected = Ok("fu") - result == expected +expect { + phrase = "no" + affine_cipher = AffineCipher.new({ a: 15, b: 18 })? + result = affine_cipher.encode(phrase) + expected = "fu" + result == expected +} # encode OMG -expect - phrase = "OMG" - key = { a: 21, b: 3 } - result = encode(phrase, key) - expected = Ok("lvz") - result == expected +expect { + phrase = "OMG" + affine_cipher = AffineCipher.new({ a: 21, b: 3 })? + result = affine_cipher.encode(phrase) + expected = "lvz" + result == expected +} # encode O M G -expect - phrase = "O M G" - key = { a: 25, b: 47 } - result = encode(phrase, key) - expected = Ok("hjp") - result == expected +expect { + phrase = "O M G" + affine_cipher = AffineCipher.new({ a: 25, b: 47 })? + result = affine_cipher.encode(phrase) + expected = "hjp" + result == expected +} # encode mindblowingly -expect - phrase = "mindblowingly" - key = { a: 11, b: 15 } - result = encode(phrase, key) - expected = Ok("rzcwa gnxzc dgt") - result == expected +expect { + phrase = "mindblowingly" + affine_cipher = AffineCipher.new({ a: 11, b: 15 })? + result = affine_cipher.encode(phrase) + expected = "rzcwa gnxzc dgt" + result == expected +} # encode numbers -expect - phrase = "Testing,1 2 3, testing." - key = { a: 3, b: 4 } - result = encode(phrase, key) - expected = Ok("jqgjc rw123 jqgjc rw") - result == expected +expect { + phrase = "Testing,1 2 3, testing." + affine_cipher = AffineCipher.new({ a: 3, b: 4 })? + result = affine_cipher.encode(phrase) + expected = "jqgjc rw123 jqgjc rw" + result == expected +} # encode deep thought -expect - phrase = "Truth is fiction." - key = { a: 5, b: 17 } - result = encode(phrase, key) - expected = Ok("iynia fdqfb ifje") - result == expected +expect { + phrase = "Truth is fiction." + affine_cipher = AffineCipher.new({ a: 5, b: 17 })? + result = affine_cipher.encode(phrase) + expected = "iynia fdqfb ifje" + result == expected +} # encode all the letters -expect - phrase = "The quick brown fox jumps over the lazy dog." - key = { a: 17, b: 33 } - result = encode(phrase, key) - expected = Ok("swxtj npvyk lruol iejdc blaxk swxmh qzglf") - result == expected +expect { + phrase = "The quick brown fox jumps over the lazy dog." + affine_cipher = AffineCipher.new({ a: 17, b: 33 })? + result = affine_cipher.encode(phrase) + expected = "swxtj npvyk lruol iejdc blaxk swxmh qzglf" + result == expected +} # encode with a not coprime to m -expect - phrase = "This is a test." - key = { a: 6, b: 17 } - result = encode(phrase, key) - result |> Result.is_err +expect { + affine_cipher = AffineCipher.new({ a: 6, b: 17 }) + affine_cipher.is_err() + # AffineCipher could not be created, so cannot encode or decode +} ## ## decode ## # decode exercism -expect - phrase = "tytgn fjr" - key = { a: 3, b: 7 } - result = decode(phrase, key) - expected = Ok("exercism") - result == expected +expect { + phrase = "tytgn fjr" + affine_cipher = AffineCipher.new({ a: 3, b: 7 })? + result = affine_cipher.decode(phrase) + expected = Ok("exercism") + result == expected +} # decode a sentence -expect - phrase = "qdwju nqcro muwhn odqun oppmd aunwd o" - key = { a: 19, b: 16 } - result = decode(phrase, key) - expected = Ok("anobstacleisoftenasteppingstone") - result == expected +expect { + phrase = "qdwju nqcro muwhn odqun oppmd aunwd o" + affine_cipher = AffineCipher.new({ a: 19, b: 16 })? + result = affine_cipher.decode(phrase) + expected = Ok("anobstacleisoftenasteppingstone") + result == expected +} # decode numbers -expect - phrase = "odpoz ub123 odpoz ub" - key = { a: 25, b: 7 } - result = decode(phrase, key) - expected = Ok("testing123testing") - result == expected +expect { + phrase = "odpoz ub123 odpoz ub" + affine_cipher = AffineCipher.new({ a: 25, b: 7 })? + result = affine_cipher.decode(phrase) + expected = Ok("testing123testing") + result == expected +} # decode all the letters -expect - phrase = "swxtj npvyk lruol iejdc blaxk swxmh qzglf" - key = { a: 17, b: 33 } - result = decode(phrase, key) - expected = Ok("thequickbrownfoxjumpsoverthelazydog") - result == expected +expect { + phrase = "swxtj npvyk lruol iejdc blaxk swxmh qzglf" + affine_cipher = AffineCipher.new({ a: 17, b: 33 })? + result = affine_cipher.decode(phrase) + expected = Ok("thequickbrownfoxjumpsoverthelazydog") + result == expected +} # decode with no spaces in input -expect - phrase = "swxtjnpvyklruoliejdcblaxkswxmhqzglf" - key = { a: 17, b: 33 } - result = decode(phrase, key) - expected = Ok("thequickbrownfoxjumpsoverthelazydog") - result == expected +expect { + phrase = "swxtjnpvyklruoliejdcblaxkswxmhqzglf" + affine_cipher = AffineCipher.new({ a: 17, b: 33 })? + result = affine_cipher.decode(phrase) + expected = Ok("thequickbrownfoxjumpsoverthelazydog") + result == expected +} # decode with too many spaces -expect - phrase = "vszzm cly yd cg qdp" - key = { a: 15, b: 16 } - result = decode(phrase, key) - expected = Ok("jollygreengiant") - result == expected +expect { + phrase = "vszzm cly yd cg qdp" + affine_cipher = AffineCipher.new({ a: 15, b: 16 })? + result = affine_cipher.decode(phrase) + expected = Ok("jollygreengiant") + result == expected +} # decode with a not coprime to m -expect - phrase = "Test" - key = { a: 13, b: 5 } - result = decode(phrase, key) - result |> Result.is_err +expect { + affine_cipher = AffineCipher.new({ a: 13, b: 5 }) + affine_cipher.is_err() + # AffineCipher could not be created, so cannot encode or decode +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/all-your-base/.meta/Example.roc b/exercises/practice/all-your-base/.meta/Example.roc index 75bdf080..42265136 100644 --- a/exercises/practice/all-your-base/.meta/Example.roc +++ b/exercises/practice/all-your-base/.meta/Example.roc @@ -1,44 +1,33 @@ -module [rebase] +AllYourBase :: {}.{ + rebase : { input_base : U64, output_base : U64, digits : List(U64) } -> Try(List(U64), _) + rebase = |{ input_base, output_base, digits }| { + if input_base <= 1 { + Err(InputBaseMustBeGreaterThanOne) + } else if output_base <= 1 { + Err(OutputBaseMustBeGreaterThanOne) + } else if digits.any( + |digit| { + digit >= input_base + }, + ) { + Err(DigitsMustBeLessThanInputBase) + } else { + number = digits.fold( + 0, + |acc, digit| { + acc * input_base + digit + }, + ) + to_digits(number, output_base)->Ok + } + } +} -rebase : { input_base : U64, output_base : U64, digits : List U64 } -> Result (List U64) _ -rebase = |{ input_base, output_base, digits }| - if input_base <= 1 then - Err(InputBaseMustBeGreaterThanOne) - else if output_base <= 1 then - Err(OutputBaseMustBeGreaterThanOne) - else if List.any(digits, |digit| digit >= input_base) then - Err(DigitsMustBeLessThanInputBase) - else - number_in_base_10 = - List.reverse(digits) - |> List.map_with_index( - |digit, index| - digit * (Num.pow_int(input_base, index)), - ) - |> List.sum - - to_digits(number_in_base_10, output_base) |> Ok - -to_digits : U64, U64 -> List U64 -to_digits = |number, base| - help = |digits, remaining, exponent| - if exponent == 0 then - digits |> List.append(remaining) - else - power_of_base = Num.pow_int(base, exponent) - digit = remaining // power_of_base - List.append(digits, digit) - |> help(Num.rem(remaining, power_of_base), (exponent - 1)) - - leading_exponent = int_log(number, base) - help([], number, leading_exponent) - -# Right now the builtins only have natural log so we have to implement this behaviour ourselves. https://github.com/roc-lang/roc/issues/5107 -int_log : U64, U64 -> U64 -int_log = |number, base| - help = |remaining, exponent| - if remaining < base then - exponent - else - help((remaining // base), (exponent + 1)) - help(number, 0) +to_digits : U64, U64 -> List(U64) +to_digits = |number, base| { + if number < base { + [number] + } else { + to_digits(number // base, base).append(number % base) + } +} diff --git a/exercises/practice/all-your-base/.meta/template.j2 b/exercises/practice/all-your-base/.meta/template.j2 index dbba8591..43c2f13b 100644 --- a/exercises/practice/all-your-base/.meta/template.j2 +++ b/exercises/practice/all-your-base/.meta/template.j2 @@ -6,12 +6,15 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({ input_base: {{ case["input"]["inputBase"] | to_roc }}, output_base: {{ case["input"]["outputBase"] | to_roc }}, digits: {{ case["input"]["digits"] | to_roc }} }) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} result == Ok({{ case["expected"] }}) {%- endif %} +} -{% endfor %} \ No newline at end of file +{% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/all-your-base/AllYourBase.roc b/exercises/practice/all-your-base/AllYourBase.roc index 8d70e5ee..2c61a6c7 100644 --- a/exercises/practice/all-your-base/AllYourBase.roc +++ b/exercises/practice/all-your-base/AllYourBase.roc @@ -1,5 +1,6 @@ -module [rebase] - -rebase : { input_base : U64, output_base : U64, digits : List U64 } -> Result (List U64) _ -rebase = |{ input_base, output_base, digits }| - crash("Please implement 'rebase'") +AllYourBase :: {}.{ + rebase : { input_base : U64, output_base : U64, digits : List(U64) } -> Try(List(U64), _) + rebase = |{ input_base, output_base, digits }| { + crash "Please implement the 'rebase' function" + } +} diff --git a/exercises/practice/all-your-base/all-your-base-test.roc b/exercises/practice/all-your-base/all-your-base-test.roc index 98d5ada4..622dff25 100644 --- a/exercises/practice/all-your-base/all-your-base-test.roc +++ b/exercises/practice/all-your-base/all-your-base-test.roc @@ -1,99 +1,112 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/all-your-base/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import AllYourBase exposing [rebase] # single bit one to decimal -expect - result = rebase({ input_base: 2, output_base: 10, digits: [1] }) - result == Ok([1]) +expect { + result = rebase({ input_base: 2, output_base: 10, digits: [1] }) + result == Ok([1]) +} # binary to single decimal -expect - result = rebase({ input_base: 2, output_base: 10, digits: [1, 0, 1] }) - result == Ok([5]) +expect { + result = rebase({ input_base: 2, output_base: 10, digits: [1, 0, 1] }) + result == Ok([5]) +} # single decimal to binary -expect - result = rebase({ input_base: 10, output_base: 2, digits: [5] }) - result == Ok([1, 0, 1]) +expect { + result = rebase({ input_base: 10, output_base: 2, digits: [5] }) + result == Ok([1, 0, 1]) +} # binary to multiple decimal -expect - result = rebase({ input_base: 2, output_base: 10, digits: [1, 0, 1, 0, 1, 0] }) - result == Ok([4, 2]) +expect { + result = rebase({ input_base: 2, output_base: 10, digits: [1, 0, 1, 0, 1, 0] }) + result == Ok([4, 2]) +} # decimal to binary -expect - result = rebase({ input_base: 10, output_base: 2, digits: [4, 2] }) - result == Ok([1, 0, 1, 0, 1, 0]) +expect { + result = rebase({ input_base: 10, output_base: 2, digits: [4, 2] }) + result == Ok([1, 0, 1, 0, 1, 0]) +} # trinary to hexadecimal -expect - result = rebase({ input_base: 3, output_base: 16, digits: [1, 1, 2, 0] }) - result == Ok([2, 10]) +expect { + result = rebase({ input_base: 3, output_base: 16, digits: [1, 1, 2, 0] }) + result == Ok([2, 10]) +} # hexadecimal to trinary -expect - result = rebase({ input_base: 16, output_base: 3, digits: [2, 10] }) - result == Ok([1, 1, 2, 0]) +expect { + result = rebase({ input_base: 16, output_base: 3, digits: [2, 10] }) + result == Ok([1, 1, 2, 0]) +} # 15-bit integer -expect - result = rebase({ input_base: 97, output_base: 73, digits: [3, 46, 60] }) - result == Ok([6, 10, 45]) +expect { + result = rebase({ input_base: 97, output_base: 73, digits: [3, 46, 60] }) + result == Ok([6, 10, 45]) +} # empty list -expect - result = rebase({ input_base: 2, output_base: 10, digits: [] }) - result == Ok([0]) +expect { + result = rebase({ input_base: 2, output_base: 10, digits: [] }) + result == Ok([0]) +} # single zero -expect - result = rebase({ input_base: 10, output_base: 2, digits: [0] }) - result == Ok([0]) +expect { + result = rebase({ input_base: 10, output_base: 2, digits: [0] }) + result == Ok([0]) +} # multiple zeros -expect - result = rebase({ input_base: 10, output_base: 2, digits: [0, 0, 0] }) - result == Ok([0]) +expect { + result = rebase({ input_base: 10, output_base: 2, digits: [0, 0, 0] }) + result == Ok([0]) +} # leading zeros -expect - result = rebase({ input_base: 7, output_base: 10, digits: [0, 6, 0] }) - result == Ok([4, 2]) +expect { + result = rebase({ input_base: 7, output_base: 10, digits: [0, 6, 0] }) + result == Ok([4, 2]) +} # input base is one -expect - result = rebase({ input_base: 1, output_base: 10, digits: [0] }) - result |> Result.is_err +expect { + result = rebase({ input_base: 1, output_base: 10, digits: [0] }) + result.is_err() +} # input base is zero -expect - result = rebase({ input_base: 0, output_base: 10, digits: [] }) - result |> Result.is_err +expect { + result = rebase({ input_base: 0, output_base: 10, digits: [] }) + result.is_err() +} # invalid positive digit -expect - result = rebase({ input_base: 2, output_base: 10, digits: [1, 2, 1, 0, 1, 0] }) - result |> Result.is_err +expect { + result = rebase({ input_base: 2, output_base: 10, digits: [1, 2, 1, 0, 1, 0] }) + result.is_err() +} # output base is one -expect - result = rebase({ input_base: 2, output_base: 1, digits: [1, 0, 1, 0, 1, 0] }) - result |> Result.is_err +expect { + result = rebase({ input_base: 2, output_base: 1, digits: [1, 0, 1, 0, 1, 0] }) + result.is_err() +} # output base is zero -expect - result = rebase({ input_base: 10, output_base: 0, digits: [7] }) - result |> Result.is_err +expect { + result = rebase({ input_base: 10, output_base: 0, digits: [7] }) + 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/allergies/.meta/Example.roc b/exercises/practice/allergies/.meta/Example.roc index 342f0894..634116ca 100644 --- a/exercises/practice/allergies/.meta/Example.roc +++ b/exercises/practice/allergies/.meta/Example.roc @@ -1,19 +1,21 @@ -module [allergic_to, set] +Allergies :: {}.{ + Allergen : [Eggs, Peanuts, Shellfish, Strawberries, Tomatoes, Chocolate, Pollen, Cats] -Allergen : [Eggs, Peanuts, Shellfish, Strawberries, Tomatoes, Chocolate, Pollen, Cats] + allergic_to : Allergen, U64 -> Bool + allergic_to = |allergen, score| { + set(score).contains(allergen) + } -allergic_to : Allergen, U64 -> Bool -allergic_to = |allergen, score| - set(score) - |> Set.contains(allergen) - -set : U64 -> Set Allergen -set = |score| - [Eggs, Peanuts, Shellfish, Strawberries, Tomatoes, Chocolate, Pollen, Cats] - |> List.walk( - { bits: score, allergies: Set.empty({}) }, - |{ bits, allergies }, allergy| - updated_allergies = if bits % 2 == 0 then allergies else allergies |> Set.insert(allergy) - { bits: bits // 2, allergies: updated_allergies }, - ) - |> .allergies + set : U64 -> Set(Allergen) + set = |score| { + [Eggs, Peanuts, Shellfish, Strawberries, Tomatoes, Chocolate, Pollen, Cats] + .fold( + { bits: score, allergies: Set.empty() }, + |{ bits, allergies }, allergy| { + updated_allergies = if bits % 2 == 0 allergies else allergies.insert(allergy) + { bits: bits // 2, allergies: updated_allergies } + }, + ) + .allergies + } +} diff --git a/exercises/practice/allergies/.meta/template.j2 b/exercises/practice/allergies/.meta/template.j2 index 33f1aa05..d6fa4a2b 100644 --- a/exercises/practice/allergies/.meta/template.j2 +++ b/exercises/practice/allergies/.meta/template.j2 @@ -7,14 +7,18 @@ import {{ exercise | to_pascal }} exposing [allergic_to, set] {% for outerCase in cases -%} {% for case in outerCase["cases"] %} # {{ outerCase["description"] }} {{ case["description"] | default('') }} -expect +expect { {%- if case["property"] == "allergicTo" %} result = allergic_to({{ case["input"]["item"] | to_pascal }}, {{ case["input"]["score"] | to_roc }}) result == {{ case["expected"] | to_roc }} {%- else %} result = set({{ case["input"]["score"] }}) - result == Set.from_list([{{ case["expected"] | map('to_pascal') | join(", ") }}]) + result == [{{ case["expected"] | map('to_pascal') | join(", ") }}]->Set.from_list {%- endif %} +} {% endfor %} -{% endfor %} \ No newline at end of file +{% endfor %} + +{{ macros.footer() }} + diff --git a/exercises/practice/allergies/Allergies.roc b/exercises/practice/allergies/Allergies.roc index 3b437e64..67381c72 100644 --- a/exercises/practice/allergies/Allergies.roc +++ b/exercises/practice/allergies/Allergies.roc @@ -1,11 +1,13 @@ -module [allergic_to, set] +Allergies :: {}.{ + Allergen : [Eggs, Peanuts, Shellfish, Strawberries, Tomatoes, Chocolate, Pollen, Cats] -Allergen : [Eggs, Peanuts, Shellfish, Strawberries, Tomatoes, Chocolate, Pollen, Cats] + allergic_to : Allergen, U64 -> Bool + allergic_to = |allergen, score| { + crash "Please implement the 'allergic_to' function" + } -allergic_to : Allergen, U64 -> Bool -allergic_to = |allergen, score| - crash("Please implement 'allergic_to'") - -set : U64 -> Set Allergen -set = |score| - crash("Please implement 'set'") + set : U64 -> Set(Allergen) + set = |score| { + crash "Please implement the 'set' function" + } +} diff --git a/exercises/practice/allergies/allergies-test.roc b/exercises/practice/allergies/allergies-test.roc index c0d481c7..580241ec 100644 --- a/exercises/practice/allergies/allergies-test.roc +++ b/exercises/practice/allergies/allergies-test.roc @@ -1,264 +1,310 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/allergies/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Allergies exposing [allergic_to, set] # testing for eggs allergy not allergic to anything -expect - result = allergic_to(Eggs, 0) - result == Bool.false +expect { + result = allergic_to(Eggs, 0) + result == Bool.False +} # testing for eggs allergy allergic only to eggs -expect - result = allergic_to(Eggs, 1) - result == Bool.true +expect { + result = allergic_to(Eggs, 1) + result == Bool.True +} # testing for eggs allergy allergic to eggs and something else -expect - result = allergic_to(Eggs, 3) - result == Bool.true +expect { + result = allergic_to(Eggs, 3) + result == Bool.True +} # testing for eggs allergy allergic to something, but not eggs -expect - result = allergic_to(Eggs, 2) - result == Bool.false +expect { + result = allergic_to(Eggs, 2) + result == Bool.False +} # testing for eggs allergy allergic to everything -expect - result = allergic_to(Eggs, 255) - result == Bool.true +expect { + result = allergic_to(Eggs, 255) + result == Bool.True +} # testing for peanuts allergy not allergic to anything -expect - result = allergic_to(Peanuts, 0) - result == Bool.false +expect { + result = allergic_to(Peanuts, 0) + result == Bool.False +} # testing for peanuts allergy allergic only to peanuts -expect - result = allergic_to(Peanuts, 2) - result == Bool.true +expect { + result = allergic_to(Peanuts, 2) + result == Bool.True +} # testing for peanuts allergy allergic to peanuts and something else -expect - result = allergic_to(Peanuts, 7) - result == Bool.true +expect { + result = allergic_to(Peanuts, 7) + result == Bool.True +} # testing for peanuts allergy allergic to something, but not peanuts -expect - result = allergic_to(Peanuts, 5) - result == Bool.false +expect { + result = allergic_to(Peanuts, 5) + result == Bool.False +} # testing for peanuts allergy allergic to everything -expect - result = allergic_to(Peanuts, 255) - result == Bool.true +expect { + result = allergic_to(Peanuts, 255) + result == Bool.True +} # testing for shellfish allergy not allergic to anything -expect - result = allergic_to(Shellfish, 0) - result == Bool.false +expect { + result = allergic_to(Shellfish, 0) + result == Bool.False +} # testing for shellfish allergy allergic only to shellfish -expect - result = allergic_to(Shellfish, 4) - result == Bool.true +expect { + result = allergic_to(Shellfish, 4) + result == Bool.True +} # testing for shellfish allergy allergic to shellfish and something else -expect - result = allergic_to(Shellfish, 14) - result == Bool.true +expect { + result = allergic_to(Shellfish, 14) + result == Bool.True +} # testing for shellfish allergy allergic to something, but not shellfish -expect - result = allergic_to(Shellfish, 10) - result == Bool.false +expect { + result = allergic_to(Shellfish, 10) + result == Bool.False +} # testing for shellfish allergy allergic to everything -expect - result = allergic_to(Shellfish, 255) - result == Bool.true +expect { + result = allergic_to(Shellfish, 255) + result == Bool.True +} # testing for strawberries allergy not allergic to anything -expect - result = allergic_to(Strawberries, 0) - result == Bool.false +expect { + result = allergic_to(Strawberries, 0) + result == Bool.False +} # testing for strawberries allergy allergic only to strawberries -expect - result = allergic_to(Strawberries, 8) - result == Bool.true +expect { + result = allergic_to(Strawberries, 8) + result == Bool.True +} # testing for strawberries allergy allergic to strawberries and something else -expect - result = allergic_to(Strawberries, 28) - result == Bool.true +expect { + result = allergic_to(Strawberries, 28) + result == Bool.True +} # testing for strawberries allergy allergic to something, but not strawberries -expect - result = allergic_to(Strawberries, 20) - result == Bool.false +expect { + result = allergic_to(Strawberries, 20) + result == Bool.False +} # testing for strawberries allergy allergic to everything -expect - result = allergic_to(Strawberries, 255) - result == Bool.true +expect { + result = allergic_to(Strawberries, 255) + result == Bool.True +} # testing for tomatoes allergy not allergic to anything -expect - result = allergic_to(Tomatoes, 0) - result == Bool.false +expect { + result = allergic_to(Tomatoes, 0) + result == Bool.False +} # testing for tomatoes allergy allergic only to tomatoes -expect - result = allergic_to(Tomatoes, 16) - result == Bool.true +expect { + result = allergic_to(Tomatoes, 16) + result == Bool.True +} # testing for tomatoes allergy allergic to tomatoes and something else -expect - result = allergic_to(Tomatoes, 56) - result == Bool.true +expect { + result = allergic_to(Tomatoes, 56) + result == Bool.True +} # testing for tomatoes allergy allergic to something, but not tomatoes -expect - result = allergic_to(Tomatoes, 40) - result == Bool.false +expect { + result = allergic_to(Tomatoes, 40) + result == Bool.False +} # testing for tomatoes allergy allergic to everything -expect - result = allergic_to(Tomatoes, 255) - result == Bool.true +expect { + result = allergic_to(Tomatoes, 255) + result == Bool.True +} # testing for chocolate allergy not allergic to anything -expect - result = allergic_to(Chocolate, 0) - result == Bool.false +expect { + result = allergic_to(Chocolate, 0) + result == Bool.False +} # testing for chocolate allergy allergic only to chocolate -expect - result = allergic_to(Chocolate, 32) - result == Bool.true +expect { + result = allergic_to(Chocolate, 32) + result == Bool.True +} # testing for chocolate allergy allergic to chocolate and something else -expect - result = allergic_to(Chocolate, 112) - result == Bool.true +expect { + result = allergic_to(Chocolate, 112) + result == Bool.True +} # testing for chocolate allergy allergic to something, but not chocolate -expect - result = allergic_to(Chocolate, 80) - result == Bool.false +expect { + result = allergic_to(Chocolate, 80) + result == Bool.False +} # testing for chocolate allergy allergic to everything -expect - result = allergic_to(Chocolate, 255) - result == Bool.true +expect { + result = allergic_to(Chocolate, 255) + result == Bool.True +} # testing for pollen allergy not allergic to anything -expect - result = allergic_to(Pollen, 0) - result == Bool.false +expect { + result = allergic_to(Pollen, 0) + result == Bool.False +} # testing for pollen allergy allergic only to pollen -expect - result = allergic_to(Pollen, 64) - result == Bool.true +expect { + result = allergic_to(Pollen, 64) + result == Bool.True +} # testing for pollen allergy allergic to pollen and something else -expect - result = allergic_to(Pollen, 224) - result == Bool.true +expect { + result = allergic_to(Pollen, 224) + result == Bool.True +} # testing for pollen allergy allergic to something, but not pollen -expect - result = allergic_to(Pollen, 160) - result == Bool.false +expect { + result = allergic_to(Pollen, 160) + result == Bool.False +} # testing for pollen allergy allergic to everything -expect - result = allergic_to(Pollen, 255) - result == Bool.true +expect { + result = allergic_to(Pollen, 255) + result == Bool.True +} # testing for cats allergy not allergic to anything -expect - result = allergic_to(Cats, 0) - result == Bool.false +expect { + result = allergic_to(Cats, 0) + result == Bool.False +} # testing for cats allergy allergic only to cats -expect - result = allergic_to(Cats, 128) - result == Bool.true +expect { + result = allergic_to(Cats, 128) + result == Bool.True +} # testing for cats allergy allergic to cats and something else -expect - result = allergic_to(Cats, 192) - result == Bool.true +expect { + result = allergic_to(Cats, 192) + result == Bool.True +} # testing for cats allergy allergic to something, but not cats -expect - result = allergic_to(Cats, 64) - result == Bool.false +expect { + result = allergic_to(Cats, 64) + result == Bool.False +} # testing for cats allergy allergic to everything -expect - result = allergic_to(Cats, 255) - result == Bool.true +expect { + result = allergic_to(Cats, 255) + result == Bool.True +} # list when: no allergies -expect - result = set(0) - result == Set.from_list([]) +expect { + result = set(0) + result == []->Set.from_list() +} # list when: just eggs -expect - result = set(1) - result == Set.from_list([Eggs]) +expect { + result = set(1) + result == [Eggs]->Set.from_list() +} # list when: just peanuts -expect - result = set(2) - result == Set.from_list([Peanuts]) +expect { + result = set(2) + result == [Peanuts]->Set.from_list() +} # list when: just strawberries -expect - result = set(8) - result == Set.from_list([Strawberries]) +expect { + result = set(8) + result == [Strawberries]->Set.from_list() +} # list when: eggs and peanuts -expect - result = set(3) - result == Set.from_list([Eggs, Peanuts]) +expect { + result = set(3) + result == [Eggs, Peanuts]->Set.from_list() +} # list when: more than eggs but not peanuts -expect - result = set(5) - result == Set.from_list([Eggs, Shellfish]) +expect { + result = set(5) + result == [Eggs, Shellfish]->Set.from_list() +} # list when: lots of stuff -expect - result = set(248) - result == Set.from_list([Strawberries, Tomatoes, Chocolate, Pollen, Cats]) +expect { + result = set(248) + result == [Strawberries, Tomatoes, Chocolate, Pollen, Cats]->Set.from_list() +} # list when: everything -expect - result = set(255) - result == Set.from_list([Eggs, Peanuts, Shellfish, Strawberries, Tomatoes, Chocolate, Pollen, Cats]) +expect { + result = set(255) + result == [Eggs, Peanuts, Shellfish, Strawberries, Tomatoes, Chocolate, Pollen, Cats]->Set.from_list() +} # list when: no allergen score parts -expect - result = set(509) - result == Set.from_list([Eggs, Shellfish, Strawberries, Tomatoes, Chocolate, Pollen, Cats]) +expect { + result = set(509) + result == [Eggs, Shellfish, Strawberries, Tomatoes, Chocolate, Pollen, Cats]->Set.from_list() +} # list when: no allergen score parts without highest valid score -expect - result = set(257) - result == Set.from_list([Eggs]) +expect { + result = set(257) + result == [Eggs]->Set.from_list() +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/alphametics/.meta/Example.roc b/exercises/practice/alphametics/.meta/Example.roc index 6751d842..3bd97a3e 100644 --- a/exercises/practice/alphametics/.meta/Example.roc +++ b/exercises/practice/alphametics/.meta/Example.roc @@ -1,100 +1,151 @@ -module [solve] +Alphametics :: {}.{ + solve : Str -> Try(List((U8, U8)), [InvalidAssignment, ..]) + solve = |problem| { + { addends, sum } = parse(problem)? -solve : Str -> Result (List (U8, U8)) _ -solve = |problem| - { addends, sum } = parse(problem)? + # We can represent the equation as a dictionary of the letters mapped to their coefficients + # when we simplify the equation. For example, we can write AB + A + B == C as 11A + 2B + (-1)C == 0. + # That then becomes this dictionary: `Dict.from_list([('A', 11), ('B', 2), ('C', -1)]) + equation : Dict(U8, I64) + equation = { + addends.fold( + Dict.empty(), + |dict, term| { + dict->insert_term(term, 1) + }, + )->insert_term(sum, -1) + } - # We can represent the equation as a dictionary of the letters mapped to their coefficients - # when we simplify the equation. For example, we can write AB + A + B == C as 11A + 2B + (-1)C == 0. - # That then becomes this dictionary: `Dict.from_list [('A', 11), ('B', 2), ('C', -1)] - equation = - List.walk( - addends, - Dict.empty({}), - |dict, term| - insert_term(dict, term, 1), - ) - |> insert_term(sum, -1) + leading_digits : Set(U8) + leading_digits = { + addends.map( + |letters| { + letters.first() ?? 0 + }, + ) + ->Set.from_list() + .insert( + sum.first() ?? 0, + ) + } - leading_digits = - List.map( - addends, - |letters| - List.first(letters) |> Result.with_default(0), - ) - |> Set.from_list - |> Set.insert((List.first(sum) |> Result.with_default(0))) + find_match : List((U8, U8)), List(U8), Set(U8) -> Try(List((U8, U8)), [InvalidAssignment, ..]) + find_match = |assignments, remaining_vars, remaining_digits| { + match remaining_vars { + [] => { + total_val : I64 + total_val = + assignments.fold( + 0, + |total, (letter, value)| { + (equation.get(letter) ?? 0) * value.to_i64() + total + }, + ) - find_match = |assignments, remaining_vars, remaining_digits| - when remaining_vars is - [] -> - total_val = - List.walk( - assignments, - 0, - |total, (letter, value)| - Dict.get(equation, letter) - |> Result.with_default(0) - |> Num.mul(Num.to_i64(value)) - |> Num.add(total), - ) + if total_val != 0 { + Err(InvalidAssignment) + } else { + Ok(assignments) + } + } + [letter, .. as rest] => { + find_first_ok( + remaining_digits, + |digit| { + if digit == 0 and leading_digits.contains(letter) { + Err(InvalidAssignment) + } else { + # Each digit has to be unique, so once we use a digit we remove it from the pool + find_match(assignments.append((letter, digit)), rest, remaining_digits.remove(digit)) + } + }, + ) + } + } + } - if total_val != 0 then - Err(InvalidAssignment) - else - Ok(assignments) - - [letter, .. as rest] -> - find_first_ok( - remaining_digits, - |digit| - if digit == 0 and Set.contains(leading_digits, letter) then - Err(InvalidAssignment) - else - # Each digit has to be unique, so once we use a digit we remove it from the pool - find_match(List.append(assignments, (letter, digit)), rest, Set.remove(remaining_digits, digit)), - ) - - digits = List.range({ start: At(0), end: At(9) }) |> Set.from_list - find_match([], Dict.keys(equation), digits) + digits = Set.from_list([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + find_match([], equation.keys(), digits) + } +} # Apply a function to each element of a list until the function returns an Ok, then return that value -find_first_ok : Set a, (a -> Result b err) -> Result b [NotFound] -find_first_ok = |set, func| - Set.walk_until( - set, - Err(NotFound), - |state, elem| - when func(elem) is - Err(_) -> Continue(state) - Ok(val) -> Break(Ok(val)), - ) +find_first_ok : Set(a), (a -> Try(b, err)) -> Try(b, [InvalidAssignment, ..]) +find_first_ok = |set, func| { + set.to_list().fold_until( + Err(InvalidAssignment), + |state, elem| { + match func(elem) { + Err(_) => Continue(state) + Ok(val) => Break(Ok(val)) + } + }, + ) +} # Update the equation with the values of a term -insert_term : Dict U8 I64, List U8, I64 -> Dict U8 I64 -insert_term = |equation, letters, polarity| - List.reverse(letters) - |> List.walk_with_index( - equation, - |dict, letter, index| - coeff = - Num.pow_int(10, index) - |> Num.to_i64 - |> Num.mul(polarity) - Dict.update( - dict, - letter, - |val| - when val is - Err(Missing) -> Ok(coeff) - Ok(c) -> Ok((c + coeff)), - ), - ) +insert_term : Dict(U8, I64), List(U8), I64 -> Dict(U8, I64) +insert_term = |equation, letters, polarity| { + letters + .rev() + .fold_with_index( + equation, + |dict, letter, index| { + coeff = pow_int(10, index) * polarity + dict.update( + letter, + |val| { + match val { + Err(Missing) => Ok(coeff) + Ok(c) => Ok((c + coeff)) + } + }, + ) + }, + ) +} + +parse : Str -> Try({ addends : List(List(U8)), sum : List(U8) }, _) +parse = |problem| { + { before, after } = problem->split_first(" == ")? + addends = + before + .split_on( + " + ", + ) + .map( + |s| { + s.to_utf8() + }, + ) + Ok({ addends, sum: after.to_utf8() }) +} + +reverse : Str -> Str +reverse = |str| { + str + .to_utf8() + .rev() + ->Str.from_utf8() + ?? "" +} + +# The following function should soon be available in Roc's builtins +split_first : Str, Str -> Try({ before : Str, after : Str }, [InvalidAssignment, ..]) +split_first = |str, sep| { + match str.split_on(sep) { + [] => Err(InvalidAssignment) + [_] => Err(InvalidAssignment) + [before, .. as rest] => Ok({ before, after: rest->Str.join_with(sep) }) + } +} -parse : Str -> Result { addends : List (List U8), sum : List U8 } _ -parse = |problem| - { before, after } = Str.split_first(problem, " == ")? - addends = - Str.split_on(before, " + ") - |> List.map(Str.to_utf8) - Ok({ addends, sum: Str.to_utf8(after) }) +pow_int : I64, U64 -> I64 +pow_int = |number, pow| { + (1..=pow).fold( + 1, + |acc, _| { + acc * number + }, + ) +} diff --git a/exercises/practice/alphametics/.meta/template.j2 b/exercises/practice/alphametics/.meta/template.j2 index 543145f4..d35ad15b 100644 --- a/exercises/practice/alphametics/.meta/template.j2 +++ b/exercises/practice/alphametics/.meta/template.j2 @@ -6,16 +6,20 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect - result = {{ case["property"] | to_snake }}({{ case["input"]["puzzle"] | to_roc }}) +expect { {%- if case["expected"] %} - Result.with_default(result, []) |> Set.from_list == Set.from_list([ + result = {{ case["property"] | to_snake }}({{ case["input"]["puzzle"] | to_roc }})? + Set.from_list(result) == Set.from_list([ {%- for letter, value in case["expected"].items() %} ('{{ letter }}', {{ value }}), {%- endfor %} ]) {%- else %} - Result.is_err(result) + result = {{ case["property"] | to_snake }}({{ case["input"]["puzzle"] | to_roc }}) + result.is_err() {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/alphametics/Alphametics.roc b/exercises/practice/alphametics/Alphametics.roc index 4379b251..35fc4fba 100644 --- a/exercises/practice/alphametics/Alphametics.roc +++ b/exercises/practice/alphametics/Alphametics.roc @@ -1,5 +1,6 @@ -module [solve] - -solve : Str -> Result (List (U8, U8)) _ -solve = |problem| - crash("Please implement 'solve'") +Alphametics :: {}.{ + solve : Str -> Try(List((U8, U8)), _) + solve = |problem| { + crash "Please implement the 'solve' function" + } +} diff --git a/exercises/practice/alphametics/alphametics-test.roc b/exercises/practice/alphametics/alphametics-test.roc index 6fdbe00b..46f54a77 100644 --- a/exercises/practice/alphametics/alphametics-test.roc +++ b/exercises/practice/alphametics/alphametics-test.roc @@ -1,155 +1,145 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/alphametics/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Alphametics exposing [solve] # puzzle with three letters -expect - result = solve("I + BB == ILL") - Result.with_default(result, []) - |> Set.from_list - == Set.from_list( - [ - ('I', 1), - ('B', 9), - ('L', 0), - ], - ) +expect { + result = solve("I + BB == ILL")? + Set.from_list(result) == Set.from_list( + [ + ('I', 1), + ('B', 9), + ('L', 0), + ], + ) +} # solution must have unique value for each letter -expect - result = solve("A == B") - Result.is_err(result) +expect { + result = solve("A == B") + result.is_err() +} # leading zero solution is invalid -expect - result = solve("ACA + DD == BD") - Result.is_err(result) +expect { + result = solve("ACA + DD == BD") + result.is_err() +} # puzzle with two digits final carry -expect - result = solve("A + A + A + A + A + A + A + A + A + A + A + B == BCC") - Result.with_default(result, []) - |> Set.from_list - == Set.from_list( - [ - ('A', 9), - ('B', 1), - ('C', 0), - ], - ) +expect { + result = solve("A + A + A + A + A + A + A + A + A + A + A + B == BCC")? + Set.from_list(result) == Set.from_list( + [ + ('A', 9), + ('B', 1), + ('C', 0), + ], + ) +} # puzzle with four letters -expect - result = solve("AS + A == MOM") - Result.with_default(result, []) - |> Set.from_list - == Set.from_list( - [ - ('A', 9), - ('S', 2), - ('M', 1), - ('O', 0), - ], - ) +expect { + result = solve("AS + A == MOM")? + Set.from_list(result) == Set.from_list( + [ + ('A', 9), + ('S', 2), + ('M', 1), + ('O', 0), + ], + ) +} # puzzle with six letters -expect - result = solve("NO + NO + TOO == LATE") - Result.with_default(result, []) - |> Set.from_list - == Set.from_list( - [ - ('N', 7), - ('O', 4), - ('T', 9), - ('L', 1), - ('A', 0), - ('E', 2), - ], - ) +expect { + result = solve("NO + NO + TOO == LATE")? + Set.from_list(result) == Set.from_list( + [ + ('N', 7), + ('O', 4), + ('T', 9), + ('L', 1), + ('A', 0), + ('E', 2), + ], + ) +} # puzzle with seven letters -expect - result = solve("HE + SEES + THE == LIGHT") - Result.with_default(result, []) - |> Set.from_list - == Set.from_list( - [ - ('E', 4), - ('G', 2), - ('H', 5), - ('I', 0), - ('L', 1), - ('S', 9), - ('T', 7), - ], - ) +expect { + result = solve("HE + SEES + THE == LIGHT")? + Set.from_list(result) == Set.from_list( + [ + ('E', 4), + ('G', 2), + ('H', 5), + ('I', 0), + ('L', 1), + ('S', 9), + ('T', 7), + ], + ) +} # puzzle with eight letters -expect - result = solve("SEND + MORE == MONEY") - Result.with_default(result, []) - |> Set.from_list - == Set.from_list( - [ - ('S', 9), - ('E', 5), - ('N', 6), - ('D', 7), - ('M', 1), - ('O', 0), - ('R', 8), - ('Y', 2), - ], - ) +expect { + result = solve("SEND + MORE == MONEY")? + Set.from_list(result) == Set.from_list( + [ + ('S', 9), + ('E', 5), + ('N', 6), + ('D', 7), + ('M', 1), + ('O', 0), + ('R', 8), + ('Y', 2), + ], + ) +} # puzzle with ten letters -expect - result = solve("AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE") - Result.with_default(result, []) - |> Set.from_list - == Set.from_list( - [ - ('A', 5), - ('D', 3), - ('E', 4), - ('F', 7), - ('G', 8), - ('N', 0), - ('O', 2), - ('R', 1), - ('S', 6), - ('T', 9), - ], - ) +expect { + result = solve("AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE")? + Set.from_list(result) == Set.from_list( + [ + ('A', 5), + ('D', 3), + ('E', 4), + ('F', 7), + ('G', 8), + ('N', 0), + ('O', 2), + ('R', 1), + ('S', 6), + ('T', 9), + ], + ) +} # puzzle with ten letters and 199 addends -expect - result = solve("THIS + A + FIRE + THEREFORE + FOR + ALL + HISTORIES + I + TELL + A + TALE + THAT + FALSIFIES + ITS + TITLE + TIS + A + LIE + THE + TALE + OF + THE + LAST + FIRE + HORSES + LATE + AFTER + THE + FIRST + FATHERS + FORESEE + THE + HORRORS + THE + LAST + FREE + TROLL + TERRIFIES + THE + HORSES + OF + FIRE + THE + TROLL + RESTS + AT + THE + HOLE + OF + LOSSES + IT + IS + THERE + THAT + SHE + STORES + ROLES + OF + LEATHERS + AFTER + SHE + SATISFIES + HER + HATE + OFF + THOSE + FEARS + A + TASTE + RISES + AS + SHE + HEARS + THE + LEAST + FAR + HORSE + THOSE + FAST + HORSES + THAT + FIRST + HEAR + THE + TROLL + FLEE + OFF + TO + THE + FOREST + THE + HORSES + THAT + ALERTS + RAISE + THE + STARES + OF + THE + OTHERS + AS + THE + TROLL + ASSAILS + AT + THE + TOTAL + SHIFT + HER + TEETH + TEAR + HOOF + OFF + TORSO + AS + THE + LAST + HORSE + FORFEITS + ITS + LIFE + THE + FIRST + FATHERS + HEAR + OF + THE + HORRORS + THEIR + FEARS + THAT + THE + FIRES + FOR + THEIR + FEASTS + ARREST + AS + THE + FIRST + FATHERS + RESETTLE + THE + LAST + OF + THE + FIRE + HORSES + THE + LAST + TROLL + HARASSES + THE + FOREST + HEART + FREE + AT + LAST + OF + THE + LAST + TROLL + ALL + OFFER + THEIR + FIRE + HEAT + TO + THE + ASSISTERS + FAR + OFF + THE + TROLL + FASTS + ITS + LIFE + SHORTER + AS + STARS + RISE + THE + HORSES + REST + SAFE + AFTER + ALL + SHARE + HOT + FISH + AS + THEIR + AFFILIATES + TAILOR + A + ROOFS + FOR + THEIR + SAFE == FORTRESSES") - Result.with_default(result, []) - |> Set.from_list - == Set.from_list( - [ - ('A', 1), - ('E', 0), - ('F', 5), - ('H', 8), - ('I', 7), - ('L', 2), - ('O', 6), - ('R', 3), - ('S', 4), - ('T', 9), - ], - ) +expect { + result = solve("THIS + A + FIRE + THEREFORE + FOR + ALL + HISTORIES + I + TELL + A + TALE + THAT + FALSIFIES + ITS + TITLE + TIS + A + LIE + THE + TALE + OF + THE + LAST + FIRE + HORSES + LATE + AFTER + THE + FIRST + FATHERS + FORESEE + THE + HORRORS + THE + LAST + FREE + TROLL + TERRIFIES + THE + HORSES + OF + FIRE + THE + TROLL + RESTS + AT + THE + HOLE + OF + LOSSES + IT + IS + THERE + THAT + SHE + STORES + ROLES + OF + LEATHERS + AFTER + SHE + SATISFIES + HER + HATE + OFF + THOSE + FEARS + A + TASTE + RISES + AS + SHE + HEARS + THE + LEAST + FAR + HORSE + THOSE + FAST + HORSES + THAT + FIRST + HEAR + THE + TROLL + FLEE + OFF + TO + THE + FOREST + THE + HORSES + THAT + ALERTS + RAISE + THE + STARES + OF + THE + OTHERS + AS + THE + TROLL + ASSAILS + AT + THE + TOTAL + SHIFT + HER + TEETH + TEAR + HOOF + OFF + TORSO + AS + THE + LAST + HORSE + FORFEITS + ITS + LIFE + THE + FIRST + FATHERS + HEAR + OF + THE + HORRORS + THEIR + FEARS + THAT + THE + FIRES + FOR + THEIR + FEASTS + ARREST + AS + THE + FIRST + FATHERS + RESETTLE + THE + LAST + OF + THE + FIRE + HORSES + THE + LAST + TROLL + HARASSES + THE + FOREST + HEART + FREE + AT + LAST + OF + THE + LAST + TROLL + ALL + OFFER + THEIR + FIRE + HEAT + TO + THE + ASSISTERS + FAR + OFF + THE + TROLL + FASTS + ITS + LIFE + SHORTER + AS + STARS + RISE + THE + HORSES + REST + SAFE + AFTER + ALL + SHARE + HOT + FISH + AS + THEIR + AFFILIATES + TAILOR + A + ROOFS + FOR + THEIR + SAFE == FORTRESSES")? + Set.from_list(result) == Set.from_list( + [ + ('A', 1), + ('E', 0), + ('F', 5), + ('H', 8), + ('I', 7), + ('L', 2), + ('O', 6), + ('R', 3), + ('S', 4), + ('T', 9), + ], + ) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/anagram/.meta/template.j2 b/exercises/practice/anagram/.meta/template.j2 index 3af1c1ad..a19db5a1 100644 --- a/exercises/practice/anagram/.meta/template.j2 +++ b/exercises/practice/anagram/.meta/template.j2 @@ -6,8 +6,10 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["subject"] | to_roc }}, {{ case["input"]["candidates"] | to_roc }}) result == {{ case["expected"] | to_roc }} - +} {% endfor %} + +{{ macros.footer() }} \ No newline at end of file diff --git a/exercises/practice/anagram/anagram-test.roc b/exercises/practice/anagram/anagram-test.roc index ba392366..9d9638f1 100644 --- a/exercises/practice/anagram/anagram-test.roc +++ b/exercises/practice/anagram/anagram-test.roc @@ -1,100 +1,102 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/anagram/canonical-data.json -# File last updated on 2025-09-15 +# File last updated on 2026-06-21 app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", - unicode: "https://github.com/roc-lang/unicode/releases/download/0.3.0/9KKFsA4CdOz0JIOL7iBSI_2jGIXQ6TsFBXgd086idpY.tar.br", + pf: platform "https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/download/0.9/8GdFEvQYS3TeAZxKvTzCLVdQiomweGtXcdZkXNDEeABq.tar.zst", + unicode: "https://github.com/roc-lang/unicode/...", # TODO: update when a zig-compatible release is available } import pf.Stdout -main! = |_args| - Stdout.line!("") - import Anagram exposing [find_anagrams] # no matches -expect - result = find_anagrams("diaper", ["hello", "world", "zombies", "pants"]) - result == [] - +expect { + result = find_anagrams("diaper", ["hello", "world", "zombies", "pants"]) + result == [] +} # detects two anagrams -expect - result = find_anagrams("solemn", ["lemons", "cherry", "melons"]) - result == ["lemons", "melons"] - +expect { + result = find_anagrams("solemn", ["lemons", "cherry", "melons"]) + result == ["lemons", "melons"] +} # does not detect anagram subsets -expect - result = find_anagrams("good", ["dog", "goody"]) - result == [] - +expect { + result = find_anagrams("good", ["dog", "goody"]) + result == [] +} # detects anagram -expect - result = find_anagrams("listen", ["enlists", "google", "inlets", "banana"]) - result == ["inlets"] - +expect { + result = find_anagrams("listen", ["enlists", "google", "inlets", "banana"]) + result == ["inlets"] +} # detects three anagrams -expect - result = find_anagrams("allergy", ["gallery", "ballerina", "regally", "clergy", "largely", "leading"]) - result == ["gallery", "regally", "largely"] - +expect { + result = find_anagrams("allergy", ["gallery", "ballerina", "regally", "clergy", "largely", "leading"]) + result == ["gallery", "regally", "largely"] +} # detects multiple anagrams with different case -expect - result = find_anagrams("nose", ["Eons", "ONES"]) - result == ["Eons", "ONES"] - +expect { + result = find_anagrams("nose", ["Eons", "ONES"]) + result == ["Eons", "ONES"] +} # does not detect non-anagrams with identical checksum -expect - result = find_anagrams("mass", ["last"]) - result == [] - +expect { + result = find_anagrams("mass", ["last"]) + result == [] +} # detects anagrams case-insensitively -expect - result = find_anagrams("Orchestra", ["cashregister", "Carthorse", "radishes"]) - result == ["Carthorse"] - +expect { + result = find_anagrams("Orchestra", ["cashregister", "Carthorse", "radishes"]) + result == ["Carthorse"] +} # detects anagrams using case-insensitive subject -expect - result = find_anagrams("Orchestra", ["cashregister", "carthorse", "radishes"]) - result == ["carthorse"] - +expect { + result = find_anagrams("Orchestra", ["cashregister", "carthorse", "radishes"]) + result == ["carthorse"] +} # detects anagrams using case-insensitive possible matches -expect - result = find_anagrams("orchestra", ["cashregister", "Carthorse", "radishes"]) - result == ["Carthorse"] - +expect { + result = find_anagrams("orchestra", ["cashregister", "Carthorse", "radishes"]) + result == ["Carthorse"] +} # does not detect an anagram if the original word is repeated -expect - result = find_anagrams("go", ["goGoGO"]) - result == [] - +expect { + result = find_anagrams("go", ["goGoGO"]) + result == [] +} # anagrams must use all letters exactly once -expect - result = find_anagrams("tapper", ["patter"]) - result == [] - +expect { + result = find_anagrams("tapper", ["patter"]) + result == [] +} # words are not anagrams of themselves -expect - result = find_anagrams("BANANA", ["BANANA"]) - result == [] - +expect { + result = find_anagrams("BANANA", ["BANANA"]) + result == [] +} # words are not anagrams of themselves even if letter case is partially different -expect - result = find_anagrams("BANANA", ["Banana"]) - result == [] - +expect { + result = find_anagrams("BANANA", ["Banana"]) + result == [] +} # words are not anagrams of themselves even if letter case is completely different -expect - result = find_anagrams("BANANA", ["banana"]) - result == [] - +expect { + result = find_anagrams("BANANA", ["banana"]) + result == [] +} # words other than themselves can be anagrams -expect - result = find_anagrams("LISTEN", ["LISTEN", "Silent"]) - result == ["Silent"] - +expect { + result = find_anagrams("LISTEN", ["LISTEN", "Silent"]) + result == ["Silent"] +} # different characters may have the same bytes -expect - result = find_anagrams("a⬂", ["€a"]) - result == [] +expect { + result = find_anagrams("a⬂", ["€a"]) + result == [] +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/armstrong-numbers/.meta/Example.roc b/exercises/practice/armstrong-numbers/.meta/Example.roc index 87894170..c579579c 100644 --- a/exercises/practice/armstrong-numbers/.meta/Example.roc +++ b/exercises/practice/armstrong-numbers/.meta/Example.roc @@ -1,17 +1,36 @@ -module [is_armstrong_number] +ArmstrongNumbers :: {}.{ + is_armstrong_number : U64 -> Bool + is_armstrong_number = |number| { + digits = list_digits(number) + len = digits.len() + candidate = + digits + .map( + |digit| { + digit->pow_int(len) + }, + ) + .sum() + candidate == number + } +} -list_digits = |number| - if number < 10 then - [number] - else - (list_digits((number // 10))) |> List.append((number % 10)) +list_digits : U64 -> List(U64) +list_digits = |number| { + if number < 10 { + [number] + } else { + list_digits((number // 10)).append((number % 10)) + } +} -is_armstrong_number : U64 -> Bool -is_armstrong_number = |number| - digits = list_digits(number) - len = List.len(digits) - candidate = - digits - |> List.map(|digit| digit |> Num.pow_int(len)) - |> List.sum - candidate == number +# This function should soon be available in Roc's builtins +pow_int : U64, U64 -> U64 +pow_int = |number, pow| { + (1..=pow).fold( + 1, + |acc, _| { + acc * number + }, + ) +} diff --git a/exercises/practice/armstrong-numbers/.meta/template.j2 b/exercises/practice/armstrong-numbers/.meta/template.j2 index 3bb36c52..dbae90c8 100644 --- a/exercises/practice/armstrong-numbers/.meta/template.j2 +++ b/exercises/practice/armstrong-numbers/.meta/template.j2 @@ -2,12 +2,15 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [is_armstrong_number] +import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake }}] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["number"] }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/armstrong-numbers/ArmstrongNumbers.roc b/exercises/practice/armstrong-numbers/ArmstrongNumbers.roc index 7f3708af..e59f3046 100644 --- a/exercises/practice/armstrong-numbers/ArmstrongNumbers.roc +++ b/exercises/practice/armstrong-numbers/ArmstrongNumbers.roc @@ -1,5 +1,6 @@ -module [is_armstrong_number] - -is_armstrong_number : U64 -> Bool -is_armstrong_number = |number| - crash("Please implement the 'is_armstrong_number' function") +ArmstrongNumbers :: {}.{ + is_armstrong_number : U64 -> Bool + is_armstrong_number = |number| { + crash "Please implement the 'is_armstrong_number' function" + } +} diff --git a/exercises/practice/armstrong-numbers/armstrong-numbers-test.roc b/exercises/practice/armstrong-numbers/armstrong-numbers-test.roc index 694cb99c..ec2d62df 100644 --- a/exercises/practice/armstrong-numbers/armstrong-numbers-test.roc +++ b/exercises/practice/armstrong-numbers/armstrong-numbers-test.roc @@ -1,59 +1,64 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/armstrong-numbers/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import ArmstrongNumbers exposing [is_armstrong_number] # Zero is an Armstrong number -expect - result = is_armstrong_number(0) - result == Bool.true +expect { + result = is_armstrong_number(0) + result == Bool.True +} # Single-digit numbers are Armstrong numbers -expect - result = is_armstrong_number(5) - result == Bool.true +expect { + result = is_armstrong_number(5) + result == Bool.True +} # There are no two-digit Armstrong numbers -expect - result = is_armstrong_number(10) - result == Bool.false +expect { + result = is_armstrong_number(10) + result == Bool.False +} # Three-digit number that is an Armstrong number -expect - result = is_armstrong_number(153) - result == Bool.true +expect { + result = is_armstrong_number(153) + result == Bool.True +} # Three-digit number that is not an Armstrong number -expect - result = is_armstrong_number(100) - result == Bool.false +expect { + result = is_armstrong_number(100) + result == Bool.False +} # Four-digit number that is an Armstrong number -expect - result = is_armstrong_number(9474) - result == Bool.true +expect { + result = is_armstrong_number(9474) + result == Bool.True +} # Four-digit number that is not an Armstrong number -expect - result = is_armstrong_number(9475) - result == Bool.false +expect { + result = is_armstrong_number(9475) + result == Bool.False +} # Seven-digit number that is an Armstrong number -expect - result = is_armstrong_number(9926315) - result == Bool.true +expect { + result = is_armstrong_number(9926315) + result == Bool.True +} # Seven-digit number that is not an Armstrong number -expect - result = is_armstrong_number(9926314) - result == Bool.false +expect { + result = is_armstrong_number(9926314) + result == Bool.False +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/atbash-cipher/.meta/Example.roc b/exercises/practice/atbash-cipher/.meta/Example.roc index c28723a6..052dfeab 100644 --- a/exercises/practice/atbash-cipher/.meta/Example.roc +++ b/exercises/practice/atbash-cipher/.meta/Example.roc @@ -1,36 +1,101 @@ -module [encode, decode] +AtbashCipher :: {}.{ + encode : Str -> Try(Str, _) + encode = |phrase| { + phrase + .to_utf8() + ->join_map( + |char| { + if char >= 'A' and char <= 'Z' { + [invert((char - 'A' + 'a'))] + } else if char >= 'a' and char <= 'z' { + [invert(char)] + } else if char >= '0' and char <= '9' { + [char] + } else { + [] + } + }, + ) + ->chunks_of(5) + ->intersperse([' ']) + ->join() + ->Str.from_utf8() + } + + decode : Str -> Try(Str, _) + decode = |phrase| { + phrase + .to_utf8() + .drop_if( + |c| { + c == ' ' + }, + ) + .map( + invert, + ) + ->Str.from_utf8() + } +} invert : U8 -> U8 -invert = |char| - if char >= 'a' and char <= 'z' then - 'z' - char + 'a' - else - char +invert = |char| { + if char >= 'a' and char <= 'z' { + 'z' - char + 'a' + } else { + char + } +} + +# The following functions should soon be available in Roc's builtins +chunks_of = |iter, size| { + var $state = [] + var $chunk = [] + for item in iter { + $chunk = $chunk.append(item) + if $chunk.len() == size { + $state = $state.append($chunk) + $chunk = [] + } + } + if $chunk.len() > 0 { + $state = $state.append($chunk) + } + $state +} + +collect = |iter| { + var $state = [] + for item in iter { + $state = $state.append(item) + } + $state +} + +intersperse = |list, sep| { + match list { + [] => [] + [_] => list + [first, .. as rest] => [first, sep].concat(intersperse(rest, sep)) + } +} -encode : Str -> Result Str _ -encode = |phrase| - phrase - |> Str.to_utf8 - |> List.join_map( - |char| - if char >= 'A' and char <= 'Z' then - [invert((char - 'A' + 'a'))] - else if char >= 'a' and char <= 'z' then - [invert(char)] - else if char >= '0' and char <= '9' then - [char] - else - [], - ) - |> List.chunks_of(5) - |> List.intersperse([' ']) - |> List.join - |> Str.from_utf8 +join = |iter| { + var $state = [] + for sublist in iter { + for item in sublist { + $state = $state.append(item) + } + } + $state +} -decode : Str -> Result Str _ -decode = |phrase| - phrase - |> Str.to_utf8 - |> List.drop_if(|c| c == ' ') - |> List.map(invert) - |> Str.from_utf8 +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} diff --git a/exercises/practice/atbash-cipher/.meta/template.j2 b/exercises/practice/atbash-cipher/.meta/template.j2 index 5c7ae3ef..010309da 100644 --- a/exercises/practice/atbash-cipher/.meta/template.j2 +++ b/exercises/practice/atbash-cipher/.meta/template.j2 @@ -11,11 +11,14 @@ import {{ exercise | to_pascal }} exposing [encode, decode] {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect - phrase = {{ case["input"]["phrase"] | to_roc }} - result = phrase |> {{ case["property"] | to_snake }} - expected = {{ case["expected"] | to_roc }} +expect { + phrase = {{ case["input"]["phrase"] | to_roc }} + result = phrase->AtbashCipher.{{ case["property"] | to_snake }} + expected = {{ case["expected"] | to_roc }} result == Ok(expected) +} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/atbash-cipher/AtbashCipher.roc b/exercises/practice/atbash-cipher/AtbashCipher.roc index 789132c8..f854d09b 100644 --- a/exercises/practice/atbash-cipher/AtbashCipher.roc +++ b/exercises/practice/atbash-cipher/AtbashCipher.roc @@ -1,9 +1,11 @@ -module [encode, decode] +AtbashCipher :: {}.{ + encode : Str -> Try(Str, _) + encode = |phrase| { + crash "Please implement the 'encode' function" + } -encode : Str -> Result Str _ -encode = |phrase| - crash("Please implement the 'encode' function") - -decode : Str -> Result Str _ -decode = |phrase| - crash("Please implement the 'decode' function") + decode : Str -> Try(Str, _) + decode = |phrase| { + crash "Please implement the 'decode' function" + } +} diff --git a/exercises/practice/atbash-cipher/atbash-cipher-test.roc b/exercises/practice/atbash-cipher/atbash-cipher-test.roc index d5deff3d..03b819e8 100644 --- a/exercises/practice/atbash-cipher/atbash-cipher-test.roc +++ b/exercises/practice/atbash-cipher/atbash-cipher-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/atbash-cipher/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import AtbashCipher exposing [encode, decode] @@ -17,104 +9,122 @@ import AtbashCipher exposing [encode, decode] ## # encode yes -expect - phrase = "yes" - result = phrase |> encode - expected = "bvh" - result == Ok(expected) +expect { + phrase = "yes" + result = phrase->AtbashCipher.encode() + expected = "bvh" + result == Ok(expected) +} # encode no -expect - phrase = "no" - result = phrase |> encode - expected = "ml" - result == Ok(expected) +expect { + phrase = "no" + result = phrase->AtbashCipher.encode() + expected = "ml" + result == Ok(expected) +} # encode OMG -expect - phrase = "OMG" - result = phrase |> encode - expected = "lnt" - result == Ok(expected) +expect { + phrase = "OMG" + result = phrase->AtbashCipher.encode() + expected = "lnt" + result == Ok(expected) +} # encode spaces -expect - phrase = "O M G" - result = phrase |> encode - expected = "lnt" - result == Ok(expected) +expect { + phrase = "O M G" + result = phrase->AtbashCipher.encode() + expected = "lnt" + result == Ok(expected) +} # encode mindblowingly -expect - phrase = "mindblowingly" - result = phrase |> encode - expected = "nrmwy oldrm tob" - result == Ok(expected) +expect { + phrase = "mindblowingly" + result = phrase->AtbashCipher.encode() + expected = "nrmwy oldrm tob" + result == Ok(expected) +} # encode numbers -expect - phrase = "Testing,1 2 3, testing." - result = phrase |> encode - expected = "gvhgr mt123 gvhgr mt" - result == Ok(expected) +expect { + phrase = "Testing,1 2 3, testing." + result = phrase->AtbashCipher.encode() + expected = "gvhgr mt123 gvhgr mt" + result == Ok(expected) +} # encode deep thought -expect - phrase = "Truth is fiction." - result = phrase |> encode - expected = "gifgs rhurx grlm" - result == Ok(expected) +expect { + phrase = "Truth is fiction." + result = phrase->AtbashCipher.encode() + expected = "gifgs rhurx grlm" + result == Ok(expected) +} # encode all the letters -expect - phrase = "The quick brown fox jumps over the lazy dog." - result = phrase |> encode - expected = "gsvjf rxpyi ldmul cqfnk hlevi gsvoz abwlt" - result == Ok(expected) +expect { + phrase = "The quick brown fox jumps over the lazy dog." + result = phrase->AtbashCipher.encode() + expected = "gsvjf rxpyi ldmul cqfnk hlevi gsvoz abwlt" + result == Ok(expected) +} ## ## decode ## # decode exercism -expect - phrase = "vcvix rhn" - result = phrase |> decode - expected = "exercism" - result == Ok(expected) +expect { + phrase = "vcvix rhn" + result = phrase->AtbashCipher.decode() + expected = "exercism" + result == Ok(expected) +} # decode a sentence -expect - phrase = "zmlyh gzxov rhlug vmzhg vkkrm thglm v" - result = phrase |> decode - expected = "anobstacleisoftenasteppingstone" - result == Ok(expected) +expect { + phrase = "zmlyh gzxov rhlug vmzhg vkkrm thglm v" + result = phrase->AtbashCipher.decode() + expected = "anobstacleisoftenasteppingstone" + result == Ok(expected) +} # decode numbers -expect - phrase = "gvhgr mt123 gvhgr mt" - result = phrase |> decode - expected = "testing123testing" - result == Ok(expected) +expect { + phrase = "gvhgr mt123 gvhgr mt" + result = phrase->AtbashCipher.decode() + expected = "testing123testing" + result == Ok(expected) +} # decode all the letters -expect - phrase = "gsvjf rxpyi ldmul cqfnk hlevi gsvoz abwlt" - result = phrase |> decode - expected = "thequickbrownfoxjumpsoverthelazydog" - result == Ok(expected) +expect { + phrase = "gsvjf rxpyi ldmul cqfnk hlevi gsvoz abwlt" + result = phrase->AtbashCipher.decode() + expected = "thequickbrownfoxjumpsoverthelazydog" + result == Ok(expected) +} # decode with too many spaces -expect - phrase = "vc vix r hn" - result = phrase |> decode - expected = "exercism" - result == Ok(expected) +expect { + phrase = "vc vix r hn" + result = phrase->AtbashCipher.decode() + expected = "exercism" + result == Ok(expected) +} # decode with no spaces -expect - phrase = "zmlyhgzxovrhlugvmzhgvkkrmthglmv" - result = phrase |> decode - expected = "anobstacleisoftenasteppingstone" - result == Ok(expected) +expect { + phrase = "zmlyhgzxovrhlugvmzhgvkkrmthglmv" + result = phrase->AtbashCipher.decode() + expected = "anobstacleisoftenasteppingstone" + result == Ok(expected) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/binary-search-tree/.meta/Example.roc b/exercises/practice/binary-search-tree/.meta/Example.roc index 26a0cafb..61a8aa70 100644 --- a/exercises/practice/binary-search-tree/.meta/Example.roc +++ b/exercises/practice/binary-search-tree/.meta/Example.roc @@ -1,24 +1,30 @@ -module [from_list, to_list] +BinarySearchTree := [Nil, Node({ value : U64, left : BinarySearchTree, right : BinarySearchTree })].{ + from_list : List(U64) -> BinarySearchTree + from_list = |data| { + data.fold(Nil, insert) + } -BinaryTree : [Nil, Node { value : U64, left : BinaryTree, right : BinaryTree }] + to_list : BinarySearchTree -> List(U64) + to_list = |tree| { + match tree { + Nil => [] + Node(node) => { + to_list(node.left).append(node.value).concat(to_list(node.right)) + } + } + } -from_list : List U64 -> BinaryTree -from_list = |data| - data |> List.walk(Nil, insert) - -insert : BinaryTree, U64 -> BinaryTree -insert = |tree, value| - when tree is - Nil -> Node({ value, left: Nil, right: Nil }) - Node(node) -> - if value <= node.value then - Node({ node & left: (node.left |> insert(value)) }) - else - Node({ node & right: (node.right |> insert(value)) }) - -to_list : BinaryTree -> List U64 -to_list = |tree| - when tree is - Nil -> [] - Node(node) -> - node.left |> to_list |> List.append(node.value) |> List.concat((node.right |> to_list)) + insert : BinarySearchTree, U64 -> BinarySearchTree + insert = |tree, value| { + match tree { + Nil => Node({ value, left: Nil, right: Nil }) + Node(node) => { + if value <= node.value { + Node({ ..node, left: node.left.insert(value) }) + } else { + Node({ ..node, right: node.right.insert(value) }) + } + } + } + } +} diff --git a/exercises/practice/binary-search-tree/.meta/template.j2 b/exercises/practice/binary-search-tree/.meta/template.j2 index 1abf72ce..c24133f2 100644 --- a/exercises/practice/binary-search-tree/.meta/template.j2 +++ b/exercises/practice/binary-search-tree/.meta/template.j2 @@ -30,7 +30,7 @@ import {{ exercise | to_pascal }} exposing [from_list, to_list] {%- if subcase["description"] != supercase["description"] %} # {{ subcase["description"] }} {%- endif %} -expect +expect { data = {{ to_int_list(subcase["input"]["treeData"]) }} {%- if subcase["property"] == "data" %} result = from_list(data) @@ -42,9 +42,10 @@ expect expected = {{ to_int_list(subcase["expected"]) }} result == expected {%- endif %} +} {% endfor %} {% endfor %} - +{{ macros.footer() }} diff --git a/exercises/practice/binary-search-tree/BinarySearchTree.roc b/exercises/practice/binary-search-tree/BinarySearchTree.roc index 8d8ef7f5..325c7e48 100644 --- a/exercises/practice/binary-search-tree/BinarySearchTree.roc +++ b/exercises/practice/binary-search-tree/BinarySearchTree.roc @@ -1,11 +1,11 @@ -module [from_list, to_list] +BinarySearchTree := [Nil, Node({ value : U64, left : BinarySearchTree, right : BinarySearchTree })].{ + from_list : List(U64) -> BinarySearchTree + from_list = |data| { + crash "Please implement the 'from_list' function" + } -BinaryTree : [Nil, Node { value : U64, left : BinaryTree, right : BinaryTree }] - -from_list : List U64 -> BinaryTree -from_list = |data| - crash("Please implement the 'from_list' function") - -to_list : BinaryTree -> List U64 -to_list = |tree| - crash("Please implement the 'to_list' function") + to_list : BinarySearchTree -> List(U64) + to_list = |tree| { + crash "Please implement the 'to_list' function" + } +} diff --git a/exercises/practice/binary-search-tree/binary-search-tree-test.roc b/exercises/practice/binary-search-tree/binary-search-tree-test.roc index 08e1e714..bf144964 100644 --- a/exercises/practice/binary-search-tree/binary-search-tree-test.roc +++ b/exercises/practice/binary-search-tree/binary-search-tree-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/binary-search-tree/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import BinarySearchTree exposing [from_list, to_list] @@ -16,172 +8,241 @@ import BinarySearchTree exposing [from_list, to_list] ## data is retained ## -expect - data = [4] - result = from_list(data) - expected = Node( - { - value: 4, - left: Nil, - right: Nil, - }, - ) - result == expected +expect { + data = [ + 4, + ] + result = from_list(data) + expected = Node( + { + value: 4, + left: Nil, + right: Nil, + }, + ) + result == expected +} ## ## insert data at proper node ## # smaller number at left node -expect - data = [4, 2] - result = from_list(data) - expected = Node( - { - value: 4, - left: Node( - { - value: 2, - left: Nil, - right: Nil, - }, - ), - right: Nil, - }, - ) - result == expected +expect { + data = [ + 4, + 2, + ] + result = from_list(data) + expected = Node( + { + value: 4, + left: Node( + { + value: 2, + left: Nil, + right: Nil, + }, + ), + right: Nil, + }, + ) + result == expected +} # same number at left node -expect - data = [4, 4] - result = from_list(data) - expected = Node( - { - value: 4, - left: Node( - { - value: 4, - left: Nil, - right: Nil, - }, - ), - right: Nil, - }, - ) - result == expected +expect { + data = [ + 4, + 4, + ] + result = from_list(data) + expected = Node( + { + value: 4, + left: Node( + { + value: 4, + left: Nil, + right: Nil, + }, + ), + right: Nil, + }, + ) + result == expected +} # greater number at right node -expect - data = [4, 5] - result = from_list(data) - expected = Node( - { - value: 4, - left: Nil, - right: Node( - { - value: 5, - left: Nil, - right: Nil, - }, - ), - }, - ) - result == expected +expect { + data = [ + 4, + 5, + ] + result = from_list(data) + expected = Node( + { + value: 4, + left: Nil, + right: Node( + { + value: 5, + left: Nil, + right: Nil, + }, + ), + }, + ) + result == expected +} ## ## can create complex tree ## -expect - data = [4, 2, 6, 1, 3, 5, 7] - result = from_list(data) - expected = Node( - { - value: 4, - left: Node( - { - value: 2, - left: Node( - { - value: 1, - left: Nil, - right: Nil, - }, - ), - right: Node( - { - value: 3, - left: Nil, - right: Nil, - }, - ), - }, - ), - right: Node( - { - value: 6, - left: Node( - { - value: 5, - left: Nil, - right: Nil, - }, - ), - right: Node( - { - value: 7, - left: Nil, - right: Nil, - }, - ), - }, - ), - }, - ) - result == expected +expect { + data = [ + 4, + 2, + 6, + 1, + 3, + 5, + 7, + ] + result = from_list(data) + expected = Node( + { + value: 4, + left: Node( + { + value: 2, + left: Node( + { + value: 1, + left: Nil, + right: Nil, + }, + ), + right: Node( + { + value: 3, + left: Nil, + right: Nil, + }, + ), + }, + ), + right: Node( + { + value: 6, + left: Node( + { + value: 5, + left: Nil, + right: Nil, + }, + ), + right: Node( + { + value: 7, + left: Nil, + right: Nil, + }, + ), + }, + ), + }, + ) + result == expected +} ## ## can sort data ## # can sort single number -expect - data = [2] - tree = from_list(data) - result = to_list(tree) - expected = [2] - result == expected +expect { + data = [ + 2, + ] + tree = from_list(data) + result = to_list(tree) + expected = [ + 2, + ] + result == expected +} # can sort if second number is smaller than first -expect - data = [2, 1] - tree = from_list(data) - result = to_list(tree) - expected = [1, 2] - result == expected +expect { + data = [ + 2, + 1, + ] + tree = from_list(data) + result = to_list(tree) + expected = [ + 1, + 2, + ] + result == expected +} # can sort if second number is same as first -expect - data = [2, 2] - tree = from_list(data) - result = to_list(tree) - expected = [2, 2] - result == expected +expect { + data = [ + 2, + 2, + ] + tree = from_list(data) + result = to_list(tree) + expected = [ + 2, + 2, + ] + result == expected +} # can sort if second number is greater than first -expect - data = [2, 3] - tree = from_list(data) - result = to_list(tree) - expected = [2, 3] - result == expected +expect { + data = [ + 2, + 3, + ] + tree = from_list(data) + result = to_list(tree) + expected = [ + 2, + 3, + ] + result == expected +} # can sort complex tree -expect - data = [2, 1, 3, 6, 7, 5] - tree = from_list(data) - result = to_list(tree) - expected = [1, 2, 3, 5, 6, 7] - result == expected +expect { + data = [ + 2, + 1, + 3, + 6, + 7, + 5, + ] + tree = from_list(data) + result = to_list(tree) + expected = [ + 1, + 2, + 3, + 5, + 6, + 7, + ] + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/binary-search/.meta/Example.roc b/exercises/practice/binary-search/.meta/Example.roc index 12921133..ebf378da 100644 --- a/exercises/practice/binary-search/.meta/Example.roc +++ b/exercises/practice/binary-search/.meta/Example.roc @@ -1,18 +1,23 @@ -module [find] +BinarySearch :: {}.{ + find : List(U64), U64 -> Try(U64, [ValueWasNotFound(List(U64), U64)]) + find = |array, value| { + binary_search = |min_index, max_index| { + middle_index = (min_index + max_index) // 2 + middle_value = array.get(middle_index)? + if middle_value == value { + Ok(middle_index) + } else if min_index == max_index { + Err(ValueWasNotFound(array, value)) + } else if middle_value < value { + Ok(binary_search((middle_index + 1), max_index)?) + } else { + Ok(binary_search(min_index, (middle_index - 1))?) + } + } -find : List U64, U64 -> Result U64 [ValueWasNotFound (List U64) U64] -find = |array, value| - binary_search = |min_index, max_index| - middle_index = (min_index + max_index) // 2 - middle_value = List.get(array, middle_index)? - if middle_value == value then - Ok(middle_index) - else if min_index == max_index then - Err(ValueWasNotFound(array, value)) - else if middle_value < value then - Ok(binary_search((middle_index + 1), max_index)?) - else - Ok(binary_search(min_index, (middle_index - 1))?) - - binary_search(0, List.len(array)) - |> Result.on_err(|_| Err(ValueWasNotFound(array, value))) + binary_search(0, array.len()) + .map_err( + |_| ValueWasNotFound(array, value), + ) + } +} diff --git a/exercises/practice/binary-search/.meta/template.j2 b/exercises/practice/binary-search/.meta/template.j2 index c24afc3a..5ddc957c 100644 --- a/exercises/practice/binary-search/.meta/template.j2 +++ b/exercises/practice/binary-search/.meta/template.j2 @@ -6,12 +6,15 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect - result = {{ case["input"]["array"] | to_roc }} |> {{ case["property"] | to_snake }}({{ case["input"]["value"] }}) +expect { + result = {{ case["property"] | to_snake }}({{ case["input"]["array"] | to_roc }}, {{ case["input"]["value"] }}) {%- if case["expected"]["error"] %} - Result.is_err(result) + result.is_err() {%- else %} result == Ok({{ case["expected"] }}) {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/binary-search/BinarySearch.roc b/exercises/practice/binary-search/BinarySearch.roc index e090506e..a9ca6600 100644 --- a/exercises/practice/binary-search/BinarySearch.roc +++ b/exercises/practice/binary-search/BinarySearch.roc @@ -1,5 +1,6 @@ -module [find] - -find : List U64, U64 -> Result U64 _ -find = |array, value| - crash("Please implement the 'find' function") +BinarySearch :: {}.{ + find : List(U64), U64 -> Try(U64, _) + find = |array, value| { + crash "Please implement the 'find' function" + } +} diff --git a/exercises/practice/binary-search/binary-search-test.roc b/exercises/practice/binary-search/binary-search-test.roc index 9da25f04..412a2d34 100644 --- a/exercises/practice/binary-search/binary-search-test.roc +++ b/exercises/practice/binary-search/binary-search-test.roc @@ -1,69 +1,76 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/binary-search/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import BinarySearch exposing [find] # finds a value in an array with one element -expect - result = [6] |> find(6) - result == Ok(0) +expect { + result = find([6], 6) + result == Ok(0) +} # finds a value in the middle of an array -expect - result = [1, 3, 4, 6, 8, 9, 11] |> find(6) - result == Ok(3) +expect { + result = find([1, 3, 4, 6, 8, 9, 11], 6) + result == Ok(3) +} # finds a value at the beginning of an array -expect - result = [1, 3, 4, 6, 8, 9, 11] |> find(1) - result == Ok(0) +expect { + result = find([1, 3, 4, 6, 8, 9, 11], 1) + result == Ok(0) +} # finds a value at the end of an array -expect - result = [1, 3, 4, 6, 8, 9, 11] |> find(11) - result == Ok(6) +expect { + result = find([1, 3, 4, 6, 8, 9, 11], 11) + result == Ok(6) +} # finds a value in an array of odd length -expect - result = [1, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 634] |> find(144) - result == Ok(9) +expect { + result = find([1, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 634], 144) + result == Ok(9) +} # finds a value in an array of even length -expect - result = [1, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377] |> find(21) - result == Ok(5) +expect { + result = find([1, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377], 21) + result == Ok(5) +} # identifies that a value is not included in the array -expect - result = [1, 3, 4, 6, 8, 9, 11] |> find(7) - Result.is_err(result) +expect { + result = find([1, 3, 4, 6, 8, 9, 11], 7) + result.is_err() +} # a value smaller than the array's smallest value is not found -expect - result = [1, 3, 4, 6, 8, 9, 11] |> find(0) - Result.is_err(result) +expect { + result = find([1, 3, 4, 6, 8, 9, 11], 0) + result.is_err() +} # a value larger than the array's largest value is not found -expect - result = [1, 3, 4, 6, 8, 9, 11] |> find(13) - Result.is_err(result) +expect { + result = find([1, 3, 4, 6, 8, 9, 11], 13) + result.is_err() +} # nothing is found in an empty array -expect - result = [] |> find(1) - Result.is_err(result) +expect { + result = find([], 1) + result.is_err() +} # nothing is found when the left and right bounds cross -expect - result = [1, 2] |> find(0) - Result.is_err(result) +expect { + result = find([1, 2], 0) + 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/binary/.meta/Example.roc b/exercises/practice/binary/.meta/Example.roc index 044fb406..e2cc3381 100644 --- a/exercises/practice/binary/.meta/Example.roc +++ b/exercises/practice/binary/.meta/Example.roc @@ -1,19 +1,32 @@ -module [decimal] +Binary :: {}.{ + decimal : Str -> Try(U64, [InvalidCharacter(U8)]) + decimal = |binary_str| { + binary_str + .to_utf8() + ->map_try( + |char| { + match char { + '0' => Ok(0) + '1' => Ok(1) + c => Err(InvalidCharacter(c)) + } + }, + )? + .fold( + 0, + |dec, bit| { + dec * 2 + bit + }, + ) + ->Ok + } +} -decimal : Str -> Result U64 [InvalidCharacter U8] -decimal = |binary_str| - binary_str - |> Str.to_utf8 - |> List.map_try( - |char| - when char is - '0' -> Ok(0) - '1' -> Ok(1) - c -> Err(InvalidCharacter(c)), - )? - |> List.walk( - 0, - |dec, bit| - dec * 2 + bit, - ) - |> Ok +# The following function should soon be available in Roc's builtins +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/binary/.meta/template.j2 b/exercises/practice/binary/.meta/template.j2 index 4cca58ab..6b787ece 100644 --- a/exercises/practice/binary/.meta/template.j2 +++ b/exercises/practice/binary/.meta/template.j2 @@ -6,12 +6,15 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["binary"] | to_roc }}) {%- if case["expected"] is not none %} result == Ok({{ case["expected"] | to_roc }}) {%- else %} - result |> Result.is_err + result.is_err() {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/binary/Binary.roc b/exercises/practice/binary/Binary.roc index e23cbbca..c8870095 100644 --- a/exercises/practice/binary/Binary.roc +++ b/exercises/practice/binary/Binary.roc @@ -1,5 +1,6 @@ -module [decimal] - -decimal : Str -> Result U64 _ -decimal = |binary_str| - crash("Please implement the 'decimal' function") +Binary :: {}.{ + decimal : Str -> Try(U64, _) + decimal = |binary_str| { + crash "Please implement the 'decimal' function" + } +} diff --git a/exercises/practice/binary/binary-test.roc b/exercises/practice/binary/binary-test.roc index 4686cbc3..a4090e9d 100644 --- a/exercises/practice/binary/binary-test.roc +++ b/exercises/practice/binary/binary-test.roc @@ -1,89 +1,100 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/binary/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Binary exposing [decimal] # binary 0 is decimal 0 -expect - result = decimal("0") - result == Ok(0) +expect { + result = decimal("0") + result == Ok(0) +} # binary 1 is decimal 1 -expect - result = decimal("1") - result == Ok(1) +expect { + result = decimal("1") + result == Ok(1) +} # binary 10 is decimal 2 -expect - result = decimal("10") - result == Ok(2) +expect { + result = decimal("10") + result == Ok(2) +} # binary 11 is decimal 3 -expect - result = decimal("11") - result == Ok(3) +expect { + result = decimal("11") + result == Ok(3) +} # binary 100 is decimal 4 -expect - result = decimal("100") - result == Ok(4) +expect { + result = decimal("100") + result == Ok(4) +} # binary 1001 is decimal 9 -expect - result = decimal("1001") - result == Ok(9) +expect { + result = decimal("1001") + result == Ok(9) +} # binary 11010 is decimal 26 -expect - result = decimal("11010") - result == Ok(26) +expect { + result = decimal("11010") + result == Ok(26) +} # binary 10001101000 is decimal 1128 -expect - result = decimal("10001101000") - result == Ok(1128) +expect { + result = decimal("10001101000") + result == Ok(1128) +} # binary ignores leading zeros -expect - result = decimal("000011111") - result == Ok(31) +expect { + result = decimal("000011111") + result == Ok(31) +} # 2 is not a valid binary digit -expect - result = decimal("2") - result |> Result.is_err +expect { + result = decimal("2") + result.is_err() +} # a number containing a non-binary digit is invalid -expect - result = decimal("01201") - result |> Result.is_err +expect { + result = decimal("01201") + result.is_err() +} # a number with trailing non-binary characters is invalid -expect - result = decimal("10nope") - result |> Result.is_err +expect { + result = decimal("10nope") + result.is_err() +} # a number with leading non-binary characters is invalid -expect - result = decimal("nope10") - result |> Result.is_err +expect { + result = decimal("nope10") + result.is_err() +} # a number with internal non-binary characters is invalid -expect - result = decimal("10nope10") - result |> Result.is_err +expect { + result = decimal("10nope10") + result.is_err() +} # a number and a word whitespace separated is invalid -expect - result = decimal("001 nope") - result |> Result.is_err +expect { + result = decimal("001 nope") + 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/bob/.meta/Example.roc b/exercises/practice/bob/.meta/Example.roc index 74d8346e..274c7773 100644 --- a/exercises/practice/bob/.meta/Example.roc +++ b/exercises/practice/bob/.meta/Example.roc @@ -1,29 +1,36 @@ -module [response] +Bob :: {}.{ + response : Str -> Str + response = |hey_bob| { + trimmed = hey_bob.trim() + if trimmed == "" { + "Fine. Be that way!" + } else { + is_q = is_question(trimmed) + is_y = is_yelling(trimmed) + if is_q and is_y { + "Calm down, I know what I'm doing!" + } else if is_q { + "Sure." + } else if is_y { + "Whoa, chill out!" + } else { + "Whatever." + } + } + } +} -is_question = |hey_bob| - Str.ends_with(hey_bob, "?") +is_question = |hey_bob| { + hey_bob.ends_with("?") +} -is_yelling = |hey_bob| - is_lower = |c| - c >= 'a' and c <= 'z' - is_upper = |c| - c >= 'A' and c <= 'Z' - chars = Str.to_utf8(hey_bob) - (chars |> List.any(is_upper)) and !(chars |> List.any(is_lower)) - -response : Str -> Str -response = |hey_bob| - trimmed = Str.trim(hey_bob) - if trimmed == "" then - "Fine. Be that way!" - else - is_q = is_question(trimmed) - is_y = is_yelling(trimmed) - if is_q and is_y then - "Calm down, I know what I'm doing!" - else if is_q then - "Sure." - else if is_y then - "Whoa, chill out!" - else - "Whatever." +is_yelling = |hey_bob| { + is_lower = |c| { + c >= 'a' and c <= 'z' + } + is_upper = |c| { + c >= 'A' and c <= 'Z' + } + chars = hey_bob.to_utf8() + (chars.any(is_upper)) and !(chars.any(is_lower)) +} diff --git a/exercises/practice/bob/.meta/template.j2 b/exercises/practice/bob/.meta/template.j2 index 9995b77c..d9f72605 100644 --- a/exercises/practice/bob/.meta/template.j2 +++ b/exercises/practice/bob/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [response] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["heyBob"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} -{% endfor %} \ No newline at end of file +{% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/bob/Bob.roc b/exercises/practice/bob/Bob.roc index 1501f4cb..2ea44ca1 100644 --- a/exercises/practice/bob/Bob.roc +++ b/exercises/practice/bob/Bob.roc @@ -1,5 +1,6 @@ -module [response] - -response : Str -> Str -response = |hey_bob| - crash("Please implement the `response` function") +Bob :: {}.{ + response : Str -> Str + response = |hey_bob| { + crash "Please implement the 'response' function" + } +} diff --git a/exercises/practice/bob/bob-test.roc b/exercises/practice/bob/bob-test.roc index d8da4762..a59f214e 100644 --- a/exercises/practice/bob/bob-test.roc +++ b/exercises/practice/bob/bob-test.roc @@ -1,139 +1,160 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/bob/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Bob exposing [response] # stating something -expect - result = response("Tom-ay-to, tom-aaaah-to.") - result == "Whatever." +expect { + result = response("Tom-ay-to, tom-aaaah-to.") + result == "Whatever." +} # shouting -expect - result = response("WATCH OUT!") - result == "Whoa, chill out!" +expect { + result = response("WATCH OUT!") + result == "Whoa, chill out!" +} # shouting gibberish -expect - result = response("FCECDFCAAB") - result == "Whoa, chill out!" +expect { + result = response("FCECDFCAAB") + result == "Whoa, chill out!" +} # asking a question -expect - result = response("Does this cryogenic chamber make me look fat?") - result == "Sure." +expect { + result = response("Does this cryogenic chamber make me look fat?") + result == "Sure." +} # asking a numeric question -expect - result = response("You are, what, like 15?") - result == "Sure." +expect { + result = response("You are, what, like 15?") + result == "Sure." +} # asking gibberish -expect - result = response("fffbbcbeab?") - result == "Sure." +expect { + result = response("fffbbcbeab?") + result == "Sure." +} # talking forcefully -expect - result = response("Hi there!") - result == "Whatever." +expect { + result = response("Hi there!") + result == "Whatever." +} # using acronyms in regular speech -expect - result = response("It's OK if you don't want to go work for NASA.") - result == "Whatever." +expect { + result = response("It's OK if you don't want to go work for NASA.") + result == "Whatever." +} # forceful question -expect - result = response("WHAT'S GOING ON?") - result == "Calm down, I know what I'm doing!" +expect { + result = response("WHAT'S GOING ON?") + result == "Calm down, I know what I'm doing!" +} # shouting numbers -expect - result = response("1, 2, 3 GO!") - result == "Whoa, chill out!" +expect { + result = response("1, 2, 3 GO!") + result == "Whoa, chill out!" +} # no letters -expect - result = response("1, 2, 3") - result == "Whatever." +expect { + result = response("1, 2, 3") + result == "Whatever." +} # question with no letters -expect - result = response("4?") - result == "Sure." +expect { + result = response("4?") + result == "Sure." +} # shouting with special characters -expect - result = response("ZOMG THE %^*@#\$(*^ ZOMBIES ARE COMING!!11!!1!") - result == "Whoa, chill out!" +expect { + result = response("ZOMG THE %^*@#\$(*^ ZOMBIES ARE COMING!!11!!1!") + result == "Whoa, chill out!" +} # shouting with no exclamation mark -expect - result = response("I HATE THE DENTIST") - result == "Whoa, chill out!" +expect { + result = response("I HATE THE DENTIST") + result == "Whoa, chill out!" +} # statement containing question mark -expect - result = response("Ending with ? means a question.") - result == "Whatever." +expect { + result = response("Ending with ? means a question.") + result == "Whatever." +} # non-letters with question -expect - result = response(":) ?") - result == "Sure." +expect { + result = response(":) ?") + result == "Sure." +} # prattling on -expect - result = response("Wait! Hang on. Are you going to be OK?") - result == "Sure." +expect { + result = response("Wait! Hang on. Are you going to be OK?") + result == "Sure." +} # silence -expect - result = response("") - result == "Fine. Be that way!" +expect { + result = response("") + result == "Fine. Be that way!" +} # prolonged silence -expect - result = response(" ") - result == "Fine. Be that way!" +expect { + result = response(" ") + result == "Fine. Be that way!" +} # alternate silence -expect - result = response("\t\t\t\t\t\t\t\t\t\t") - result == "Fine. Be that way!" +expect { + result = response("\t\t\t\t\t\t\t\t\t\t") + result == "Fine. Be that way!" +} # starting with whitespace -expect - result = response(" hmmmmmmm...") - result == "Whatever." +expect { + result = response(" hmmmmmmm...") + result == "Whatever." +} # ending with whitespace -expect - result = response("Okay if like my spacebar quite a bit? ") - result == "Sure." +expect { + result = response("Okay if like my spacebar quite a bit? ") + result == "Sure." +} # other whitespace -expect - result = response("\n\r \t") - result == "Fine. Be that way!" +expect { + result = response("\n\r \t") + result == "Fine. Be that way!" +} # non-question ending with whitespace -expect - result = response("This is a statement ending with whitespace ") - result == "Whatever." +expect { + result = response("This is a statement ending with whitespace ") + result == "Whatever." +} # multiple line question -expect - result = response("\nDoes this cryogenic chamber make\n me look fat?") - result == "Sure." +expect { + result = response("\nDoes this cryogenic chamber make\n me look fat?") + result == "Sure." +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/bowling/.meta/Example.roc b/exercises/practice/bowling/.meta/Example.roc index 88157f3d..8de495c9 100644 --- a/exercises/practice/bowling/.meta/Example.roc +++ b/exercises/practice/bowling/.meta/Example.roc @@ -1,126 +1,193 @@ -module [Game, create, roll, score] - -Frame : [ - Ball1 U64, # unfinished frame - Ball2 U64 U64, - Spare U64 U64, - Strike, - SpareFill U64, - StrikeFill1 U64, # unfinished frame - StrikeFill2 U64 U64, +Bowling :: { frames : List(Frame) }.{ + new : Bowling + new = { frames: [] } + + roll : Bowling, U64 -> Try(Bowling, [MoreThan10Pins, GameOver]) + roll = |game, pins| { + if game.is_over() { + Err(GameOver) + } else { + { frames } = game + last_frame = frames.last() ?? Ball2(0, 0) + check_max_10_pins(last_frame, pins)? + updated_frames = { + match last_frame { + Ball1(pins1) => { + frames + .drop_last( + 1, + ) + .append( + if pins1 + pins == 10 { + Spare(pins1, pins) + } else { + Ball2(pins1, pins) + }, + ) + } + + StrikeFill1(pins1) => { + frames.drop_last(1).append(StrikeFill2(pins1, pins)) + } + + Ball2(_, _) | Spare(_, _) | Strike if frames.len() < 10 => { + if pins == 10 { + frames.append(Strike) + } else { + frames.append(Ball1(pins)) + } + } + + Spare(_, _) => { + frames.append(SpareFill(pins)) + } + + Strike => { + frames.append(StrikeFill1(pins)) + } + + Ball2(_, _) | SpareFill(_) | StrikeFill2(_, _) => { + crash "Impossible, an unfinished game cannot have these in the last frame after the 10th frame" + } + } + } + Ok({ frames: updated_frames }) + } + } + + score : Bowling -> Try(U64, [GameIsNotOver]) + score = |game| { + if game.is_over() { + { frames } = game + points = { + frames->map_triplets( + |frame1, frame2, frame3| { + match frame1 { + Ball2(pins1, pins2) => { + pins1 + pins2 + } + Spare(pins1, pins2) => { + pins1 + pins2 + first_pins(frame2) + } + Strike => { + match frame2 { + Strike => { + 10 + 10 + first_pins(frame3) + } + _ => { + 10 + total_pins(frame2) + } + } + } + SpareFill(_) => 0 # already counted in the Spare + StrikeFill2(_, _) => 0 # already counter in the Strike + Ball1(_) | StrikeFill1(_) => { + crash "Impossible, unfinished frames should not exist in a finished game" + } + } + }, + ) + } + Ok(points.sum()) + } + else { + Err(GameIsNotOver) + } + } + + is_over : Bowling -> Bool + is_over = |game| { + { frames } = game + match frames { + _ if frames.len() < 10 => Bool.False + [.., Ball1(_)] | [.., Spare(_, _)] | [.., Strike] | [.., StrikeFill1(_)] => Bool.False + [.., Ball2(_, _)] | [.., SpareFill(_)] | [.., StrikeFill2(_, _)] => Bool.True + _ => Bool.False + } + } + +} + +Frame := [ + Ball1(U64), # unfinished frame + Ball2(U64, U64), + Spare(U64, U64), + Strike, + SpareFill(U64), + StrikeFill1(U64), # unfinished frame + StrikeFill2(U64, U64), ] -Game := { frames : List Frame } - -create : { previous_rolls ?? List U64 } -> Result Game [MoreThan10Pins, GameOver] -create = |{ previous_rolls ?? [] }| - List.walk_try( - previous_rolls, - @Game({ frames: [] }), - |game, pins| - roll(game, pins), - ) - -check_max_10_pins : Frame, U64 -> Result {} [MoreThan10Pins, GameOver] -check_max_10_pins = |last_frame, pins| - when last_frame is - Ball1(pins1) -> if pins1 + pins > 10 then Err(MoreThan10Pins) else Ok({}) - StrikeFill1(pins1) -> - if pins > 10 or (pins1 < 10 and pins1 + pins > 10) then Err(MoreThan10Pins) else Ok({}) - - Ball2(_, _) | Spare(_, _) | Strike -> if pins > 10 then Err(MoreThan10Pins) else Ok({}) - SpareFill(_) | StrikeFill2(_, _) -> Err(GameOver) - -is_over : Game -> Bool -is_over = |@Game({ frames })| - when frames is - _ if List.len(frames) < 10 -> Bool.false - [.., Ball1(_)] | [.., Spare(_, _)] | [.., Strike] | [.., StrikeFill1(_)] -> Bool.false - [.., Ball2(_, _)] | [.., SpareFill(_)] | [.., StrikeFill2(_, _)] -> Bool.true - _ -> Bool.false - -roll : Game, U64 -> Result Game [MoreThan10Pins, GameOver] -roll = |@Game({ frames }), pins| - if @Game({ frames }) |> is_over then - Err(GameOver) - else - last_frame = frames |> List.last |> Result.with_default(Ball2(0, 0)) - check_max_10_pins(last_frame, pins)? - updated_frames = - when last_frame is - Ball1(pins1) -> - frames - |> List.drop_last(1) - |> List.append( - ( - if pins1 + pins == 10 then - Spare(pins1, pins) - else - Ball2(pins1, pins) - ), - ) - - StrikeFill1(pins1) -> - frames |> List.drop_last(1) |> List.append(StrikeFill2(pins1, pins)) - - Ball2(_, _) | Spare(_, _) | Strike if List.len(frames) < 10 -> - if pins == 10 then - frames |> List.append(Strike) - else - frames |> List.append(Ball1(pins)) - - Spare(_, _) -> - frames |> List.append(SpareFill(pins)) - - Strike -> - frames |> List.append(StrikeFill1(pins)) - - Ball2(_, _) | SpareFill(_) | StrikeFill2(_, _) -> - crash("Impossible, an unfinished game cannot have these in the last frame after the 10th frame") - @Game({ frames: updated_frames }) |> Ok - -map_triplets : List Frame, (Frame, Frame, Frame -> U64) -> List U64 -map_triplets = |list, score_func| - get_or_0 = |index| list |> List.get(index) |> Result.with_default(Ball2(0, 0)) - List.range({ start: At(0), end: Before(List.len(list)) }) - |> List.map( - |index| - score_func(get_or_0(index), get_or_0((index + 1)), get_or_0((index + 2))), - ) +check_max_10_pins : Frame, U64 -> Try({}, [MoreThan10Pins, GameOver]) +check_max_10_pins = |last_frame, pins| { + match last_frame { + Ball1(pins1) => if pins1 + pins > 10 { + Err(MoreThan10Pins) + } else { + Ok({}) + } + StrikeFill1(pins1) => { + if pins > 10 or (pins1 < 10 and pins1 + pins > 10) { + Err(MoreThan10Pins) + } else { + Ok({}) + } + } + Ball2(_, _) | Spare(_, _) | Strike => if pins > 10 { + Err(MoreThan10Pins) + } else { + Ok({}) + } + SpareFill(_) | StrikeFill2(_, _) => Err(GameOver) + } +} + +map_triplets : List(Frame), (Frame, Frame, Frame -> U64) -> List(U64) +map_triplets = |list, score_func| { + get_or_0 = |index| { + list.get(index) ?? Ball2(0, 0) + } + (0..List.from_iter() +} first_pins : Frame -> U64 -first_pins = |frame| - when frame is - Ball1(pins) | Ball2(pins, _) | Spare(pins, _) | SpareFill(pins) | StrikeFill1(pins) | StrikeFill2(pins, _) -> pins - Strike -> 10 +first_pins = |frame| { + match frame { + Ball1(pins) | Ball2(pins, _) | Spare(pins, _) | SpareFill(pins) | StrikeFill1(pins) | StrikeFill2(pins, _) => pins + Strike => 10 + } +} total_pins : Frame -> U64 -total_pins = |frame| - when frame is - Ball2(pins1, pins2) | Spare(pins1, pins2) | StrikeFill2(pins1, pins2) -> pins1 + pins2 - Ball1(pins) | SpareFill(pins) | StrikeFill1(pins) -> pins - Strike -> 10 - -score : Game -> Result U64 [GameIsNotOver] -score = |@Game({ frames })| - if @Game({ frames }) |> is_over then - frames - |> map_triplets( - |frame1, frame2, frame3| - when frame1 is - Ball2(pins1, pins2) -> pins1 + pins2 - Spare(pins1, pins2) -> pins1 + pins2 + first_pins(frame2) - Strike -> - when frame2 is - Strike -> 10 + 10 + first_pins(frame3) - _ -> 10 + total_pins(frame2) - - SpareFill(_) -> 0 # already counted in the Spare - StrikeFill2(_, _) -> 0 # already counter in the Strike - Ball1(_) | StrikeFill1(_) -> - crash("Impossible, unfinished frames should not exist in a finished game"), - ) - |> List.sum - |> Ok - else - Err(GameIsNotOver) +total_pins = |frame| { + match frame { + Ball2(pins1, pins2) | Spare(pins1, pins2) | StrikeFill2(pins1, pins2) => pins1 + pins2 + Ball1(pins) | SpareFill(pins) | StrikeFill1(pins) => pins + Strike => 10 + } +} + +# The following function should soon be available in Roc's builtins +fold_try : List(a), b, (b, a -> Try(b, err)) -> Try(b, err) +fold_try = |list, init, func| { + list.fold_until( + Ok(init), + |state, item| { + match state { + Ok(internal_state) => { + match func(internal_state, item) { + Ok(new_state) => Continue(Ok(new_state)) + Err(final_err) => Break(Err(final_err)) + } + } + Err(_) => { + crash "Unreachable" + } + } + }, + ) +} diff --git a/exercises/practice/bowling/.meta/template.j2 b/exercises/practice/bowling/.meta/template.j2 index 17e2f490..89d5fd93 100644 --- a/exercises/practice/bowling/.meta/template.j2 +++ b/exercises/practice/bowling/.meta/template.j2 @@ -2,49 +2,45 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [Game, create, roll, score] - -replay_game : List U64 -> Result Game _ -replay_game = |rolls| - new_game = create({})? - rolls - |> List.walk_until( - Ok(new_game), - |state, pins| - when state is - Ok(game) -> - when game |> roll(pins) is - Ok(updated_game) -> Continue(Ok(updated_game)) - Err(err) -> Break(Err(err)) - - Err(_) -> crash "Impossible, we don't start or Continue with an Err" - ) - +import {{ exercise | to_pascal }} {% for case in cases -%} # {{ case["description"] }} -expect - maybe_game = create({ previous_rolls : {{ case["input"]["previousRolls"] | to_roc }} }) +expect { + rolls = {{ case["input"]["previousRolls"] | to_roc }} + game = game_with_previous_rolls(rolls)? {%- if case["property"] == "score" %} - result = maybe_game |> Result.try(|game| score(game)) + result = game.score() {%- else %} - result = maybe_game |> Result.try(|game| - game |> {{ case["property"] | to_snake }}({{ case["input"]["roll"] }})) + result = game.{{ case["property"] | to_snake }}({{ case["input"]["roll"] }}) {%- endif %} {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} result == Ok({{ case["expected"] | to_roc }}) {%- endif %} +} {%- if case["property"] == "score" and not case["expected"]["error"] %} # should be able to replay this finished game from the start -expect +expect { rolls = {{ case["input"]["previousRolls"] | to_roc }} - result = replay_game(rolls) - result |> Result.is_ok + result = game_with_previous_rolls(rolls) + result.is_ok() +} {%- endif %} -{% endfor %} \ No newline at end of file +{% endfor %} + +game_with_previous_rolls : List(U64) -> Try(Bowling, _) +game_with_previous_rolls = |rolls| { + var $game = Bowling.new + for pins in rolls { + $game = $game.roll(pins)? + } + Ok($game) +} + +{{ macros.footer() }} diff --git a/exercises/practice/bowling/Bowling.roc b/exercises/practice/bowling/Bowling.roc index a6a49b09..8a75ff57 100644 --- a/exercises/practice/bowling/Bowling.roc +++ b/exercises/practice/bowling/Bowling.roc @@ -1,21 +1,22 @@ -module [Game, create, roll, score] +Bowling :: { + # TODO: change this opaque type however you need + todo1 : U64, + todo2 : U64, + todo3 : U64, + # etc. +}.{ + new : Bowling + new = { + crash "Please implement the 'new' constant" + } -Game := { - # TODO: change this opaque type however you need - todo : U64, - todo2 : U64, - todo3 : U64, - # etc. -} - -create : { previous_rolls ?? List U64 } -> Result Game _ -create = |{ previous_rolls ?? [] }| - crash("Please implement the 'create' function") + roll : Bowling, U64 -> Try(Bowling, _) + roll = |game, pins| { + crash "Please implement the 'roll' function" + } -roll : Game, U64 -> Result Game _ -roll = |game, pins| - crash("Please implement the 'roll' function") - -score : Game -> Result U64 _ -score = |finished_game| - crash("Please implement the 'score' function") + score : Bowling -> Try(U64, _) + score = |game| { + crash "Please implement the 'score' function" + } +} diff --git a/exercises/practice/bowling/bowling-test.roc b/exercises/practice/bowling/bowling-test.roc index abf9a29b..5fc170d1 100644 --- a/exercises/practice/bowling/bowling-test.roc +++ b/exercises/practice/bowling/bowling-test.roc @@ -1,351 +1,371 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/bowling/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") - -import Bowling exposing [Game, create, roll, score] +# File last updated on 2026-06-22 -replay_game : List U64 -> Result Game _ -replay_game = |rolls| - new_game = create({})? - rolls - |> List.walk_until( - Ok(new_game), - |state, pins| - when state is - Ok(game) -> - when game |> roll(pins) is - Ok(updated_game) -> Continue(Ok(updated_game)) - Err(err) -> Break(Err(err)) - - Err(_) -> crash "Impossible, we don't start or Continue with an Err", - ) +import Bowling # should be able to score a game with all zeros -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(0) +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(0) +} # should be able to replay this finished game from the start -expect - rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # should be able to score a game with no strikes or spares -expect - maybe_game = create({ previous_rolls: [3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(90) +expect { + rolls = [3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(90) +} # should be able to replay this finished game from the start -expect - rolls = [3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # a spare followed by zeros is worth ten points -expect - maybe_game = create({ previous_rolls: [6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(10) +expect { + rolls = [6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(10) +} # should be able to replay this finished game from the start -expect - rolls = [6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [6, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # points scored in the roll after a spare are counted twice -expect - maybe_game = create({ previous_rolls: [6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(16) +expect { + rolls = [6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(16) +} # should be able to replay this finished game from the start -expect - rolls = [6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [6, 4, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # consecutive spares each get a one roll bonus -expect - maybe_game = create({ previous_rolls: [5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(31) +expect { + rolls = [5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(31) +} # should be able to replay this finished game from the start -expect - rolls = [5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [5, 5, 3, 7, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # a spare in the last frame gets a one roll bonus that is counted once -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(17) +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(17) +} # should be able to replay this finished game from the start -expect - rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 7] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # a strike earns ten points in a frame with a single roll -expect - maybe_game = create({ previous_rolls: [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(10) +expect { + rolls = [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(10) +} # should be able to replay this finished game from the start -expect - rolls = [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # points scored in the two rolls after a strike are counted twice as a bonus -expect - maybe_game = create({ previous_rolls: [10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(26) +expect { + rolls = [10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(26) +} # should be able to replay this finished game from the start -expect - rolls = [10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # consecutive strikes each get the two roll bonus -expect - maybe_game = create({ previous_rolls: [10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(81) +expect { + rolls = [10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(81) +} # should be able to replay this finished game from the start -expect - rolls = [10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [10, 10, 10, 5, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # a strike in the last frame gets a two roll bonus that is counted once -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 1] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(18) +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 1] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(18) +} # should be able to replay this finished game from the start -expect - rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 1] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 1] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # rolling a spare with the two roll bonus does not get a bonus roll -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 3] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(20) +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 3] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(20) +} # should be able to replay this finished game from the start -expect - rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 3] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 7, 3] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # strikes with the two roll bonus do not get bonus rolls -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(30) +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(30) +} # should be able to replay this finished game from the start -expect - rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 10] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # last two strikes followed by only last bonus with non strike points -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 0, 1] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(31) +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 0, 1] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(31) +} # should be able to replay this finished game from the start -expect - rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 0, 1] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 0, 1] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # a strike with the one roll bonus after a spare in the last frame does not get a bonus -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 10] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(20) +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 10] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(20) +} # should be able to replay this finished game from the start -expect - rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 10] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 10] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # all strikes is a perfect game -expect - maybe_game = create({ previous_rolls: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(300) +expect { + rolls = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(300) +} # should be able to replay this finished game from the start -expect - rolls = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # a roll cannot score more than 10 points -expect - maybe_game = create({ previous_rolls: [] }) - result = - maybe_game - |> Result.try( - |game| - game |> roll(11), - ) - result |> Result.is_err +expect { + rolls = [] + game = game_with_previous_rolls(rolls)? + result = game.roll(11) + result.is_err() +} # two rolls in a frame cannot score more than 10 points -expect - maybe_game = create({ previous_rolls: [5] }) - result = - maybe_game - |> Result.try( - |game| - game |> roll(6), - ) - result |> Result.is_err +expect { + rolls = [5] + game = game_with_previous_rolls(rolls)? + result = game.roll(6) + result.is_err() +} # bonus roll after a strike in the last frame cannot score more than 10 points -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10] }) - result = - maybe_game - |> Result.try( - |game| - game |> roll(11), - ) - result |> Result.is_err +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10] + game = game_with_previous_rolls(rolls)? + result = game.roll(11) + result.is_err() +} # two bonus rolls after a strike in the last frame cannot score more than 10 points -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 5] }) - result = - maybe_game - |> Result.try( - |game| - game |> roll(6), - ) - result |> Result.is_err +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 5] + game = game_with_previous_rolls(rolls)? + result = game.roll(6) + result.is_err() +} # two bonus rolls after a strike in the last frame can score more than 10 points if one is a strike -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 6] }) - result = maybe_game |> Result.try(|game| score(game)) - result == Ok(26) +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 6] + game = game_with_previous_rolls(rolls)? + result = game.score() + result == Ok(26) +} # should be able to replay this finished game from the start -expect - rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 6] - result = replay_game(rolls) - result |> Result.is_ok +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10, 6] + result = game_with_previous_rolls(rolls) + result.is_ok() +} # the second bonus rolls after a strike in the last frame cannot be a strike if the first one is not a strike -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 6] }) - result = - maybe_game - |> Result.try( - |game| - game |> roll(10), - ) - result |> Result.is_err +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 6] + game = game_with_previous_rolls(rolls)? + result = game.roll(10) + result.is_err() +} # second bonus roll after a strike in the last frame cannot score more than 10 points -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10] }) - result = - maybe_game - |> Result.try( - |game| - game |> roll(11), - ) - result |> Result.is_err +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10] + game = game_with_previous_rolls(rolls)? + result = game.roll(11) + result.is_err() +} # an unstarted game cannot be scored -expect - maybe_game = create({ previous_rolls: [] }) - result = maybe_game |> Result.try(|game| score(game)) - result |> Result.is_err +expect { + rolls = [] + game = game_with_previous_rolls(rolls)? + result = game.score() + result.is_err() +} # an incomplete game cannot be scored -expect - maybe_game = create({ previous_rolls: [0, 0] }) - result = maybe_game |> Result.try(|game| score(game)) - result |> Result.is_err +expect { + rolls = [0, 0] + game = game_with_previous_rolls(rolls)? + result = game.score() + result.is_err() +} # cannot roll if game already has ten frames -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }) - result = - maybe_game - |> Result.try( - |game| - game |> roll(0), - ) - result |> Result.is_err +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + game = game_with_previous_rolls(rolls)? + result = game.roll(0) + result.is_err() +} # bonus rolls for a strike in the last frame must be rolled before score can be calculated -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10] }) - result = maybe_game |> Result.try(|game| score(game)) - result |> Result.is_err +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10] + game = game_with_previous_rolls(rolls)? + result = game.score() + result.is_err() +} # both bonus rolls for a strike in the last frame must be rolled before score can be calculated -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10] }) - result = maybe_game |> Result.try(|game| score(game)) - result |> Result.is_err +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 10] + game = game_with_previous_rolls(rolls)? + result = game.score() + result.is_err() +} # bonus roll for a spare in the last frame must be rolled before score can be calculated -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3] }) - result = maybe_game |> Result.try(|game| score(game)) - result |> Result.is_err +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3] + game = game_with_previous_rolls(rolls)? + result = game.score() + result.is_err() +} # cannot roll after bonus roll for spare -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 2] }) - result = - maybe_game - |> Result.try( - |game| - game |> roll(2), - ) - result |> Result.is_err +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 3, 2] + game = game_with_previous_rolls(rolls)? + result = game.roll(2) + result.is_err() +} # cannot roll after bonus rolls for strike -expect - maybe_game = create({ previous_rolls: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 3, 2] }) - result = - maybe_game - |> Result.try( - |game| - game |> roll(2), - ) - result |> Result.is_err +expect { + rolls = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 3, 2] + game = game_with_previous_rolls(rolls)? + result = game.roll(2) + result.is_err() +} +game_with_previous_rolls : List(U64) -> Try(Bowling, _) +game_with_previous_rolls = |rolls| { + var $game = Bowling.new + for pins in rolls { + $game = $game.roll(pins)? + } + Ok($game) +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/change/.meta/Example.roc b/exercises/practice/change/.meta/Example.roc index dd32c0dd..f1632b37 100644 --- a/exercises/practice/change/.meta/Example.roc +++ b/exercises/practice/change/.meta/Example.roc @@ -1,30 +1,65 @@ -module [find_fewest_coins] +Change :: {}.{ + find_fewest_coins : List(U64), U64 -> Try(List(U64), [NotFound]) + find_fewest_coins = |coins, target| { + help = |sorted_coins, sub_target, max_length| { + if sub_target == 0 { + Ok([]) + } else if max_length == 0 { + Err(NotFound) + } else { + match sorted_coins { + [] => Err(NotFound) + [largest_coin, .. as other_coins] => { + if largest_coin == sub_target { + Ok([largest_coin]) + } else if largest_coin < sub_target { + match help(sorted_coins, (sub_target - largest_coin), (max_length - 1)) { + Ok(other_coins_with) => { + coins_with = other_coins_with.append(largest_coin) + match help(other_coins, sub_target, (coins_with.len() - 1)) { + Ok(coins_without) => Ok(coins_without) + Err(NotFound) => Ok(coins_with) + } + } + Err(NotFound) => help(other_coins, sub_target, max_length) + } + } else { + help(other_coins, sub_target, max_length) + } + } + } + } + } -find_fewest_coins : List U64, U64 -> Result (List U64) [NotFound] -find_fewest_coins = |coins, target| - help = |sorted_coins, sub_target, max_length| - if sub_target == 0 then - Ok([]) - else if max_length == 0 then - Err(NotFound) - else - when sorted_coins is - [] -> Err(NotFound) - [largest_coin, .. as other_coins] -> - if largest_coin == sub_target then - Ok([largest_coin]) - else if largest_coin < sub_target then - when help(sorted_coins, (sub_target - largest_coin), (max_length - 1)) is - Ok(other_coins_with) -> - coins_with = other_coins_with |> List.append(largest_coin) - when help(other_coins, sub_target, (List.len(coins_with) - 1)) is - Ok(coins_without) -> Ok(coins_without) - Err(NotFound) -> Ok(coins_with) + help(coins->sort_desc(), target, max_u64)? + } + ->sort_asc() + ->Ok() +} - Err(NotFound) -> help(other_coins, sub_target, max_length) - else - help(other_coins, sub_target, max_length) +# The following functions should soon be available in Roc's builtins +sort_asc = |list| { + list.sort_with( + |a, b| if a < b { + LT + } else if a > b { + GT + } else { + EQ + }, + ) +} - help((coins |> List.sort_desc), target, Num.max_u64)? - |> List.sort_asc - |> Ok +sort_desc = |list| { + list.sort_with( + |a, b| if a < b { + GT + } else if a > b { + LT + } else { + EQ + }, + ) +} + +max_u64 = 18_446_744_073_709_551_615 diff --git a/exercises/practice/change/.meta/template.j2 b/exercises/practice/change/.meta/template.j2 index 1aa34c3d..fef56d1c 100644 --- a/exercises/practice/change/.meta/template.j2 +++ b/exercises/practice/change/.meta/template.j2 @@ -6,13 +6,16 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { coins = {{ case["input"]["coins"] | to_roc }} - result = coins |> {{ case["property"] | to_snake }}({{ case["input"]["target"] }}) + result = {{ case["property"] | to_snake }}(coins, {{ case["input"]["target"] }}) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} result == Ok({{ case["expected"] | to_roc }}) {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/change/Change.roc b/exercises/practice/change/Change.roc index f1ac7871..dfaf4dca 100644 --- a/exercises/practice/change/Change.roc +++ b/exercises/practice/change/Change.roc @@ -1,5 +1,6 @@ -module [find_fewest_coins] - -find_fewest_coins : List U64, U64 -> Result (List U64) _ -find_fewest_coins = |coins, target| - crash("Please implement the 'find_fewest_coins' function") +Change :: {}.{ + find_fewest_coins : List(U64), U64 -> Try(List(U64), _) + find_fewest_coins = |coins, target| { + crash "Please implement the 'find_fewest_coins' function" + } +} diff --git a/exercises/practice/change/change-test.roc b/exercises/practice/change/change-test.roc index 4a5d2576..b7d6bfab 100644 --- a/exercises/practice/change/change-test.roc +++ b/exercises/practice/change/change-test.roc @@ -1,86 +1,94 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/change/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Change exposing [find_fewest_coins] # change for 1 cent -expect - coins = [1, 5, 10, 25] - result = coins |> find_fewest_coins(1) - result == Ok([1]) +expect { + coins = [1, 5, 10, 25] + result = find_fewest_coins(coins, 1) + result == Ok([1]) +} # single coin change -expect - coins = [1, 5, 10, 25, 100] - result = coins |> find_fewest_coins(25) - result == Ok([25]) +expect { + coins = [1, 5, 10, 25, 100] + result = find_fewest_coins(coins, 25) + result == Ok([25]) +} # multiple coin change -expect - coins = [1, 5, 10, 25, 100] - result = coins |> find_fewest_coins(15) - result == Ok([5, 10]) +expect { + coins = [1, 5, 10, 25, 100] + result = find_fewest_coins(coins, 15) + result == Ok([5, 10]) +} # change with Lilliputian Coins -expect - coins = [1, 4, 15, 20, 50] - result = coins |> find_fewest_coins(23) - result == Ok([4, 4, 15]) +expect { + coins = [1, 4, 15, 20, 50] + result = find_fewest_coins(coins, 23) + result == Ok([4, 4, 15]) +} # change with Lower Elbonia Coins -expect - coins = [1, 5, 10, 21, 25] - result = coins |> find_fewest_coins(63) - result == Ok([21, 21, 21]) +expect { + coins = [1, 5, 10, 21, 25] + result = find_fewest_coins(coins, 63) + result == Ok([21, 21, 21]) +} # large target values -expect - coins = [1, 2, 5, 10, 20, 50, 100] - result = coins |> find_fewest_coins(999) - result == Ok([2, 2, 5, 20, 20, 50, 100, 100, 100, 100, 100, 100, 100, 100, 100]) +expect { + coins = [1, 2, 5, 10, 20, 50, 100] + result = find_fewest_coins(coins, 999) + result == Ok([2, 2, 5, 20, 20, 50, 100, 100, 100, 100, 100, 100, 100, 100, 100]) +} # possible change without unit coins available -expect - coins = [2, 5, 10, 20, 50] - result = coins |> find_fewest_coins(21) - result == Ok([2, 2, 2, 5, 10]) +expect { + coins = [2, 5, 10, 20, 50] + result = find_fewest_coins(coins, 21) + result == Ok([2, 2, 2, 5, 10]) +} # another possible change without unit coins available -expect - coins = [4, 5] - result = coins |> find_fewest_coins(27) - result == Ok([4, 4, 4, 5, 5, 5]) +expect { + coins = [4, 5] + result = find_fewest_coins(coins, 27) + result == Ok([4, 4, 4, 5, 5, 5]) +} # a greedy approach is not optimal -expect - coins = [1, 10, 11] - result = coins |> find_fewest_coins(20) - result == Ok([10, 10]) +expect { + coins = [1, 10, 11] + result = find_fewest_coins(coins, 20) + result == Ok([10, 10]) +} # no coins make 0 change -expect - coins = [1, 5, 10, 21, 25] - result = coins |> find_fewest_coins(0) - result == Ok([]) +expect { + coins = [1, 5, 10, 21, 25] + result = find_fewest_coins(coins, 0) + result == Ok([]) +} # error testing for change smaller than the smallest of coins -expect - coins = [5, 10] - result = coins |> find_fewest_coins(3) - result |> Result.is_err +expect { + coins = [5, 10] + result = find_fewest_coins(coins, 3) + result.is_err() +} # error if no combination can add up to target -expect - coins = [5, 10] - result = coins |> find_fewest_coins(94) - result |> Result.is_err +expect { + coins = [5, 10] + result = find_fewest_coins(coins, 94) + 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/circular-buffer/.meta/Example.roc b/exercises/practice/circular-buffer/.meta/Example.roc index a181b6dc..660c41e7 100644 --- a/exercises/practice/circular-buffer/.meta/Example.roc +++ b/exercises/practice/circular-buffer/.meta/Example.roc @@ -1,41 +1,60 @@ -module [create, read, write, overwrite, clear] +CircularBuffer :: { data : List(I64), start : U64, length : U64 }.{ + create : { capacity : U64 } -> CircularBuffer + create = |{ capacity }| { + { data: List.repeat(0, capacity), start: 0, length: 0 } + } -CircularBuffer : { data : List I64, start : U64, length : U64 } + read : CircularBuffer -> Try({ updated_buffer : CircularBuffer, value : I64 }, [BufferEmpty]) + read = |{ data, start, length }| { + if length == 0 { + Err(BufferEmpty) + } else { + increment_start = (start + 1) % data.len() + updated_buffer = { data, start: increment_start, length: length - 1 } + match data.get(start) { + Ok(value) => Ok({ updated_buffer, value }) + Err(OutOfBounds) => { + crash "Unreachable: start should never be out of bounds" + } + } + } + } -create : { capacity : U64 } -> CircularBuffer -create = |{ capacity }| - { data: List.repeat(0, capacity), start: 0, length: 0 } + write : CircularBuffer, I64 -> Try(CircularBuffer, [BufferFull]) + write = |{ data, start, length }, value| { + if length == data.len() { + Err(BufferFull) + } else { + index = (start + length) % data.len() + new_data = match data.replace(index, value) { + Ok(d) => d + Err(_) => { + crash "Unreachable: the index is guaranteed to be within bounds." + } + }.list + Ok({ data: new_data, start, length: length + 1 }) + } + } -read : CircularBuffer -> Result { new_buffer : CircularBuffer, value : I64 } [BufferEmpty] -read = |{ data, start, length }| - if length == 0 then - Err(BufferEmpty) - else - increment_start = (start + 1) % List.len(data) - new_buffer = { data, start: increment_start, length: length - 1 } - when data |> List.get(start) is - Ok(value) -> Ok({ new_buffer, value }) - Err(OutOfBounds) -> crash("Unreachable: start should never be out of bounds") + overwrite : CircularBuffer, I64 -> CircularBuffer + overwrite = |{ data, start, length }, value| { + index = (start + length) % data.len() + new_data = match data.replace(index, value) { + Ok(d) => d + Err(_) => { + crash "Unreachable: the index is guaranteed to be within bounds." + } + }.list + if length == data.len() { + inc_start = (start + 1) % data.len() + { data: new_data, start: inc_start, length: length } + } else { + { data: new_data, start, length: length + 1 } + } + } -write : CircularBuffer, I64 -> Result CircularBuffer [BufferFull] -write = |{ data, start, length }, value| - if length == List.len(data) then - Err(BufferFull) - else - index = (start + length) % List.len(data) - new_data = data |> List.replace(index, value) |> .list - Ok({ data: new_data, start, length: length + 1 }) - -overwrite : CircularBuffer, I64 -> CircularBuffer -overwrite = |{ data, start, length }, value| - index = (start + length) % List.len(data) - new_data = data |> List.replace(index, value) |> .list - if length == List.len(data) then - inc_start = (start + 1) % List.len(data) - { data: new_data, start: inc_start, length: length } - else - { data: new_data, start, length: length + 1 } - -clear : CircularBuffer -> CircularBuffer -clear = |circular_buffer| - { data: circular_buffer.data, start: 0, length: 0 } + clear : CircularBuffer -> CircularBuffer + clear = |circular_buffer| { + { data: circular_buffer.data, start: 0, length: 0 } + } +} diff --git a/exercises/practice/circular-buffer/.meta/template.j2 b/exercises/practice/circular-buffer/.meta/template.j2 index a03dbc83..8125594e 100644 --- a/exercises/practice/circular-buffer/.meta/template.j2 +++ b/exercises/practice/circular-buffer/.meta/template.j2 @@ -2,45 +2,53 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [create, read, write, overwrite, clear] +import {{ exercise | to_pascal }} + +expect_value = |read_result, expected| { + expect read_result.value == expected + read_result.updated_buffer +} {% for case in cases -%} # {{ case["description"] }} -run_operations{{ loop.index }} = |_| - result = - create({ capacity: {{ case["input"]["capacity"] }} }) +expect { + {% if case["input"]["operations"][-1]["should_succeed"] -%} + _ + {%- endif -%} + result = {{ exercise | to_pascal }}.create({ capacity: {{ case["input"]["capacity"] }} }) {%- for op in case["input"]["operations"] -%} {%- if op["operation"] == "clear" %} - |> clear + .clear() {%- elif op["operation"] == "overwrite" %} - |> overwrite({{ op["item"] }}) + .overwrite({{ op["item"] }}) {%- elif op["operation"] == "write" %} {%- if op["should_succeed"] %} - |> write({{ op["item"] }})? + .write({{ op["item"] }})? {%- else %} - |> |buffer_before_write| - write_result = buffer_before_write |> write({{ op["item"] }}) - expect write_result == Err(BufferFull) - buffer_before_write + .write({{ op["item"] }}) + + result == Err(BufferFull) +} {%- endif %} {%- elif op["operation"] == "read" %} {%- if op["should_succeed"] %} - |> read? |> |read_result| - expect read_result.value == {{ op["expected"] }} - read_result.new_buffer + .read()? + -> expect_value({{ op["expected"] }}) {%- else %} - |> |buffer_before_read| - read_result = buffer_before_read |> read - expect read_result == Err(BufferEmpty) - buffer_before_read + .read() + + result == Err(BufferEmpty) +} {%- endif %} {%- endif %} {%- endfor %} - Ok(result) -expect - - result = run_operations{{ loop.index }}({}) - result |> Result.is_ok + {% if case["input"]["operations"][-1]["should_succeed"] -%} + Bool.True +} + + {% endif -%} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/circular-buffer/CircularBuffer.roc b/exercises/practice/circular-buffer/CircularBuffer.roc index 9fffc10b..8ddff23d 100644 --- a/exercises/practice/circular-buffer/CircularBuffer.roc +++ b/exercises/practice/circular-buffer/CircularBuffer.roc @@ -1,23 +1,26 @@ -module [create, read, write, overwrite, clear] +CircularBuffer :: { data : List(I64), start : U64, length : U64 }.{ + create : { capacity : U64 } -> CircularBuffer + create = |{ capacity }| { + crash "Please implement the 'create' function" + } -CircularBuffer : { data : List I64, start : U64, length : U64 } + read : CircularBuffer -> Try({ updated_buffer : CircularBuffer, value : I64 }, [BufferEmpty, ..]) + read = |circular_buffer| { + crash "Please implement the 'read' function" + } -create : { capacity : U64 } -> CircularBuffer -create = |{ capacity }| - crash("Please implement the 'create' function") + write : CircularBuffer, I64 -> Try(CircularBuffer, [BufferFull, ..]) + write = |circular_buffer, value| { + crash "Please implement the 'write' function" + } -read : CircularBuffer -> Result { new_buffer : CircularBuffer, value : I64 } [BufferEmpty] -read = |{ data, start, length }| - crash("Please implement the 'read' function") + overwrite : CircularBuffer, I64 -> CircularBuffer + overwrite = |circular_buffer, value| { + crash "Please implement the 'overwrite' function" + } -write : CircularBuffer, I64 -> Result CircularBuffer [BufferFull] -write = |circular_buffer, value| - crash("Please implement the 'write' function") - -overwrite : CircularBuffer, I64 -> CircularBuffer -overwrite = |{ data, start, length }, value| - crash("Please implement the 'overwrite' function") - -clear : CircularBuffer -> CircularBuffer -clear = |circular_buffer| - crash("Please implement the 'clear' function") + clear : CircularBuffer -> CircularBuffer + clear = |circular_buffer| { + crash "Please implement the 'clear' function" + } +} diff --git a/exercises/practice/circular-buffer/circular-buffer-test.roc b/exercises/practice/circular-buffer/circular-buffer-test.roc index 2409d32b..926730a8 100644 --- a/exercises/practice/circular-buffer/circular-buffer-test.roc +++ b/exercises/practice/circular-buffer/circular-buffer-test.roc @@ -1,290 +1,192 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/circular-buffer/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout +# File last updated on 2026-06-22 -main! = |_args| - Stdout.line!("") +import CircularBuffer -import CircularBuffer exposing [create, read, write, overwrite, clear] +expect_value = |read_result, expected| { + expect read_result.value == expected + read_result.updated_buffer +} # reading empty buffer should fail -run_operations1 = |_| - result = - create({ capacity: 1 }) - |> |buffer_before_read| - read_result = buffer_before_read |> read - expect read_result == Err(BufferEmpty) - buffer_before_read - Ok(result) +expect { + result = CircularBuffer.create({ capacity: 1 }) + .read() -expect - result = run_operations1({}) - result |> Result.is_ok + result == Err(BufferEmpty) +} # can read an item just written -run_operations2 = |_| - result = - create({ capacity: 1 }) - |> write(1)? - |> read? - |> |read_result| - expect read_result.value == 1 - read_result.new_buffer - Ok(result) +expect { + _result = CircularBuffer.create({ capacity: 1 }) + .write(1)? + .read()? + ->expect_value(1) -expect - result = run_operations2({}) - result |> Result.is_ok + Bool.True +} # each item may only be read once -run_operations3 = |_| - result = - create({ capacity: 1 }) - |> write(1)? - |> read? - |> |read_result| - expect read_result.value == 1 - read_result.new_buffer - |> |buffer_before_read| - read_result = buffer_before_read |> read - expect read_result == Err(BufferEmpty) - buffer_before_read - Ok(result) - -expect - result = run_operations3({}) - result |> Result.is_ok +expect { + result = CircularBuffer.create({ capacity: 1 }) + .write(1)? + .read()? + ->expect_value(1) + .read() + + result == Err(BufferEmpty) +} # items are read in the order they are written -run_operations4 = |_| - result = - create({ capacity: 2 }) - |> write(1)? - |> write(2)? - |> read? - |> |read_result| - expect read_result.value == 1 - read_result.new_buffer - |> read? - |> |read_result| - expect read_result.value == 2 - read_result.new_buffer - Ok(result) - -expect - result = run_operations4({}) - result |> Result.is_ok +expect { + _result = CircularBuffer.create({ capacity: 2 }) + .write(1)? + .write(2)? + .read()? + ->expect_value(1) + .read()? + ->expect_value(2) + + Bool.True +} # full buffer can't be written to -run_operations5 = |_| - result = - create({ capacity: 1 }) - |> write(1)? - |> |buffer_before_write| - write_result = buffer_before_write |> write(2) - expect write_result == Err(BufferFull) - buffer_before_write - Ok(result) +expect { + result = CircularBuffer.create({ capacity: 1 }) + .write(1)? + .write(2) -expect - result = run_operations5({}) - result |> Result.is_ok + result == Err(BufferFull) +} # a read frees up capacity for another write -run_operations6 = |_| - result = - create({ capacity: 1 }) - |> write(1)? - |> read? - |> |read_result| - expect read_result.value == 1 - read_result.new_buffer - |> write(2)? - |> read? - |> |read_result| - expect read_result.value == 2 - read_result.new_buffer - Ok(result) - -expect - result = run_operations6({}) - result |> Result.is_ok +expect { + _result = CircularBuffer.create({ capacity: 1 }) + .write(1)? + .read()? + ->expect_value(1) + .write(2)? + .read()? + ->expect_value(2) + + Bool.True +} # read position is maintained even across multiple writes -run_operations7 = |_| - result = - create({ capacity: 3 }) - |> write(1)? - |> write(2)? - |> read? - |> |read_result| - expect read_result.value == 1 - read_result.new_buffer - |> write(3)? - |> read? - |> |read_result| - expect read_result.value == 2 - read_result.new_buffer - |> read? - |> |read_result| - expect read_result.value == 3 - read_result.new_buffer - Ok(result) - -expect - result = run_operations7({}) - result |> Result.is_ok +expect { + _result = CircularBuffer.create({ capacity: 3 }) + .write(1)? + .write(2)? + .read()? + ->expect_value(1) + .write(3)? + .read()? + ->expect_value(2) + .read()? + ->expect_value(3) + + Bool.True +} # items cleared out of buffer can't be read -run_operations8 = |_| - result = - create({ capacity: 1 }) - |> write(1)? - |> clear - |> |buffer_before_read| - read_result = buffer_before_read |> read - expect read_result == Err(BufferEmpty) - buffer_before_read - Ok(result) +expect { + result = CircularBuffer.create({ capacity: 1 }) + .write(1)? + .clear() + .read() -expect - result = run_operations8({}) - result |> Result.is_ok + result == Err(BufferEmpty) +} # clear frees up capacity for another write -run_operations9 = |_| - result = - create({ capacity: 1 }) - |> write(1)? - |> clear - |> write(2)? - |> read? - |> |read_result| - expect read_result.value == 2 - read_result.new_buffer - Ok(result) - -expect - result = run_operations9({}) - result |> Result.is_ok +expect { + _result = CircularBuffer.create({ capacity: 1 }) + .write(1)? + .clear() + .write(2)? + .read()? + ->expect_value(2) + + Bool.True +} # clear does nothing on empty buffer -run_operations10 = |_| - result = - create({ capacity: 1 }) - |> clear - |> write(1)? - |> read? - |> |read_result| - expect read_result.value == 1 - read_result.new_buffer - Ok(result) - -expect - result = run_operations10({}) - result |> Result.is_ok +expect { + _result = CircularBuffer.create({ capacity: 1 }) + .clear() + .write(1)? + .read()? + ->expect_value(1) + + Bool.True +} # overwrite acts like write on non-full buffer -run_operations11 = |_| - result = - create({ capacity: 2 }) - |> write(1)? - |> overwrite(2) - |> read? - |> |read_result| - expect read_result.value == 1 - read_result.new_buffer - |> read? - |> |read_result| - expect read_result.value == 2 - read_result.new_buffer - Ok(result) - -expect - result = run_operations11({}) - result |> Result.is_ok +expect { + _result = CircularBuffer.create({ capacity: 2 }) + .write(1)? + .overwrite(2) + .read()? + ->expect_value(1) + .read()? + ->expect_value(2) + + Bool.True +} # overwrite replaces the oldest item on full buffer -run_operations12 = |_| - result = - create({ capacity: 2 }) - |> write(1)? - |> write(2)? - |> overwrite(3) - |> read? - |> |read_result| - expect read_result.value == 2 - read_result.new_buffer - |> read? - |> |read_result| - expect read_result.value == 3 - read_result.new_buffer - Ok(result) - -expect - result = run_operations12({}) - result |> Result.is_ok +expect { + _result = CircularBuffer.create({ capacity: 2 }) + .write(1)? + .write(2)? + .overwrite(3) + .read()? + ->expect_value(2) + .read()? + ->expect_value(3) + + Bool.True +} # overwrite replaces the oldest item remaining in buffer following a read -run_operations13 = |_| - result = - create({ capacity: 3 }) - |> write(1)? - |> write(2)? - |> write(3)? - |> read? - |> |read_result| - expect read_result.value == 1 - read_result.new_buffer - |> write(4)? - |> overwrite(5) - |> read? - |> |read_result| - expect read_result.value == 3 - read_result.new_buffer - |> read? - |> |read_result| - expect read_result.value == 4 - read_result.new_buffer - |> read? - |> |read_result| - expect read_result.value == 5 - read_result.new_buffer - Ok(result) - -expect - result = run_operations13({}) - result |> Result.is_ok +expect { + _result = CircularBuffer.create({ capacity: 3 }) + .write(1)? + .write(2)? + .write(3)? + .read()? + ->expect_value(1) + .write(4)? + .overwrite(5) + .read()? + ->expect_value(3) + .read()? + ->expect_value(4) + .read()? + ->expect_value(5) + + Bool.True +} # initial clear does not affect wrapping around -run_operations14 = |_| - result = - create({ capacity: 2 }) - |> clear - |> write(1)? - |> write(2)? - |> overwrite(3) - |> overwrite(4) - |> read? - |> |read_result| - expect read_result.value == 3 - read_result.new_buffer - |> read? - |> |read_result| - expect read_result.value == 4 - read_result.new_buffer - |> |buffer_before_read| - read_result = buffer_before_read |> read - expect read_result == Err(BufferEmpty) - buffer_before_read - Ok(result) - -expect - result = run_operations14({}) - result |> Result.is_ok +expect { + result = CircularBuffer.create({ capacity: 2 }) + .clear() + .write(1)? + .write(2)? + .overwrite(3) + .overwrite(4) + .read()? + ->expect_value(3) + .read()? + ->expect_value(4) + .read() + + result == Err(BufferEmpty) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/clock/.meta/Example.roc b/exercises/practice/clock/.meta/Example.roc index 1bbbd56e..e16c50ce 100644 --- a/exercises/practice/clock/.meta/Example.roc +++ b/exercises/practice/clock/.meta/Example.roc @@ -1,29 +1,34 @@ -module [create, to_str, add, subtract] +Clock :: { hour : U8, minute : U8 }.{ + create : { hour : I64, minute : I64 } -> Clock + create = |{ hour, minute }| { + hour24 = (hour % 24 + minute // 60) % 24 + minute60 = minute % 60 + minutes_per_day = 24 * 60 + total_minute = ((hour24 * 60 + minute60) % minutes_per_day + minutes_per_day) % minutes_per_day + hh = (total_minute // 60).to_u8_try() ?? { + crash "Unreachable" + } + mm = (total_minute % 60).to_u8_try() ?? { + crash "Unreachable" + } + { hour: hh, minute: mm } + } -Clock : { hour : U8, minute : U8 } + to_str : Clock -> Str + to_str = |{ hour, minute }| { + zero_padded = |num| "${if num < 10 "0" else ""}${num.to_str()}" + "${zero_padded(hour)}:${zero_padded(minute)}" + } -minutes_per_day = 24 * 60 + add : Clock, { hour : I64, minute : I64 } -> Clock + add = |clock, { hour, minute }| { + total_hour = clock.hour.to_i64() + (hour % 24 + minute // 60) + total_minute = clock.minute.to_i64() + minute % 60 + create({ hour: total_hour, minute: total_minute }) + } -create : { hours ?? I64, minutes ?? I64 }* -> Clock -create = |{ hours ?? 0, minutes ?? 0 }| - hours24 = (hours % 24 + minutes // 60) % 24 - minutes60 = minutes % 60 - total_minutes = ((hours24 * 60 + minutes60) % minutes_per_day + minutes_per_day) % minutes_per_day - hh = total_minutes // 60 |> Num.to_u8 - mm = total_minutes % 60 |> Num.to_u8 - { hour: hh, minute: mm } - -to_str : Clock -> Str -to_str = |{ hour, minute }| - zero_padded = |num| "${if num < 10 then "0" else ""}${num |> Num.to_str}" - "${zero_padded(hour)}:${zero_padded(minute)}" - -add : Clock, { hours ?? I64, minutes ?? I64 }* -> Clock -add = |{ hour, minute }, { hours ?? 0, minutes ?? 0 }| - total_hours = Num.to_i64(hour) + (hours % 24 + minutes // 60) - total_minutes = Num.to_i64(minute) + minutes % 60 - create({ hours: total_hours, minutes: total_minutes }) - -subtract : Clock, { hours ?? I64, minutes ?? I64 }* -> Clock -subtract = |clock, { hours ?? 0, minutes ?? 0 }| - clock |> add({ hours: -(hours % 24 + minutes // 60), minutes: -(minutes % 60) }) + subtract : Clock, { hour : I64, minute : I64 } -> Clock + subtract = |clock, { hour, minute }| { + clock.add({ hour: -(hour % 24 + minute // 60), minute: -(minute % 60) }) + } +} diff --git a/exercises/practice/clock/.meta/plugins.py b/exercises/practice/clock/.meta/plugins.py index 6be898e6..ff8f8f05 100644 --- a/exercises/practice/clock/.meta/plugins.py +++ b/exercises/practice/clock/.meta/plugins.py @@ -1,13 +1,4 @@ -def to_hours_minutes_record(input): - hours = input.get("hour", 0) - minutes = input.get("minute", 0) - fields = [] - if hours != 0: - fields.append(f"hours: {hours}") - if minutes != 0: - fields.append(f"minutes: {minutes}") - if fields: - content = ", ".join(fields) - return "{ " + content + " }" - else: - return "{}" +def to_hour_minute_record(input): + hour = input.get("hour", 0) + minute = input.get("minute", 0) + return f'{{hour: {hour}, minute: {minute}}}' \ No newline at end of file diff --git a/exercises/practice/clock/.meta/template.j2 b/exercises/practice/clock/.meta/template.j2 index 0455601a..90b02c56 100644 --- a/exercises/practice/clock/.meta/template.j2 +++ b/exercises/practice/clock/.meta/template.j2 @@ -8,22 +8,25 @@ import {{ exercise | to_pascal }} exposing [create, add, subtract, to_str] # {{ case["description"] }} {%- if case["property"] == "create" %} -expect - clock = create({{ plugins.to_hours_minutes_record(case["input"]) }}) - result = clock |> to_str +expect { + clock = create({{ plugins.to_hour_minute_record(case["input"]) }}) + result = clock.to_str() expected = {{ case["expected"] | to_roc }} result == expected +} {%- elif case["property"] in ["add", "subtract"] %} -expect - clock = create({{ plugins.to_hours_minutes_record(case["input"]) }}) - result = clock |> {{ case["property"] | to_snake }}({ minutes: {{ case["input"]["value"] }} }) |> to_str +expect { + clock = create({{ plugins.to_hour_minute_record(case["input"]) }}) + result = clock.{{ case["property"] | to_snake }}({ hour: 0, minute: {{ case["input"]["value"] }} }).to_str() expected = {{ case["expected"] | to_roc }} result == expected +} {%- elif case["property"] == "equal" %} -expect - clock1 = create({{ plugins.to_hours_minutes_record(case["input"]["clock1"]) }}) - clock2 = create({{ plugins.to_hours_minutes_record(case["input"]["clock2"]) }}) +expect { + clock1 = create({{ plugins.to_hour_minute_record(case["input"]["clock1"]) }}) + clock2 = create({{ plugins.to_hour_minute_record(case["input"]["clock2"]) }}) clock1 {%- if case["expected"] %} == {%- else %} != {% endif %} clock2 +} {%- else %} # this test case is not implemented yet. Perhaps you can give it a try? @@ -48,3 +51,5 @@ expect {% for case in additional_cases -%} {{ test_case(case) }} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/clock/Clock.roc b/exercises/practice/clock/Clock.roc index e08c7e53..83255724 100644 --- a/exercises/practice/clock/Clock.roc +++ b/exercises/practice/clock/Clock.roc @@ -1,20 +1,21 @@ -module [create, to_str, add, subtract] +Clock :: { hour : U8, minute : U8 }.{ + create : { hour : I64, minute : I64 } -> Clock + create = |{ hour, minute }| { + crash "Please implement the 'create' function" + } -Clock : { hour : U8, minute : U8 } + to_str : Clock -> Str + to_str = |clock| { + crash "Please implement the 'to_str' function" + } -create : { hours ?? I64, minutes ?? I64 }* -> Clock -create = |{ hours ?? 0, minutes ?? 0 }| - crash("Please implement the 'create' function") - -to_str : Clock -> Str -to_str = |{ hour, minute }| - crash("Please implement the 'to_str' function") - -add : Clock, { hours ?? I64, minutes ?? I64 }* -> Clock -add = |{ hour, minute }, { hours ?? 0, minutes ?? 0 }| - crash("Please implement the 'add' function") - -subtract : Clock, { hours ?? I64, minutes ?? I64 }* -> Clock -subtract = |clock, { hours ?? 0, minutes ?? 0 }| - crash("Please implement the 'subtract' function") + add : Clock, { hour : I64, minute : I64 } -> Clock + add = |clock, { hour, minute }| { + crash "Please implement the 'add' function" + } + subtract : Clock, { hour : I64, minute : I64 } -> Clock + subtract = |clock, { hour, minute }| { + crash "Please implement the 'subtract' function" + } +} diff --git a/exercises/practice/clock/clock-test.roc b/exercises/practice/clock/clock-test.roc index 4630af40..2fa44427 100644 --- a/exercises/practice/clock/clock-test.roc +++ b/exercises/practice/clock/clock-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/clock/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Clock exposing [create, add, subtract, to_str] @@ -17,408 +9,470 @@ import Clock exposing [create, add, subtract, to_str] ## # on the hour -expect - clock = create({ hours: 8 }) - result = clock |> to_str - expected = "08:00" - result == expected +expect { + clock = create({ hour: 8, minute: 0 }) + result = clock.to_str() + expected = "08:00" + result == expected +} # past the hour -expect - clock = create({ hours: 11, minutes: 9 }) - result = clock |> to_str - expected = "11:09" - result == expected +expect { + clock = create({ hour: 11, minute: 9 }) + result = clock.to_str() + expected = "11:09" + result == expected +} # midnight is zero hours -expect - clock = create({ hours: 24 }) - result = clock |> to_str - expected = "00:00" - result == expected +expect { + clock = create({ hour: 24, minute: 0 }) + result = clock.to_str() + expected = "00:00" + result == expected +} # hour rolls over -expect - clock = create({ hours: 25 }) - result = clock |> to_str - expected = "01:00" - result == expected +expect { + clock = create({ hour: 25, minute: 0 }) + result = clock.to_str() + expected = "01:00" + result == expected +} # hour rolls over continuously -expect - clock = create({ hours: 100 }) - result = clock |> to_str - expected = "04:00" - result == expected +expect { + clock = create({ hour: 100, minute: 0 }) + result = clock.to_str() + expected = "04:00" + result == expected +} # sixty minutes is next hour -expect - clock = create({ hours: 1, minutes: 60 }) - result = clock |> to_str - expected = "02:00" - result == expected +expect { + clock = create({ hour: 1, minute: 60 }) + result = clock.to_str() + expected = "02:00" + result == expected +} # minutes roll over -expect - clock = create({ minutes: 160 }) - result = clock |> to_str - expected = "02:40" - result == expected +expect { + clock = create({ hour: 0, minute: 160 }) + result = clock.to_str() + expected = "02:40" + result == expected +} # minutes roll over continuously -expect - clock = create({ minutes: 1723 }) - result = clock |> to_str - expected = "04:43" - result == expected +expect { + clock = create({ hour: 0, minute: 1723 }) + result = clock.to_str() + expected = "04:43" + result == expected +} # hour and minutes roll over -expect - clock = create({ hours: 25, minutes: 160 }) - result = clock |> to_str - expected = "03:40" - result == expected +expect { + clock = create({ hour: 25, minute: 160 }) + result = clock.to_str() + expected = "03:40" + result == expected +} # hour and minutes roll over continuously -expect - clock = create({ hours: 201, minutes: 3001 }) - result = clock |> to_str - expected = "11:01" - result == expected +expect { + clock = create({ hour: 201, minute: 3001 }) + result = clock.to_str() + expected = "11:01" + result == expected +} # hour and minutes roll over to exactly midnight -expect - clock = create({ hours: 72, minutes: 8640 }) - result = clock |> to_str - expected = "00:00" - result == expected +expect { + clock = create({ hour: 72, minute: 8640 }) + result = clock.to_str() + expected = "00:00" + result == expected +} # negative hour -expect - clock = create({ hours: -1, minutes: 15 }) - result = clock |> to_str - expected = "23:15" - result == expected +expect { + clock = create({ hour: -1, minute: 15 }) + result = clock.to_str() + expected = "23:15" + result == expected +} # negative hour rolls over -expect - clock = create({ hours: -25 }) - result = clock |> to_str - expected = "23:00" - result == expected +expect { + clock = create({ hour: -25, minute: 0 }) + result = clock.to_str() + expected = "23:00" + result == expected +} # negative hour rolls over continuously -expect - clock = create({ hours: -91 }) - result = clock |> to_str - expected = "05:00" - result == expected +expect { + clock = create({ hour: -91, minute: 0 }) + result = clock.to_str() + expected = "05:00" + result == expected +} # negative minutes -expect - clock = create({ hours: 1, minutes: -40 }) - result = clock |> to_str - expected = "00:20" - result == expected +expect { + clock = create({ hour: 1, minute: -40 }) + result = clock.to_str() + expected = "00:20" + result == expected +} # negative minutes roll over -expect - clock = create({ hours: 1, minutes: -160 }) - result = clock |> to_str - expected = "22:20" - result == expected +expect { + clock = create({ hour: 1, minute: -160 }) + result = clock.to_str() + expected = "22:20" + result == expected +} # negative minutes roll over continuously -expect - clock = create({ hours: 1, minutes: -4820 }) - result = clock |> to_str - expected = "16:40" - result == expected +expect { + clock = create({ hour: 1, minute: -4820 }) + result = clock.to_str() + expected = "16:40" + result == expected +} # negative sixty minutes is previous hour -expect - clock = create({ hours: 2, minutes: -60 }) - result = clock |> to_str - expected = "01:00" - result == expected +expect { + clock = create({ hour: 2, minute: -60 }) + result = clock.to_str() + expected = "01:00" + result == expected +} # negative hour and minutes both roll over -expect - clock = create({ hours: -25, minutes: -160 }) - result = clock |> to_str - expected = "20:20" - result == expected +expect { + clock = create({ hour: -25, minute: -160 }) + result = clock.to_str() + expected = "20:20" + result == expected +} # negative hour and minutes both roll over continuously -expect - clock = create({ hours: -121, minutes: -5810 }) - result = clock |> to_str - expected = "22:10" - result == expected +expect { + clock = create({ hour: -121, minute: -5810 }) + result = clock.to_str() + expected = "22:10" + result == expected +} ## ## Add minutes ## # add minutes -expect - clock = create({ hours: 10 }) - result = clock |> add({ minutes: 3 }) |> to_str - expected = "10:03" - result == expected +expect { + clock = create({ hour: 10, minute: 0 }) + result = clock.add({ hour: 0, minute: 3 }).to_str() + expected = "10:03" + result == expected +} # add no minutes -expect - clock = create({ hours: 6, minutes: 41 }) - result = clock |> add({ minutes: 0 }) |> to_str - expected = "06:41" - result == expected +expect { + clock = create({ hour: 6, minute: 41 }) + result = clock.add({ hour: 0, minute: 0 }).to_str() + expected = "06:41" + result == expected +} # add to next hour -expect - clock = create({ minutes: 45 }) - result = clock |> add({ minutes: 40 }) |> to_str - expected = "01:25" - result == expected +expect { + clock = create({ hour: 0, minute: 45 }) + result = clock.add({ hour: 0, minute: 40 }).to_str() + expected = "01:25" + result == expected +} # add more than one hour -expect - clock = create({ hours: 10 }) - result = clock |> add({ minutes: 61 }) |> to_str - expected = "11:01" - result == expected +expect { + clock = create({ hour: 10, minute: 0 }) + result = clock.add({ hour: 0, minute: 61 }).to_str() + expected = "11:01" + result == expected +} # add more than two hours with carry -expect - clock = create({ minutes: 45 }) - result = clock |> add({ minutes: 160 }) |> to_str - expected = "03:25" - result == expected +expect { + clock = create({ hour: 0, minute: 45 }) + result = clock.add({ hour: 0, minute: 160 }).to_str() + expected = "03:25" + result == expected +} # add across midnight -expect - clock = create({ hours: 23, minutes: 59 }) - result = clock |> add({ minutes: 2 }) |> to_str - expected = "00:01" - result == expected +expect { + clock = create({ hour: 23, minute: 59 }) + result = clock.add({ hour: 0, minute: 2 }).to_str() + expected = "00:01" + result == expected +} # add more than one day (1500 min = 25 hrs) -expect - clock = create({ hours: 5, minutes: 32 }) - result = clock |> add({ minutes: 1500 }) |> to_str - expected = "06:32" - result == expected +expect { + clock = create({ hour: 5, minute: 32 }) + result = clock.add({ hour: 0, minute: 1500 }).to_str() + expected = "06:32" + result == expected +} # add more than two days -expect - clock = create({ hours: 1, minutes: 1 }) - result = clock |> add({ minutes: 3500 }) |> to_str - expected = "11:21" - result == expected +expect { + clock = create({ hour: 1, minute: 1 }) + result = clock.add({ hour: 0, minute: 3500 }).to_str() + expected = "11:21" + result == expected +} ## ## Subtract minutes ## # subtract minutes -expect - clock = create({ hours: 10, minutes: 3 }) - result = clock |> subtract({ minutes: 3 }) |> to_str - expected = "10:00" - result == expected +expect { + clock = create({ hour: 10, minute: 3 }) + result = clock.subtract({ hour: 0, minute: 3 }).to_str() + expected = "10:00" + result == expected +} # subtract to previous hour -expect - clock = create({ hours: 10, minutes: 3 }) - result = clock |> subtract({ minutes: 30 }) |> to_str - expected = "09:33" - result == expected +expect { + clock = create({ hour: 10, minute: 3 }) + result = clock.subtract({ hour: 0, minute: 30 }).to_str() + expected = "09:33" + result == expected +} # subtract more than an hour -expect - clock = create({ hours: 10, minutes: 3 }) - result = clock |> subtract({ minutes: 70 }) |> to_str - expected = "08:53" - result == expected +expect { + clock = create({ hour: 10, minute: 3 }) + result = clock.subtract({ hour: 0, minute: 70 }).to_str() + expected = "08:53" + result == expected +} # subtract across midnight -expect - clock = create({ minutes: 3 }) - result = clock |> subtract({ minutes: 4 }) |> to_str - expected = "23:59" - result == expected +expect { + clock = create({ hour: 0, minute: 3 }) + result = clock.subtract({ hour: 0, minute: 4 }).to_str() + expected = "23:59" + result == expected +} # subtract more than two hours -expect - clock = create({}) - result = clock |> subtract({ minutes: 160 }) |> to_str - expected = "21:20" - result == expected +expect { + clock = create({ hour: 0, minute: 0 }) + result = clock.subtract({ hour: 0, minute: 160 }).to_str() + expected = "21:20" + result == expected +} # subtract more than two hours with borrow -expect - clock = create({ hours: 6, minutes: 15 }) - result = clock |> subtract({ minutes: 160 }) |> to_str - expected = "03:35" - result == expected +expect { + clock = create({ hour: 6, minute: 15 }) + result = clock.subtract({ hour: 0, minute: 160 }).to_str() + expected = "03:35" + result == expected +} # subtract more than one day (1500 min = 25 hrs) -expect - clock = create({ hours: 5, minutes: 32 }) - result = clock |> subtract({ minutes: 1500 }) |> to_str - expected = "04:32" - result == expected +expect { + clock = create({ hour: 5, minute: 32 }) + result = clock.subtract({ hour: 0, minute: 1500 }).to_str() + expected = "04:32" + result == expected +} # subtract more than two days -expect - clock = create({ hours: 2, minutes: 20 }) - result = clock |> subtract({ minutes: 3000 }) |> to_str - expected = "00:20" - result == expected +expect { + clock = create({ hour: 2, minute: 20 }) + result = clock.subtract({ hour: 0, minute: 3000 }).to_str() + expected = "00:20" + result == expected +} ## ## Compare two clocks for equality ## # clocks with same time -expect - clock1 = create({ hours: 15, minutes: 37 }) - clock2 = create({ hours: 15, minutes: 37 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 15, minute: 37 }) + clock2 = create({ hour: 15, minute: 37 }) + clock1 == clock2 +} # clocks a minute apart -expect - clock1 = create({ hours: 15, minutes: 36 }) - clock2 = create({ hours: 15, minutes: 37 }) - clock1 != clock2 +expect { + clock1 = create({ hour: 15, minute: 36 }) + clock2 = create({ hour: 15, minute: 37 }) + clock1 != clock2 +} # clocks an hour apart -expect - clock1 = create({ hours: 14, minutes: 37 }) - clock2 = create({ hours: 15, minutes: 37 }) - clock1 != clock2 +expect { + clock1 = create({ hour: 14, minute: 37 }) + clock2 = create({ hour: 15, minute: 37 }) + clock1 != clock2 +} # clocks with hour overflow -expect - clock1 = create({ hours: 10, minutes: 37 }) - clock2 = create({ hours: 34, minutes: 37 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 10, minute: 37 }) + clock2 = create({ hour: 34, minute: 37 }) + clock1 == clock2 +} # clocks with hour overflow by several days -expect - clock1 = create({ hours: 3, minutes: 11 }) - clock2 = create({ hours: 99, minutes: 11 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 3, minute: 11 }) + clock2 = create({ hour: 99, minute: 11 }) + clock1 == clock2 +} # clocks with negative hour -expect - clock1 = create({ hours: 22, minutes: 40 }) - clock2 = create({ hours: -2, minutes: 40 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 22, minute: 40 }) + clock2 = create({ hour: -2, minute: 40 }) + clock1 == clock2 +} # clocks with negative hour that wraps -expect - clock1 = create({ hours: 17, minutes: 3 }) - clock2 = create({ hours: -31, minutes: 3 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 17, minute: 3 }) + clock2 = create({ hour: -31, minute: 3 }) + clock1 == clock2 +} # clocks with negative hour that wraps multiple times -expect - clock1 = create({ hours: 13, minutes: 49 }) - clock2 = create({ hours: -83, minutes: 49 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 13, minute: 49 }) + clock2 = create({ hour: -83, minute: 49 }) + clock1 == clock2 +} # clocks with minute overflow -expect - clock1 = create({ minutes: 1 }) - clock2 = create({ minutes: 1441 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 0, minute: 1 }) + clock2 = create({ hour: 0, minute: 1441 }) + clock1 == clock2 +} # clocks with minute overflow by several days -expect - clock1 = create({ hours: 2, minutes: 2 }) - clock2 = create({ hours: 2, minutes: 4322 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 2, minute: 2 }) + clock2 = create({ hour: 2, minute: 4322 }) + clock1 == clock2 +} # clocks with negative minute -expect - clock1 = create({ hours: 2, minutes: 40 }) - clock2 = create({ hours: 3, minutes: -20 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 2, minute: 40 }) + clock2 = create({ hour: 3, minute: -20 }) + clock1 == clock2 +} # clocks with negative minute that wraps -expect - clock1 = create({ hours: 4, minutes: 10 }) - clock2 = create({ hours: 5, minutes: -1490 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 4, minute: 10 }) + clock2 = create({ hour: 5, minute: -1490 }) + clock1 == clock2 +} # clocks with negative minute that wraps multiple times -expect - clock1 = create({ hours: 6, minutes: 15 }) - clock2 = create({ hours: 6, minutes: -4305 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 6, minute: 15 }) + clock2 = create({ hour: 6, minute: -4305 }) + clock1 == clock2 +} # clocks with negative hours and minutes -expect - clock1 = create({ hours: 7, minutes: 32 }) - clock2 = create({ hours: -12, minutes: -268 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 7, minute: 32 }) + clock2 = create({ hour: -12, minute: -268 }) + clock1 == clock2 +} # clocks with negative hours and minutes that wrap -expect - clock1 = create({ hours: 18, minutes: 7 }) - clock2 = create({ hours: -54, minutes: -11513 }) - clock1 == clock2 +expect { + clock1 = create({ hour: 18, minute: 7 }) + clock2 = create({ hour: -54, minute: -11513 }) + clock1 == clock2 +} # full clock and zeroed clock -expect - clock1 = create({ hours: 24 }) - clock2 = create({}) - clock1 == clock2 +expect { + clock1 = create({ hour: 24, minute: 0 }) + clock2 = create({ hour: 0, minute: 0 }) + clock1 == clock2 +} ## ## Extreme I64 values should not crash with overflow errors ## # Can create a clock with max I64 values -expect - clock = create({ hours: 9223372036854775807, minutes: 9223372036854775807 }) - result = clock |> to_str - expected = "01:07" - result == expected +expect { + clock = create({ hour: 9223372036854775807, minute: 9223372036854775807 }) + result = clock.to_str() + expected = "01:07" + result == expected +} # Can create a clock with min I64 values -expect - clock = create({ hours: -9223372036854775808, minutes: -9223372036854775808 }) - result = clock |> to_str - expected = "21:52" - result == expected +expect { + clock = create({ hour: -9223372036854775808, minute: -9223372036854775808 }) + result = clock.to_str() + expected = "21:52" + result == expected +} # Can add max I64 values to a clock -expect - clock = create({ hours: 23, minutes: 59 }) - result = clock |> add({ minutes: 9223372036854775807 }) |> to_str - expected = "18:06" - result == expected +expect { + clock = create({ hour: 23, minute: 59 }) + result = clock.add({ hour: 0, minute: 9223372036854775807 }).to_str() + expected = "18:06" + result == expected +} # Can add min I64 values to a clock -expect - clock = create({ hours: 23, minutes: 59 }) - result = clock |> add({ minutes: -9223372036854775808 }) |> to_str - expected = "05:51" - result == expected +expect { + clock = create({ hour: 23, minute: 59 }) + result = clock.add({ hour: 0, minute: -9223372036854775808 }).to_str() + expected = "05:51" + result == expected +} # Can subtract max I64 values from a clock -expect - clock = create({ hours: 23, minutes: 59 }) - result = clock |> subtract({ minutes: 9223372036854775807 }) |> to_str - expected = "05:52" - result == expected +expect { + clock = create({ hour: 23, minute: 59 }) + result = clock.subtract({ hour: 0, minute: 9223372036854775807 }).to_str() + expected = "05:52" + result == expected +} # Can subtract min I64 values from a clock -expect - clock = create({ hours: 23, minutes: 59 }) - result = clock |> subtract({ minutes: -9223372036854775808 }) |> to_str - expected = "18:07" - result == expected +expect { + clock = create({ hour: 23, minute: 59 }) + result = clock.subtract({ hour: 0, minute: -9223372036854775808 }).to_str() + expected = "18:07" + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/collatz-conjecture/.meta/Example.roc b/exercises/practice/collatz-conjecture/.meta/Example.roc index 907382c2..b6136ed0 100644 --- a/exercises/practice/collatz-conjecture/.meta/Example.roc +++ b/exercises/practice/collatz-conjecture/.meta/Example.roc @@ -1,12 +1,14 @@ -module [steps] - -steps : U64 -> Result U64 [NumberArgWasZero] -steps = |number| - if number <= 0 then - Err(NumberArgWasZero) - else if number == 1 then - Ok(0) - else if Num.is_even(number) then - Ok(steps(number // 2)? + 1) - else - Ok(steps(3 * number + 1)? + 1) +CollatzConjecture :: {}.{ + steps : U64 -> Try(U64, [NumberArgWasZero, ..]) + steps = |number| { + if number <= 0 { + Err(NumberArgWasZero) + } else if number == 1 { + Ok(0) + } else if number % 2 == 0 { + Ok(steps(number // 2)? + 1) + } else { + Ok(steps(3 * number + 1)? + 1) + } + } +} diff --git a/exercises/practice/collatz-conjecture/.meta/template.j2 b/exercises/practice/collatz-conjecture/.meta/template.j2 index ce67bec3..ed9ef422 100644 --- a/exercises/practice/collatz-conjecture/.meta/template.j2 +++ b/exercises/practice/collatz-conjecture/.meta/template.j2 @@ -6,12 +6,15 @@ import {{ exercise | to_pascal }} exposing [steps] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["number"] }}) {%- if case["expected"]["error"] %} - Result.is_err(result) + result.is_err() {%- else %} result == Ok({{ case["expected"] }}) {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/collatz-conjecture/CollatzConjecture.roc b/exercises/practice/collatz-conjecture/CollatzConjecture.roc index f9d24564..7dd2696d 100644 --- a/exercises/practice/collatz-conjecture/CollatzConjecture.roc +++ b/exercises/practice/collatz-conjecture/CollatzConjecture.roc @@ -1,5 +1,6 @@ -module [steps] - -steps : U64 -> Result U64 _ -steps = |number| - crash("Please implement the `steps` function") +CollatzConjecture :: {}.{ + steps : U64 -> Try(U64, _) + steps = |number| { + crash "Please implement the 'steps' function" + } +} diff --git a/exercises/practice/collatz-conjecture/collatz-conjecture-test.roc b/exercises/practice/collatz-conjecture/collatz-conjecture-test.roc index d8044726..b696f35d 100644 --- a/exercises/practice/collatz-conjecture/collatz-conjecture-test.roc +++ b/exercises/practice/collatz-conjecture/collatz-conjecture-test.roc @@ -1,39 +1,40 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/collatz-conjecture/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import CollatzConjecture exposing [steps] # zero steps for one -expect - result = steps(1) - result == Ok(0) +expect { + result = steps(1) + result == Ok(0) +} # divide if even -expect - result = steps(16) - result == Ok(4) +expect { + result = steps(16) + result == Ok(4) +} # even and odd steps -expect - result = steps(12) - result == Ok(9) +expect { + result = steps(12) + result == Ok(9) +} # large number of even and odd steps -expect - result = steps(1000000) - result == Ok(152) +expect { + result = steps(1000000) + result == Ok(152) +} # zero is an error -expect - result = steps(0) - Result.is_err(result) +expect { + result = steps(0) + 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/complex-numbers/.meta/Example.roc b/exercises/practice/complex-numbers/.meta/Example.roc index a0e872f4..d895e7b3 100644 --- a/exercises/practice/complex-numbers/.meta/Example.roc +++ b/exercises/practice/complex-numbers/.meta/Example.roc @@ -1,53 +1,229 @@ -module [real, imaginary, add, sub, mul, div, conjugate, abs, exp] +Complex := { real : F64, imag : F64 }.{ + new : { real : F64, imag : F64 } -> Complex + new = |{ real, imag }| { + { real, imag } + } -Complex : { re : F64, im : F64 } + # # The user can write plus(z1, z2), z1.plus(z2), or simply z1 + z2 + plus : Complex, Complex -> Complex + plus = |{ real: a, imag: b }, { real: c, imag: d }| { + { + real: a + c, + imag: b + d, + } + } -real : Complex -> F64 -real = |z| z.re + # # The user can write minus(z1, z2), z1.minus(z2), or simply z1 - z2 + minus : Complex, Complex -> Complex + minus = |{ real: a, imag: b }, { real: c, imag: d }| { + { + real: a - c, + imag: b - d, + } + } -imaginary : Complex -> F64 -imaginary = |z| z.im + # # The user can write times(z1, z2), z1.times(z2), or simply z1 * z2 + times : Complex, Complex -> Complex + times = |{ real: a, imag: b }, { real: c, imag: d }| { + { + real: a * c - b * d, + imag: a * d + b * c, + } + } -add : Complex, Complex -> Complex -add = |{ re: a, im: b }, { re: c, im: d }| { - re: a + c, - im: b + d, + # # The user can write div_by(z1, z2), z1.div_by(z2), or simply z1 / z2 + div_by : Complex, Complex -> Complex + div_by = |{ real: a, imag: b }, { real: c, imag: d }| { + denominator = c * c + d * d + { + real: (a * c + b * d) / denominator, + imag: (b * c - a * d) / denominator, + } + } + + conjugate : Complex -> Complex + conjugate = |z| { + { + real: z.real, + imag: -z.imag, + } + } + + abs : Complex -> F64 + abs = |{ real: a, imag: b }| { + sqrt(a * a + b * b) + } + + exp : Complex -> Complex + exp = |z| { + factor = e->pow(z.real) + { + real: factor * cos(z.imag), + imag: factor * sin(z.imag), + } + } } -sub : Complex, Complex -> Complex -sub = |{ re: a, im: b }, { re: c, im: d }| { - re: a - c, - im: b - d, +# The following function should soon be available in Roc's builtins + +e = 2.718281828459045.F64 + +pi = 3.141592653589793.F64 + +# Calculates the natural logarithm of x, ln(x). +ln : F64 -> F64 +ln = |x| { + if x <= 0.0 { + # Natural log is undefined for zero and negative numbers + crash "ln is undefined for zero or negative numbers" + } else { + var $norm_x = x + var $log_offset = 0.0 + + # Range reduction: keep x between [1/e, e] + while $norm_x > e { + $norm_x = $norm_x / e + $log_offset = $log_offset + 1.0 + } + while $norm_x < 1 / e { + $norm_x = $norm_x * e + $log_offset = $log_offset - 1.0 + } + + # Area hyperbolic tangent series + z = ($norm_x - 1.0) / ($norm_x + 1.0) + z2 = z * z + var $z_term = z + var $ln_sum = 0.0 + var $ln_n = 1.0 + + for _ in 0..<30.U8 { + $ln_sum = $ln_sum + ($z_term / $ln_n) + $z_term = $z_term * z2 + $ln_n = $ln_n + 2.0 + } + + (2.0 * $ln_sum) + $log_offset + } } -mul : Complex, Complex -> Complex -mul = |{ re: a, im: b }, { re: c, im: d }| { - re: a * c - b * d, - im: a * d + b * c, +# Calculates e^x using the Taylor series. +exp : F64 -> F64 +exp = |x| { + var $norm_x = x + var $exp_mult = 1.0 + + # Range reduction: keep x between [-1.0, 1.0] + while $norm_x > 1.0 { + $norm_x = $norm_x - 1.0 + $exp_mult = $exp_mult * e + } + while $norm_x < -1.0 { + $norm_x = $norm_x + 1.0 + $exp_mult = $exp_mult / e + } + + # Taylor series + var $exp_term = 1.0 + var $exp_sum = 1.0 + var $exp_n = 1.0 + + for _ in 0..<25.U8 { + $exp_term = $exp_term * $norm_x / $exp_n + $exp_sum = $exp_sum + $exp_term + $exp_n = $exp_n + 1.0 + } + + $exp_sum * $exp_mult } -div : Complex, Complex -> Complex -div = |{ re: a, im: b }, { re: c, im: d }| - denominator = c * c + d * d - { - re: (a * c + b * d) / denominator, - im: (b * c - a * d) / denominator, - } - -conjugate : Complex -> Complex -conjugate = |z| { - re: z.re, - im: -z.im, +# Calculates x^p for 64-bit values using the newly separated functions. +pow : F64, F64 -> F64 +pow = |x, p| { + if x == 0.0 { + if p == 0.0 { + 1.0 + } else { + 0.0 + } + } else if x < 0.0 { + # Fractional powers of negative numbers are undefined in pure real 64-bit math + crash "Raising a negative number to a fractional power is undefined" + } else if p == 0.0 { + 1.0 + } else { + # The core mathematical calculation + exp(p * ln(x)) + } } -abs : Complex -> F64 -abs = |{ re: a, im: b }| - a * a + b * b |> Num.sqrt - -exp : Complex -> Complex -exp = |z| - factor = Num.e |> Num.pow(z.re) - { - re: factor * Num.cos(z.im), - im: factor * Num.sin(z.im), - } +# cos uses the Taylor series expansion. +# We first normalize `x` to the [-PI, PI] range to ensure rapid, stable convergence. +cos : F64 -> F64 +cos = |x| { + var $norm_x = x + while $norm_x > pi { + $norm_x = $norm_x - 2 * pi + } + while $norm_x < -pi { + $norm_x = $norm_x + 2 * pi + } + + var $term = 1.0 + var $sum = 1.0 + var $n = 0.0 + + # 20 iterations is more than enough for F64 precision in this range + for _ in 0..<20.U8 { + $term = { + -($term) * $norm_x * $norm_x / (($n + 1.0) * ($n + 2.0)) + } + $sum = $sum + $term + $n = $n + 2.0 + } + + $sum +} + +# sin uses the Taylor series expansion. +sin : F64 -> F64 +sin = |x| { + var $norm_x = x + while $norm_x > pi { + $norm_x = $norm_x - 2 * pi + } + while $norm_x < -pi { + $norm_x = $norm_x + 2 * pi + } + + var $term = $norm_x + var $sum = $norm_x + var $n = 1.0 + + for _ in 0..<20.U8 { + $term = { + -($term) * $norm_x * $norm_x / (($n + 1.0) * ($n + 2.0)) + } + $sum = $sum + $term + $n = $n + 2.0 + } + + $sum +} + +# sqrt uses the Babylonian method (Newton-Raphson approach). +sqrt : F64 -> F64 +sqrt = |x| { + if x <= 0.0 { + 0.0 + } else { + var $guess = x / 2.0 + + # 25 iterations will aggressively converge for almost all F64 values + for _ in 0..<25.U8 { + $guess = ($guess + (x / $guess)) / 2.0 + } + + $guess + } +} diff --git a/exercises/practice/complex-numbers/.meta/config.json b/exercises/practice/complex-numbers/.meta/config.json index 4270326b..5a17e1f9 100644 --- a/exercises/practice/complex-numbers/.meta/config.json +++ b/exercises/practice/complex-numbers/.meta/config.json @@ -4,7 +4,7 @@ ], "files": { "solution": [ - "ComplexNumbers.roc" + "Complex.roc" ], "test": [ "complex-numbers-test.roc" diff --git a/exercises/practice/complex-numbers/.meta/plugins.py b/exercises/practice/complex-numbers/.meta/plugins.py index 5459402e..9d4947ee 100644 --- a/exercises/practice/complex-numbers/.meta/plugins.py +++ b/exercises/practice/complex-numbers/.meta/plugins.py @@ -1,9 +1,9 @@ float_map = { - "e": "Num.e", - "ln(2)": "Num.log(2f64)", - "ln(2)/2": "Num.log(2f64) / 2", - "pi": "Num.pi", - "pi/4": "Num.pi / 4", + "e": "2.718281828459045.F64", + "ln(2)": "0.6931471805599453.F64", + "ln(2)/2": "0.6931471805599453.F64 / 2", + "pi": "3.141592653589793.F64", + "pi/4": "3.141592653589793.F64 / 4", } @@ -22,4 +22,4 @@ def to_complex_number(value): else: re = to_roc(value) im = 0 - return f"{{ re: {re}, im: {im} }}" + return f"Complex.new({{real: {re}, imag: {im}}})" diff --git a/exercises/practice/complex-numbers/.meta/template.j2 b/exercises/practice/complex-numbers/.meta/template.j2 index 64464d1e..01868630 100644 --- a/exercises/practice/complex-numbers/.meta/template.j2 +++ b/exercises/practice/complex-numbers/.meta/template.j2 @@ -1,11 +1,19 @@ {%- import "generator_macros.j2" as macros with context -%} {{ macros.canonical_ref() }} {{ macros.header() }} +{% set equations = { + "real": "z.real", + "imaginary": "z.imag", + "conjugate": "z.conjugate()", + "abs": "z.abs()", + "exp": "z.exp()", + "add": "z1 + z2", + "sub": "z1 - z2", + "mul": "z1 * z2", + "div": "z1 / z2" +} -%} -import {{ exercise | to_pascal }} exposing [real, imaginary, add, sub, mul, div, conjugate, abs, exp] - -is_approx_eq = |z1, z2| - z1.re |> Num.is_approx_eq(z2.re, {}) && z1.im |> Num.is_approx_eq(z2.im, {}) +import Complex {% for supercase in cases %} ### @@ -23,22 +31,24 @@ is_approx_eq = |z1, z2| {% for subcase in subcases -%} # {{ subcase["description"] }} {%- if subcase["input"]["z"] %} -expect +expect { z = {{ plugins.to_complex_number(subcase["input"]["z"]) }} - result = {{ subcase["property"] }}(z) + result = {{ equations[subcase["property"]] }} {%- if subcase["expected"] is iterable and subcase["expected"] is not string %} expected = {{ plugins.to_complex_number(subcase["expected"]) }} - result |> is_approx_eq(expected) + result -> complex_is_approx_eq(expected) {%- else %} - result |> Num.is_approx_eq({{ subcase["expected"] | to_roc }}, {}) + result -> is_approx_eq({{ subcase["expected"] | to_roc }}) {%- endif %} +} {%- elif subcase["input"]["z1"] %} -expect +expect { z1 = {{ plugins.to_complex_number(subcase["input"]["z1"]) }} z2 = {{ plugins.to_complex_number(subcase["input"]["z2"]) }} - result = z1 |> {{ subcase["property"] }}(z2) + result = {{ equations[subcase["property"]] }} expected = {{ plugins.to_complex_number(subcase["expected"]) }} - result |> is_approx_eq(expected) + result -> complex_is_approx_eq(expected) +} {%- endif %} {% endfor %} @@ -46,3 +56,14 @@ expect {% endfor %} {% endfor %} +is_approx_eq = |x1, x2| { + i1 = (x1 * 1000 + 0.5).to_i64_try() ?? { crash "Unreachable" } + i2 = (x2 * 1000 + 0.5).to_i64_try() ?? { crash "Unreachable" } + i1 == i2 +} + +complex_is_approx_eq = |z1, z2| { + is_approx_eq(z1.real, z2.real) and is_approx_eq(z1.imag, z2.imag) +} + +{{ macros.footer() }} diff --git a/exercises/practice/complex-numbers/Complex.roc b/exercises/practice/complex-numbers/Complex.roc new file mode 100644 index 00000000..6d00162a --- /dev/null +++ b/exercises/practice/complex-numbers/Complex.roc @@ -0,0 +1,45 @@ +Complex := { real : F64, imag : F64 }.{ + new : { real : F64, imag : F64 } -> Complex + new = |{ real, imag }| { + crash "Please implement the 'new' function" + } + + # # The user can write plus(z1, z2), z1.plus(z2), or simply z1 + z2 + plus : Complex, Complex -> Complex + plus = |z1, z2| { + crash "Please implement the 'plus' function" + } + + # # The user can write minus(z1, z2), z1.minus(z2), or simply z1 - z2 + minus : Complex, Complex -> Complex + minus = |z1, z2| { + crash "Please implement the 'minus' function" + } + + # # The user can write times(z1, z2), z1.times(z2), or simply z1 * z2 + times : Complex, Complex -> Complex + times = |z1, z2| { + crash "Please implement the 'times' function" + } + + # # The user can write div_by(z1, z2), z1.div_by(z2), or simply z1 / z2 + div_by : Complex, Complex -> Complex + div_by = |z1, z2| { + crash "Please implement the 'div_by' function" + } + + conjugate : Complex -> Complex + conjugate = |z| { + crash "Please implement the 'conjugate' function" + } + + abs : Complex -> F64 + abs = |z| { + crash "Please implement the 'abs' function" + } + + exp : Complex -> Complex + exp = |z| { + crash "Please implement the 'exp' function" + } +} diff --git a/exercises/practice/complex-numbers/ComplexNumbers.roc b/exercises/practice/complex-numbers/ComplexNumbers.roc deleted file mode 100644 index 207ef99c..00000000 --- a/exercises/practice/complex-numbers/ComplexNumbers.roc +++ /dev/null @@ -1,39 +0,0 @@ -module [real, imaginary, add, sub, mul, div, conjugate, abs, exp] - -Complex : { re : F64, im : F64 } - -real : Complex -> F64 -real = |z| - crash("Please implement the 'real' function") - -imaginary : Complex -> F64 -imaginary = |z| - crash("Please implement the 'imaginary' function") - -add : Complex, Complex -> Complex -add = |z1, z2| - crash("Please implement the 'add' function") - -sub : Complex, Complex -> Complex -sub = |z1, z2| - crash("Please implement the 'sub' function") - -mul : Complex, Complex -> Complex -mul = |z1, z2| - crash("Please implement the 'mul' function") - -div : Complex, Complex -> Complex -div = |z1, z2| - crash("Please implement the 'div' function") - -conjugate : Complex -> Complex -conjugate = |z| - crash("Please implement the 'conjugate' function") - -abs : Complex -> F64 -abs = |z| - crash("Please implement the 'abs' function") - -exp : Complex -> Complex -exp = |z| - crash("Please implement the 'exp' function") diff --git a/exercises/practice/complex-numbers/complex-numbers-test.roc b/exercises/practice/complex-numbers/complex-numbers-test.roc index 3b02296a..55591ce8 100644 --- a/exercises/practice/complex-numbers/complex-numbers-test.roc +++ b/exercises/practice/complex-numbers/complex-numbers-test.roc @@ -1,63 +1,58 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/complex-numbers/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-23 -import ComplexNumbers exposing [real, imaginary, add, sub, mul, div, conjugate, abs, exp] - -is_approx_eq = |z1, z2| - z1.re |> Num.is_approx_eq(z2.re, {}) and z1.im |> Num.is_approx_eq(z2.im, {}) +import Complex ### ### Real part ### # Real part of a purely real number -expect - z = { re: 1, im: 0 } - result = real(z) - result |> Num.is_approx_eq(1, {}) +expect { + z = Complex.new({ real: 1, imag: 0 }) + result = z.real + result->is_approx_eq(1) +} # Real part of a purely imaginary number -expect - z = { re: 0, im: 1 } - result = real(z) - result |> Num.is_approx_eq(0, {}) +expect { + z = Complex.new({ real: 0, imag: 1 }) + result = z.real + result->is_approx_eq(0) +} # Real part of a number with real and imaginary part -expect - z = { re: 1, im: 2 } - result = real(z) - result |> Num.is_approx_eq(1, {}) +expect { + z = Complex.new({ real: 1, imag: 2 }) + result = z.real + result->is_approx_eq(1) +} ### ### Imaginary part ### # Imaginary part of a purely real number -expect - z = { re: 1, im: 0 } - result = imaginary(z) - result |> Num.is_approx_eq(0, {}) +expect { + z = Complex.new({ real: 1, imag: 0 }) + result = z.imag + result->is_approx_eq(0) +} # Imaginary part of a purely imaginary number -expect - z = { re: 0, im: 1 } - result = imaginary(z) - result |> Num.is_approx_eq(1, {}) +expect { + z = Complex.new({ real: 0, imag: 1 }) + result = z.imag + result->is_approx_eq(1) +} # Imaginary part of a number with real and imaginary part -expect - z = { re: 1, im: 2 } - result = imaginary(z) - result |> Num.is_approx_eq(2, {}) +expect { + z = Complex.new({ real: 1, imag: 2 }) + result = z.imag + result->is_approx_eq(2) +} ### ### Imaginary unit @@ -70,270 +65,321 @@ expect ## Addition # Add purely real numbers -expect - z1 = { re: 1, im: 0 } - z2 = { re: 2, im: 0 } - result = z1 |> add(z2) - expected = { re: 3, im: 0 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 1, imag: 0 }) + z2 = Complex.new({ real: 2, imag: 0 }) + result = z1 + z2 + expected = Complex.new({ real: 3, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Add purely imaginary numbers -expect - z1 = { re: 0, im: 1 } - z2 = { re: 0, im: 2 } - result = z1 |> add(z2) - expected = { re: 0, im: 3 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 0, imag: 1 }) + z2 = Complex.new({ real: 0, imag: 2 }) + result = z1 + z2 + expected = Complex.new({ real: 0, imag: 3 }) + result->complex_is_approx_eq(expected) +} # Add numbers with real and imaginary part -expect - z1 = { re: 1, im: 2 } - z2 = { re: 3, im: 4 } - result = z1 |> add(z2) - expected = { re: 4, im: 6 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 1, imag: 2 }) + z2 = Complex.new({ real: 3, imag: 4 }) + result = z1 + z2 + expected = Complex.new({ real: 4, imag: 6 }) + result->complex_is_approx_eq(expected) +} ## Subtraction # Subtract purely real numbers -expect - z1 = { re: 1, im: 0 } - z2 = { re: 2, im: 0 } - result = z1 |> sub(z2) - expected = { re: -1, im: 0 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 1, imag: 0 }) + z2 = Complex.new({ real: 2, imag: 0 }) + result = z1 - z2 + expected = Complex.new({ real: -1, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Subtract purely imaginary numbers -expect - z1 = { re: 0, im: 1 } - z2 = { re: 0, im: 2 } - result = z1 |> sub(z2) - expected = { re: 0, im: -1 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 0, imag: 1 }) + z2 = Complex.new({ real: 0, imag: 2 }) + result = z1 - z2 + expected = Complex.new({ real: 0, imag: -1 }) + result->complex_is_approx_eq(expected) +} # Subtract numbers with real and imaginary part -expect - z1 = { re: 1, im: 2 } - z2 = { re: 3, im: 4 } - result = z1 |> sub(z2) - expected = { re: -2, im: -2 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 1, imag: 2 }) + z2 = Complex.new({ real: 3, imag: 4 }) + result = z1 - z2 + expected = Complex.new({ real: -2, imag: -2 }) + result->complex_is_approx_eq(expected) +} ## Multiplication # Multiply purely real numbers -expect - z1 = { re: 1, im: 0 } - z2 = { re: 2, im: 0 } - result = z1 |> mul(z2) - expected = { re: 2, im: 0 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 1, imag: 0 }) + z2 = Complex.new({ real: 2, imag: 0 }) + result = z1 * z2 + expected = Complex.new({ real: 2, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Multiply purely imaginary numbers -expect - z1 = { re: 0, im: 1 } - z2 = { re: 0, im: 2 } - result = z1 |> mul(z2) - expected = { re: -2, im: 0 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 0, imag: 1 }) + z2 = Complex.new({ real: 0, imag: 2 }) + result = z1 * z2 + expected = Complex.new({ real: -2, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Multiply numbers with real and imaginary part -expect - z1 = { re: 1, im: 2 } - z2 = { re: 3, im: 4 } - result = z1 |> mul(z2) - expected = { re: -5, im: 10 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 1, imag: 2 }) + z2 = Complex.new({ real: 3, imag: 4 }) + result = z1 * z2 + expected = Complex.new({ real: -5, imag: 10 }) + result->complex_is_approx_eq(expected) +} ## Division # Divide purely real numbers -expect - z1 = { re: 1, im: 0 } - z2 = { re: 2, im: 0 } - result = z1 |> div(z2) - expected = { re: 0.5, im: 0 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 1, imag: 0 }) + z2 = Complex.new({ real: 2, imag: 0 }) + result = z1 / z2 + expected = Complex.new({ real: 0.5, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Divide purely imaginary numbers -expect - z1 = { re: 0, im: 1 } - z2 = { re: 0, im: 2 } - result = z1 |> div(z2) - expected = { re: 0.5, im: 0 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 0, imag: 1 }) + z2 = Complex.new({ real: 0, imag: 2 }) + result = z1 / z2 + expected = Complex.new({ real: 0.5, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Divide numbers with real and imaginary part -expect - z1 = { re: 1, im: 2 } - z2 = { re: 3, im: 4 } - result = z1 |> div(z2) - expected = { re: 0.44, im: 0.08 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 1, imag: 2 }) + z2 = Complex.new({ real: 3, imag: 4 }) + result = z1 / z2 + expected = Complex.new({ real: 0.44, imag: 0.08 }) + result->complex_is_approx_eq(expected) +} ### ### Absolute value ### # Absolute value of a positive purely real number -expect - z = { re: 5, im: 0 } - result = abs(z) - result |> Num.is_approx_eq(5, {}) +expect { + z = Complex.new({ real: 5, imag: 0 }) + result = z.abs() + result->is_approx_eq(5) +} # Absolute value of a negative purely real number -expect - z = { re: -5, im: 0 } - result = abs(z) - result |> Num.is_approx_eq(5, {}) +expect { + z = Complex.new({ real: -5, imag: 0 }) + result = z.abs() + result->is_approx_eq(5) +} # Absolute value of a purely imaginary number with positive imaginary part -expect - z = { re: 0, im: 5 } - result = abs(z) - result |> Num.is_approx_eq(5, {}) +expect { + z = Complex.new({ real: 0, imag: 5 }) + result = z.abs() + result->is_approx_eq(5) +} # Absolute value of a purely imaginary number with negative imaginary part -expect - z = { re: 0, im: -5 } - result = abs(z) - result |> Num.is_approx_eq(5, {}) +expect { + z = Complex.new({ real: 0, imag: -5 }) + result = z.abs() + result->is_approx_eq(5) +} # Absolute value of a number with real and imaginary part -expect - z = { re: 3, im: 4 } - result = abs(z) - result |> Num.is_approx_eq(5, {}) +expect { + z = Complex.new({ real: 3, imag: 4 }) + result = z.abs() + result->is_approx_eq(5) +} ### ### Complex conjugate ### # Conjugate a purely real number -expect - z = { re: 5, im: 0 } - result = conjugate(z) - expected = { re: 5, im: 0 } - result |> is_approx_eq(expected) +expect { + z = Complex.new({ real: 5, imag: 0 }) + result = z.conjugate() + expected = Complex.new({ real: 5, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Conjugate a purely imaginary number -expect - z = { re: 0, im: 5 } - result = conjugate(z) - expected = { re: 0, im: -5 } - result |> is_approx_eq(expected) +expect { + z = Complex.new({ real: 0, imag: 5 }) + result = z.conjugate() + expected = Complex.new({ real: 0, imag: -5 }) + result->complex_is_approx_eq(expected) +} # Conjugate a number with real and imaginary part -expect - z = { re: 1, im: 1 } - result = conjugate(z) - expected = { re: 1, im: -1 } - result |> is_approx_eq(expected) +expect { + z = Complex.new({ real: 1, imag: 1 }) + result = z.conjugate() + expected = Complex.new({ real: 1, imag: -1 }) + result->complex_is_approx_eq(expected) +} ### ### Complex exponential function ### # Euler's identity/formula -expect - z = { re: 0, im: Num.pi } - result = exp(z) - expected = { re: -1, im: 0 } - result |> is_approx_eq(expected) +expect { + z = Complex.new({ real: 0, imag: 3.141592653589793.F64 }) + result = z.exp() + expected = Complex.new({ real: -1, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Exponential of 0 -expect - z = { re: 0, im: 0 } - result = exp(z) - expected = { re: 1, im: 0 } - result |> is_approx_eq(expected) +expect { + z = Complex.new({ real: 0, imag: 0 }) + result = z.exp() + expected = Complex.new({ real: 1, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Exponential of a purely real number -expect - z = { re: 1, im: 0 } - result = exp(z) - expected = { re: Num.e, im: 0 } - result |> is_approx_eq(expected) +expect { + z = Complex.new({ real: 1, imag: 0 }) + result = z.exp() + expected = Complex.new({ real: 2.718281828459045.F64, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Exponential of a number with real and imaginary part -expect - z = { re: Num.log(2f64), im: Num.pi } - result = exp(z) - expected = { re: -2, im: 0 } - result |> is_approx_eq(expected) +expect { + z = Complex.new({ real: 0.6931471805599453.F64, imag: 3.141592653589793.F64 }) + result = z.exp() + expected = Complex.new({ real: -2, imag: 0 }) + result->complex_is_approx_eq(expected) +} # Exponential resulting in a number with real and imaginary part -expect - z = { re: Num.log(2f64) / 2, im: Num.pi / 4 } - result = exp(z) - expected = { re: 1, im: 1 } - result |> is_approx_eq(expected) +expect { + z = Complex.new({ real: 0.6931471805599453.F64 / 2, imag: 3.141592653589793.F64 / 4 }) + result = z.exp() + expected = Complex.new({ real: 1, imag: 1 }) + result->complex_is_approx_eq(expected) +} ### ### Operations between real numbers and complex numbers ### # Add real number to complex number -expect - z1 = { re: 1, im: 2 } - z2 = { re: 5, im: 0 } - result = z1 |> add(z2) - expected = { re: 6, im: 2 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 1, imag: 2 }) + z2 = Complex.new({ real: 5, imag: 0 }) + result = z1 + z2 + expected = Complex.new({ real: 6, imag: 2 }) + result->complex_is_approx_eq(expected) +} # Add complex number to real number -expect - z1 = { re: 5, im: 0 } - z2 = { re: 1, im: 2 } - result = z1 |> add(z2) - expected = { re: 6, im: 2 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 5, imag: 0 }) + z2 = Complex.new({ real: 1, imag: 2 }) + result = z1 + z2 + expected = Complex.new({ real: 6, imag: 2 }) + result->complex_is_approx_eq(expected) +} # Subtract real number from complex number -expect - z1 = { re: 5, im: 7 } - z2 = { re: 4, im: 0 } - result = z1 |> sub(z2) - expected = { re: 1, im: 7 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 5, imag: 7 }) + z2 = Complex.new({ real: 4, imag: 0 }) + result = z1 - z2 + expected = Complex.new({ real: 1, imag: 7 }) + result->complex_is_approx_eq(expected) +} # Subtract complex number from real number -expect - z1 = { re: 4, im: 0 } - z2 = { re: 5, im: 7 } - result = z1 |> sub(z2) - expected = { re: -1, im: -7 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 4, imag: 0 }) + z2 = Complex.new({ real: 5, imag: 7 }) + result = z1 - z2 + expected = Complex.new({ real: -1, imag: -7 }) + result->complex_is_approx_eq(expected) +} # Multiply complex number by real number -expect - z1 = { re: 2, im: 5 } - z2 = { re: 5, im: 0 } - result = z1 |> mul(z2) - expected = { re: 10, im: 25 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 2, imag: 5 }) + z2 = Complex.new({ real: 5, imag: 0 }) + result = z1 * z2 + expected = Complex.new({ real: 10, imag: 25 }) + result->complex_is_approx_eq(expected) +} # Multiply real number by complex number -expect - z1 = { re: 5, im: 0 } - z2 = { re: 2, im: 5 } - result = z1 |> mul(z2) - expected = { re: 10, im: 25 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 5, imag: 0 }) + z2 = Complex.new({ real: 2, imag: 5 }) + result = z1 * z2 + expected = Complex.new({ real: 10, imag: 25 }) + result->complex_is_approx_eq(expected) +} # Divide complex number by real number -expect - z1 = { re: 10, im: 100 } - z2 = { re: 10, im: 0 } - result = z1 |> div(z2) - expected = { re: 1, im: 10 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 10, imag: 100 }) + z2 = Complex.new({ real: 10, imag: 0 }) + result = z1 / z2 + expected = Complex.new({ real: 1, imag: 10 }) + result->complex_is_approx_eq(expected) +} # Divide real number by complex number -expect - z1 = { re: 5, im: 0 } - z2 = { re: 1, im: 1 } - result = z1 |> div(z2) - expected = { re: 2.5, im: -2.5 } - result |> is_approx_eq(expected) +expect { + z1 = Complex.new({ real: 5, imag: 0 }) + z2 = Complex.new({ real: 1, imag: 1 }) + result = z1 / z2 + expected = Complex.new({ real: 2.5, imag: -2.5 }) + result->complex_is_approx_eq(expected) +} + +is_approx_eq = |x1, x2| { + i1 = (x1 * 1000 + 0.5).to_i64_try() ?? { + crash "Unreachable" + } + i2 = (x2 * 1000 + 0.5).to_i64_try() ?? { + crash "Unreachable" + } + i1 == i2 +} +complex_is_approx_eq = |z1, z2| { + is_approx_eq(z1.real, z2.real) and is_approx_eq(z1.imag, z2.imag) +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/connect/.meta/Example.roc b/exercises/practice/connect/.meta/Example.roc index 815be141..9f4fe58c 100644 --- a/exercises/practice/connect/.meta/Example.roc +++ b/exercises/practice/connect/.meta/Example.roc @@ -1,112 +1,199 @@ -module [winner] +Connect :: {}.{ + winner : Str -> Try([PlayerO, PlayerX], [NotFinished, InvalidCharacter(U8), InvalidBoardShape, ..]) + winner = |board_str| { + board = parse(board_str)? + _ = validate(board)? + if board->has_north_south_path(StoneO) { + Ok(PlayerO) + } else if board->transpose()->has_north_south_path(StoneX) { + Ok(PlayerX) + } else { + Err(NotFinished) + } + } +} -Cell : [StoneO, StoneX, Empty] -Board : List (List Cell) -Position : { x : U64, y : U64 } +Cell := [StoneO, StoneX, Empty] + +Board : List(List(Cell)) -## Parse a string to a Board -parse : Str -> Result Board [InvalidCharacter U8] -parse = |board_str| - board_str - |> Str.trim - |> Str.to_utf8 - |> List.split_on('\n') - |> List.map_try( - |row| - row - |> List.drop_if(|char| char == ' ') - |> List.map_try( - |char| - when char is - 'O' -> Ok(StoneO) - 'X' -> Ok(StoneX) - '.' -> Ok(Empty) - _ -> Err(InvalidCharacter(char)), - ), - ) +Position : { x : U64, y : U64 } -## Ensure that the board has a least one cell, and that all rows have the same length -validate : Board -> Result {} [InvalidBoardShape] -validate = |board| - row_lengths = board |> List.map(List.len) |> Set.from_list - if Set.len(row_lengths) != 1 or row_lengths == Set.from_list([0]) then - Err(InvalidBoardShape) - else - Ok({}) +# # Parse a string to a Board +parse : Str -> Try(Board, [InvalidCharacter(U8), ..]) +parse = |board_str| { + board_str + .trim() + .to_utf8() + .split_on( + '\n', + ) + ->map_try( + |row| { + row + .drop_if( + |char| char == ' ', + ) + ->map_try( + |char| { + match char { + 'O' => Ok(StoneO) + 'X' => Ok(StoneX) + '.' => Ok(Empty) + _ => Err(InvalidCharacter(char)) + } + }, + ) + }, + ) +} -winner : Str -> Result [PlayerO, PlayerX] [NotFinished, InvalidCharacter U8, InvalidBoardShape] -winner = |board_str| - board = parse(board_str)? - validate(board)? - if board |> has_north_south_path(StoneO) then - Ok(PlayerO) - else if board |> transpose |> has_north_south_path(StoneX) then - Ok(PlayerX) - else - Err(NotFinished) +# # Ensure that the board has a least one cell, and that all rows have the same length +validate : Board -> Try({}, [InvalidBoardShape, ..]) +validate = |board| { + row_lengths = board.map(List.len)->Set.from_list() + if row_lengths.len() != 1 or row_lengths == Set.from_list([0]) { + Err(InvalidBoardShape) + } else { + Ok({}) + } +} transpose : Board -> Board -transpose = |board| - width = board |> first_row |> List.len - List.range({ start: At(0), end: Before(width) }) - |> List.map( - |x| - List.range({ start: At(0), end: Before((board |> List.len)) }) - |> List.map( - |y| - when board |> get_cell({ x, y }) is - Ok(cell) -> cell - Err(OutOfBounds) -> crash("Unreachable: all rows have the same length"), - ), - ) +transpose = |board| { + width = (board->first_row()).len() + (0..get_cell({ x, y }) { + Ok(cell) => cell + Err(OutOfBounds) => { + crash "Unreachable: all rows have the same length" + } + } + }, + ) + ->List.from_iter() + }, + ) + ->List.from_iter() +} -first_row : Board -> List Cell -first_row = |board| - when board |> List.first is - Ok(row) -> row - Err(ListWasEmpty) -> crash("Unreachable: the board has at least one cell") +first_row : Board -> List(Cell) +first_row = |board| { + match board.first() { + Ok(row) => row + Err(ListWasEmpty) => { + crash "Unreachable: the board has at least one cell" + } + } +} -get_cell : Board, Position -> Result Cell [OutOfBounds] -get_cell = |board, { x, y }| - board |> List.get(y)? |> List.get(x) +get_cell : Board, Position -> Try(Cell, [OutOfBounds, ..]) +get_cell = |board, { x, y }| { + board.get(y)?.get(x) +} has_north_south_path : Board, Cell -> Bool -has_north_south_path = |board, stone| - has_path_to_south : List Position, Set Position -> Bool - has_path_to_south = |to_visit, visited| - when to_visit is - [] -> Bool.false - [position, .. as rest] -> - is_player_stone = board |> get_cell(position) == Ok(stone) - if is_player_stone and !(visited |> Set.contains(position)) then - { x, y } = position - if y + 1 == List.len(board) then - Bool.true # we've reached the South! - else - neighbors = - [(-1, 0), (1, 0), (0, -1), (1, -1), (-1, 1), (0, 1)] - |> List.join_map( - |(dx, dy)| - nx = (x |> Num.to_i64) + dx - ny = (y |> Num.to_i64) + dy - if nx >= 0 and ny >= 0 then - [{ x: nx |> Num.to_u64, y: ny |> Num.to_u64 }] - else - [], - ) - has_path_to_south((rest |> List.concat(neighbors)), (visited |> Set.insert(position))) - else - has_path_to_south(rest, visited) +has_north_south_path = |board, stone| { + has_path_to_south : List(Position), Set(Position) -> Bool + has_path_to_south = |to_visit, visited| { + match to_visit { + [] => Bool.False + [position, .. as rest] => { + is_player_stone = board->get_cell(position) == Ok(stone) + if is_player_stone and !(visited.contains(position)) { + { x, y } = position + if y + 1 == board.len() { + Bool.True # we've reached the South! + } else { + neighbors = { + [(-1, 0), (1, 0), (0, -1), (1, -1), (-1, 1), (0, 1)] + ->join_map( + |(dx, dy)| { + nx = ( + x.to_i64_try() ?? { + crash "Unreachable" + }, + ) + dx + ny = ( + y.to_i64_try() ?? { + crash "Unreachable" + }, + ) + dy + if nx >= 0 and ny >= 0 { + [ + { + x: nx.to_u64_try() ?? { + crash "Unreachable" + }, + y: ny.to_u64_try() ?? { + crash "Unreachable" + }, + }, + ] + } else { + [] + } + }, + ) + } + has_path_to_south((rest.concat(neighbors)), (visited.insert(position))) + } + } else { + has_path_to_south(rest, visited) + } + } + } + } + + north_stones : List(Position) + north_stones = { + board + ->first_row() + .map_with_index( + |cell, x| { + match cell { + StoneO | StoneX => if cell == stone Ok({ x, y: 0 }) else Err(NotPlayerStone) + Empty => Err(NotPlayerStone) + } + }, + ) + ->keep_oks(|id| id) + } + has_path_to_south(north_stones, Set.empty()) +} + +# The following functions should soon be available in Roc's builtins +keep_oks = |iter, func| { + iter + ->join_map( + |item| { + match func(item) { + Ok(result) => [result] + Err(_) => [] + } + }, + ) +} + +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} - north_stones : List Position - north_stones = - board - |> first_row - |> List.map_with_index( - |cell, x| - when cell is - StoneO | StoneX -> if cell == stone then Ok({ x, y: 0 }) else Err(NotPlayerStone) - Empty -> Err(NotPlayerStone), - ) - |> List.keep_oks(|id| id) - has_path_to_south(north_stones, Set.empty({})) +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/connect/.meta/template.j2 b/exercises/practice/connect/.meta/template.j2 index 25aa62ac..a9eba0a2 100644 --- a/exercises/practice/connect/.meta/template.j2 +++ b/exercises/practice/connect/.meta/template.j2 @@ -6,9 +6,12 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { board = {{ case["input"]["board"] | to_roc_multiline_string | indent(8) }} - result = board |> {{ case["property"] | to_snake }} + result = board -> {{ case["property"] | to_snake }} result == {% if case["expected"] == "" %}Err(NotFinished){% else %}Ok(Player{{ case["expected"] }}){% endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/connect/.meta/tests.toml b/exercises/practice/connect/.meta/tests.toml index 6ada8773..951b87e5 100644 --- a/exercises/practice/connect/.meta/tests.toml +++ b/exercises/practice/connect/.meta/tests.toml @@ -30,6 +30,12 @@ description = "nobody wins crossing adjacent angles" [cd61c143-92f6-4a8d-84d9-cb2b359e226b] description = "X wins crossing from left to right" +[495e33ed-30a9-4012-b46e-d7c4d5fe13c3] +description = "X wins with left-hand dead end fork" + +[ab167ab0-4a98-4d0f-a1c0-e1cddddc3d58] +description = "X wins with right-hand dead end fork" + [73d1eda6-16ab-4460-9904-b5f5dd401d0b] description = "O wins crossing from top to bottom" diff --git a/exercises/practice/connect/Connect.roc b/exercises/practice/connect/Connect.roc index 1f485b8a..15248f78 100644 --- a/exercises/practice/connect/Connect.roc +++ b/exercises/practice/connect/Connect.roc @@ -1,5 +1,6 @@ -module [winner] - -winner : Str -> Result [PlayerO, PlayerX] _ -winner = |board_str| - crash("Please implement the 'winner' function") +Connect :: {}.{ + winner : Str -> Try([PlayerO, PlayerX], [NotFinished, ..]) + winner = |board_str| { + crash "Please implement the 'winner' function" + } +} diff --git a/exercises/practice/connect/connect-test.roc b/exercises/practice/connect/connect-test.roc index 237fa492..0bc75a0f 100644 --- a/exercises/practice/connect/connect-test.roc +++ b/exercises/practice/connect/connect-test.roc @@ -1,133 +1,155 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/connect/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Connect exposing [winner] # an empty board has no winner -expect - board = - """ - . . . . . - . . . . . - . . . . . - . . . . . - . . . . . - """ - result = board |> winner - result == Err(NotFinished) +expect { + board = + \\. . . . . + \\ . . . . . + \\ . . . . . + \\ . . . . . + \\ . . . . . + + result = board->winner() + result == Err(NotFinished) +} # X can win on a 1x1 board -expect - board = "X" - result = board |> winner - result == Ok(PlayerX) +expect { + board = "X" + result = board->winner() + result == Ok(PlayerX) +} # O can win on a 1x1 board -expect - board = "O" - result = board |> winner - result == Ok(PlayerO) +expect { + board = "O" + result = board->winner() + result == Ok(PlayerO) +} # only edges does not make a winner -expect - board = - """ - O O O X - X . . X - X . . X - X O O O - """ - result = board |> winner - result == Err(NotFinished) +expect { + board = + \\O O O X + \\ X . . X + \\ X . . X + \\ X O O O + + result = board->winner() + result == Err(NotFinished) +} # illegal diagonal does not make a winner -expect - board = - """ - X O . . - O X X X - O X O . - . O X . - X X O O - """ - result = board |> winner - result == Err(NotFinished) +expect { + board = + \\X O . . + \\ O X X X + \\ O X O . + \\ . O X . + \\ X X O O + + result = board->winner() + result == Err(NotFinished) +} # nobody wins crossing adjacent angles -expect - board = - """ - X . . . - . X O . - O . X O - . O . X - . . O . - """ - result = board |> winner - result == Err(NotFinished) +expect { + board = + \\X . . . + \\ . X O . + \\ O . X O + \\ . O . X + \\ . . O . + + result = board->winner() + result == Err(NotFinished) +} # X wins crossing from left to right -expect - board = - """ - . O . . - O X X X - O X O . - X X O X - . O X . - """ - result = board |> winner - result == Ok(PlayerX) +expect { + board = + \\. O . . + \\ O X X X + \\ O X O . + \\ X X O X + \\ . O X . + + result = board->winner() + result == Ok(PlayerX) +} + +# X wins with left-hand dead end fork +expect { + board = + \\. . X . + \\ X X . . + \\ . X X X + \\ O O O O + + result = board->winner() + result == Ok(PlayerX) +} + +# X wins with right-hand dead end fork +expect { + board = + \\. . X X + \\ X X . . + \\ . X X . + \\ O O O O + + result = board->winner() + result == Ok(PlayerX) +} # O wins crossing from top to bottom -expect - board = - """ - . O . . - O X X X - O O O . - X X O X - . O X . - """ - result = board |> winner - result == Ok(PlayerO) +expect { + board = + \\. O . . + \\ O X X X + \\ O O O . + \\ X X O X + \\ . O X . + + result = board->winner() + result == Ok(PlayerO) +} # X wins using a convoluted path -expect - board = - """ - . X X . . - X . X . X - . X . X . - . X X . . - O O O O O - """ - result = board |> winner - result == Ok(PlayerX) +expect { + board = + \\. X X . . + \\ X . X . X + \\ . X . X . + \\ . X X . . + \\ O O O O O + + result = board->winner() + result == Ok(PlayerX) +} # X wins using a spiral path -expect - board = - """ - O X X X X X X X X - O X O O O O O O O - O X O X X X X X O - O X O X O O O X O - O X O X X X O X O - O X O O O X O X O - O X X X X X O X O - O O O O O O O X O - X X X X X X X X O - """ - result = board |> winner - result == Ok(PlayerX) +expect { + board = + \\O X X X X X X X X + \\ O X O O O O O O O + \\ O X O X X X X X O + \\ O X O X O O O X O + \\ O X O X X X O X O + \\ O X O O O X O X O + \\ O X X X X X O X O + \\ O O O O O O O X O + \\ X X X X X X X X O + result = board->winner() + result == Ok(PlayerX) +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/crypto-square/.meta/Example.roc b/exercises/practice/crypto-square/.meta/Example.roc index 9364e643..4615d4a2 100644 --- a/exercises/practice/crypto-square/.meta/Example.roc +++ b/exercises/practice/crypto-square/.meta/Example.roc @@ -1,33 +1,80 @@ -module [ciphertext] +CryptoSquare :: {}.{ + ciphertext : Str -> Str + ciphertext = |text| { + chars = { + text + .to_utf8() + ->join_map( + |char| { + if (char >= 'a' and char <= 'z') or (char >= '0' and char <= '9') { + [char] + } else if char >= 'A' and char <= 'Z' { + [char - 'A' + 'a'] + } else { + [] + } + }, + ) + .map( + |c| [c]->Str.from_utf8() ?? { + crash "Unreachable" + }, + ) + } + length = chars.len() + width = length->sqrt_ceiling() # to_f64().sqrt().ceiling().to_u64() + rows = chars->chunks_of(width) -ciphertext : Str -> Result Str _ -ciphertext = |text| - chars = - text - |> Str.to_utf8 - |> List.join_map( - |char| - if (char >= 'a' and char <= 'z') or (char >= '0' and char <= '9') then - [char] - else if char >= 'A' and char <= 'Z' then - [char - 'A' + 'a'] - else - [], - ) + if width == 0 { + "" + } else { + (0..Str.join_with("") + }, + ) + ->List.from_iter() + ->Str.join_with(" ") + } + } +} - length = List.len(chars) - width = length |> Num.to_f64 |> Num.sqrt |> Num.ceiling |> Num.to_u64 - rows = chars |> List.chunks_of(width) +# The following function should soon be available in Roc's builtins +chunks_of = |iter, size| { + var $state = [] + var $chunk = [] + for item in iter { + $chunk = $chunk.append(item) + if $chunk.len() == size { + $state = $state.append($chunk) + $chunk = [] + } + } + if $chunk.len() > 0 { + $state = $state.append($chunk) + } + $state +} - List.range({ start: At(0), end: Before(width) }) - |> List.map( - |column| - rows - |> List.map( - |row| - row |> List.get(column) |> Result.with_default(' '), - ), - ) - |> List.intersperse([' ']) - |> List.join - |> Str.from_utf8 +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} + +sqrt_ceiling = |n| { + var $i = 0 + while $i * $i < n { + $i = $i + 1 + } + $i +} diff --git a/exercises/practice/crypto-square/.meta/template.j2 b/exercises/practice/crypto-square/.meta/template.j2 index be3e8b42..a51bd600 100644 --- a/exercises/practice/crypto-square/.meta/template.j2 +++ b/exercises/practice/crypto-square/.meta/template.j2 @@ -6,10 +6,13 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { text = {{ case["input"]["plaintext"] | to_roc }} result = {{ case["property"] | to_snake }}(text) - expected = Ok({{ case["expected"] | to_roc }}) + expected = {{ case["expected"] | to_roc }} result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/crypto-square/CryptoSquare.roc b/exercises/practice/crypto-square/CryptoSquare.roc index 0074be06..e952adc4 100644 --- a/exercises/practice/crypto-square/CryptoSquare.roc +++ b/exercises/practice/crypto-square/CryptoSquare.roc @@ -1,5 +1,6 @@ -module [ciphertext] - -ciphertext : Str -> Result Str _ -ciphertext = |text| - crash("Please implement the 'ciphertext' function") +CryptoSquare :: {}.{ + ciphertext : Str -> Str + ciphertext = |text| { + crash "Please implement the 'ciphertext' function" + } +} diff --git a/exercises/practice/crypto-square/crypto-square-test.roc b/exercises/practice/crypto-square/crypto-square-test.roc index c9109ae2..abde0ee0 100644 --- a/exercises/practice/crypto-square/crypto-square-test.roc +++ b/exercises/practice/crypto-square/crypto-square-test.roc @@ -1,70 +1,74 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/crypto-square/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import CryptoSquare exposing [ciphertext] # empty plaintext results in an empty ciphertext -expect - text = "" - result = ciphertext(text) - expected = Ok("") - result == expected +expect { + text = "" + result = ciphertext(text) + expected = "" + result == expected +} # normalization results in empty plaintext -expect - text = "... --- ..." - result = ciphertext(text) - expected = Ok("") - result == expected +expect { + text = "... --- ..." + result = ciphertext(text) + expected = "" + result == expected +} # Lowercase -expect - text = "A" - result = ciphertext(text) - expected = Ok("a") - result == expected +expect { + text = "A" + result = ciphertext(text) + expected = "a" + result == expected +} # Remove spaces -expect - text = " b " - result = ciphertext(text) - expected = Ok("b") - result == expected +expect { + text = " b " + result = ciphertext(text) + expected = "b" + result == expected +} # Remove punctuation -expect - text = "@1,%!" - result = ciphertext(text) - expected = Ok("1") - result == expected +expect { + text = "@1,%!" + result = ciphertext(text) + expected = "1" + result == expected +} # 9 character plaintext results in 3 chunks of 3 characters -expect - text = "This is fun!" - result = ciphertext(text) - expected = Ok("tsf hiu isn") - result == expected +expect { + text = "This is fun!" + result = ciphertext(text) + expected = "tsf hiu isn" + result == expected +} # 8 character plaintext results in 3 chunks, the last one with a trailing space -expect - text = "Chill out." - result = ciphertext(text) - expected = Ok("clu hlt io ") - result == expected +expect { + text = "Chill out." + result = ciphertext(text) + expected = "clu hlt io " + result == expected +} # 54 character plaintext results in 8 chunks, the last two with trailing spaces -expect - text = "If man was meant to stay on the ground, god would have given us roots." - result = ciphertext(text) - expected = Ok("imtgdvs fearwer mayoogo anouuio ntnnlvt wttddes aohghn sseoau ") - result == expected +expect { + text = "If man was meant to stay on the ground, god would have given us roots." + result = ciphertext(text) + expected = "imtgdvs fearwer mayoogo anouuio ntnnlvt wttddes aohghn sseoau " + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/custom-set/.meta/Example.roc b/exercises/practice/custom-set/.meta/Example.roc index 4e7132f9..55f657bb 100644 --- a/exercises/practice/custom-set/.meta/Example.roc +++ b/exercises/practice/custom-set/.meta/Example.roc @@ -1,81 +1,87 @@ -module [ - contains, - difference, - from_list, - insert, - intersection, - is_disjoint_with, - is_empty, - is_eq, - is_subset_of, - to_list, - to_list, - union, -] +CustomSet :: { items : List(U64) }.{ + Item : U64 -Element : U64 + contains : CustomSet, Item -> Bool + contains = |{ items }, item| items.contains(item) -CustomSet := { items : List Element } implements [Eq] + difference : CustomSet, CustomSet -> CustomSet + difference = |{ items: items1 }, { items: items2 }| { + { items: items1.drop_if(|item| items2.contains(item)) } + } -contains : CustomSet, Element -> Bool -contains = |@CustomSet({ items }), element| - items |> List.contains(element) + from_list : List(Item) -> CustomSet + from_list = |list| { + match sort_asc(list) { + [] => { items: [] } + [first, .. as rest] => { + state_res = rest.fold( + { items: [first], previous: first }, + |state, item| { + if item == state.previous { + state + } else { + { items: state.items.append(item), previous: item } + } + }, + ) + { items: state_res.items } + } + } + } -difference : CustomSet, CustomSet -> CustomSet -difference = |@CustomSet({ items: items1 }), @CustomSet({ items: items2 })| - items = items1 |> List.drop_if(|item| items2 |> List.contains(item)) - @CustomSet({ items }) + insert : CustomSet, Item -> CustomSet + insert = |{ items }, item| { + if items.contains(item) { + { items: items } + } else { + { items: items.append(item) } + } + } -from_list : List Element -> CustomSet -from_list = |list| - when list |> List.sort_asc is - [] -> @CustomSet({ items: [] }) - [first, .. as rest] -> - items = - rest - |> List.walk( - { items: [first], previous: first }, - |state, item| - if item == state.previous then - state - else - { items: state.items |> List.append(item), previous: item }, - ) - |> .items - @CustomSet({ items }) + intersection : CustomSet, CustomSet -> CustomSet + intersection = |{ items: items1 }, { items: items2 }| { + { items: items1.keep_if(|item| items2.contains(item)) } + } -insert : CustomSet, Element -> CustomSet -insert = |@CustomSet({ items }), element| - if items |> List.contains(element) then - @CustomSet({ items }) - else - @CustomSet({ items: items |> List.append(element) }) + is_disjoint_with : CustomSet, CustomSet -> Bool + is_disjoint_with = |set1, set2| { + intersection(set1, set2).is_empty() + } -intersection : CustomSet, CustomSet -> CustomSet -intersection = |@CustomSet({ items: items1 }), @CustomSet({ items: items2 })| - items = items1 |> List.keep_if(|item| items2 |> List.contains(item)) - @CustomSet({ items }) + is_empty : CustomSet -> Bool + is_empty = |{ items }| { + items.is_empty() + } -is_disjoint_with : CustomSet, CustomSet -> Bool -is_disjoint_with = |set1, set2| - set1 |> intersection(set2) |> is_empty + is_eq : CustomSet, CustomSet -> Bool + is_eq = |{ items: items1 }, { items: items2 }| { + sort_asc(items1) == sort_asc(items2) + } -is_empty : CustomSet -> Bool -is_empty = |@CustomSet({ items })| - items |> List.is_empty + is_subset_of : CustomSet, CustomSet -> Bool + is_subset_of = |{ items: items1 }, { items: items2 }| { + items1.all(|item| items2.contains(item)) + } -is_eq : CustomSet, CustomSet -> Bool -is_eq = |@CustomSet({ items: items1 }), @CustomSet({ items: items2 })| - items1 |> List.sort_asc == items2 |> List.sort_asc + to_list : CustomSet -> List(Item) + to_list = |{ items }| items -is_subset_of : CustomSet, CustomSet -> Bool -is_subset_of = |@CustomSet({ items: items1 }), @CustomSet({ items: items2 })| - items1 |> List.all(|item| items2 |> List.contains(item)) + union : CustomSet, CustomSet -> CustomSet + union = |set1, set2| { + set1.to_list().concat(set2.to_list())->from_list() + } +} -to_list : CustomSet -> List Element -to_list = |@CustomSet({ items })| - items - -union : CustomSet, CustomSet -> CustomSet -union = |set1, set2| - set1 |> to_list |> List.concat((set2 |> to_list)) |> from_list +sort_asc = |list| { + list.sort_with( + |a, b| { + if a < b { + LT + } else if a > b { + GT + } else { + EQ + } + }, + ) +} diff --git a/exercises/practice/custom-set/.meta/template.j2 b/exercises/practice/custom-set/.meta/template.j2 index e99d682e..e549defe 100644 --- a/exercises/practice/custom-set/.meta/template.j2 +++ b/exercises/practice/custom-set/.meta/template.j2 @@ -2,20 +2,7 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [ - contains, - difference, - from_list, - insert, - intersection, - is_disjoint_with, - is_empty, - is_eq, - is_subset_of, - to_list, - union, -] - +import {{ exercise | to_pascal }} {% set property_map = { "add": "insert", @@ -34,23 +21,23 @@ set property_map = { {% for case in supercase["cases"] -%} # {{ case["description"] }} {% set property = property_map.get(case["property"], case["property"]) %} -expect +expect { {%- if "set" in case["input"] %} - set = from_list({{ case["input"]["set"] | to_roc }}) - result = set |> {{ property | to_snake }} - {%- if "element" in case["input"] %}({{(case["input"]["element"])}}){%- endif %} + set = {{ exercise | to_pascal }}.from_list({{ case["input"]["set"] | to_roc }}) + result = set.{{ property | to_snake }}({{(case["input"]["element"])}}) {%- else %} - set1 = from_list({{ case["input"]["set1"] | to_roc }}) - set2 = from_list({{ case["input"]["set2"] | to_roc }}) - result = set1 |> {{ property | to_snake }}(set2) + set1 = {{ exercise | to_pascal }}.from_list({{ case["input"]["set1"] | to_roc }}) + set2 = {{ exercise | to_pascal }}.from_list({{ case["input"]["set2"] | to_roc }}) + result = set1.{{ property | to_snake }}(set2) {%- endif %} {%- if case["expected"] is iterable %} - expected = {{ case["expected"] | to_roc }} |> from_list - result |> is_eq(expected) + expected = {{ case["expected"] | to_roc }} -> {{ exercise | to_pascal }}.from_list() + result.is_eq(expected) {%- else %} expected = {{ case["expected"] | to_roc }} result == expected {%- endif %} +} {% endfor %} {% endfor %} @@ -61,10 +48,27 @@ expect {% for case in additional_cases -%} # {{ case["description"] }} -expect - set = from_list({{ case["input"]["set"] | to_roc }}) - result = set |> to_list |> List.sort_asc +expect { + set = {{ exercise | to_pascal }}.from_list({{ case["input"]["set"] | to_roc }}) + result = set.to_list()->sort_asc() expected = {{ case["expected"] | to_roc }} result == expected +} + +{% endfor %} + +sort_asc = |list| { + list.sort_with( + |a, b| { + if a < b { + LT + } else if a > b { + GT + } else { + EQ + } + }, + ) +} -{% endfor %} \ No newline at end of file +{{ macros.footer() }} diff --git a/exercises/practice/custom-set/CustomSet.roc b/exercises/practice/custom-set/CustomSet.roc index 8311e872..6ae7c53f 100644 --- a/exercises/practice/custom-set/CustomSet.roc +++ b/exercises/practice/custom-set/CustomSet.roc @@ -1,68 +1,64 @@ -module [ - contains, - difference, - from_list, - insert, - intersection, - is_disjoint_with, - is_empty, - is_eq, - is_subset_of, - to_list, - union, -] +CustomSet :: { + # TODO: change this opaque type however you need + todo1 : U64, + todo2 : U64, + todo3 : U64, + # etc. +}.{ + Item : U64 -Element : U64 + contains : CustomSet, Item -> Bool + contains = |custom_set, item| { + crash "Please implement the 'contains' function" + } -CustomSet := { - # TODO: change this opaque type however you need - todo : U64, - todo2 : U64, - todo3 : U64, - # etc. -} - implements [Eq] - -contains : CustomSet, Element -> Bool -contains = |set, element| - crash("Please implement the 'contains' function") + difference : CustomSet, CustomSet -> CustomSet + difference = |custom_set1, custom_set2| { + crash "Please implement the 'difference' function" + } -difference : CustomSet, CustomSet -> CustomSet -difference = |set1, set2| - crash("Please implement the 'difference' function") + from_list : List(Item) -> CustomSet + from_list = |list| { + crash "Please implement the 'from_list' function" + } -from_list : List Element -> CustomSet -from_list = |list| - crash("Please implement the 'from_list' function") + insert : CustomSet, Item -> CustomSet + insert = |custom_set, item| { + crash "Please implement the 'insert' function" + } -insert : CustomSet, Element -> CustomSet -insert = |set, element| - crash("Please implement the 'insert' function") + intersection : CustomSet, CustomSet -> CustomSet + intersection = |custom_set1, custom_set2| { + crash "Please implement the 'intersection' function" + } -intersection : CustomSet, CustomSet -> CustomSet -intersection = |set1, set2| - crash("Please implement the 'intersection' function") + is_disjoint_with : CustomSet, CustomSet -> Bool + is_disjoint_with = |set1, set2| { + crash "Please implement the 'is_disjoint_with' function" + } -is_disjoint_with : CustomSet, CustomSet -> Bool -is_disjoint_with = |set1, set2| - crash("Please implement the 'is_disjoint_with' function") + is_empty : CustomSet -> Bool + is_empty = |custom_set| { + crash "Please implement the 'is_empty' function" + } -is_empty : CustomSet -> Bool -is_empty = |set| - crash("Please implement the 'is_empty' function") + is_eq : CustomSet, CustomSet -> Bool + is_eq = |custom_set1, custom_set2| { + crash "Please implement the 'is_eq' function" + } -is_eq : CustomSet, CustomSet -> Bool -is_eq = |set1, set2| - crash("Please implement the 'is_eq' function") + is_subset_of : CustomSet, CustomSet -> Bool + is_subset_of = |custom_set1, custom_set2| { + crash "Please implement the 'is_subset_of' function" + } -is_subset_of : CustomSet, CustomSet -> Bool -is_subset_of = |set1, set2| - crash("Please implement the 'is_subset_of' function") + to_list : CustomSet -> List(Item) + to_list = |custom_set| { + crash "Please implement the 'to_list' function" + } -to_list : CustomSet -> List Element -to_list = |set| - crash("Please implement the 'to_list' function") - -union : CustomSet, CustomSet -> CustomSet -union = |set1, set2| - crash("Please implement the 'union' function") + union : CustomSet, CustomSet -> CustomSet + union = |set1, set2| { + crash "Please implement the 'union' function" + } +} diff --git a/exercises/practice/custom-set/custom-set-test.roc b/exercises/practice/custom-set/custom-set-test.roc index a9cdf184..076d7613 100644 --- a/exercises/practice/custom-set/custom-set-test.roc +++ b/exercises/practice/custom-set/custom-set-test.roc @@ -1,28 +1,8 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/custom-set/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") - -import CustomSet exposing [ - contains, - difference, - from_list, - insert, - intersection, - is_disjoint_with, - is_empty, - is_eq, - is_subset_of, - to_list, - union, -] +# File last updated on 2026-06-22 + +import CustomSet ## ## Returns true if the set contains no elements @@ -30,19 +10,21 @@ import CustomSet exposing [ # sets with no elements are empty -expect - set = from_list([]) - result = set |> is_empty - expected = Bool.true - result == expected +expect { + set = CustomSet.from_list([]) + result = set.is_empty() + expected = Bool.True + result == expected +} # sets with elements are not empty -expect - set = from_list([1]) - result = set |> is_empty - expected = Bool.false - result == expected +expect { + set = CustomSet.from_list([1]) + result = set.is_empty() + expected = Bool.False + result == expected +} ## ## Sets can report if they contain an element @@ -50,27 +32,30 @@ expect # nothing is contained in an empty set -expect - set = from_list([]) - result = set |> contains(1) - expected = Bool.false - result == expected +expect { + set = CustomSet.from_list([]) + result = set.contains(1) + expected = Bool.False + result == expected +} # when the element is in the set -expect - set = from_list([1, 2, 3]) - result = set |> contains(1) - expected = Bool.true - result == expected +expect { + set = CustomSet.from_list([1, 2, 3]) + result = set.contains(1) + expected = Bool.True + result == expected +} # when the element is not in the set -expect - set = from_list([1, 2, 3]) - result = set |> contains(4) - expected = Bool.false - result == expected +expect { + set = CustomSet.from_list([1, 2, 3]) + result = set.contains(4) + expected = Bool.False + result == expected +} ## ## A set is a subset if all of its elements are contained in the other set @@ -78,57 +63,63 @@ expect # empty set is a subset of another empty set -expect - set1 = from_list([]) - set2 = from_list([]) - result = set1 |> is_subset_of(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([]) + result = set1.is_subset_of(set2) + expected = Bool.True + result == expected +} # empty set is a subset of non-empty set -expect - set1 = from_list([]) - set2 = from_list([1]) - result = set1 |> is_subset_of(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([1]) + result = set1.is_subset_of(set2) + expected = Bool.True + result == expected +} # non-empty set is not a subset of empty set -expect - set1 = from_list([1]) - set2 = from_list([]) - result = set1 |> is_subset_of(set2) - expected = Bool.false - result == expected +expect { + set1 = CustomSet.from_list([1]) + set2 = CustomSet.from_list([]) + result = set1.is_subset_of(set2) + expected = Bool.False + result == expected +} # set is a subset of set with exact same elements -expect - set1 = from_list([1, 2, 3]) - set2 = from_list([1, 2, 3]) - result = set1 |> is_subset_of(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([1, 2, 3]) + set2 = CustomSet.from_list([1, 2, 3]) + result = set1.is_subset_of(set2) + expected = Bool.True + result == expected +} # set is a subset of larger set with same elements -expect - set1 = from_list([1, 2, 3]) - set2 = from_list([4, 1, 2, 3]) - result = set1 |> is_subset_of(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([1, 2, 3]) + set2 = CustomSet.from_list([4, 1, 2, 3]) + result = set1.is_subset_of(set2) + expected = Bool.True + result == expected +} # set is not a subset of set that does not contain its elements -expect - set1 = from_list([1, 2, 3]) - set2 = from_list([4, 1, 3]) - result = set1 |> is_subset_of(set2) - expected = Bool.false - result == expected +expect { + set1 = CustomSet.from_list([1, 2, 3]) + set2 = CustomSet.from_list([4, 1, 3]) + result = set1.is_subset_of(set2) + expected = Bool.False + result == expected +} ## ## Sets are disjoint if they share no elements @@ -136,48 +127,53 @@ expect # the empty set is disjoint with itself -expect - set1 = from_list([]) - set2 = from_list([]) - result = set1 |> is_disjoint_with(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([]) + result = set1.is_disjoint_with(set2) + expected = Bool.True + result == expected +} # empty set is disjoint with non-empty set -expect - set1 = from_list([]) - set2 = from_list([1]) - result = set1 |> is_disjoint_with(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([1]) + result = set1.is_disjoint_with(set2) + expected = Bool.True + result == expected +} # non-empty set is disjoint with empty set -expect - set1 = from_list([1]) - set2 = from_list([]) - result = set1 |> is_disjoint_with(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([1]) + set2 = CustomSet.from_list([]) + result = set1.is_disjoint_with(set2) + expected = Bool.True + result == expected +} # sets are not disjoint if they share an element -expect - set1 = from_list([1, 2]) - set2 = from_list([2, 3]) - result = set1 |> is_disjoint_with(set2) - expected = Bool.false - result == expected +expect { + set1 = CustomSet.from_list([1, 2]) + set2 = CustomSet.from_list([2, 3]) + result = set1.is_disjoint_with(set2) + expected = Bool.False + result == expected +} # sets are disjoint if they share no elements -expect - set1 = from_list([1, 2]) - set2 = from_list([3, 4]) - result = set1 |> is_disjoint_with(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([1, 2]) + set2 = CustomSet.from_list([3, 4]) + result = set1.is_disjoint_with(set2) + expected = Bool.True + result == expected +} ## ## Sets with the same elements are equal @@ -185,66 +181,73 @@ expect # empty sets are equal -expect - set1 = from_list([]) - set2 = from_list([]) - result = set1 |> is_eq(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([]) + result = set1.is_eq(set2) + expected = Bool.True + result == expected +} # empty set is not equal to non-empty set -expect - set1 = from_list([]) - set2 = from_list([1, 2, 3]) - result = set1 |> is_eq(set2) - expected = Bool.false - result == expected +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([1, 2, 3]) + result = set1.is_eq(set2) + expected = Bool.False + result == expected +} # non-empty set is not equal to empty set -expect - set1 = from_list([1, 2, 3]) - set2 = from_list([]) - result = set1 |> is_eq(set2) - expected = Bool.false - result == expected +expect { + set1 = CustomSet.from_list([1, 2, 3]) + set2 = CustomSet.from_list([]) + result = set1.is_eq(set2) + expected = Bool.False + result == expected +} # sets with the same elements are equal -expect - set1 = from_list([1, 2]) - set2 = from_list([2, 1]) - result = set1 |> is_eq(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([1, 2]) + set2 = CustomSet.from_list([2, 1]) + result = set1.is_eq(set2) + expected = Bool.True + result == expected +} # sets with different elements are not equal -expect - set1 = from_list([1, 2, 3]) - set2 = from_list([1, 2, 4]) - result = set1 |> is_eq(set2) - expected = Bool.false - result == expected +expect { + set1 = CustomSet.from_list([1, 2, 3]) + set2 = CustomSet.from_list([1, 2, 4]) + result = set1.is_eq(set2) + expected = Bool.False + result == expected +} # set is not equal to larger set with same elements -expect - set1 = from_list([1, 2, 3]) - set2 = from_list([1, 2, 3, 4]) - result = set1 |> is_eq(set2) - expected = Bool.false - result == expected +expect { + set1 = CustomSet.from_list([1, 2, 3]) + set2 = CustomSet.from_list([1, 2, 3, 4]) + result = set1.is_eq(set2) + expected = Bool.False + result == expected +} # set is equal to a set constructed from an array with duplicates -expect - set1 = from_list([1]) - set2 = from_list([1, 1]) - result = set1 |> is_eq(set2) - expected = Bool.true - result == expected +expect { + set1 = CustomSet.from_list([1]) + set2 = CustomSet.from_list([1, 1]) + result = set1.is_eq(set2) + expected = Bool.True + result == expected +} ## ## Unique elements can be added to a set @@ -252,27 +255,30 @@ expect # add to empty set -expect - set = from_list([]) - result = set |> insert(3) - expected = [3] |> from_list - result |> is_eq(expected) +expect { + set = CustomSet.from_list([]) + result = set.insert(3) + expected = [3]->CustomSet.from_list() + result.is_eq(expected) +} # add to non-empty set -expect - set = from_list([1, 2, 4]) - result = set |> insert(3) - expected = [1, 2, 3, 4] |> from_list - result |> is_eq(expected) +expect { + set = CustomSet.from_list([1, 2, 4]) + result = set.insert(3) + expected = [1, 2, 3, 4]->CustomSet.from_list() + result.is_eq(expected) +} # adding an existing element does not change the set -expect - set = from_list([1, 2, 3]) - result = set |> insert(3) - expected = [1, 2, 3] |> from_list - result |> is_eq(expected) +expect { + set = CustomSet.from_list([1, 2, 3]) + result = set.insert(3) + expected = [1, 2, 3]->CustomSet.from_list() + result.is_eq(expected) +} ## ## Intersection returns a set of all shared elements @@ -280,48 +286,53 @@ expect # intersection of two empty sets is an empty set -expect - set1 = from_list([]) - set2 = from_list([]) - result = set1 |> intersection(set2) - expected = [] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([]) + result = set1.intersection(set2) + expected = []->CustomSet.from_list() + result.is_eq(expected) +} # intersection of an empty set and non-empty set is an empty set -expect - set1 = from_list([]) - set2 = from_list([3, 2, 5]) - result = set1 |> intersection(set2) - expected = [] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([3, 2, 5]) + result = set1.intersection(set2) + expected = []->CustomSet.from_list() + result.is_eq(expected) +} # intersection of a non-empty set and an empty set is an empty set -expect - set1 = from_list([1, 2, 3, 4]) - set2 = from_list([]) - result = set1 |> intersection(set2) - expected = [] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([1, 2, 3, 4]) + set2 = CustomSet.from_list([]) + result = set1.intersection(set2) + expected = []->CustomSet.from_list() + result.is_eq(expected) +} # intersection of two sets with no shared elements is an empty set -expect - set1 = from_list([1, 2, 3]) - set2 = from_list([4, 5, 6]) - result = set1 |> intersection(set2) - expected = [] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([1, 2, 3]) + set2 = CustomSet.from_list([4, 5, 6]) + result = set1.intersection(set2) + expected = []->CustomSet.from_list() + result.is_eq(expected) +} # intersection of two sets with shared elements is a set of the shared elements -expect - set1 = from_list([1, 2, 3, 4]) - set2 = from_list([3, 2, 5]) - result = set1 |> intersection(set2) - expected = [2, 3] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([1, 2, 3, 4]) + set2 = CustomSet.from_list([3, 2, 5]) + result = set1.intersection(set2) + expected = [2, 3]->CustomSet.from_list() + result.is_eq(expected) +} ## ## Difference (or Complement) of a set is a set of all elements that are only in the first set @@ -329,48 +340,53 @@ expect # difference of two empty sets is an empty set -expect - set1 = from_list([]) - set2 = from_list([]) - result = set1 |> difference(set2) - expected = [] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([]) + result = set1.difference(set2) + expected = []->CustomSet.from_list() + result.is_eq(expected) +} # difference of empty set and non-empty set is an empty set -expect - set1 = from_list([]) - set2 = from_list([3, 2, 5]) - result = set1 |> difference(set2) - expected = [] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([3, 2, 5]) + result = set1.difference(set2) + expected = []->CustomSet.from_list() + result.is_eq(expected) +} # difference of a non-empty set and an empty set is the non-empty set -expect - set1 = from_list([1, 2, 3, 4]) - set2 = from_list([]) - result = set1 |> difference(set2) - expected = [1, 2, 3, 4] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([1, 2, 3, 4]) + set2 = CustomSet.from_list([]) + result = set1.difference(set2) + expected = [1, 2, 3, 4]->CustomSet.from_list() + result.is_eq(expected) +} # difference of two non-empty sets is a set of elements that are only in the first set -expect - set1 = from_list([3, 2, 1]) - set2 = from_list([2, 4]) - result = set1 |> difference(set2) - expected = [1, 3] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([3, 2, 1]) + set2 = CustomSet.from_list([2, 4]) + result = set1.difference(set2) + expected = [1, 3]->CustomSet.from_list() + result.is_eq(expected) +} # difference removes all duplicates in the first set -expect - set1 = from_list([1, 1]) - set2 = from_list([1]) - result = set1 |> difference(set2) - expected = [] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([1, 1]) + set2 = CustomSet.from_list([1]) + result = set1.difference(set2) + expected = []->CustomSet.from_list() + result.is_eq(expected) +} ## ## Union returns a set of all elements in either set @@ -378,62 +394,87 @@ expect # union of empty sets is an empty set -expect - set1 = from_list([]) - set2 = from_list([]) - result = set1 |> union(set2) - expected = [] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([]) + result = set1.union(set2) + expected = []->CustomSet.from_list() + result.is_eq(expected) +} # union of an empty set and non-empty set is the non-empty set -expect - set1 = from_list([]) - set2 = from_list([2]) - result = set1 |> union(set2) - expected = [2] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([]) + set2 = CustomSet.from_list([2]) + result = set1.union(set2) + expected = [2]->CustomSet.from_list() + result.is_eq(expected) +} # union of a non-empty set and empty set is the non-empty set -expect - set1 = from_list([1, 3]) - set2 = from_list([]) - result = set1 |> union(set2) - expected = [1, 3] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([1, 3]) + set2 = CustomSet.from_list([]) + result = set1.union(set2) + expected = [1, 3]->CustomSet.from_list() + result.is_eq(expected) +} # union of non-empty sets contains all unique elements -expect - set1 = from_list([1, 3]) - set2 = from_list([2, 3]) - result = set1 |> union(set2) - expected = [3, 2, 1] |> from_list - result |> is_eq(expected) +expect { + set1 = CustomSet.from_list([1, 3]) + set2 = CustomSet.from_list([2, 3]) + result = set1.union(set2) + expected = [3, 2, 1]->CustomSet.from_list() + result.is_eq(expected) +} ## ## A set can be converted to a list of items ## # an empty set has an empty list of items -expect - set = from_list([]) - result = set |> to_list |> List.sort_asc - expected = [] - result == expected +expect { + set = CustomSet.from_list([]) + result = set.to_list()->sort_asc() + expected = [] + result == expected +} # a set can provide the list of its items -expect - set = from_list([1, 2, 3, 4]) - result = set |> to_list |> List.sort_asc - expected = [1, 2, 3, 4] - result == expected +expect { + set = CustomSet.from_list([1, 2, 3, 4]) + result = set.to_list()->sort_asc() + expected = [1, 2, 3, 4] + result == expected +} # duplicate items must be removed -expect - set = from_list([1, 2, 2, 3, 3, 3, 4, 4, 4, 4]) - result = set |> to_list |> List.sort_asc - expected = [1, 2, 3, 4] - result == expected +expect { + set = CustomSet.from_list([1, 2, 2, 3, 3, 3, 4, 4, 4, 4]) + result = set.to_list()->sort_asc() + expected = [1, 2, 3, 4] + result == expected +} +sort_asc = |list| { + list.sort_with( + |a, b| { + if a < b { + LT + } else if a > b { + GT + } else { + EQ + } + }, + ) +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/darts/.meta/Example.roc b/exercises/practice/darts/.meta/Example.roc index 2b9853ed..2b85ab80 100644 --- a/exercises/practice/darts/.meta/Example.roc +++ b/exercises/practice/darts/.meta/Example.roc @@ -1,16 +1,19 @@ -module [score] +Darts :: {}.{ + score : F64, F64 -> U64 + score = |x, y| { + d2 = distance_squared(x, y) + if d2 > 100 { + 0 + } else if d2 > 25 { + 1 + } else if d2 > 1 { + 5 + } else { + 10 + } + } +} -distance_squared = |x, y| - x * x + y * y - -score : F64, F64 -> U64 -score = |x, y| - d2 = distance_squared(x, y) - if d2 > 100 then - 0 - else if d2 > 25 then - 1 - else if d2 > 1 then - 5 - else - 10 +distance_squared = |x, y| { + x * x + y * y +} diff --git a/exercises/practice/darts/.meta/template.j2 b/exercises/practice/darts/.meta/template.j2 index 1aa085d5..346a8d66 100644 --- a/exercises/practice/darts/.meta/template.j2 +++ b/exercises/practice/darts/.meta/template.j2 @@ -6,8 +6,11 @@ import Darts exposing [score] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["x"] | to_roc_float }}, {{ case["input"]["y"] | to_roc_float }}) result == {{ case["expected"] }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/darts/Darts.roc b/exercises/practice/darts/Darts.roc index aaacb58e..e0b70b91 100644 --- a/exercises/practice/darts/Darts.roc +++ b/exercises/practice/darts/Darts.roc @@ -1,6 +1,6 @@ -module [score] - -score : F64, F64 -> U64 -score = |x, y| - crash("Please implement the `score` function") - +Darts :: {}.{ + score : F64, F64 -> U64 + score = |x, y| { + crash "Please implement the 'score' function" + } +} diff --git a/exercises/practice/darts/darts-test.roc b/exercises/practice/darts/darts-test.roc index 57830798..7e6a4557 100644 --- a/exercises/practice/darts/darts-test.roc +++ b/exercises/practice/darts/darts-test.roc @@ -1,79 +1,88 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/darts/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Darts exposing [score] # Missed target -expect - result = score(-9.0f64, 9.0f64) - result == 0 +expect { + result = score(-9.0.F64, 9.0.F64) + result == 0 +} # On the outer circle -expect - result = score(0.0f64, 10.0f64) - result == 1 +expect { + result = score(0.0.F64, 10.0.F64) + result == 1 +} # On the middle circle -expect - result = score(-5.0f64, 0.0f64) - result == 5 +expect { + result = score(-5.0.F64, 0.0.F64) + result == 5 +} # On the inner circle -expect - result = score(0.0f64, -1.0f64) - result == 10 +expect { + result = score(0.0.F64, -1.0.F64) + result == 10 +} # Exactly on center -expect - result = score(0.0f64, 0.0f64) - result == 10 +expect { + result = score(0.0.F64, 0.0.F64) + result == 10 +} # Near the center -expect - result = score(-0.1f64, -0.1f64) - result == 10 +expect { + result = score(-0.1.F64, -0.1.F64) + result == 10 +} # Just within the inner circle -expect - result = score(0.7f64, 0.7f64) - result == 10 +expect { + result = score(0.7.F64, 0.7.F64) + result == 10 +} # Just outside the inner circle -expect - result = score(0.8f64, -0.8f64) - result == 5 +expect { + result = score(0.8.F64, -0.8.F64) + result == 5 +} # Just within the middle circle -expect - result = score(-3.5f64, 3.5f64) - result == 5 +expect { + result = score(-3.5.F64, 3.5.F64) + result == 5 +} # Just outside the middle circle -expect - result = score(-3.6f64, -3.6f64) - result == 1 +expect { + result = score(-3.6.F64, -3.6.F64) + result == 1 +} # Just within the outer circle -expect - result = score(-7.0f64, 7.0f64) - result == 1 +expect { + result = score(-7.0.F64, 7.0.F64) + result == 1 +} # Just outside the outer circle -expect - result = score(7.1f64, -7.1f64) - result == 0 +expect { + result = score(7.1.F64, -7.1.F64) + result == 0 +} # Asymmetric position between the inner and middle circles -expect - result = score(0.5f64, -4.0f64) - result == 5 +expect { + result = score(0.5.F64, -4.0.F64) + result == 5 +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/diamond/.meta/Example.roc b/exercises/practice/diamond/.meta/Example.roc index 3ca3e219..ebf612b3 100644 --- a/exercises/practice/diamond/.meta/Example.roc +++ b/exercises/practice/diamond/.meta/Example.roc @@ -1,26 +1,44 @@ -module [diamond] +Diamond :: {}.{ + diamond : U8 -> Str + diamond = |letter| { + letter_index = (letter - 'A').to_i8_try() ?? { + crash "Unreachable" + } + ((-letter_index)..=letter_index) + .map( + |row_index| { + ((-letter_index)..=letter_index) + .map( + |col_index| get_char(row_index, col_index, letter_index), + ) + ->List.from_iter() + ->unwrap_from_utf8() + }, + ) + ->List.from_iter() + ->Str.join_with("\n") + } +} get_char : I8, I8, I8 -> U8 -get_char = |row_index, col_index, letter_index| - if Num.abs(row_index) + Num.abs(col_index) == letter_index then - letter_index - Num.abs(row_index) |> Num.to_u8 |> Num.add('A') - else - ' ' +get_char = |row_index, col_index, letter_index| { + if row_index.abs() + col_index.abs() == letter_index { + ( + (letter_index - row_index.abs()).to_u8_try() ?? { + crash "Unreachable" + }, + ) + 'A' + } else { + ' ' + } +} -unwrap_from_utf8 : List U8 -> Str -unwrap_from_utf8 = |chars| - when chars |> Str.from_utf8 is - Ok(result) -> result - Err(_) -> crash("Str.from_utf8 should never fail here") - -diamond : U8 -> Str -diamond = |letter| - letter_index = letter - 'A' |> Num.to_i8 - List.range({ start: At(-letter_index), end: At(letter_index) }) - |> List.map( - |row_index| - List.range({ start: At(-letter_index), end: At(letter_index) }) - |> List.map(|col_index| get_char(row_index, col_index, letter_index)) - |> unwrap_from_utf8, - ) - |> Str.join_with("\n") +unwrap_from_utf8 : List(U8) -> Str +unwrap_from_utf8 = |chars| { + match Str.from_utf8(chars) { + Ok(result) => result + Err(_) => { + crash "Str.from_utf8 should never fail here" + } + } +} diff --git a/exercises/practice/diamond/.meta/template.j2 b/exercises/practice/diamond/.meta/template.j2 index 348be2e9..635b8b55 100644 --- a/exercises/practice/diamond/.meta/template.j2 +++ b/exercises/practice/diamond/.meta/template.j2 @@ -6,9 +6,12 @@ import {{ exercise | to_pascal }} exposing [{{ exercise | to_snake }}] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ exercise | to_snake }}('{{ case["input"]["letter"] }}') - expected = {{ case["expected"] | to_roc_multiline_string | replace(" ", "·") | indent(8) }} |> Str.replace_each("·", " ") + expected = {{ case["expected"] | to_roc_multiline_string | indent(8) }} result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/diamond/Diamond.roc b/exercises/practice/diamond/Diamond.roc index 46f3b524..e8931caa 100644 --- a/exercises/practice/diamond/Diamond.roc +++ b/exercises/practice/diamond/Diamond.roc @@ -1,5 +1,6 @@ -module [diamond] - -diamond : U8 -> Str -diamond = |letter| - crash("Please implement the 'diamond' function") +Diamond :: {}.{ + diamond : U8 -> Str + diamond = |letter| { + crash "Please implement the 'diamond' function" + } +} diff --git a/exercises/practice/diamond/diamond-test.roc b/exercises/practice/diamond/diamond-test.roc index 46e8b7d3..8ad5ccb4 100644 --- a/exercises/practice/diamond/diamond-test.roc +++ b/exercises/practice/diamond/diamond-test.roc @@ -1,122 +1,115 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/diamond/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Diamond exposing [diamond] # Degenerate case with a single 'A' row -expect - result = diamond('A') - expected = "A" |> Str.replace_each("·", " ") - result == expected +expect { + result = diamond('A') + expected = "A" + result == expected +} # Degenerate case with no row containing 3 distinct groups of spaces -expect - result = diamond('B') - expected = - """ - ·A· - B·B - ·A· - """ - |> Str.replace_each("·", " ") - result == expected +expect { + result = diamond('B') + expected = + \\ A + \\B B + \\ A + + result == expected +} # Smallest non-degenerate case with odd diamond side length -expect - result = diamond('C') - expected = - """ - ··A·· - ·B·B· - C···C - ·B·B· - ··A·· - """ - |> Str.replace_each("·", " ") - result == expected +expect { + result = diamond('C') + expected = + \\ A + \\ B B + \\C C + \\ B B + \\ A + + result == expected +} # Smallest non-degenerate case with even diamond side length -expect - result = diamond('D') - expected = - """ - ···A··· - ··B·B·· - ·C···C· - D·····D - ·C···C· - ··B·B·· - ···A··· - """ - |> Str.replace_each("·", " ") - result == expected +expect { + result = diamond('D') + expected = + \\ A + \\ B B + \\ C C + \\D D + \\ C C + \\ B B + \\ A + + result == expected +} # Largest possible diamond -expect - result = diamond('Z') - expected = - """ - ·························A························· - ························B·B························ - ·······················C···C······················· - ······················D·····D······················ - ·····················E·······E····················· - ····················F·········F···················· - ···················G···········G··················· - ··················H·············H·················· - ·················I···············I················· - ················J·················J················ - ···············K···················K··············· - ··············L·····················L·············· - ·············M·······················M············· - ············N·························N············ - ···········O···························O··········· - ··········P·····························P·········· - ·········Q·······························Q········· - ········R·································R········ - ·······S···································S······· - ······T·····································T······ - ·····U·······································U····· - ····V·········································V···· - ···W···········································W··· - ··X·············································X·· - ·Y···············································Y· - Z·················································Z - ·Y···············································Y· - ··X·············································X·· - ···W···········································W··· - ····V·········································V···· - ·····U·······································U····· - ······T·····································T······ - ·······S···································S······· - ········R·································R········ - ·········Q·······························Q········· - ··········P·····························P·········· - ···········O···························O··········· - ············N·························N············ - ·············M·······················M············· - ··············L·····················L·············· - ···············K···················K··············· - ················J·················J················ - ·················I···············I················· - ··················H·············H·················· - ···················G···········G··················· - ····················F·········F···················· - ·····················E·······E····················· - ······················D·····D······················ - ·······················C···C······················· - ························B·B························ - ·························A························· - """ - |> Str.replace_each("·", " ") - result == expected +expect { + result = diamond('Z') + expected = + \\ A + \\ B B + \\ C C + \\ D D + \\ E E + \\ F F + \\ G G + \\ H H + \\ I I + \\ J J + \\ K K + \\ L L + \\ M M + \\ N N + \\ O O + \\ P P + \\ Q Q + \\ R R + \\ S S + \\ T T + \\ U U + \\ V V + \\ W W + \\ X X + \\ Y Y + \\Z Z + \\ Y Y + \\ X X + \\ W W + \\ V V + \\ U U + \\ T T + \\ S S + \\ R R + \\ Q Q + \\ P P + \\ O O + \\ N N + \\ M M + \\ L L + \\ K K + \\ J J + \\ I I + \\ H H + \\ G G + \\ F F + \\ E E + \\ D D + \\ C C + \\ B B + \\ A + result == expected +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/difference-of-squares/.meta/Example.roc b/exercises/practice/difference-of-squares/.meta/Example.roc index b4528d80..c878aea0 100644 --- a/exercises/practice/difference-of-squares/.meta/Example.roc +++ b/exercises/practice/difference-of-squares/.meta/Example.roc @@ -1,19 +1,23 @@ -module [square_of_sum, sum_of_squares, difference_of_squares] +DifferenceOfSquares :: {}.{ + square_of_sum : U64 -> U64 + square_of_sum = |number| { + s = sum(number) + s * s + } -sum : U64 -> U64 -sum = |number| - number * (number + 1) // 2 - -square_of_sum : U64 -> U64 -square_of_sum = |number| - s = sum(number) - s * s + sum_of_squares : U64 -> U64 + sum_of_squares = |number| { + s = sum(number) + s * (2 * number + 1) // 3 + } -sum_of_squares : U64 -> U64 -sum_of_squares = |number| - s = sum(number) - s * (2 * number + 1) // 3 + difference_of_squares : U64 -> U64 + difference_of_squares = |number| { + (square_of_sum(number)) - (sum_of_squares(number)) + } +} -difference_of_squares : U64 -> U64 -difference_of_squares = |number| - (square_of_sum(number)) - (sum_of_squares(number)) +sum : U64 -> U64 +sum = |number| { + number * (number + 1) // 2 +} diff --git a/exercises/practice/difference-of-squares/.meta/template.j2 b/exercises/practice/difference-of-squares/.meta/template.j2 index 23bf22a3..99af89c3 100644 --- a/exercises/practice/difference-of-squares/.meta/template.j2 +++ b/exercises/practice/difference-of-squares/.meta/template.j2 @@ -11,9 +11,12 @@ import DifferenceOfSquares exposing [square_of_sum, sum_of_squares, difference_o {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["number"] }}) result == {{ case["expected"] }} +} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/difference-of-squares/DifferenceOfSquares.roc b/exercises/practice/difference-of-squares/DifferenceOfSquares.roc index 313925d2..9cd25d31 100644 --- a/exercises/practice/difference-of-squares/DifferenceOfSquares.roc +++ b/exercises/practice/difference-of-squares/DifferenceOfSquares.roc @@ -1,13 +1,16 @@ -module [square_of_sum, sum_of_squares, difference_of_squares] +DifferenceOfSquares :: {}.{ + square_of_sum : U64 -> U64 + square_of_sum = |number| { + crash "Please implement the 'square_of_sum' function" + } -square_of_sum : U64 -> U64 -square_of_sum = |number| - crash("Please implement the `square_of_sum` function") + sum_of_squares : U64 -> U64 + sum_of_squares = |number| { + crash "Please implement the 'sum_of_squares' function" + } -sum_of_squares : U64 -> U64 -sum_of_squares = |number| - crash("Please implement the `sum_of_squares` function") - -difference_of_squares : U64 -> U64 -difference_of_squares = |number| - crash("Please implement the `difference_of_squares` function") + difference_of_squares : U64 -> U64 + difference_of_squares = |number| { + crash "Please implement the 'difference_of_squares' function" + } +} diff --git a/exercises/practice/difference-of-squares/difference-of-squares-test.roc b/exercises/practice/difference-of-squares/difference-of-squares-test.roc index 020e8d09..7ee9a23c 100644 --- a/exercises/practice/difference-of-squares/difference-of-squares-test.roc +++ b/exercises/practice/difference-of-squares/difference-of-squares-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/difference-of-squares/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import DifferenceOfSquares exposing [square_of_sum, sum_of_squares, difference_of_squares] @@ -17,55 +9,68 @@ import DifferenceOfSquares exposing [square_of_sum, sum_of_squares, difference_o ## # square of sum 1 -expect - result = square_of_sum(1) - result == 1 +expect { + result = square_of_sum(1) + result == 1 +} # square of sum 5 -expect - result = square_of_sum(5) - result == 225 +expect { + result = square_of_sum(5) + result == 225 +} # square of sum 100 -expect - result = square_of_sum(100) - result == 25502500 +expect { + result = square_of_sum(100) + result == 25502500 +} ## ## Sum the squares of the numbers up to the given number ## # sum of squares 1 -expect - result = sum_of_squares(1) - result == 1 +expect { + result = sum_of_squares(1) + result == 1 +} # sum of squares 5 -expect - result = sum_of_squares(5) - result == 55 +expect { + result = sum_of_squares(5) + result == 55 +} # sum of squares 100 -expect - result = sum_of_squares(100) - result == 338350 +expect { + result = sum_of_squares(100) + result == 338350 +} ## ## Subtract sum of squares from square of sums ## # difference of squares 1 -expect - result = difference_of_squares(1) - result == 0 +expect { + result = difference_of_squares(1) + result == 0 +} # difference of squares 5 -expect - result = difference_of_squares(5) - result == 170 +expect { + result = difference_of_squares(5) + result == 170 +} # difference of squares 100 -expect - result = difference_of_squares(100) - result == 25164150 +expect { + result = difference_of_squares(100) + result == 25164150 +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/dominoes/.meta/Example.roc b/exercises/practice/dominoes/.meta/Example.roc index fdcbf632..1322a33a 100644 --- a/exercises/practice/dominoes/.meta/Example.roc +++ b/exercises/practice/dominoes/.meta/Example.roc @@ -1,36 +1,43 @@ -module [find_chain] +Dominoes :: {}.{ + Domino : (U8, U8) -Domino : (U8, U8) + find_chain : List(Domino) -> Try(List(Domino), [NoChainExists]) + find_chain = |dominoes| { + find_chain_helper = |used, available| { + match available { + [] => match used { + [] => Ok([]) + [(single_left, single_right)] => + if single_left == single_right Ok(used) else Err(NoChainExists) + [(first_left, _), .., (_, last_right)] => + if first_left == last_right Ok(used) else Err(NoChainExists) + } + [first_available, .. as rest_available] => match used { + [] => find_chain_helper([first_available], rest_available) + [.., (_, last_used_right)] => { + available + .fold_with_index_until( + Err(NoChainExists), + |_, (domino_left, domino_right), index| { + maybe_chain = + if last_used_right == domino_left { + find_chain_helper(used.append((domino_left, domino_right)), available.drop_at(index)) + } else if last_used_right == domino_right { + find_chain_helper(used.append((domino_right, domino_left)), available.drop_at(index)) + } else { + Err(NoChainExists) + } + match maybe_chain { + Ok(chain) => Break(Ok(chain)) + Err(NoChainExists) => Continue(Err(NoChainExists)) + } + }, + ) + } + } + } + } -find_chain : List Domino -> Result (List Domino) [NoChainExists] -find_chain = |dominoes| - find_chain_helper = |used, available| - when available is - [] -> - when used is - [] -> Ok([]) - [single] -> - if single.0 == single.1 then Ok(used) else Err(NoChainExists) - - [first, .., last] -> if first.0 == last.1 then Ok(used) else Err(NoChainExists) - - [first_available, .. as rest_available] -> - when used is - [] -> find_chain_helper([first_available], rest_available) - [.., last_used] -> - available - |> List.walk_with_index_until( - Err(NoChainExists), - |_, domino, index| - maybe_chain = - if last_used.1 == domino.0 then - find_chain_helper((used |> List.append(domino)), (available |> List.drop_at(index))) - else if last_used.1 == domino.1 then - find_chain_helper((used |> List.append((domino.1, domino.0))), (available |> List.drop_at(index))) - else - Err(NoChainExists) - when maybe_chain is - Ok(chain) -> Break(Ok(chain)) - Err(NoChainExists) -> Continue(Err(NoChainExists)), - ) - find_chain_helper([], dominoes) + find_chain_helper([], dominoes) + } +} diff --git a/exercises/practice/dominoes/.meta/template.j2 b/exercises/practice/dominoes/.meta/template.j2 index cd28e4a8..9df4df99 100644 --- a/exercises/practice/dominoes/.meta/template.j2 +++ b/exercises/practice/dominoes/.meta/template.j2 @@ -2,7 +2,7 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [find_chain] +import {{ exercise | to_pascal }} exposing [Domino, find_chain] {% macro to_list_of_pairs(dominoes) -%} [ @@ -12,51 +12,60 @@ import {{ exercise | to_pascal }} exposing [find_chain] ] {%- endmacro %} -Domino : (U8, U8) - -## Rotate each domino if needed to ensure that the small side is on the left -canonicalize : List Domino -> List Domino -canonicalize = |dominoes| - dominoes - |> List.map(|domino| - if domino.0 > domino.1 then (domino.1, domino.0) else domino - ) - -## Ensure that the given result is Ok and is a valid chain for the -## given list of dominoes -is_valid_chain_for : Result (List Domino) _, List Domino -> Bool -is_valid_chain_for = |maybe_chain, dominoes| - when maybe_chain is - Err(_) -> Bool.false - Ok(chain) -> - if Set.from_list(canonicalize(chain)) == Set.from_list(canonicalize(dominoes)) then - when chain is - [] -> Bool.true - [.., last] -> - chain - |> List.walk_until( - Ok(last), - |state, domino| - when state is - Err(InvalidChain) -> crash "Unreachable" - Ok(previous) -> - if previous.1 == domino.0 then - Continue(Ok(domino)) - else - Break(Err(InvalidChain)) - ) - |> Result.is_ok - else - Bool.false - {% for case in cases -%} # {{ case["description"] }} -expect - result = find_chain({{ to_list_of_pairs(case["input"]["dominoes"]) }}) +expect { + dominoes = {{ to_list_of_pairs(case["input"]["dominoes"]) }} + result = find_chain(dominoes) {%- if case["expected"] %} - result |> is_valid_chain_for({{ to_list_of_pairs(case["input"]["dominoes"]) }}) + result -> is_valid_chain_for(dominoes) {%- else %} - result |> Result.is_err + result.is_err() {%- endif %} +} {% endfor %} + +# # Compare two dominoes in lexicographical order, for example (3, 1) > (2, 5) +compare_dominoes : (U8, U8), (U8, U8) -> [LT, GT, EQ] +compare_dominoes = |a, b| { + if a.0 < b.0 { LT } else if a.0 > b.0 { GT } else + if a.1 < b.1 { LT } else if a.1 > b.1 { GT } else { EQ } +} + +# # Rotate each domino if needed to ensure that the small side is on the left +# # then sort the dominoes in lexicographical order +canonicalize : List((U8, U8)) -> List((U8, U8)) +canonicalize = |dominoes| { + maybe_flip : (U8, U8) -> (U8, U8) + maybe_flip = |domino| { + if domino.0 > domino.1 (domino.1, domino.0) else domino + } + dominoes.map(maybe_flip).sort_with(compare_dominoes) +} + +# # Checks whether the list of dominoes forms a valid chain +is_valid_chain = |dominoes| { + match dominoes { + [] => Bool.True + [(first_left, first_right), .. as rest] => { + var $previous = first_right + for (next_left, next_right) in rest { + if $previous != next_left { return Bool.False } + $previous = next_right + } + $previous == first_left + } + } +} + +# # Check whether the given chain (if Ok) is valid and corresponds to the given +# # list of dominoes +is_valid_chain_for = |maybe_chain, dominoes| { + match maybe_chain { + Ok(chain) => (canonicalize(chain) == canonicalize(dominoes)) and is_valid_chain(chain) + Err(_) => Bool.False + } +} + +{{ macros.footer() }} diff --git a/exercises/practice/dominoes/Dominoes.roc b/exercises/practice/dominoes/Dominoes.roc index 36652837..9f18eab2 100644 --- a/exercises/practice/dominoes/Dominoes.roc +++ b/exercises/practice/dominoes/Dominoes.roc @@ -1,7 +1,8 @@ -module [find_chain] +Dominoes :: {}.{ + Domino : (U8, U8) -Domino : (U8, U8) - -find_chain : List Domino -> Result (List Domino) _ -find_chain = |dominoes| - crash("Please implement the 'find_chain' function") + find_chain : List(Domino) -> Try(List(Domino), _) + find_chain = |dominoes| { + crash "Please implement the 'find_chain' function" + } +} diff --git a/exercises/practice/dominoes/dominoes-test.roc b/exercises/practice/dominoes/dominoes-test.roc index bffbeb94..4401433e 100644 --- a/exercises/practice/dominoes/dominoes-test.roc +++ b/exercises/practice/dominoes/dominoes-test.roc @@ -1,117 +1,214 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/dominoes/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") - -import Dominoes exposing [find_chain] - -Domino : (U8, U8) - -## Rotate each domino if needed to ensure that the small side is on the left -canonicalize : List Domino -> List Domino -canonicalize = |dominoes| - dominoes - |> List.map( - |domino| - if domino.0 > domino.1 then (domino.1, domino.0) else domino, - ) - -## Ensure that the given result is Ok and is a valid chain for the -## given list of dominoes -is_valid_chain_for : Result (List Domino) _, List Domino -> Bool -is_valid_chain_for = |maybe_chain, dominoes| - when maybe_chain is - Err(_) -> Bool.false - Ok(chain) -> - if Set.from_list(canonicalize(chain)) == Set.from_list(canonicalize(dominoes)) then - when chain is - [] -> Bool.true - [.., last] -> - chain - |> List.walk_until( - Ok(last), - |state, domino| - when state is - Err(InvalidChain) -> crash "Unreachable" - Ok(previous) -> - if previous.1 == domino.0 then - Continue(Ok(domino)) - else - Break(Err(InvalidChain)), - ) - |> Result.is_ok - else - Bool.false +# File last updated on 2026-06-22 + +import Dominoes exposing [Domino, find_chain] # empty input = empty output -expect - result = find_chain([]) - result |> is_valid_chain_for([]) +expect { + dominoes = [] + result = find_chain(dominoes) + result->is_valid_chain_for(dominoes) +} # singleton input = singleton output -expect - result = find_chain([(1, 1)]) - result |> is_valid_chain_for([(1, 1)]) +expect { + dominoes = [ + (1, 1), + ] + result = find_chain(dominoes) + result->is_valid_chain_for(dominoes) +} # singleton that can't be chained -expect - result = find_chain([(1, 2)]) - result |> Result.is_err +expect { + dominoes = [ + (1, 2), + ] + result = find_chain(dominoes) + result.is_err() +} # three elements -expect - result = find_chain([(1, 2), (3, 1), (2, 3)]) - result |> is_valid_chain_for([(1, 2), (3, 1), (2, 3)]) +expect { + dominoes = [ + (1, 2), + (3, 1), + (2, 3), + ] + result = find_chain(dominoes) + result->is_valid_chain_for(dominoes) +} # can reverse dominoes -expect - result = find_chain([(1, 2), (1, 3), (2, 3)]) - result |> is_valid_chain_for([(1, 2), (1, 3), (2, 3)]) +expect { + dominoes = [ + (1, 2), + (1, 3), + (2, 3), + ] + result = find_chain(dominoes) + result->is_valid_chain_for(dominoes) +} # can't be chained -expect - result = find_chain([(1, 2), (4, 1), (2, 3)]) - result |> Result.is_err +expect { + dominoes = [ + (1, 2), + (4, 1), + (2, 3), + ] + result = find_chain(dominoes) + result.is_err() +} # disconnected - simple -expect - result = find_chain([(1, 1), (2, 2)]) - result |> Result.is_err +expect { + dominoes = [ + (1, 1), + (2, 2), + ] + result = find_chain(dominoes) + result.is_err() +} # disconnected - double loop -expect - result = find_chain([(1, 2), (2, 1), (3, 4), (4, 3)]) - result |> Result.is_err +expect { + dominoes = [ + (1, 2), + (2, 1), + (3, 4), + (4, 3), + ] + result = find_chain(dominoes) + result.is_err() +} # disconnected - single isolated -expect - result = find_chain([(1, 2), (2, 3), (3, 1), (4, 4)]) - result |> Result.is_err +expect { + dominoes = [ + (1, 2), + (2, 3), + (3, 1), + (4, 4), + ] + result = find_chain(dominoes) + result.is_err() +} # need backtrack -expect - result = find_chain([(1, 2), (2, 3), (3, 1), (2, 4), (2, 4)]) - result |> is_valid_chain_for([(1, 2), (2, 3), (3, 1), (2, 4), (2, 4)]) +expect { + dominoes = [ + (1, 2), + (2, 3), + (3, 1), + (2, 4), + (2, 4), + ] + result = find_chain(dominoes) + result->is_valid_chain_for(dominoes) +} # separate loops -expect - result = find_chain([(1, 2), (2, 3), (3, 1), (1, 1), (2, 2), (3, 3)]) - result |> is_valid_chain_for([(1, 2), (2, 3), (3, 1), (1, 1), (2, 2), (3, 3)]) +expect { + dominoes = [ + (1, 2), + (2, 3), + (3, 1), + (1, 1), + (2, 2), + (3, 3), + ] + result = find_chain(dominoes) + result->is_valid_chain_for(dominoes) +} # nine elements -expect - result = find_chain([(1, 2), (5, 3), (3, 1), (1, 2), (2, 4), (1, 6), (2, 3), (3, 4), (5, 6)]) - result |> is_valid_chain_for([(1, 2), (5, 3), (3, 1), (1, 2), (2, 4), (1, 6), (2, 3), (3, 4), (5, 6)]) +expect { + dominoes = [ + (1, 2), + (5, 3), + (3, 1), + (1, 2), + (2, 4), + (1, 6), + (2, 3), + (3, 4), + (5, 6), + ] + result = find_chain(dominoes) + result->is_valid_chain_for(dominoes) +} # separate three-domino loops -expect - result = find_chain([(1, 2), (2, 3), (3, 1), (4, 5), (5, 6), (6, 4)]) - result |> Result.is_err +expect { + dominoes = [ + (1, 2), + (2, 3), + (3, 1), + (4, 5), + (5, 6), + (6, 4), + ] + result = find_chain(dominoes) + result.is_err() +} + +# # Compare two dominoes in lexicographical order, for example (3, 1) > (2, 5) +compare_dominoes : (U8, U8), (U8, U8) -> [LT, GT, EQ] +compare_dominoes = |a, b| { + if a.0 < b.0 { + LT + } else if a.0 > b.0 { + GT + } else + if a.1 < b.1 { + LT + } else if a.1 > b.1 { + GT + } else { + EQ + } +} + +# # Rotate each domino if needed to ensure that the small side is on the left +# # then sort the dominoes in lexicographical order +canonicalize : List((U8, U8)) -> List((U8, U8)) +canonicalize = |dominoes| { + maybe_flip : (U8, U8) -> (U8, U8) + maybe_flip = |domino| { + if domino.0 > domino.1 (domino.1, domino.0) else domino + } + dominoes.map(maybe_flip).sort_with(compare_dominoes) +} +# # Checks whether the list of dominoes forms a valid chain +is_valid_chain = |dominoes| { + match dominoes { + [] => Bool.True + [(first_left, first_right), .. as rest] => { + var $previous = first_right + for (next_left, next_right) in rest { + if $previous != next_left { + return Bool.False + } + $previous = next_right + } + $previous == first_left + } + } +} + +# # Check whether the given chain (if Ok) is valid and corresponds to the given +# # list of dominoes +is_valid_chain_for = |maybe_chain, dominoes| { + match maybe_chain { + Ok(chain) => (canonicalize(chain) == canonicalize(dominoes)) and is_valid_chain(chain) + Err(_) => Bool.False + } +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/eliuds-eggs/.meta/Example.roc b/exercises/practice/eliuds-eggs/.meta/Example.roc index af844f63..349cc6b1 100644 --- a/exercises/practice/eliuds-eggs/.meta/Example.roc +++ b/exercises/practice/eliuds-eggs/.meta/Example.roc @@ -1,12 +1,15 @@ -module [egg_count] +EliudsEggs :: {}.{ + egg_count : U64 -> U64 + egg_count = |number| { + help = |count, remaining| { + if remaining == 0 { + count + } else { + digit = remaining % 2 + help((count + digit), (remaining // 2)) + } + } -egg_count : U64 -> U64 -egg_count = |number| - help = |count, remaining| - if remaining == 0 then - count - else - digit = Num.rem(remaining, 2) - help((count + digit), (remaining // 2)) - - help(0, number) + help(0, number) + } +} diff --git a/exercises/practice/eliuds-eggs/.meta/template.j2 b/exercises/practice/eliuds-eggs/.meta/template.j2 index bfe5d436..d21aa9e8 100644 --- a/exercises/practice/eliuds-eggs/.meta/template.j2 +++ b/exercises/practice/eliuds-eggs/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["number"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/eliuds-eggs/EliudsEggs.roc b/exercises/practice/eliuds-eggs/EliudsEggs.roc index dcaa72d7..969d3ef1 100644 --- a/exercises/practice/eliuds-eggs/EliudsEggs.roc +++ b/exercises/practice/eliuds-eggs/EliudsEggs.roc @@ -1,5 +1,6 @@ -module [egg_count] - -egg_count : U64 -> U64 -egg_count = |number| - crash("Please implement 'egg_count'") +EliudsEggs :: {}.{ + egg_count : U64 -> U64 + egg_count = |number| { + crash "Please implement the 'egg_count' function" + } +} diff --git a/exercises/practice/eliuds-eggs/eliuds-eggs-test.roc b/exercises/practice/eliuds-eggs/eliuds-eggs-test.roc index 50ba292c..a6e0660b 100644 --- a/exercises/practice/eliuds-eggs/eliuds-eggs-test.roc +++ b/exercises/practice/eliuds-eggs/eliuds-eggs-test.roc @@ -1,34 +1,34 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/eliuds-eggs/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import EliudsEggs exposing [egg_count] # 0 eggs -expect - result = egg_count(0) - result == 0 +expect { + result = egg_count(0) + result == 0 +} # 1 egg -expect - result = egg_count(16) - result == 1 +expect { + result = egg_count(16) + result == 1 +} # 4 eggs -expect - result = egg_count(89) - result == 4 +expect { + result = egg_count(89) + result == 4 +} # 13 eggs -expect - result = egg_count(2000000000) - result == 13 +expect { + result = egg_count(2000000000) + result == 13 +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/error-handling/.docs/instructions.append.md b/exercises/practice/error-handling/.docs/instructions.append.md index b5625706..e4519f4f 100644 --- a/exercises/practice/error-handling/.docs/instructions.append.md +++ b/exercises/practice/error-handling/.docs/instructions.append.md @@ -6,35 +6,35 @@ You are building a tiny web server that queries an even tinier user database. So - the connection may be insecure: the URL must start with `"https://"` - the domain name may be invalid: in this exercise, it should be `"example.com"` -- the page may not be found: only `"https://example.com/"`, `"https://example.com/users/"` and `"https://example.com/users/"` are allowed -- the `userId` may not be a positive integer -- no user may exist in the database with this `userId` +- the page may not be found: only `"https://example.com/"`, `"https://example.com/users/"` and `"https://example.com/users/"` are allowed +- the `user_id` may not be a positive integer +- no user may exist in the database with this `user_id` When things go wrong, it's important to give the end user a nice and helpful error message so that they can solve the issue. For this, you need to ensure that your code propagates informative errors from the point where the error is detected to the point where the error message is produced. -Luckily, Roc allows you to carry payload (i.e., data) inside your `Err` tag. It's tempting to carry an error message directly (e.g., `Err "User #42 was not found"`), and this may be fine in some simple cases, but this has several limitations: +Luckily, Roc allows you to carry payload (i.e., data) inside your `Err` tag. It's tempting to carry an error message directly (e.g., `Err("User #42 was not found")`), and this may be fine in some simple cases, but this has several limitations: - you might not have enough context inside the function that detects the error to produce a sufficiently helpful error message. - For example, the `Str.to_u64` function can be used to parse all sorts of integers: days, seconds, user IDs, and more. If the error message just says `Could not convert string "0.5" to a positive integer`, the user may not have enough context to solve the issue. - in many cases your error handling code may need to handle some errors differently than others. However, if the errors only carry string payloads, your error handling code will have to parse that string to know what problem occurred: this is inefficient and it can easily break if the error message is ever tweaked. - if your website is multilingual, you will need to translate the error message to the user's language. It's going to be much easier to do that if the error payload is machine-friendly data rather than an English string. -So in this exercise, your errors will instead carry a meaningful tag along with its own helpful payload. For example, if the user is not found, the error will look like `Err (UserNotFound 42)`. +So in this exercise, your errors will instead carry a meaningful tag along with its own helpful payload. For example, if the user is not found, the error will look like `Err(UserNotFound(42))`. Okay, let's get started! Here's what you need to do: -1. Implement `getUser` to return the requested user from the `users` "database" (it's actually just a `Dict`). Make sure the function returns `Err (UserNotFound userId)` in case the user is not found, instead of `Err KeyNotFound`. -2. Implement `parseUserId` to convert the URL's path (such as `"/users/123"`) to a positive integer user ID (`123`). In case of error, return `Err (InvalidUserId userIdStr)`. +1. Implement `get_user` to return the requested user from the `users` "database" (it's actually just a `Dict`). Make sure the function returns `Err(UserNotFound(user_id))` in case the user is not found, instead of `Err(KeyNotFound)`. +2. Implement `parse_user_id` to convert the URL's path (such as `"/users/123"`) to a positive integer user ID (`123`). In case of error, return `Err(InvalidUserId(user_id_str))`. 3. Implement `getPage`: - - If the URL is `"https://example.com/"`, return `Ok "Home page"` - - If the URL is `"https://example.com/users/"`, return `Ok "Users page"` - - If the URL is `"https://example.com/users/"`, parse the user ID, load the user with that ID, and return `Ok "'s page"` - - If the URL prefix is not `"https://"`, return `Err (InsecureConnection url)` - - If the URL domain name is not `"example.com"`, return `Err (InvalidDomain url)` - - If the path is not `/` or `/users/` or `/users/`, return `Err (PageNotFound path)` - - If the user ID is not a positive integer, return `Err (InvalidUserId userIdStr)` - - If the user does not exist, return `Err (UserNotFound userId)` -4. Implement `errorMessage` to convert the previous errors to translated error messages. The function should at least handle English, but you are encouraged to try handling another language as well. The English error messages should like this: + - If the URL is `"https://example.com/"`, return `Ok("Home page")` + - If the URL is `"https://example.com/users/"`, return `Ok("Users page")` + - If the URL is `"https://example.com/users/"`, parse the user ID, load the user with that ID, and return `Ok("'s page")` + - If the URL prefix is not `"https://"`, return `Err(InsecureConnection(url))` + - If the URL domain name is not `"example.com"`, return `Err(InvalidDomain(url))` + - If the path is not `/` or `/users/` or `/users/`, return `Err(PageNotFound(path))` + - If the user ID is not a positive integer, return `Err(InvalidUserId(user_id_str))` + - If the user does not exist, return `Err(UserNotFound(user_id))` +4. Implement `error_essage` to convert the previous errors to translated error messages. The function should at least handle English, but you are encouraged to try handling another language as well. The English error messages should like this: - `"Insecure connection (non HTTPS): http://example.com/users/789"` - `"Invalid domain name: https://google.com/wrong"` diff --git a/exercises/practice/error-handling/.meta/Example.roc b/exercises/practice/error-handling/.meta/Example.roc index 1a7c077b..301a2c84 100644 --- a/exercises/practice/error-handling/.meta/Example.roc +++ b/exercises/practice/error-handling/.meta/Example.roc @@ -1,64 +1,91 @@ -module [get_user, parse_user_id, get_page, error_message] +ErrorHandling :: {}.{ + User : { name : Str } + UserId : U64 + users : Dict(UserId, User) + users = Dict.from_list( + [ + (123, { name: "Alice" }), + (456, { name: "Bob" }), + (789, { name: "Charlie" }), + ], + ) -User : { name : Str } -UserId : U64 + # # Returns the user with the given user_id, or UserNotFound(user_id) + get_user : UserId -> Try(User, [UserNotFound(UserId), ..]) + get_user = |user_id| { + users.get(user_id).map_err(|_| UserNotFound(user_id)) + } -users : Dict UserId User -users = - Dict.from_list( - [ - (123, { name: "Alice" }), - (456, { name: "Bob" }), - (789, { name: "Charlie" }), - ], - ) + # # Parses a string formatted as "/users/" and returns the user_id + # # or InvalidUserId(user_id_str) is the part of the path cannot + # # be parsed to a U64 + parse_user_id : Str -> Try(UserId, [InvalidUserId(Str), ..]) + parse_user_id = |path| { + user_id_str = path.drop_prefix("/users/") + user_id_str->U64.from_str().map_err(|_| InvalidUserId(user_id_str)) + } -get_user : UserId -> Result User [UserNotFound UserId] -get_user = |user_id| - users |> Dict.get(user_id) |> Result.map_err(|KeyNotFound| UserNotFound(user_id)) + # # Takes a URL and returns the page content, or the appropriate error + get_page : Str -> Try(Str, [InsecureConnection(Str), InvalidDomain(Str), InvalidUserId(Str), UserNotFound(UserId), PageNotFound(Str)]) + get_page = |url| { + match parse_path(url) { + Ok(path) => { + match path { + "/" => Ok("Home page") + "/users/" => Ok("Users page") + user_path if user_path.starts_with("/users/") => { + user = user_path->parse_user_id()?->get_user()? + Ok("${user.name}'s page") + } + unknown_path => Err(PageNotFound(unknown_path)) + } + } + Err(InsecureConnection(url_err)) => Err(InsecureConnection(url_err)) + Err(InvalidDomain(url_err)) => Err(InvalidDomain(url_err)) + } + } -parse_user_id : Str -> Result UserId [InvalidUserId Str] -parse_user_id = |path| - user_id_str = path |> Str.replace_first("/users/", "") - user_id_str |> Str.to_u64 |> Result.map_err(|InvalidNumStr| InvalidUserId(user_id_str)) + # # Takes an error and returns the corresponding error message in the + # # given language + error_message : [InsecureConnection(Str), InvalidDomain(Str), InvalidUserId(Str), UserNotFound(UserId), PageNotFound(Str)], [English, French] -> Str + error_message = |err, language| { + match language { + English => { + match err { + InsecureConnection(url) => "Insecure connection (non HTTPS): ${url}" + InvalidDomain(url) => "Invalid domain name: ${url}" + InvalidUserId(user_id_str) => "User ID is not a positive integer: ${user_id_str}" + UserNotFound(user_id) => "User #${user_id.to_str()} was not found" + PageNotFound(path) => "Page not found: ${path}" + } + } -parse_path : Str -> Result Str [InvalidDomain Str, InsecureConnection Str] -parse_path = |url| - prefix = "https://example.com" - if url |> Str.starts_with(prefix) then - url |> Str.replace_first(prefix, "") |> Ok - else if url |> Str.starts_with("https://") then - Err(InvalidDomain(url)) - else - Err(InsecureConnection(url)) + French => { + match err { + InsecureConnection(url) => "Connexion non sécurisée (non HTTPS): ${url}" + InvalidDomain(url) => "Ce nom de domaine est incorrect: ${url}" + InvalidUserId(user_id_str) => "Cet identifiant utilisateur devrait être un entier positif: ${user_id_str}" + UserNotFound(user_id) => "L'utilisateur #${user_id.to_str()} est inconnu" + PageNotFound(path) => "Cette page est inconnue: ${path}" + } + } + } + } +} -get_page : Str -> Result Str [InsecureConnection Str, InvalidDomain Str, InvalidUserId Str, UserNotFound UserId, PageNotFound Str] -get_page = |url| - when parse_path(url)? is - "/" -> Ok("Home page") - "/users/" -> Ok("Users page") - user_path if user_path |> Str.starts_with("/users/") -> - user_id = parse_user_id(user_path)? - user = get_user(user_id)? - Ok("${user.name}'s page") - - unknown_path -> Err(PageNotFound(unknown_path)) - -error_message : [InsecureConnection Str, InvalidDomain Str, InvalidUserId Str, UserNotFound UserId, PageNotFound Str], [English, French] -> Str -error_message = |err, language| - when language is - English -> - when err is - InsecureConnection(url) -> "Insecure connection (non HTTPS): ${url}" - InvalidDomain(url) -> "Invalid domain name: ${url}" - InvalidUserId(user_id_str) -> "User ID is not a positive integer: ${user_id_str}" - UserNotFound(user_id) -> "User #${user_id |> Num.to_str} was not found" - PageNotFound(path) -> "Page not found: ${path}" - - French -> - when err is - InsecureConnection(url) -> "Connexion non sécurisée (non HTTPS): ${url}" - InvalidDomain(url) -> "Ce nom de domaine est incorrect: ${url}" - InvalidUserId(user_id_str) -> "Cet identifiant utilisateur devrait être un entier positif: ${user_id_str}" - UserNotFound(user_id) -> "L'utilisateur #${user_id |> Num.to_str} est inconnu" - PageNotFound(path) -> "Cette page est inconnue: ${path}" +# # Parses a URL formatted as https://example.com/ and returns "/" +# # or the appropriate error if the URL is malformed or http instead of https +parse_path : Str -> Try(Str, [InvalidDomain(Str), InsecureConnection(Str), ..]) +parse_path = |url| { + prefix = "https://example.com" + if url.starts_with(prefix) { + path = url.drop_prefix(prefix) + if path == "" { + Ok("/") + } else if path.starts_with("/") Ok(path) else Err(InvalidDomain(url)) + } else if url.starts_with("https://") { + Err(InvalidDomain(url)) + } else { + Err(InsecureConnection(url)) + } +} diff --git a/exercises/practice/error-handling/ErrorHandling.roc b/exercises/practice/error-handling/ErrorHandling.roc index a9d54371..d1758e5b 100644 --- a/exercises/practice/error-handling/ErrorHandling.roc +++ b/exercises/practice/error-handling/ErrorHandling.roc @@ -1,30 +1,39 @@ -module [get_user, parse_user_id, get_page, error_message] +ErrorHandling :: {}.{ + User : { name : Str } + UserId : U64 + users : Dict(UserId, User) + users = Dict.from_list( + [ + (123, { name: "Alice" }), + (456, { name: "Bob" }), + (789, { name: "Charlie" }), + ], + ) -User : { name : Str } -UserId : U64 + # # Returns the user with the given user_id, or UserNotFound(user_id) + get_user : UserId -> Try(User, _) + get_user = |user_id| { + crash "Please implement the 'get_user' function" + } -users : Dict UserId User -users = - Dict.from_list( - [ - (123, { name: "Alice" }), - (456, { name: "Bob" }), - (789, { name: "Charlie" }), - ], - ) + # # Parses a string formatted as "/users/" and returns the user_id + # # or InvalidUserId(user_id_str) is the part of the path cannot + # # be parsed to a U64 + parse_user_id : Str -> Try(UserId, _) + parse_user_id = |path| { + crash "Please implement the 'parse_user_id' function" + } -get_user : UserId -> Result User [UserNotFound Str] -get_user = |user_id| - crash("Please implement the 'get_user' function") + # # Takes a URL and returns the page content, or the appropriate error + get_page : Str -> Try(Str, _) + get_page = |url| { + crash "Please implement the 'get_page' function" + } -parse_user_id : Str -> Result UserId [InvalidUserId Str] -parse_user_id = |path| - crash("Please implement the 'parse_user_id' function") - -get_page : Str -> Result Str _ -get_page = |url| - crash("Please implement the 'get_page' function") - -error_message : _, [English] -> Str -error_message = |err, language| - crash("Please implement the 'error_message' function") + # # Takes an error and returns the corresponding error message in the + # # given language + error_message : [InsecureConnection(Str), InvalidDomain(Str), InvalidUserId(Str), UserNotFound(UserId), PageNotFound(Str)], [English, French] -> Str + error_message = |err, language| { + crash "Please implement the 'error_message' function" + } +} diff --git a/exercises/practice/error-handling/error-handling-test.roc b/exercises/practice/error-handling/error-handling-test.roc index f6bb817d..d9d7e613 100644 --- a/exercises/practice/error-handling/error-handling-test.roc +++ b/exercises/practice/error-handling/error-handling-test.roc @@ -1,78 +1,82 @@ -# File last updated on 2024-09-12 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-13 import ErrorHandling exposing [get_user, parse_user_id, get_page, error_message] ## -## get_user should return Ok or Err UserNotFound +## get_user should return Ok() or Err(UserNotFound()) ## # get_user 123 should return Alice -expect - result = get_user(123) - result == Ok({ name: "Alice" }) +expect { + result = get_user(123) + result == Ok({ name: "Alice" }) +} # get_user 456 should return Bob -expect - result = get_user(456) - result == Ok({ name: "Bob" }) +expect { + result = get_user(456) + result == Ok({ name: "Bob" }) +} # get_user 789 should return Charlie -expect - result = get_user(789) - result == Ok({ name: "Charlie" }) +expect { + result = get_user(789) + result == Ok({ name: "Charlie" }) +} # get_user 42 should return an error -expect - result = get_user(42) - result == Err(UserNotFound(42)) +expect { + result = get_user(42) + result == Err(UserNotFound(42)) +} ## -## parse_user_id should parse a string to a positive integer and return -## Ok if successful, or Err InvalidUserId otherwise +## parse_user_id should parse a string formatted as "/users/" to a +## positive integer, and return Ok() if successful, or +## Err(InvalidUserId()) otherwise ## -# Parsing a valid userId should return Ok -expect - result = parse_user_id("123") - result == Ok(123) +# Parsing a valid user ID should return Ok() +expect { + result = parse_user_id("/users/123") + result == Ok(123) +} # Parsing an empty string should fail -expect - result = parse_user_id("") - result == Err(InvalidUserId("")) +expect { + result = parse_user_id("/users/") + result == Err(InvalidUserId("")) +} # Parsing a negative number should fail -expect - result = parse_user_id("-123") - result == Err(InvalidUserId("-123")) +expect { + result = parse_user_id("/users/-123") + result == Err(InvalidUserId("-123")) +} # Parsing a fractional number should fail -expect - result = parse_user_id("123.456") - result == Err(InvalidUserId("123.456")) +expect { + result = parse_user_id("/users/123.456") + result == Err(InvalidUserId("123.456")) +} # Parsing a number in scientific format should fail -expect - result = parse_user_id("1e03") - result == Err(InvalidUserId("1e03")) +expect { + result = parse_user_id("/users/1e03") + result == Err(InvalidUserId("1e03")) +} # Parsing a string containing letters should fail -expect - result = parse_user_id("abc") - result == Err(InvalidUserId("abc")) +expect { + result = parse_user_id("/users/abc") + result == Err(InvalidUserId("abc")) +} -# Parsing a string containing a valid userId followed by junk should fail -expect - result = parse_user_id("123 abc") - result == Err(InvalidUserId("123 abc")) +# Parsing a string containing a valid user_id followed by junk should fail +expect { + result = parse_user_id("/users/123 abc") + result == Err(InvalidUserId("123 abc")) +} ## ## get_page should return Ok with the desired page if it exists @@ -80,120 +84,110 @@ expect ## # No error for root URL -expect - result = get_page("https://example.com/") - result == Ok("Home page") +expect { + result = get_page("https://example.com/") + result == Ok("Home page") +} # No error for users URL -expect - result = get_page("https://example.com/users/") - result == Ok("Users page") +expect { + result = get_page("https://example.com/users/") + result == Ok("Users page") +} # No error for specific user URL -expect - result = get_page("https://example.com/users/123") - result == Ok("Alice's page") +expect { + result = get_page("https://example.com/users/123") + result == Ok("Alice's page") +} # No error for specific user URL -expect - result = get_page("https://example.com/users/456") - result == Ok("Bob's page") +expect { + result = get_page("https://example.com/users/456") + result == Ok("Bob's page") +} # No error for specific user URL -expect - result = get_page("https://example.com/users/789") - result == Ok("Charlie's page") +expect { + result = get_page("https://example.com/users/789") + result == Ok("Charlie's page") +} # Error: insecure connection -expect - result = get_page("http://example.com/users/789") - result == Err(InsecureConnection("http://example.com/users/789")) +expect { + result = get_page("http://example.com/users/789") + result == Err(InsecureConnection("http://example.com/users/789")) +} # Error: invalid domain name -expect - result = get_page("https://google.com/wrong") - result == Err(InvalidDomain("https://google.com/wrong")) +expect { + result = get_page("https://google.com/wrong") + result == Err(InvalidDomain("https://google.com/wrong")) +} # Error: page not found -expect - result = get_page("https://example.com/oops") - result == Err(PageNotFound("/oops")) +expect { + result = get_page("https://example.com/oops") + result == Err(PageNotFound("/oops")) +} -# Error: invalid userId -expect - result = get_page("https://example.com/users/abc") - result == Err(InvalidUserId("abc")) +# Error: invalid user_id +expect { + result = get_page("https://example.com/users/abc") + result == Err(InvalidUserId("abc")) +} # Error: user not found -expect - result = get_page("https://example.com/users/42") - result == Err(UserNotFound(42)) +expect { + result = get_page("https://example.com/users/42") + result == Err(UserNotFound(42)) +} ## -## Handle errors and return a clear message to the user, in the user's language +## Handle errors and return a clear message to the user, in the user's language. ## Your implementation must at least handle English, but you can handle other ## languages if you want ## -# No error for root URL: just return the Ok result -expect - page_result = get_page("https://example.com/") - result = page_result |> Result.map_err(|err| err |> error_message(English)) - result == Ok("Home page") - -# No error for users URL: just return the Ok result -expect - page_result = get_page("https://example.com/users/") - result = page_result |> Result.map_err(|err| err |> error_message(English)) - result == Ok("Users page") - -# No error for specific user URL: just return the Ok result -expect - page_result = get_page("https://example.com/users/123") - result = page_result |> Result.map_err(|err| err |> error_message(English)) - result == Ok("Alice's page") - -# No error for specific user URL: just return the Ok result -expect - page_result = get_page("https://example.com/users/456") - result = page_result |> Result.map_err(|err| err |> error_message(English)) - result == Ok("Bob's page") - -# No error for specific user URL: just return the Ok result -expect - page_result = get_page("https://example.com/users/789") - result = page_result |> Result.map_err(|err| err |> error_message(English)) - result == Ok("Charlie's page") - # Error: insecure connection # Note: instead of displaying an error message, the server could automatically # redirect the user to the HTTPS URL. This is an example of a recoverable error # which would be easy to handle because the error payload is machine-friendly -expect - page_result = get_page("http://example.com/users/789") - result = page_result |> Result.map_err(|err| err |> error_message(English)) - result == Err("Insecure connection (non HTTPS): http://example.com/users/789") +expect { + page = get_page("http://example.com/users/789") + result = page.map_err(|e| e->error_message(English)) + result == Err("Insecure connection (non HTTPS): http://example.com/users/789") +} # Error: invalid domain name -expect - page_result = get_page("https://google.com/wrong") - result = page_result |> Result.map_err(|err| err |> error_message(English)) - result == Err("Invalid domain name: https://google.com/wrong") +expect { + page = get_page("https://google.com/wrong") + result = page.map_err(|e| e->error_message(English)) + result == Err("Invalid domain name: https://google.com/wrong") +} # Error: page not found -expect - page_result = get_page("https://example.com/oops") - result = page_result |> Result.map_err(|err| err |> error_message(English)) - result == Err("Page not found: /oops") +expect { + page = get_page("https://example.com/oops") + result = page.map_err(|e| e->error_message(English)) + result == Err("Page not found: /oops") +} -# Error: invalid userId -expect - page_result = get_page("https://example.com/users/abc") - result = page_result |> Result.map_err(|err| err |> error_message(English)) - result == Err("User ID is not a positive integer: abc") +# Error: invalid user_id +expect { + page = get_page("https://example.com/users/abc") + result = page.map_err(|e| e->error_message(English)) + result == Err("User ID is not a positive integer: abc") +} # Error: user not found -expect - page_result = get_page("https://example.com/users/42") - result = page_result |> Result.map_err(|err| err |> error_message(English)) - result == Err("User #42 was not found") +expect { + page = get_page("https://example.com/users/42") + result = page.map_err(|e| e->error_message(English)) + result == Err("User #42 was not found") +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/etl/.meta/Example.roc b/exercises/practice/etl/.meta/Example.roc index 4d38720e..8ac2e399 100644 --- a/exercises/practice/etl/.meta/Example.roc +++ b/exercises/practice/etl/.meta/Example.roc @@ -1,18 +1,24 @@ -module [transform] +Etl :: {}.{ + transform : Dict(U64, List(U8)) -> Dict(U8, U64) + transform = |legacy| { + legacy + .join_map( + |score, letters| { + letters + .map( + |c| (to_lower(c), score), + ) + ->Dict.from_list() + }, + ) + } +} to_lower : U8 -> U8 -to_lower = |char| - if char >= 'A' and char <= 'Z' then - char - 'A' + 'a' - else - char - -transform : Dict U64 (List U8) -> Dict U8 U64 -transform = |legacy| - legacy - |> Dict.join_map( - |score, letters| - letters - |> List.map(|c| (to_lower(c), score)) - |> Dict.from_list, - ) +to_lower = |char| { + if char >= 'A' and char <= 'Z' { + char - 'A' + 'a' + } else { + char + } +} diff --git a/exercises/practice/etl/.meta/template.j2 b/exercises/practice/etl/.meta/template.j2 index eb107999..5d64e73a 100644 --- a/exercises/practice/etl/.meta/template.j2 +++ b/exercises/practice/etl/.meta/template.j2 @@ -6,7 +6,7 @@ import {{ exercise | to_pascal }} exposing [transform] {% for case in cases -%} # {{ case["description"] }} -expect +expect { legacy = Dict.from_list([ {%- for score, letters in case["input"]["legacy"].items() %} @@ -20,5 +20,8 @@ expect {%- endfor %} ]) transform(legacy) == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/etl/Etl.roc b/exercises/practice/etl/Etl.roc index e692b2a1..927b0c29 100644 --- a/exercises/practice/etl/Etl.roc +++ b/exercises/practice/etl/Etl.roc @@ -1,5 +1,6 @@ -module [transform] - -transform : Dict U64 (List U8) -> Dict U8 U64 -transform = |legacy| - crash("Please implement the 'transform' function") +Etl :: {}.{ + transform : Dict(U64, List(U8)) -> Dict(U8, U64) + transform = |legacy| { + crash "Please implement the 'transform' function" + } +} diff --git a/exercises/practice/etl/etl-test.roc b/exercises/practice/etl/etl-test.roc index e8cc3f08..44f09180 100644 --- a/exercises/practice/etl/etl-test.roc +++ b/exercises/practice/etl/etl-test.roc @@ -1,117 +1,117 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/etl/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Etl exposing [transform] # single letter -expect - legacy = - Dict.from_list( - [ - (1, ['A']), - ], - ) - expected = - Dict.from_list( - [ - ('a', 1), - ], - ) - transform(legacy) == expected +expect { + legacy = + Dict.from_list( + [ + (1, ['A']), + ], + ) + expected = + Dict.from_list( + [ + ('a', 1), + ], + ) + transform(legacy) == expected +} # single score with multiple letters -expect - legacy = - Dict.from_list( - [ - (1, ['A', 'E', 'I', 'O', 'U']), - ], - ) - expected = - Dict.from_list( - [ - ('a', 1), - ('e', 1), - ('i', 1), - ('o', 1), - ('u', 1), - ], - ) - transform(legacy) == expected +expect { + legacy = + Dict.from_list( + [ + (1, ['A', 'E', 'I', 'O', 'U']), + ], + ) + expected = + Dict.from_list( + [ + ('a', 1), + ('e', 1), + ('i', 1), + ('o', 1), + ('u', 1), + ], + ) + transform(legacy) == expected +} # multiple scores with multiple letters -expect - legacy = - Dict.from_list( - [ - (1, ['A', 'E']), - (2, ['D', 'G']), - ], - ) - expected = - Dict.from_list( - [ - ('a', 1), - ('d', 2), - ('e', 1), - ('g', 2), - ], - ) - transform(legacy) == expected +expect { + legacy = + Dict.from_list( + [ + (1, ['A', 'E']), + (2, ['D', 'G']), + ], + ) + expected = + Dict.from_list( + [ + ('a', 1), + ('d', 2), + ('e', 1), + ('g', 2), + ], + ) + transform(legacy) == expected +} # multiple scores with differing numbers of letters -expect - legacy = - Dict.from_list( - [ - (1, ['A', 'E', 'I', 'O', 'U', 'L', 'N', 'R', 'S', 'T']), - (2, ['D', 'G']), - (3, ['B', 'C', 'M', 'P']), - (4, ['F', 'H', 'V', 'W', 'Y']), - (5, ['K']), - (8, ['J', 'X']), - (10, ['Q', 'Z']), - ], - ) - expected = - Dict.from_list( - [ - ('a', 1), - ('b', 3), - ('c', 3), - ('d', 2), - ('e', 1), - ('f', 4), - ('g', 2), - ('h', 4), - ('i', 1), - ('j', 8), - ('k', 5), - ('l', 1), - ('m', 3), - ('n', 1), - ('o', 1), - ('p', 3), - ('q', 10), - ('r', 1), - ('s', 1), - ('t', 1), - ('u', 1), - ('v', 4), - ('w', 4), - ('x', 8), - ('y', 4), - ('z', 10), - ], - ) - transform(legacy) == expected +expect { + legacy = + Dict.from_list( + [ + (1, ['A', 'E', 'I', 'O', 'U', 'L', 'N', 'R', 'S', 'T']), + (2, ['D', 'G']), + (3, ['B', 'C', 'M', 'P']), + (4, ['F', 'H', 'V', 'W', 'Y']), + (5, ['K']), + (8, ['J', 'X']), + (10, ['Q', 'Z']), + ], + ) + expected = + Dict.from_list( + [ + ('a', 1), + ('b', 3), + ('c', 3), + ('d', 2), + ('e', 1), + ('f', 4), + ('g', 2), + ('h', 4), + ('i', 1), + ('j', 8), + ('k', 5), + ('l', 1), + ('m', 3), + ('n', 1), + ('o', 1), + ('p', 3), + ('q', 10), + ('r', 1), + ('s', 1), + ('t', 1), + ('u', 1), + ('v', 4), + ('w', 4), + ('x', 8), + ('y', 4), + ('z', 10), + ], + ) + transform(legacy) == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/flatten-array/.meta/Example.roc b/exercises/practice/flatten-array/.meta/Example.roc index b394fbb1..df6e1c6b 100644 --- a/exercises/practice/flatten-array/.meta/Example.roc +++ b/exercises/practice/flatten-array/.meta/Example.roc @@ -1,10 +1,23 @@ -module [flatten] +FlattenArray :: {}.{ + NestedValue := [Value(I64), Null, NestedArray(List(NestedValue))] -NestedValue : [Value I64, Null, NestedArray (List NestedValue)] + flatten : NestedValue -> List(I64) + flatten = |array| { + match array { + NestedArray(list) => list->join_map(flatten) + Value(value) => [value] + Null => [] + } + } +} -flatten : NestedValue -> List I64 -flatten = |array| - when array is - NestedArray(list) -> list |> List.join_map(flatten) - Value(value) -> [value] - Null -> [] +# The following function should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} diff --git a/exercises/practice/flatten-array/.meta/template.j2 b/exercises/practice/flatten-array/.meta/template.j2 index 8314785c..10318665 100644 --- a/exercises/practice/flatten-array/.meta/template.j2 +++ b/exercises/practice/flatten-array/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}(({{ plugins.to_nested(case["input"]["array"]) }})) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/flatten-array/FlattenArray.roc b/exercises/practice/flatten-array/FlattenArray.roc index a3e3d37e..997b050b 100644 --- a/exercises/practice/flatten-array/FlattenArray.roc +++ b/exercises/practice/flatten-array/FlattenArray.roc @@ -1,7 +1,19 @@ -module [flatten] +FlattenArray :: {}.{ + NestedValue := [Value(I64), Null, NestedArray(List(NestedValue))] -NestedValue : [Value I64, Null, NestedArray (List NestedValue)] + flatten : NestedValue -> List(I64) + flatten = |array| { + crash "Please implement the 'flatten' function" + } +} -flatten : NestedValue -> List I64 -flatten = |array| - crash("Please implement the 'flatten' function") +# The following function should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} diff --git a/exercises/practice/flatten-array/flatten-array-test.roc b/exercises/practice/flatten-array/flatten-array-test.roc index 76197918..b9d1fad9 100644 --- a/exercises/practice/flatten-array/flatten-array-test.roc +++ b/exercises/practice/flatten-array/flatten-array-test.roc @@ -1,279 +1,306 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/flatten-array/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import FlattenArray exposing [flatten] # empty -expect - result = flatten( - NestedArray( - [ - ], - ), - ) - result == [] +expect { + result = flatten( + ( + NestedArray( + [], + ), + ), + ) + result == [] +} # no nesting -expect - result = flatten( - NestedArray( - [ - Value(0), - Value(1), - Value(2), - ], - ), - ) - result == [0, 1, 2] +expect { + result = flatten( + ( + NestedArray( + [ + Value(0), + Value(1), + Value(2), + ], + ), + ), + ) + result == [0, 1, 2] +} # flattens a nested array -expect - result = flatten( - NestedArray( - [ - NestedArray( - [ - NestedArray( - [ - ], - ), - ], - ), - ], - ), - ) - result == [] +expect { + result = flatten( + ( + NestedArray( + [ + NestedArray( + [ + NestedArray( + [], + ), + ], + ), + ], + ), + ), + ) + result == [] +} # flattens array with just integers present -expect - result = flatten( - NestedArray( - [ - Value(1), - NestedArray( - [ - Value(2), - Value(3), - Value(4), - Value(5), - Value(6), - Value(7), - ], - ), - Value(8), - ], - ), - ) - result == [1, 2, 3, 4, 5, 6, 7, 8] +expect { + result = flatten( + ( + NestedArray( + [ + Value(1), + NestedArray( + [ + Value(2), + Value(3), + Value(4), + Value(5), + Value(6), + Value(7), + ], + ), + Value(8), + ], + ), + ), + ) + result == [1, 2, 3, 4, 5, 6, 7, 8] +} # 5 level nesting -expect - result = flatten( - NestedArray( - [ - Value(0), - Value(2), - NestedArray( - [ - NestedArray( - [ - Value(2), - Value(3), - ], - ), - Value(8), - Value(100), - Value(4), - NestedArray( - [ - NestedArray( - [ - NestedArray( - [ - Value(50), - ], - ), - ], - ), - ], - ), - ], - ), - Value(-2), - ], - ), - ) - result == [0, 2, 2, 3, 8, 100, 4, 50, -2] +expect { + result = flatten( + ( + NestedArray( + [ + Value(0), + Value(2), + NestedArray( + [ + NestedArray( + [ + Value(2), + Value(3), + ], + ), + Value(8), + Value(100), + Value(4), + NestedArray( + [ + NestedArray( + [ + NestedArray( + [ + Value(50), + ], + ), + ], + ), + ], + ), + ], + ), + Value(-2), + ], + ), + ), + ) + result == [0, 2, 2, 3, 8, 100, 4, 50, -2] +} # 6 level nesting -expect - result = flatten( - NestedArray( - [ - Value(1), - NestedArray( - [ - Value(2), - NestedArray( - [ - NestedArray( - [ - Value(3), - ], - ), - ], - ), - NestedArray( - [ - Value(4), - NestedArray( - [ - NestedArray( - [ - Value(5), - ], - ), - ], - ), - ], - ), - Value(6), - Value(7), - ], - ), - Value(8), - ], - ), - ) - result == [1, 2, 3, 4, 5, 6, 7, 8] +expect { + result = flatten( + ( + NestedArray( + [ + Value(1), + NestedArray( + [ + Value(2), + NestedArray( + [ + NestedArray( + [ + Value(3), + ], + ), + ], + ), + NestedArray( + [ + Value(4), + NestedArray( + [ + NestedArray( + [ + Value(5), + ], + ), + ], + ), + ], + ), + Value(6), + Value(7), + ], + ), + Value(8), + ], + ), + ), + ) + result == [1, 2, 3, 4, 5, 6, 7, 8] +} # null values are omitted from the final result -expect - result = flatten( - NestedArray( - [ - Value(1), - Value(2), - Null, - ], - ), - ) - result == [1, 2] +expect { + result = flatten( + ( + NestedArray( + [ + Value(1), + Value(2), + Null, + ], + ), + ), + ) + result == [1, 2] +} # consecutive null values at the front of the array are omitted from the final result -expect - result = flatten( - NestedArray( - [ - Null, - Null, - Value(3), - ], - ), - ) - result == [3] +expect { + result = flatten( + ( + NestedArray( + [ + Null, + Null, + Value(3), + ], + ), + ), + ) + result == [3] +} # consecutive null values in the middle of the array are omitted from the final result -expect - result = flatten( - NestedArray( - [ - Value(1), - Null, - Null, - Value(4), - ], - ), - ) - result == [1, 4] +expect { + result = flatten( + ( + NestedArray( + [ + Value(1), + Null, + Null, + Value(4), + ], + ), + ), + ) + result == [1, 4] +} # 6 level nested array with null values -expect - result = flatten( - NestedArray( - [ - Value(0), - Value(2), - NestedArray( - [ - NestedArray( - [ - Value(2), - Value(3), - ], - ), - Value(8), - NestedArray( - [ - NestedArray( - [ - Value(100), - ], - ), - ], - ), - Null, - NestedArray( - [ - NestedArray( - [ - Null, - ], - ), - ], - ), - ], - ), - Value(-2), - ], - ), - ) - result == [0, 2, 2, 3, 8, 100, -2] +expect { + result = flatten( + ( + NestedArray( + [ + Value(0), + Value(2), + NestedArray( + [ + NestedArray( + [ + Value(2), + Value(3), + ], + ), + Value(8), + NestedArray( + [ + NestedArray( + [ + Value(100), + ], + ), + ], + ), + Null, + NestedArray( + [ + NestedArray( + [ + Null, + ], + ), + ], + ), + ], + ), + Value(-2), + ], + ), + ), + ) + result == [0, 2, 2, 3, 8, 100, -2] +} # all values in nested array are null -expect - result = flatten( - NestedArray( - [ - Null, - NestedArray( - [ - NestedArray( - [ - NestedArray( - [ - Null, - ], - ), - ], - ), - ], - ), - Null, - Null, - NestedArray( - [ - NestedArray( - [ - Null, - Null, - ], - ), - Null, - ], - ), - Null, - ], - ), - ) - result == [] +expect { + result = flatten( + ( + NestedArray( + [ + Null, + NestedArray( + [ + NestedArray( + [ + NestedArray( + [ + Null, + ], + ), + ], + ), + ], + ), + Null, + Null, + NestedArray( + [ + NestedArray( + [ + Null, + Null, + ], + ), + Null, + ], + ), + Null, + ], + ), + ), + ) + result == [] +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/flower-field/.meta/Example.roc b/exercises/practice/flower-field/.meta/Example.roc index 34120bcc..d7d69258 100644 --- a/exercises/practice/flower-field/.meta/Example.roc +++ b/exercises/practice/flower-field/.meta/Example.roc @@ -1,56 +1,78 @@ -module [annotate] +FlowerField :: {}.{ + annotate : Str -> Str + annotate = |garden| { + rows = garden.to_utf8().split_on('\n') + annotated = + rows.map_with_index( + |row, y| { + row.map_with_index( + |cell, x| { + if cell == '*' { + '*' + } else { + match count_neighbors(rows, x, y) { + 0 => ' ' + n => '0' + n + } + } + }, + ) + ->Str.from_utf8() + }, + ) -is_flower : List (List U8), I64, I64 -> Result Bool [OutOfBounds] -is_flower = |rows, nx, ny| - x = Num.to_u64_checked(nx)? - y = Num.to_u64_checked(ny)? - rows - |> List.get(y)? - |> List.get(x)? - |> Bool.is_eq('*') - |> Ok + annotated.map( + |maybe_row| { + match maybe_row { + Ok(row) => row + Err(_) => { + crash "Unreachable" + } + } + }, + ) + ->Str.join_with("\n") + } +} -count_neighbors : List (List U8), U64, U64 -> Num * -count_neighbors = |rows, x, y| - [-1, 0, 1] - |> List.map( - |dy| - [-1, 0, 1] - |> List.map( - |dx| - when is_flower(rows, (Num.to_i64(x) + dx), (Num.to_i64(y) + dy)) is - Ok(flower) -> if flower then 1 else 0 - Err(OutOfBounds) -> 0, - ) - |> List.sum, - ) - |> List.sum +count_neighbors : List(List(U8)), U64, U64 -> U8 +count_neighbors = |rows, x, y| { + [-1, 0, 1].map( + |dy| { + [-1, 0, 1].map( + |dx| { + nx = dx + ( + x.to_i64_try() ?? { + crash "Unreachable" + }, + ) + ny = dy + ( + y.to_i64_try() ?? { + crash "Unreachable" + }, + ) + if is_flower(rows, nx, ny) 1 else 0 + }, + ) + .sum() + }, + ) + .sum() +} -annotate : Str -> Str -annotate = |garden| - rows = garden |> Str.to_utf8 |> List.split_on('\n') - annotated = - rows - |> List.map_with_index( - |row, y| - row - |> List.map_with_index( - |cell, x| - if cell == '*' then - '*' - else - when count_neighbors(rows, x, y) is - 0 -> ' ' - n -> '0' + n, - ) - |> Str.from_utf8, - ) - - annotated - |> List.map( - |maybe_row| - when maybe_row is - Ok(row) -> row - Err(_) -> crash("Unreachable"), - ) # fromUtf8 cannot fail in the code above - |> Str.join_with("\n") +is_flower : List(List(U8)), I64, I64 -> Bool +is_flower = |rows, nx, ny| { + if nx < 0 or ny < 0 { + Bool.False + } else { + x = nx.to_u64_try() ?? { + crash "Unreachable" + } + y = ny.to_u64_try() ?? { + crash "Unreachable" + } + row = rows.get(y) ?? [] + cell = row.get(x) ?? ' ' + cell == '*' + } +} diff --git a/exercises/practice/flower-field/.meta/template.j2 b/exercises/practice/flower-field/.meta/template.j2 index 14eafc1f..908494cb 100644 --- a/exercises/practice/flower-field/.meta/template.j2 +++ b/exercises/practice/flower-field/.meta/template.j2 @@ -6,10 +6,13 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect - garden = {{ case["input"]["garden"] | to_roc_multiline_string | replace(" ", "·") | indent(8) }} |> Str.replace_each("·", " ") +expect { + garden = {{ case["input"]["garden"] | to_roc_multiline_string | indent(8) }} result = {{ case["property"] | to_snake }}(garden) - expected = {{ case["expected"] | to_roc_multiline_string | replace(" ", "·") | indent(8) }} |> Str.replace_each("·", " ") + expected = {{ case["expected"] | to_roc_multiline_string | indent(8) }} result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/flower-field/.meta/tests.toml b/exercises/practice/flower-field/.meta/tests.toml index c2b24fda..965ba8fd 100644 --- a/exercises/practice/flower-field/.meta/tests.toml +++ b/exercises/practice/flower-field/.meta/tests.toml @@ -44,3 +44,6 @@ description = "cross" [dd9d4ca8-9e68-4f78-a677-a2a70fd7a7b8] description = "large garden" + +[6e4ac13a-3e43-4728-a2e3-3551d4b1a996] +description = "multiple adjacent flowers" diff --git a/exercises/practice/flower-field/FlowerField.roc b/exercises/practice/flower-field/FlowerField.roc index 5e7b8931..232ed236 100644 --- a/exercises/practice/flower-field/FlowerField.roc +++ b/exercises/practice/flower-field/FlowerField.roc @@ -1,5 +1,6 @@ -module [annotate] - -annotate : Str -> Str -annotate = |garden| - crash("Please implement the 'annotate' function") +FlowerField :: {}.{ + annotate : Str -> Str + annotate = |garden| { + crash "Please implement the 'annotate' function" + } +} diff --git a/exercises/practice/flower-field/flower-field-test.roc b/exercises/practice/flower-field/flower-field-test.roc index d9cebab3..e6b7a910 100644 --- a/exercises/practice/flower-field/flower-field-test.roc +++ b/exercises/practice/flower-field/flower-field-test.roc @@ -1,212 +1,196 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/flower-field/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import FlowerField exposing [annotate] # no rows -expect - garden = "" |> Str.replace_each("·", " ") - result = annotate(garden) - expected = "" |> Str.replace_each("·", " ") - result == expected +expect { + garden = "" + result = annotate(garden) + expected = "" + result == expected +} # no columns -expect - garden = "" |> Str.replace_each("·", " ") - result = annotate(garden) - expected = "" |> Str.replace_each("·", " ") - result == expected +expect { + garden = "" + result = annotate(garden) + expected = "" + result == expected +} # no flowers -expect - garden = - """ - ··· - ··· - ··· - """ - |> Str.replace_each("·", " ") - result = annotate(garden) - expected = - """ - ··· - ··· - ··· - """ - |> Str.replace_each("·", " ") - result == expected +expect { + garden = + \\ + \\ + \\ + + result = annotate(garden) + expected = + \\ + \\ + \\ + + result == expected +} # garden full of flowers -expect - garden = - """ - *** - *** - *** - """ - |> Str.replace_each("·", " ") - result = annotate(garden) - expected = - """ - *** - *** - *** - """ - |> Str.replace_each("·", " ") - result == expected +expect { + garden = + \\*** + \\*** + \\*** + + result = annotate(garden) + expected = + \\*** + \\*** + \\*** + + result == expected +} # flower surrounded by spaces -expect - garden = - """ - ··· - ·*· - ··· - """ - |> Str.replace_each("·", " ") - result = annotate(garden) - expected = - """ - 111 - 1*1 - 111 - """ - |> Str.replace_each("·", " ") - result == expected +expect { + garden = + \\ + \\ * + \\ + + result = annotate(garden) + expected = + \\111 + \\1*1 + \\111 + + result == expected +} # space surrounded by flowers -expect - garden = - """ - *** - *·* - *** - """ - |> Str.replace_each("·", " ") - result = annotate(garden) - expected = - """ - *** - *8* - *** - """ - |> Str.replace_each("·", " ") - result == expected +expect { + garden = + \\*** + \\* * + \\*** + + result = annotate(garden) + expected = + \\*** + \\*8* + \\*** + + result == expected +} # horizontal line -expect - garden = "·*·*·" |> Str.replace_each("·", " ") - result = annotate(garden) - expected = "1*2*1" |> Str.replace_each("·", " ") - result == expected +expect { + garden = " * * " + result = annotate(garden) + expected = "1*2*1" + result == expected +} # horizontal line, flowers at edges -expect - garden = "*···*" |> Str.replace_each("·", " ") - result = annotate(garden) - expected = "*1·1*" |> Str.replace_each("·", " ") - result == expected +expect { + garden = "* *" + result = annotate(garden) + expected = "*1 1*" + result == expected +} # vertical line -expect - garden = - """ - · - * - · - * - · - """ - |> Str.replace_each("·", " ") - result = annotate(garden) - expected = - """ - 1 - * - 2 - * - 1 - """ - |> Str.replace_each("·", " ") - result == expected +expect { + garden = + \\ + \\* + \\ + \\* + \\ + + result = annotate(garden) + expected = + \\1 + \\* + \\2 + \\* + \\1 + + result == expected +} # vertical line, flowers at edges -expect - garden = - """ - * - · - · - · - * - """ - |> Str.replace_each("·", " ") - result = annotate(garden) - expected = - """ - * - 1 - · - 1 - * - """ - |> Str.replace_each("·", " ") - result == expected +expect { + garden = + \\* + \\ + \\ + \\ + \\* + + result = annotate(garden) + expected = + \\* + \\1 + \\ + \\1 + \\* + + result == expected +} # cross -expect - garden = - """ - ··*·· - ··*·· - ***** - ··*·· - ··*·· - """ - |> Str.replace_each("·", " ") - result = annotate(garden) - expected = - """ - ·2*2· - 25*52 - ***** - 25*52 - ·2*2· - """ - |> Str.replace_each("·", " ") - result == expected +expect { + garden = + \\ * + \\ * + \\***** + \\ * + \\ * + + result = annotate(garden) + expected = + \\ 2*2 + \\25*52 + \\***** + \\25*52 + \\ 2*2 + + result == expected +} # large garden -expect - garden = - """ - ·*··*· - ··*··· - ····*· - ···*·* - ·*··*· - ······ - """ - |> Str.replace_each("·", " ") - result = annotate(garden) - expected = - """ - 1*22*1 - 12*322 - ·123*2 - 112*4* - 1*22*2 - 111111 - """ - |> Str.replace_each("·", " ") - result == expected +expect { + garden = + \\ * * + \\ * + \\ * + \\ * * + \\ * * + \\ + + result = annotate(garden) + expected = + \\1*22*1 + \\12*322 + \\ 123*2 + \\112*4* + \\1*22*2 + \\111111 + + result == expected +} + +# multiple adjacent flowers +expect { + garden = " ** " + result = annotate(garden) + expected = "1**1" + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/food-chain/.meta/Example.roc b/exercises/practice/food-chain/.meta/Example.roc index d12dc249..ae0568f9 100644 --- a/exercises/practice/food-chain/.meta/Example.roc +++ b/exercises/practice/food-chain/.meta/Example.roc @@ -1,63 +1,49 @@ -module [recite] +FoodChain :: {}.{ + recite : U64, U64 -> Str + recite = |start_verse, end_verse| { + verse_list + .sublist({ start: start_verse - 1, len: end_verse - (start_verse - 1) }) + ->Str.join_with("\n\n") + } +} -recite : U64, U64 -> Str -recite = |start_verse, end_verse| - List.sublist(verse_list, { start: start_verse - 1, len: end_verse - (start_verse - 1) }) - |> Str.join_with("\n\n") - -verse_list : List Str -verse_list = - initial_state = { verses: [], verse_body: "I don't know why she swallowed the fly. Perhaps she'll die.", previous_animal: "fly" } - result = - List.walk( - animals, - initial_state, - |{ verses, verse_body, previous_animal }, animal| - description = Dict.get(animal_descriptions, previous_animal) |> Result.with_default("") - new_verse_body = - """ - She swallowed the ${animal.name} to catch the ${previous_animal}${description}. - ${verse_body} - """ - verse = - """ - I know an old lady who swallowed a ${animal.name}. - ${animal.exclamation} - ${new_verse_body} - """ - { - verses: List.append(verses, verse), - verse_body: new_verse_body, - previous_animal: animal.name, - }, - ) - List.join([[first_verse], result.verses, [last_verse]]) +verse_list : List(Str) +verse_list = { + initial_state = { verses: [], verse_body: "I don't know why she swallowed the fly. Perhaps she'll die.", previous_animal: "fly" } + result = + animals.fold( + initial_state, + |{ verses, verse_body, previous_animal }, animal| { + description = animal_descriptions.get(previous_animal) ?? "" + new_verse_body = "She swallowed the ${animal.name} to catch the ${previous_animal}${description}.\n${verse_body}" + verse = "I know an old lady who swallowed a ${animal.name}.\n${animal.exclamation}\n${new_verse_body}" + { + verses: verses.append(verse), + verse_body: new_verse_body, + previous_animal: animal.name, + } + }, + ) + [first_verse].concat(result.verses).concat([last_verse]) +} # I would prefer to use an if expression here, but I ran into a compiler bug doing that -animal_descriptions : Dict Str Str -animal_descriptions = - Dict.single("spider", " that wriggled and jiggled and tickled inside her") +animal_descriptions : Dict(Str, Str) +animal_descriptions = + Dict.single("spider", " that wriggled and jiggled and tickled inside her") -animals : List { name : Str, exclamation : Str } +animals : List({ name : Str, exclamation : Str }) animals = [ - { name: "spider", exclamation: "It wriggled and jiggled and tickled inside her." }, - { name: "bird", exclamation: "How absurd to swallow a bird!" }, - { name: "cat", exclamation: "Imagine that, to swallow a cat!" }, - { name: "dog", exclamation: "What a hog, to swallow a dog!" }, - { name: "goat", exclamation: "Just opened her throat and swallowed a goat!" }, - { name: "cow", exclamation: "I don't know how she swallowed a cow!" }, + { name: "spider", exclamation: "It wriggled and jiggled and tickled inside her." }, + { name: "bird", exclamation: "How absurd to swallow a bird!" }, + { name: "cat", exclamation: "Imagine that, to swallow a cat!" }, + { name: "dog", exclamation: "What a hog, to swallow a dog!" }, + { name: "goat", exclamation: "Just opened her throat and swallowed a goat!" }, + { name: "cow", exclamation: "I don't know how she swallowed a cow!" }, ] first_verse : Str -first_verse = - """ - I know an old lady who swallowed a fly. - I don't know why she swallowed the fly. Perhaps she'll die. - """ +first_verse = "I know an old lady who swallowed a fly.\nI don't know why she swallowed the fly. Perhaps she'll die." last_verse : Str -last_verse = - """ - I know an old lady who swallowed a horse. - She's dead, of course! - """ +last_verse = "I know an old lady who swallowed a horse.\nShe's dead, of course!" diff --git a/exercises/practice/food-chain/.meta/template.j2 b/exercises/practice/food-chain/.meta/template.j2 index 18bdbc29..ac745ddf 100644 --- a/exercises/practice/food-chain/.meta/template.j2 +++ b/exercises/practice/food-chain/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["startVerse"] | to_roc }}, {{ case["input"]["endVerse"] | to_roc }}) result == {{ case["expected"] | join('\n') | to_roc_multiline_string | indent(8) }} +} -{% endfor %} \ No newline at end of file +{% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/food-chain/FoodChain.roc b/exercises/practice/food-chain/FoodChain.roc index 32b4e738..1c1a93ae 100644 --- a/exercises/practice/food-chain/FoodChain.roc +++ b/exercises/practice/food-chain/FoodChain.roc @@ -1,5 +1,6 @@ -module [recite] - -recite : U64, U64 -> Str -recite = |start_verse, end_verse| - crash("Please implement 'recite'") +FoodChain :: {}.{ + recite : U64, U64 -> Str + recite = |start_verse, end_verse| { + crash "Please implement the 'recite' function" + } +} diff --git a/exercises/practice/food-chain/food-chain-test.roc b/exercises/practice/food-chain/food-chain-test.roc index 508c1aa4..0ed657f6 100644 --- a/exercises/practice/food-chain/food-chain-test.roc +++ b/exercises/practice/food-chain/food-chain-test.roc @@ -1,200 +1,186 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/food-chain/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import FoodChain exposing [recite] # fly -expect - result = recite(1, 1) - result - == - """ - I know an old lady who swallowed a fly. - I don't know why she swallowed the fly. Perhaps she'll die. - """ +expect { + result = recite(1, 1) + result == + \\I know an old lady who swallowed a fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + +} # spider -expect - result = recite(2, 2) - result - == - """ - I know an old lady who swallowed a spider. - It wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - """ +expect { + result = recite(2, 2) + result == + \\I know an old lady who swallowed a spider. + \\It wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + +} # bird -expect - result = recite(3, 3) - result - == - """ - I know an old lady who swallowed a bird. - How absurd to swallow a bird! - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - """ +expect { + result = recite(3, 3) + result == + \\I know an old lady who swallowed a bird. + \\How absurd to swallow a bird! + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + +} # cat -expect - result = recite(4, 4) - result - == - """ - I know an old lady who swallowed a cat. - Imagine that, to swallow a cat! - She swallowed the cat to catch the bird. - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - """ +expect { + result = recite(4, 4) + result == + \\I know an old lady who swallowed a cat. + \\Imagine that, to swallow a cat! + \\She swallowed the cat to catch the bird. + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + +} # dog -expect - result = recite(5, 5) - result - == - """ - I know an old lady who swallowed a dog. - What a hog, to swallow a dog! - She swallowed the dog to catch the cat. - She swallowed the cat to catch the bird. - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - """ +expect { + result = recite(5, 5) + result == + \\I know an old lady who swallowed a dog. + \\What a hog, to swallow a dog! + \\She swallowed the dog to catch the cat. + \\She swallowed the cat to catch the bird. + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + +} # goat -expect - result = recite(6, 6) - result - == - """ - I know an old lady who swallowed a goat. - Just opened her throat and swallowed a goat! - She swallowed the goat to catch the dog. - She swallowed the dog to catch the cat. - She swallowed the cat to catch the bird. - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - """ +expect { + result = recite(6, 6) + result == + \\I know an old lady who swallowed a goat. + \\Just opened her throat and swallowed a goat! + \\She swallowed the goat to catch the dog. + \\She swallowed the dog to catch the cat. + \\She swallowed the cat to catch the bird. + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + +} # cow -expect - result = recite(7, 7) - result - == - """ - I know an old lady who swallowed a cow. - I don't know how she swallowed a cow! - She swallowed the cow to catch the goat. - She swallowed the goat to catch the dog. - She swallowed the dog to catch the cat. - She swallowed the cat to catch the bird. - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - """ +expect { + result = recite(7, 7) + result == + \\I know an old lady who swallowed a cow. + \\I don't know how she swallowed a cow! + \\She swallowed the cow to catch the goat. + \\She swallowed the goat to catch the dog. + \\She swallowed the dog to catch the cat. + \\She swallowed the cat to catch the bird. + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + +} # horse -expect - result = recite(8, 8) - result - == - """ - I know an old lady who swallowed a horse. - She's dead, of course! - """ +expect { + result = recite(8, 8) + result == + \\I know an old lady who swallowed a horse. + \\She's dead, of course! + +} # multiple verses -expect - result = recite(1, 3) - result - == - """ - I know an old lady who swallowed a fly. - I don't know why she swallowed the fly. Perhaps she'll die. - - I know an old lady who swallowed a spider. - It wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - - I know an old lady who swallowed a bird. - How absurd to swallow a bird! - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - """ +expect { + result = recite(1, 3) + result == + \\I know an old lady who swallowed a fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + \\ + \\I know an old lady who swallowed a spider. + \\It wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + \\ + \\I know an old lady who swallowed a bird. + \\How absurd to swallow a bird! + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + +} # full song -expect - result = recite(1, 8) - result - == - """ - I know an old lady who swallowed a fly. - I don't know why she swallowed the fly. Perhaps she'll die. - - I know an old lady who swallowed a spider. - It wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - - I know an old lady who swallowed a bird. - How absurd to swallow a bird! - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - - I know an old lady who swallowed a cat. - Imagine that, to swallow a cat! - She swallowed the cat to catch the bird. - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - - I know an old lady who swallowed a dog. - What a hog, to swallow a dog! - She swallowed the dog to catch the cat. - She swallowed the cat to catch the bird. - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - - I know an old lady who swallowed a goat. - Just opened her throat and swallowed a goat! - She swallowed the goat to catch the dog. - She swallowed the dog to catch the cat. - She swallowed the cat to catch the bird. - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - - I know an old lady who swallowed a cow. - I don't know how she swallowed a cow! - She swallowed the cow to catch the goat. - She swallowed the goat to catch the dog. - She swallowed the dog to catch the cat. - She swallowed the cat to catch the bird. - She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. - She swallowed the spider to catch the fly. - I don't know why she swallowed the fly. Perhaps she'll die. - - I know an old lady who swallowed a horse. - She's dead, of course! - """ +expect { + result = recite(1, 8) + result == + \\I know an old lady who swallowed a fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + \\ + \\I know an old lady who swallowed a spider. + \\It wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + \\ + \\I know an old lady who swallowed a bird. + \\How absurd to swallow a bird! + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + \\ + \\I know an old lady who swallowed a cat. + \\Imagine that, to swallow a cat! + \\She swallowed the cat to catch the bird. + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + \\ + \\I know an old lady who swallowed a dog. + \\What a hog, to swallow a dog! + \\She swallowed the dog to catch the cat. + \\She swallowed the cat to catch the bird. + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + \\ + \\I know an old lady who swallowed a goat. + \\Just opened her throat and swallowed a goat! + \\She swallowed the goat to catch the dog. + \\She swallowed the dog to catch the cat. + \\She swallowed the cat to catch the bird. + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + \\ + \\I know an old lady who swallowed a cow. + \\I don't know how she swallowed a cow! + \\She swallowed the cow to catch the goat. + \\She swallowed the goat to catch the dog. + \\She swallowed the dog to catch the cat. + \\She swallowed the cat to catch the bird. + \\She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her. + \\She swallowed the spider to catch the fly. + \\I don't know why she swallowed the fly. Perhaps she'll die. + \\ + \\I know an old lady who swallowed a horse. + \\She's dead, of course! +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/forth/.meta/Example.roc b/exercises/practice/forth/.meta/Example.roc index 83521dc3..988a1aeb 100644 --- a/exercises/practice/forth/.meta/Example.roc +++ b/exercises/practice/forth/.meta/Example.roc @@ -1,240 +1,302 @@ -module [evaluate] +Forth :: {}.{ + Stack : List(I16) + + evaluate : Str -> Try(Stack, Str) + evaluate = |program| { + lower = to_lower(program) + match parse(lower) { + Ok(operations) => { + match interpret(operations) { + Ok(stack) => Ok(stack) + Err(err) => Err(handle_error(err)) + } + } + Err(err) => Err(handle_error(err)) + } + } +} + +Defs : Dict(Str, List(Str)) -# Types -Defs : Dict Str (List Str) -Stack : List I16 Op : [ - Dup, - Drop, - Swap, - Over, - Add, - Subtract, - Multiply, - Divide, - Number I16, + Dup, + Drop, + Swap, + Over, + Add, + Subtract, + Multiply, + Divide, + Number(I16), ] -# Evaluation -evaluate : Str -> Result Stack Str -evaluate = |program| - result = |_| - lower = to_lower(program) - operations = parse(lower)? - interpret(operations) - - Result.map_err(result({}), handle_error) - -interpret : List Op -> Result Stack _ -interpret = |program| - help = |ops, stack| - when ops is - [] -> Ok(stack) - [op, .. as rest] -> - when step(stack, op) is - Ok(new_stack) -> help(rest, new_stack) - Err(error) -> EvaluationError({ error, stack, op, ops }) |> Err - help(program, []) - -step : Stack, Op -> Result Stack _ -step = |stack, op| - when op is - Number(x) -> - List.append(stack, x) |> Ok - - Dup -> - when stack is - [.., x] -> - List.append(stack, x) |> Ok - - _ -> Err(Arity(1)) - - Drop -> - when stack is - [.. as rest, _] -> Ok(rest) - _ -> Err(Arity(1)) - - Swap -> - when stack is - [.. as rest, x, y] -> - List.append(rest, y) - |> List.append(x) - |> Ok - - _ -> Err(Arity(2)) - - Over -> - when stack is - [.. as rest, x, y] -> - List.concat(rest, [x, y, x]) |> Ok - - _ -> Err(Arity(2)) - - Add -> - when stack is - [.. as rest, x, y] -> - List.append(rest, (x + y)) |> Ok - - _ -> Err(Arity(2)) - - Subtract -> - when stack is - [.. as rest, x, y] -> - List.append(rest, (x - y)) |> Ok - - _ -> Err(Arity(2)) - - Multiply -> - when stack is - [.. as rest, x, y] -> - List.append(rest, (x * y)) |> Ok - - _ -> Err(Arity(2)) - - Divide -> - when stack is - [.. as rest, x, y] -> - quotient = Num.div_trunc_checked(x, y)? - List.append(rest, quotient) |> Ok - - _ -> Err(Arity(2)) - -# Parsing -parse : Str -> Result (List Op) _ -parse = |str| - when Str.split_on(Str.trim(str), "\n") is - [.. as def_lines, program] -> - defs = parse_defs(def_lines)? - - Str.split_on(program, " ") - |> flatten_defs(defs) - |> List.map_try(to_op) - - [] -> Ok([]) # We'll let the empty program return the empty list - -parse_defs : List Str -> Result Defs _ -parse_defs = |lines| - List.walk_try( - lines, - Dict.empty({}), - |defs, line| - when Str.split_on(line, " ") is - [":", name, .. as tokens, ";"] -> - ops = parse_def(tokens, defs)? - Dict.insert(defs, name, ops) |> Ok - - _ -> Err(UnableToParseDef(line)), - ) - -parse_def : List Str, Defs -> Result (List Str) _ -parse_def = |tokens, defs| - List.walk_try( - tokens, - [], - |ops, token| - when Dict.get(defs, token) is - Ok(body) -> List.concat(ops, body) |> Ok - _ if is_builtin(token) -> List.append(ops, token) |> Ok - _ -> Err(UnknownName(token)), - ) +interpret : List(Op) -> Try(Stack, _) +interpret = |program| { + help = |ops, stack| { + match ops { + [] => Ok(stack) + [op, .. as rest] => { + match step(stack, op) { + Ok(new_stack) => help(rest, new_stack) + Err(error) => Err(EvaluationError({ error, stack, op, ops })) + } + } + } + } + help(program, []) +} + +step : Stack, Op -> Try(Stack, _) +step = |stack, op| { + match op { + Number(x) => Ok(stack.append(x)) + Dup => { + match stack { + [.., x] => Ok(stack.append(x)) + _ => Err(Arity(1)) + } + } + + Drop => { + match stack { + [.. as rest, _] => Ok(rest) + _ => Err(Arity(1)) + } + } + + Swap => { + match stack { + [.. as rest, x, y] => Ok(rest.append(y).append(x)) + _ => Err(Arity(2)) + } + } + + Over => { + match stack { + [.. as rest, x, y] => Ok(rest.concat([x, y, x])) + _ => Err(Arity(2)) + } + } + + Add => { + match stack { + [.. as rest, x, y] => Ok(rest.append(x + y)) + _ => Err(Arity(2)) + } + } + + Subtract => { + match stack { + [.. as rest, x, y] => Ok(rest.append(x - y)) + _ => Err(Arity(2)) + } + } + + Multiply => { + match stack { + [.. as rest, x, y] => Ok(rest.append(x * y)) + _ => Err(Arity(2)) + } + } + + Divide => { + match stack { + [.. as rest, x, y] => { + if y == 0 { + Err(DivByZero) + } else { + Ok(rest.append(x // y)) + } + } + _ => Err(Arity(2)) + } + } + } +} + +parse : Str -> Try(List(Op), _) +parse = |str| { + match Str.split_on(Str.trim(str), "\n") { + [.. as def_lines, program] => { + defs = parse_defs(def_lines)? + program.split_on(" ") + ->flatten_defs(defs) + ->map_try(to_op) + } + [] => Ok([]) + } +} + +parse_defs : List(Str) -> Try(Defs, _) +parse_defs = |lines| { + List.fold_until( + lines, + Ok(Dict.empty()), + |acc_res, line| { + match acc_res { + Ok(defs) => { + match line.split_on(" ") { + [":", name, .. as tokens, ";"] => { + match parse_def(tokens, defs) { + Ok(ops) => Continue(Ok(defs.insert(name, ops))) + Err(err) => Break(Err(err)) + } + } + _ => Break(Err(UnableToParseDef(line))) + } + } + Err(err) => Break(Err(err)) + } + }, + ) +} + +parse_def : List(Str), Defs -> Try(List(Str), _) +parse_def = |tokens, defs| { + List.fold_until( + tokens, + Ok([]), + |acc_res, token| { + match acc_res { + Ok(ops) => { + match defs.get(token) { + Ok(body) => Continue(Ok(ops.concat(body))) + Err(KeyNotFound) => { + if is_builtin(token) { + Continue(Ok(ops.append(token))) + } else { + Break(Err(UnknownName(token))) + } + } + } + } + Err(err) => Break(Err(err)) + } + }, + ) +} is_builtin : Str -> Bool -is_builtin = |token| - builtins = ["dup", "drop", "swap", "over", "+", "-", "*", "/"] - (builtins |> List.contains(token)) or (Result.is_ok(Str.to_i16(token))) - -flatten_defs : List Str, Defs -> List Str -flatten_defs = |tokens, defs| - List.join_map( - tokens, - |token| - when Dict.get(defs, token) is - Ok(body) -> body - _ -> [token], - ) - -to_op : Str -> Result Op _ -to_op = |str| - when str is - "dup" -> Ok(Dup) - "drop" -> Ok(Drop) - "swap" -> Ok(Swap) - "over" -> Ok(Over) - "+" -> Ok(Add) - "-" -> Ok(Subtract) - "*" -> Ok(Multiply) - "/" -> Ok(Divide) - _ -> - when Str.to_i16(str) is - Ok(num) -> Ok(Number(num)) - Err(_) -> Err(UnknownName(str)) - -# Display +is_builtin = |token| { + builtins = ["dup", "drop", "swap", "over", "+", "-", "*", "/"] + builtins.contains(token) or ( + match I16.from_str(token) { + Ok(_) => Bool.True + Err(_) => Bool.False + }, + ) +} + +flatten_defs : List(Str), Defs -> List(Str) +flatten_defs = |tokens, defs| { + tokens->join_map( + |token| { + match defs.get(token) { + Ok(body) => body + _ => [token] + } + }, + ) +} + +to_op : Str -> Try(Op, _) +to_op = |str| { + match str { + "dup" => Ok(Dup) + "drop" => Ok(Drop) + "swap" => Ok(Swap) + "over" => Ok(Over) + "+" => Ok(Add) + "-" => Ok(Subtract) + "*" => Ok(Multiply) + "/" => Ok(Divide) + _ => { + match I16.from_str(str) { + Ok(num) => Ok(Number(num)) + Err(_) => Err(UnknownName(str)) + } + } + } +} + handle_error : _ -> Str -handle_error = |err| - when err is - UnknownName(key) -> "Hmm, I don't know any operations called '${key}'. Maybe there's a typo?" - UnableToParseDef(line) -> - """ - This is supposed to be a definition, but I'm not sure how to parse it: - ${line} - """ - - EvaluationError({ error, stack, op, ops }) -> - when error is - Arity(1) -> - """ - Oops! '${op_to_str(op)}' expected 1 argument, but the stack was empty. - ${show_execution(stack, ops)} - """ - - Arity(n) -> - """ - Oops! '${op_to_str(op)}' expected ${Num.to_str(n)} arguments, but there weren't enough on the stack. - ${show_execution(stack, ops)} - """ - - DivByZero -> - """ - Sorry, division by zero is not allowed. - ${show_execution(stack, ops)} - """ - -show_execution : Stack, List Op -> Str -show_execution = |stack, ops| - stack_str = - List.map(stack, Num.to_str) - |> Str.join_with(" ") - ops_str = - List.map(ops, op_to_str) - |> Str.join_with(" ") - "${stack_str} | ${ops_str}" +handle_error = |err| { + match err { + UnknownName(key) => "Hmm, I don't know any operations called '${key}'. Maybe there's a typo?" + UnableToParseDef(line) => "This is supposed to be a definition, but I'm not sure how to parse it:\n${line}" + EvaluationError({ error, stack, op, ops }) => { + match error { + Arity(1) => "Oops! '${op_to_str(op)}' expected 1 argument, but the stack was empty.\n${show_execution(stack, ops)}" + Arity(n) => "Oops! '${op_to_str(op)}' expected ${n.to_str()} arguments, but there weren't enough on the stack.\n${show_execution(stack, ops)}" + DivByZero => "Sorry, division by zero is not allowed.\n${show_execution(stack, ops)}" + } + } + } +} + +show_execution : Stack, List(Op) -> Str +show_execution = |stack, ops| { + stack_str = + stack.map(|n| n.to_str()) + ->Str.join_with(" ") + ops_str = + ops.map(op_to_str) + ->Str.join_with(" ") + "${stack_str} | ${ops_str}" +} op_to_str : Op -> Str -op_to_str = |op| - when op is - Dup -> "dup" - Drop -> "drop" - Swap -> "swap" - Over -> "over" - Add -> "+" - Subtract -> "-" - Multiply -> "*" - Divide -> "/" - Number(num) -> Num.to_str(num) +op_to_str = |op| { + match op { + Dup => "dup" + Drop => "drop" + Swap => "swap" + Over => "over" + Add => "+" + Subtract => "-" + Multiply => "*" + Divide => "/" + Number(num) => num.to_str() + } +} to_lower : Str -> Str -to_lower = |str| - result = - Str.to_utf8(str) - |> List.map( - |byte| - if 'A' <= byte and byte <= 'Z' then - byte - 'A' + 'a' - else - byte, - ) - |> Str.from_utf8 - when result is - Ok(s) -> s - _ -> crash("There was an unexpected error converting back to Str") +to_lower = |str| { + result = + str.to_utf8() + .map( + |byte| { + if 'A' <= byte and byte <= 'Z' { + byte - 'A' + 'a' + } else { + byte + } + }, + ) + ->Str.from_utf8() + match result { + Ok(s) => s + _ => { + crash "There was an unexpected error converting back to Str" + } + } +} + +# The following function should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} + +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/forth/.meta/template.j2 b/exercises/practice/forth/.meta/template.j2 index 2a51879e..2e12e1cb 100644 --- a/exercises/practice/forth/.meta/template.j2 +++ b/exercises/practice/forth/.meta/template.j2 @@ -7,12 +7,15 @@ import Forth exposing [evaluate] {% for case in cases -%} {% for innerCase in case["cases"] %} # {{ case["description"] }}: {{ innerCase["description"] }} -expect +expect { result = evaluate({{ innerCase["input"]["instructions"] | join('\n') | to_roc_multiline_string | indent(8) }}) {%- if innerCase["expected"]["error"] %} - Result.is_err(result) + result.is_err() {%- else %} result == Ok({{ innerCase["expected"] }}) {%- endif %} +} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/forth/Forth.roc b/exercises/practice/forth/Forth.roc index 219dddbf..6fa1d10c 100644 --- a/exercises/practice/forth/Forth.roc +++ b/exercises/practice/forth/Forth.roc @@ -1,7 +1,8 @@ -module [evaluate] +Forth :: {}.{ + Stack : List(I16) -Stack : List I16 - -evaluate : Str -> Result Stack _ -evaluate = |program| - crash("Please implement 'evaluate'") + evaluate : Str -> Try(Stack, _) + evaluate = |program| { + crash "Please implement the 'evaluate' function" + } +} diff --git a/exercises/practice/forth/forth-test.roc b/exercises/practice/forth/forth-test.roc index a84e5540..7011ddea 100644 --- a/exercises/practice/forth/forth-test.roc +++ b/exercises/practice/forth/forth-test.roc @@ -1,323 +1,362 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/forth/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Forth exposing [evaluate] # parsing and numbers: numbers just get pushed onto the stack -expect - result = evaluate("1 2 3 4 5") - result == Ok([1, 2, 3, 4, 5]) +expect { + result = evaluate("1 2 3 4 5") + result == Ok([1, 2, 3, 4, 5]) +} # parsing and numbers: pushes negative numbers onto the stack -expect - result = evaluate("-1 -2 -3 -4 -5") - result == Ok([-1, -2, -3, -4, -5]) +expect { + result = evaluate("-1 -2 -3 -4 -5") + result == Ok([-1, -2, -3, -4, -5]) +} # addition: can add two numbers -expect - result = evaluate("1 2 +") - result == Ok([3]) +expect { + result = evaluate("1 2 +") + result == Ok([3]) +} # addition: errors if there is nothing on the stack -expect - result = evaluate("+") - Result.is_err(result) +expect { + result = evaluate("+") + result.is_err() +} # addition: errors if there is only one value on the stack -expect - result = evaluate("1 +") - Result.is_err(result) +expect { + result = evaluate("1 +") + result.is_err() +} # addition: more than two values on the stack -expect - result = evaluate("1 2 3 +") - result == Ok([1, 5]) +expect { + result = evaluate("1 2 3 +") + result == Ok([1, 5]) +} # subtraction: can subtract two numbers -expect - result = evaluate("3 4 -") - result == Ok([-1]) +expect { + result = evaluate("3 4 -") + result == Ok([-1]) +} # subtraction: errors if there is nothing on the stack -expect - result = evaluate("-") - Result.is_err(result) +expect { + result = evaluate("-") + result.is_err() +} # subtraction: errors if there is only one value on the stack -expect - result = evaluate("1 -") - Result.is_err(result) +expect { + result = evaluate("1 -") + result.is_err() +} # subtraction: more than two values on the stack -expect - result = evaluate("1 12 3 -") - result == Ok([1, 9]) +expect { + result = evaluate("1 12 3 -") + result == Ok([1, 9]) +} # multiplication: can multiply two numbers -expect - result = evaluate("2 4 *") - result == Ok([8]) +expect { + result = evaluate("2 4 *") + result == Ok([8]) +} # multiplication: errors if there is nothing on the stack -expect - result = evaluate("*") - Result.is_err(result) +expect { + result = evaluate("*") + result.is_err() +} # multiplication: errors if there is only one value on the stack -expect - result = evaluate("1 *") - Result.is_err(result) +expect { + result = evaluate("1 *") + result.is_err() +} # multiplication: more than two values on the stack -expect - result = evaluate("1 2 3 *") - result == Ok([1, 6]) +expect { + result = evaluate("1 2 3 *") + result == Ok([1, 6]) +} # division: can divide two numbers -expect - result = evaluate("12 3 /") - result == Ok([4]) +expect { + result = evaluate("12 3 /") + result == Ok([4]) +} # division: performs integer division -expect - result = evaluate("8 3 /") - result == Ok([2]) +expect { + result = evaluate("8 3 /") + result == Ok([2]) +} # division: errors if dividing by zero -expect - result = evaluate("4 0 /") - Result.is_err(result) +expect { + result = evaluate("4 0 /") + result.is_err() +} # division: errors if there is nothing on the stack -expect - result = evaluate("/") - Result.is_err(result) +expect { + result = evaluate("/") + result.is_err() +} # division: errors if there is only one value on the stack -expect - result = evaluate("1 /") - Result.is_err(result) +expect { + result = evaluate("1 /") + result.is_err() +} # division: more than two values on the stack -expect - result = evaluate("1 12 3 /") - result == Ok([1, 4]) +expect { + result = evaluate("1 12 3 /") + result == Ok([1, 4]) +} # combined arithmetic: addition and subtraction -expect - result = evaluate("1 2 + 4 -") - result == Ok([-1]) +expect { + result = evaluate("1 2 + 4 -") + result == Ok([-1]) +} # combined arithmetic: multiplication and division -expect - result = evaluate("2 4 * 3 /") - result == Ok([2]) +expect { + result = evaluate("2 4 * 3 /") + result == Ok([2]) +} # combined arithmetic: multiplication and addition -expect - result = evaluate("1 3 4 * +") - result == Ok([13]) +expect { + result = evaluate("1 3 4 * +") + result == Ok([13]) +} # combined arithmetic: addition and multiplication -expect - result = evaluate("1 3 4 + *") - result == Ok([7]) +expect { + result = evaluate("1 3 4 + *") + result == Ok([7]) +} # dup: copies a value on the stack -expect - result = evaluate("1 dup") - result == Ok([1, 1]) +expect { + result = evaluate("1 dup") + result == Ok([1, 1]) +} # dup: copies the top value on the stack -expect - result = evaluate("1 2 dup") - result == Ok([1, 2, 2]) +expect { + result = evaluate("1 2 dup") + result == Ok([1, 2, 2]) +} # dup: errors if there is nothing on the stack -expect - result = evaluate("dup") - Result.is_err(result) +expect { + result = evaluate("dup") + result.is_err() +} # drop: removes the top value on the stack if it is the only one -expect - result = evaluate("1 drop") - result == Ok([]) +expect { + result = evaluate("1 drop") + result == Ok([]) +} # drop: removes the top value on the stack if it is not the only one -expect - result = evaluate("1 2 drop") - result == Ok([1]) +expect { + result = evaluate("1 2 drop") + result == Ok([1]) +} # drop: errors if there is nothing on the stack -expect - result = evaluate("drop") - Result.is_err(result) +expect { + result = evaluate("drop") + result.is_err() +} # swap: swaps the top two values on the stack if they are the only ones -expect - result = evaluate("1 2 swap") - result == Ok([2, 1]) +expect { + result = evaluate("1 2 swap") + result == Ok([2, 1]) +} # swap: swaps the top two values on the stack if they are not the only ones -expect - result = evaluate("1 2 3 swap") - result == Ok([1, 3, 2]) +expect { + result = evaluate("1 2 3 swap") + result == Ok([1, 3, 2]) +} # swap: errors if there is nothing on the stack -expect - result = evaluate("swap") - Result.is_err(result) +expect { + result = evaluate("swap") + result.is_err() +} # swap: errors if there is only one value on the stack -expect - result = evaluate("1 swap") - Result.is_err(result) +expect { + result = evaluate("1 swap") + result.is_err() +} # over: copies the second element if there are only two -expect - result = evaluate("1 2 over") - result == Ok([1, 2, 1]) +expect { + result = evaluate("1 2 over") + result == Ok([1, 2, 1]) +} # over: copies the second element if there are more than two -expect - result = evaluate("1 2 3 over") - result == Ok([1, 2, 3, 2]) +expect { + result = evaluate("1 2 3 over") + result == Ok([1, 2, 3, 2]) +} # over: errors if there is nothing on the stack -expect - result = evaluate("over") - Result.is_err(result) +expect { + result = evaluate("over") + result.is_err() +} # over: errors if there is only one value on the stack -expect - result = evaluate("1 over") - Result.is_err(result) +expect { + result = evaluate("1 over") + result.is_err() +} # user-defined words: can consist of built-in words -expect - result = evaluate( - """ - : dup-twice dup dup ; - 1 dup-twice - """, - ) - result == Ok([1, 1, 1]) +expect { + result = evaluate( + \\: dup-twice dup dup ; + \\1 dup-twice + , + ) + result == Ok([1, 1, 1]) +} # user-defined words: execute in the right order -expect - result = evaluate( - """ - : countup 1 2 3 ; - countup - """, - ) - result == Ok([1, 2, 3]) +expect { + result = evaluate( + \\: countup 1 2 3 ; + \\countup + , + ) + result == Ok([1, 2, 3]) +} # user-defined words: can override other user-defined words -expect - result = evaluate( - """ - : foo dup ; - : foo dup dup ; - 1 foo - """, - ) - result == Ok([1, 1, 1]) +expect { + result = evaluate( + \\: foo dup ; + \\: foo dup dup ; + \\1 foo + , + ) + result == Ok([1, 1, 1]) +} # user-defined words: can override built-in words -expect - result = evaluate( - """ - : swap dup ; - 1 swap - """, - ) - result == Ok([1, 1]) +expect { + result = evaluate( + \\: swap dup ; + \\1 swap + , + ) + result == Ok([1, 1]) +} # user-defined words: can override built-in operators -expect - result = evaluate( - """ - : + * ; - 3 4 + - """, - ) - result == Ok([12]) +expect { + result = evaluate( + \\: + * ; + \\3 4 + + , + ) + result == Ok([12]) +} # user-defined words: can use different words with the same name -expect - result = evaluate( - """ - : foo 5 ; - : bar foo ; - : foo 6 ; - bar foo - """, - ) - result == Ok([5, 6]) +expect { + result = evaluate( + \\: foo 5 ; + \\: bar foo ; + \\: foo 6 ; + \\bar foo + , + ) + result == Ok([5, 6]) +} # user-defined words: can define word that uses word with the same name -expect - result = evaluate( - """ - : foo 10 ; - : foo foo 1 + ; - foo - """, - ) - result == Ok([11]) +expect { + result = evaluate( + \\: foo 10 ; + \\: foo foo 1 + ; + \\foo + , + ) + result == Ok([11]) +} # user-defined words: errors if executing a non-existent word -expect - result = evaluate("foo") - Result.is_err(result) +expect { + result = evaluate("foo") + result.is_err() +} # case-insensitivity: DUP is case-insensitive -expect - result = evaluate("1 DUP Dup dup") - result == Ok([1, 1, 1, 1]) +expect { + result = evaluate("1 DUP Dup dup") + result == Ok([1, 1, 1, 1]) +} # case-insensitivity: DROP is case-insensitive -expect - result = evaluate("1 2 3 4 DROP Drop drop") - result == Ok([1]) +expect { + result = evaluate("1 2 3 4 DROP Drop drop") + result == Ok([1]) +} # case-insensitivity: SWAP is case-insensitive -expect - result = evaluate("1 2 SWAP 3 Swap 4 swap") - result == Ok([2, 3, 4, 1]) +expect { + result = evaluate("1 2 SWAP 3 Swap 4 swap") + result == Ok([2, 3, 4, 1]) +} # case-insensitivity: OVER is case-insensitive -expect - result = evaluate("1 2 OVER Over over") - result == Ok([1, 2, 1, 2, 1]) +expect { + result = evaluate("1 2 OVER Over over") + result == Ok([1, 2, 1, 2, 1]) +} # case-insensitivity: user-defined words are case-insensitive -expect - result = evaluate( - """ - : foo dup ; - 1 FOO Foo foo - """, - ) - result == Ok([1, 1, 1, 1]) +expect { + result = evaluate( + \\: foo dup ; + \\1 FOO Foo foo + , + ) + result == Ok([1, 1, 1, 1]) +} # case-insensitivity: definitions are case-insensitive -expect - result = evaluate( - """ - : SWAP DUP Dup dup ; - 1 swap - """, - ) - result == Ok([1, 1, 1, 1]) +expect { + result = evaluate( + \\: SWAP DUP Dup dup ; + \\1 swap + , + ) + result == Ok([1, 1, 1, 1]) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/gigasecond/.meta/Example.roc b/exercises/practice/gigasecond/.meta/Example.roc index 57aedcdc..50e2e2ef 100644 --- a/exercises/practice/gigasecond/.meta/Example.roc +++ b/exercises/practice/gigasecond/.meta/Example.roc @@ -1,6 +1,13 @@ -module [add] + import isodate.DateTime +Gigasecond :: {}.{ + add : Str -> Str + add = |moment| + when future_datetime(moment) is + Ok(string) -> string + Err(_) -> "Unexpected error" +} future_datetime : Str -> Result Str [InvalidDateTimeFormat] future_datetime = |moment| @@ -15,9 +22,3 @@ future_datetime = |moment| |> DateTime.to_iso_str ), ) - -add : Str -> Str -add = |moment| - when future_datetime(moment) is - Ok(string) -> string - Err(_) -> "Unexpected error" diff --git a/exercises/practice/gigasecond/.meta/template.j2 b/exercises/practice/gigasecond/.meta/template.j2 index 017461a6..42b4f7c8 100644 --- a/exercises/practice/gigasecond/.meta/template.j2 +++ b/exercises/practice/gigasecond/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [add] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["moment"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/gigasecond/Gigasecond.roc b/exercises/practice/gigasecond/Gigasecond.roc index 997a32c3..f485dc7c 100644 --- a/exercises/practice/gigasecond/Gigasecond.roc +++ b/exercises/practice/gigasecond/Gigasecond.roc @@ -1,5 +1,6 @@ -module [add] - -add : Str -> Str -add = |moment| - crash("Please implement the 'add' function") +Gigasecond :: {}.{ + add : Str -> Str + add = |moment| { + crash "Please implement the 'add' function" + } +} diff --git a/exercises/practice/gigasecond/gigasecond-test.roc b/exercises/practice/gigasecond/gigasecond-test.roc index 15dc918a..9da08629 100644 --- a/exercises/practice/gigasecond/gigasecond-test.roc +++ b/exercises/practice/gigasecond/gigasecond-test.roc @@ -1,40 +1,46 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/gigasecond/canonical-data.json -# File last updated on 2025-09-15 +# File last updated on 2026-06-21 app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", - isodate: "https://github.com/imclerran/roc-isodate/releases/download/v0.6.2/73w_H-aSJNcWqtXvMG4JQw_HoaApMBLnE92XD4OcVGU.tar.br", + pf: platform "https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/download/0.9/8GdFEvQYS3TeAZxKvTzCLVdQiomweGtXcdZkXNDEeABq.tar.zst", + isodate: "https://github.com/imclerran/roc-isodate/...", # TODO: update when a zig-compatible release is available } import pf.Stdout -main! = |_args| - Stdout.line!("") - import Gigasecond exposing [add] # date only specification of time -expect - result = add("2011-04-25") - result == "2043-01-01T01:46:40" +expect { + result = add("2011-04-25") + result == "2043-01-01T01:46:40" +} # second test for date only specification of time -expect - result = add("1977-06-13") - result == "2009-02-19T01:46:40" +expect { + result = add("1977-06-13") + result == "2009-02-19T01:46:40" +} # third test for date only specification of time -expect - result = add("1959-07-19") - result == "1991-03-27T01:46:40" +expect { + result = add("1959-07-19") + result == "1991-03-27T01:46:40" +} # full time specified -expect - result = add("2015-01-24T22:00:00") - result == "2046-10-02T23:46:40" +expect { + result = add("2015-01-24T22:00:00") + result == "2046-10-02T23:46:40" +} # full time with day roll-over -expect - result = add("2015-01-24T23:59:59") - result == "2046-10-03T01:46:39" +expect { + result = add("2015-01-24T23:59:59") + result == "2046-10-03T01:46:39" +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/go-counting/.meta/Example.roc b/exercises/practice/go-counting/.meta/Example.roc index c99d9ecf..8d7cfa93 100644 --- a/exercises/practice/go-counting/.meta/Example.roc +++ b/exercises/practice/go-counting/.meta/Example.roc @@ -1,137 +1,197 @@ -module [territory, territories] +GoCounting :: {}.{ + territory : Str, Intersection -> Try(Territory, [OutOfBounds, BoardWasEmpty, BoardWasNotRectangular, InvalidChar(U8), ..]) + territory = |board_str, intersection| { + board = parse(board_str)? + if intersection.x >= board.width or intersection.y >= board.height { + Err(OutOfBounds) + } else { + Ok(search_territory(board, intersection)) + } + } + + territories : Str -> Try(Territories, [BoardWasEmpty, BoardWasNotRectangular, InvalidChar(U8), ..]) + territories = |board_str| { + board = parse(board_str)? + empty_intersections = { + board.rows + .map_with_index( + |row, y| { + row.map_with_index( + |stone, x| { + if stone == None [{ x, y }] else [] + }, + ) + ->join() + }, + ) + ->join() + } + empty_intersections.fold( + { black: Set.empty(), white: Set.empty(), none: Set.empty() }, + |state, intersection| { + if state.black.contains(intersection) or state.white.contains(intersection) or state.none.contains(intersection) { + state + } else { + new_territory = search_territory(board, intersection) + match new_territory.owner { + Black => { ..state, black: state.black.union(new_territory.territory) } + White => { ..state, white: state.white.union(new_territory.territory) } + None => { ..state, none: state.none.union(new_territory.territory) } + } + } + }, + ) + ->Ok() + } +} Intersection : { x : U64, y : U64 } Stone : [White, Black, None] Territory : { - owner : Stone, - territory : Set Intersection, + owner : Stone, + territory : Set(Intersection), } Territories : { - black : Set Intersection, - white : Set Intersection, - none : Set Intersection, + black : Set(Intersection), + white : Set(Intersection), + none : Set(Intersection), } Board : { - rows : List (List Stone), - width : U64, - height : U64, + rows : List(List(Stone)), + width : U64, + height : U64, } -parse : Str -> Result Board [BoardWasEmpty, BoardWasNotRectangular, InvalidChar U8] -parse = |board_str| - if board_str == "" then - Err(BoardWasEmpty) - else - rows = - board_str - |> Str.to_utf8 - |> List.split_on('\n') - |> List.map_try( - |row| - row - |> List.map_try( - |char| - when char is - 'B' -> Ok(Black) - 'W' -> Ok(White) - ' ' -> Ok(None) - _ -> Err(InvalidChar(char)), - ), - )? - row_widths = rows |> List.map(List.len) - width = row_widths |> List.max |> Result.with_default(0) - if row_widths |> List.any(|w| w != width) then - Err(BoardWasNotRectangular) - else - height = List.len(rows) - Ok({ rows, width, height }) +parse : Str -> Try(Board, [BoardWasEmpty, BoardWasNotRectangular, InvalidChar(U8), ..]) +parse = |board_str| { + if board_str == "" { + Err(BoardWasEmpty) + } else { + rows = + board_str + .to_utf8() + .split_on('\n') + ->map_try( + |row| { + row->map_try( + |char| { + match char { + 'B' => Ok(Black) + 'W' => Ok(White) + ' ' => Ok(None) + _ => Err(InvalidChar(char)) + } + }, + ) + }, + )? + row_widths = rows.map(List.len) + width = row_widths.max() ?? 0 + if row_widths.any(|w| w != width) { + Err(BoardWasNotRectangular) + } else { + height = rows.len() + Ok({ rows, width, height }) + } + } +} get_stone : Board, Intersection -> Stone -get_stone = |board, { x, y }| - board.rows |> List.get(y) |> Result.with_default([]) |> List.get(x) |> Result.with_default(None) - -territory : Str, Intersection -> Result Territory [OutOfBounds, BoardWasEmpty, BoardWasNotRectangular, InvalidChar U8] -territory = |board_str, intersection| - board = parse(board_str)? - if intersection.x >= board.width or intersection.y >= board.height then - Err(OutOfBounds) - else - Ok(search_territory(board, intersection)) +get_stone = |board, { x, y }| { + (board.rows.get(y) ?? []).get(x) ?? None +} search_territory : Board, Intersection -> Territory -search_territory = |board, intersection| - help = |to_visit, visited, surrounding_stones| - when to_visit is - [] -> { visited, surrounding_stones } - [visiting, .. as rest_to_visit] -> - if visited |> Set.contains(visiting) then - help(rest_to_visit, visited, surrounding_stones) - else - stone = board |> get_stone(visiting) - when stone is - Black | White -> - new_surrounding_stones = surrounding_stones |> Set.insert(stone) - help(rest_to_visit, visited, new_surrounding_stones) +search_territory = |board, intersection| { + help = |to_visit, visited, surrounding_stones| { + match to_visit { + [] => { visited, surrounding_stones } + [visiting, .. as rest_to_visit] => { + if visited.contains(visiting) { + help(rest_to_visit, visited, surrounding_stones) + } else { + stone = get_stone(board, visiting) + match stone { + Black | White => { + new_surrounding_stones = surrounding_stones.insert(stone) + help(rest_to_visit, visited, new_surrounding_stones) + } - None -> - neighbors = - [ - { x: visiting.x |> Num.sub_saturated(1), y: visiting.y }, - { x: visiting.x + 1, y: visiting.y }, - { x: visiting.x, y: visiting.y |> Num.sub_saturated(1) }, - { x: visiting.x, y: visiting.y + 1 }, - ] - |> List.drop_if( - |neighbor| - neighbor.x >= board.width or neighbor.y >= board.height or neighbor == visiting, - ) - new_to_visit = rest_to_visit |> List.concat(neighbors) - new_visited = visited |> Set.insert(visiting) - help(new_to_visit, new_visited, surrounding_stones) - search_result = help([intersection], Set.empty({}), Set.empty({})) - if search_result.visited |> Set.is_empty then - { owner: None, territory: Set.empty({}) } - else - owner = - if search_result.surrounding_stones == Set.single(Black) then - Black - else if search_result.surrounding_stones == Set.single(White) then - White - else - None - { owner, territory: search_result.visited } + None => { + neighbors = + [ + { + x: ( + if visiting.x > 0 { + visiting.x - 1 + } else 0, + ), + y: visiting.y, + }, + { x: visiting.x + 1, y: visiting.y }, + { + x: visiting.x, + y: ( + if visiting.y > 0 { + visiting.y - 1 + } else 0, + ), + }, + { x: visiting.x, y: visiting.y + 1 }, + ] + .drop_if( + |neighbor| { + neighbor.x >= board.width or neighbor.y >= board.height or neighbor == visiting + }, + ) + new_to_visit = rest_to_visit.concat(neighbors) + new_visited = visited.insert(visiting) + help(new_to_visit, new_visited, surrounding_stones) + } + } + } + } + } + } + search_result = help([intersection], Set.empty(), Set.empty()) + if search_result.visited.is_empty() { + { owner: None, territory: Set.empty() } + } else { + owner = + if search_result.surrounding_stones == Set.from_list([Black]) { + Black + } else if search_result.surrounding_stones == Set.from_list([White]) { + White + } else { + None + } + { owner, territory: search_result.visited } + } +} -territories : Str -> Result Territories [BoardWasEmpty, BoardWasNotRectangular, InvalidChar U8] -territories = |board_str| - board = parse(board_str)? - board.rows - |> List.map_with_index( - |row, y| - row - |> List.map_with_index( - |stone, x| - if stone == None then - [{ x, y }] - else - [], - ) - |> List.join, - ) - |> List.join - |> List.walk( - { black: Set.empty({}), white: Set.empty({}), none: Set.empty({}) }, - |state, intersection| - if state.black |> Set.contains(intersection) or state.white |> Set.contains(intersection) or state.none |> Set.contains(intersection) then - state - else - new_territory = search_territory(board, intersection) - when new_territory.owner is - Black -> { black: state.black |> Set.union(new_territory.territory), white: state.white, none: state.none } - White -> { black: state.black, white: state.white |> Set.union(new_territory.territory), none: state.none } - None -> { black: state.black, white: state.white, none: state.none |> Set.union(new_territory.territory) }, - ) - |> Ok +# The following function should soon be available in Roc's builtins +join = |iter| { + var $state = [] + for sublist in iter { + for item in sublist { + $state = $state.append(item) + } + } + $state +} + +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} + +sub_saturated = |a, b| { + a.sub_checked(b) ?? 0 +} diff --git a/exercises/practice/go-counting/.meta/template.j2 b/exercises/practice/go-counting/.meta/template.j2 index c9ded94a..7cf6d364 100644 --- a/exercises/practice/go-counting/.meta/template.j2 +++ b/exercises/practice/go-counting/.meta/template.j2 @@ -4,7 +4,7 @@ {% macro to_territory(territory) %} {%- if territory == [] %} -Set.empty({}) +Set.empty() {%- else %} Set.from_list([ {%- for intersection in territory %} @@ -16,42 +16,48 @@ Set.from_list([ import {{ exercise | to_pascal }} exposing [territory, territories] -## The following two comparison functions are temporary workarounds for Roc issue #7144: -## comparing tags or records containing sets sometimes returns the wrong result -## depending on the internal order of the set data, so we have to unwrap the sets -## in order to compare them properly. -compare_territory = |maybe_result, maybe_expected| - when (maybe_result, maybe_expected) is - (Ok(result), Ok(expected)) -> result.owner == expected.owner && result.territory == expected.territory - _ -> Bool.false - -compare_territories = |maybe_result, maybe_expected| - when (maybe_result, maybe_expected) is - (Ok(result), Ok(expected)) -> result.black == expected.black && result.white == expected.white && result.none == expected.none - _ -> Bool.false - {% for case in cases -%} # {{ case["description"] }} -expect - board = {{ case["input"]["board"] | to_roc_multiline_string | replace(" ", "·") | indent(8) }} |> Str.replace_each("·", " ") - result = board |> {{ case["property"] | to_snake }} +expect { + board = {{ case["input"]["board"] | to_roc_multiline_string | indent(8) }} + result = board -> {{ case["property"] | to_snake }} {%- if case["property"] == "territory" %}({ x : {{ case["input"]["x"] }}, y : {{ case["input"]["y"] }} }){% endif %} {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- elif case["expected"]["owner"] %} expected = Ok({ owner: {{ case["expected"]["owner"] | to_pascal }}, territory: {{ to_territory(case["expected"]["territory"]) }}, }) - result |> compare_territory(expected) + result -> compare_territory(expected) {%- else %} expected = Ok({ black: {{ to_territory(case["expected"]["territoryBlack"]) }}, white: {{ to_territory(case["expected"]["territoryWhite"]) }}, none: {{ to_territory(case["expected"]["territoryNone"]) }}, }) - result |> compare_territories(expected) + result -> compare_territories(expected) {%- endif %} - +} {% endfor %} + +## The following two comparison functions are temporary workarounds for Roc issue #7144: +## comparing tags or records containing sets sometimes returns the wrong result +## depending on the internal order of the set data, so we have to unwrap the sets +## in order to compare them properly. +compare_territory = |maybe_result, maybe_expected| { + match (maybe_result, maybe_expected) { + (Ok(result), Ok(expected)) => result.owner == expected.owner and result.territory == expected.territory + _ => Bool.False + } +} + +compare_territories = |maybe_result, maybe_expected| { + match (maybe_result, maybe_expected) { + (Ok(result), Ok(expected)) => result.black == expected.black and result.white == expected.white and result.none == expected.none + _ => Bool.False + } +} + +{{ macros.footer() }} diff --git a/exercises/practice/go-counting/GoCounting.roc b/exercises/practice/go-counting/GoCounting.roc index 7a715ee2..06e64e39 100644 --- a/exercises/practice/go-counting/GoCounting.roc +++ b/exercises/practice/go-counting/GoCounting.roc @@ -1,24 +1,26 @@ -module [territory, territories] +GoCounting :: {}.{ + Intersection : { x : U64, y : U64 } -Intersection : { x : U64, y : U64 } + Stone : [White, Black, None] -Stone : [White, Black, None] + Territory : { + owner : Stone, + territory : Set(Intersection), + } -Territory : { - owner : Stone, - territory : Set Intersection, -} - -Territories : { - black : Set Intersection, - white : Set Intersection, - none : Set Intersection, -} + Territories : { + black : Set(Intersection), + white : Set(Intersection), + none : Set(Intersection), + } -territory : Str, Intersection -> Result Territory _ -territory = |board_str, { x, y }| - crash("Please implement the 'territory' function") + territory : Str, Intersection -> Try(Territory, _) + territory = |board_str, { x, y }| { + crash "Please implement the 'territory' function" + } -territories : Str -> Result Territories _ -territories = |board_str| - crash("Please implement the 'territories' function") + territories : Str -> Try(Territories, _) + territories = |board_str| { + crash "Please implement the 'territories' function" + } +} diff --git a/exercises/practice/go-counting/go-counting-test.roc b/exercises/practice/go-counting/go-counting-test.roc index 180ffb7d..1f7ee871 100644 --- a/exercises/practice/go-counting/go-counting-test.roc +++ b/exercises/practice/go-counting/go-counting-test.roc @@ -1,221 +1,210 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/go-counting/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import GoCounting exposing [territory, territories] -## The following two comparison functions are temporary workarounds for Roc issue #7144: -## comparing tags or records containing sets sometimes returns the wrong result -## depending on the internal order of the set data, so we have to unwrap the sets -## in order to compare them properly. -compare_territory = |maybe_result, maybe_expected| - when (maybe_result, maybe_expected) is - (Ok(result), Ok(expected)) -> result.owner == expected.owner and result.territory == expected.territory - _ -> Bool.false - -compare_territories = |maybe_result, maybe_expected| - when (maybe_result, maybe_expected) is - (Ok(result), Ok(expected)) -> result.black == expected.black and result.white == expected.white and result.none == expected.none - _ -> Bool.false - # Black corner territory on 5x5 board -expect - board = - """ - ··B·· - ·B·B· - B·W·B - ·W·W· - ··W·· - """ - |> Str.replace_each("·", " ") - result = board |> territory({ x: 0, y: 1 }) - expected = Ok( - { - owner: Black, - territory: Set.from_list( - [ - { x: 0, y: 0 }, - { x: 0, y: 1 }, - { x: 1, y: 0 }, - ], - ), - }, - ) - result |> compare_territory(expected) +expect { + board = + \\ B + \\ B B + \\B W B + \\ W W + \\ W + + result = board->territory({ x: 0, y: 1 }) + expected = Ok( + { + owner: Black, + territory: Set.from_list( + [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + { x: 1, y: 0 }, + ], + ), + }, + ) + result->compare_territory(expected) +} # White center territory on 5x5 board -expect - board = - """ - ··B·· - ·B·B· - B·W·B - ·W·W· - ··W·· - """ - |> Str.replace_each("·", " ") - result = board |> territory({ x: 2, y: 3 }) - expected = Ok( - { - owner: White, - territory: Set.from_list( - [ - { x: 2, y: 3 }, - ], - ), - }, - ) - result |> compare_territory(expected) +expect { + board = + \\ B + \\ B B + \\B W B + \\ W W + \\ W + + result = board->territory({ x: 2, y: 3 }) + expected = Ok( + { + owner: White, + territory: Set.from_list( + [ + { x: 2, y: 3 }, + ], + ), + }, + ) + result->compare_territory(expected) +} # Open corner territory on 5x5 board -expect - board = - """ - ··B·· - ·B·B· - B·W·B - ·W·W· - ··W·· - """ - |> Str.replace_each("·", " ") - result = board |> territory({ x: 1, y: 4 }) - expected = Ok( - { - owner: None, - territory: Set.from_list( - [ - { x: 0, y: 3 }, - { x: 0, y: 4 }, - { x: 1, y: 4 }, - ], - ), - }, - ) - result |> compare_territory(expected) +expect { + board = + \\ B + \\ B B + \\B W B + \\ W W + \\ W + + result = board->territory({ x: 1, y: 4 }) + expected = Ok( + { + owner: None, + territory: Set.from_list( + [ + { x: 0, y: 3 }, + { x: 0, y: 4 }, + { x: 1, y: 4 }, + ], + ), + }, + ) + result->compare_territory(expected) +} # A stone and not a territory on 5x5 board -expect - board = - """ - ··B·· - ·B·B· - B·W·B - ·W·W· - ··W·· - """ - |> Str.replace_each("·", " ") - result = board |> territory({ x: 1, y: 1 }) - expected = Ok( - { - owner: None, - territory: Set.empty({}), - }, - ) - result |> compare_territory(expected) +expect { + board = + \\ B + \\ B B + \\B W B + \\ W W + \\ W + + result = board->territory({ x: 1, y: 1 }) + expected = Ok( + { + owner: None, + territory: Set.empty(), + }, + ) + result->compare_territory(expected) +} # Invalid because X is too high for 5x5 board -expect - board = - """ - ··B·· - ·B·B· - B·W·B - ·W·W· - ··W·· - """ - |> Str.replace_each("·", " ") - result = board |> territory({ x: 5, y: 1 }) - result |> Result.is_err +expect { + board = + \\ B + \\ B B + \\B W B + \\ W W + \\ W + + result = board->territory({ x: 5, y: 1 }) + result.is_err() +} # Invalid because Y is too high for 5x5 board -expect - board = - """ - ··B·· - ·B·B· - B·W·B - ·W·W· - ··W·· - """ - |> Str.replace_each("·", " ") - result = board |> territory({ x: 1, y: 5 }) - result |> Result.is_err +expect { + board = + \\ B + \\ B B + \\B W B + \\ W W + \\ W + + result = board->territory({ x: 1, y: 5 }) + result.is_err() +} # One territory is the whole board -expect - board = "·" |> Str.replace_each("·", " ") - result = board |> territories - expected = Ok( - { - black: Set.empty({}), - - white: Set.empty({}), - - none: Set.from_list( - [ - { x: 0, y: 0 }, - ], - ), - }, - ) - result |> compare_territories(expected) +expect { + board = " " + result = board->territories() + expected = Ok( + { + black: Set.empty(), + white: Set.empty(), + none: Set.from_list( + [ + { x: 0, y: 0 }, + ], + ), + }, + ) + result->compare_territories(expected) +} # Two territory rectangular board -expect - board = - """ - ·BW· - ·BW· - """ - |> Str.replace_each("·", " ") - result = board |> territories - expected = Ok( - { - black: Set.from_list( - [ - { x: 0, y: 0 }, - { x: 0, y: 1 }, - ], - ), - - white: Set.from_list( - [ - { x: 3, y: 0 }, - { x: 3, y: 1 }, - ], - ), - - none: Set.empty({}), - }, - ) - result |> compare_territories(expected) +expect { + board = + \\ BW + \\ BW + + result = board->territories() + expected = Ok( + { + black: Set.from_list( + [ + { x: 0, y: 0 }, + { x: 0, y: 1 }, + ], + ), + white: Set.from_list( + [ + { x: 3, y: 0 }, + { x: 3, y: 1 }, + ], + ), + none: Set.empty(), + }, + ) + result->compare_territories(expected) +} # Two region rectangular board -expect - board = "·B·" |> Str.replace_each("·", " ") - result = board |> territories - expected = Ok( - { - black: Set.from_list( - [ - { x: 0, y: 0 }, - { x: 2, y: 0 }, - ], - ), - - white: Set.empty({}), - - none: Set.empty({}), - }, - ) - result |> compare_territories(expected) +expect { + board = " B " + result = board->territories() + expected = Ok( + { + black: Set.from_list( + [ + { x: 0, y: 0 }, + { x: 2, y: 0 }, + ], + ), + white: Set.empty(), + none: Set.empty(), + }, + ) + result->compare_territories(expected) +} +## The following two comparison functions are temporary workarounds for Roc issue #7144: +## comparing tags or records containing sets sometimes returns the wrong result +## depending on the internal order of the set data, so we have to unwrap the sets +## in order to compare them properly. +compare_territory = |maybe_result, maybe_expected| { + match (maybe_result, maybe_expected) { + (Ok(result), Ok(expected)) => result.owner == expected.owner and result.territory == expected.territory + _ => Bool.False + } +} + +compare_territories = |maybe_result, maybe_expected| { + match (maybe_result, maybe_expected) { + (Ok(result), Ok(expected)) => result.black == expected.black and result.white == expected.white and result.none == expected.none + _ => Bool.False + } +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/grains/.meta/Example.roc b/exercises/practice/grains/.meta/Example.roc index 6d069e0e..bbac6222 100644 --- a/exercises/practice/grains/.meta/Example.roc +++ b/exercises/practice/grains/.meta/Example.roc @@ -1,12 +1,26 @@ -module [grains_on_square, total_grains] +Grains :: {}.{ + grains_on_square : U8 -> Try(U64, [SquareArgWasNotBetween1And64(U8)]) + grains_on_square = |square| { + if square > 0 and square <= 64 { + Ok(2->pow_int(square.to_u64() - 1)) + } else { + Err(SquareArgWasNotBetween1And64(square)) + } + } -grains_on_square : U8 -> Result U64 [SquareArgWasNotBetween1And64 U8] -grains_on_square = |square| - if square > 0 and square <= 64 then - 2u64 |> Num.pow_int((Num.to_u64(square) - 1)) |> Ok - else - Err(SquareArgWasNotBetween1And64(square)) + total_grains : U64 + total_grains = max_u64 +} -total_grains : U64 -total_grains = - Num.max_u64 +# This function should soon be available in Roc's builtins +pow_int : U64, U64 -> U64 +pow_int = |number, pow| { + (1..=pow).fold( + 1, + |acc, _| { + acc * number + }, + ) +} + +max_u64 = 18_446_744_073_709_551_615 diff --git a/exercises/practice/grains/.meta/template.j2 b/exercises/practice/grains/.meta/template.j2 index 70005fdf..a13a7973 100644 --- a/exercises/practice/grains/.meta/template.j2 +++ b/exercises/practice/grains/.meta/template.j2 @@ -9,20 +9,24 @@ import {{ exercise | to_pascal }} exposing [grains_on_square, total_grains] ## {{ case["description"] }} ## {% if case["property"] == "total" %} -expect +expect { result = total_grains result == {{ case["expected"] }} +} {% else %} {% for subcase in case["cases"] -%} # {{ subcase["description"] }} -expect +expect { result = grains_on_square({{ subcase["input"]["square"] }}) {%- if subcase["expected"]["error"] %} - Result.is_err(result) + result.is_err() {%- else %} result == Ok({{ subcase["expected"] }}) {%- endif %} +} {% endfor %} {%- endif -%} {%- endfor -%} + +{{ macros.footer() }} diff --git a/exercises/practice/grains/Grains.roc b/exercises/practice/grains/Grains.roc index 0ef72beb..85021dbf 100644 --- a/exercises/practice/grains/Grains.roc +++ b/exercises/practice/grains/Grains.roc @@ -1,9 +1,11 @@ -module [grains_on_square, total_grains] +Grains :: {}.{ + grains_on_square : U8 -> Try(U64, _) + grains_on_square = |square| { + crash "Please implement the 'grains_on_square' function" + } -grains_on_square : U8 -> Result U64 _ -grains_on_square = |square| - crash("Please implement the 'grains_on_square' function") - -total_grains : U64 -total_grains = - crash("Please implement the 'total_grains' function") + total_grains : U64 + total_grains = { + crash "Please implement the 'total_grains' function" + } +} diff --git a/exercises/practice/grains/grains-test.roc b/exercises/practice/grains/grains-test.roc index 8525d86e..9c4f95e2 100644 --- a/exercises/practice/grains/grains-test.roc +++ b/exercises/practice/grains/grains-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/grains/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Grains exposing [grains_on_square, total_grains] @@ -17,54 +9,68 @@ import Grains exposing [grains_on_square, total_grains] ## # grains on square 1 -expect - result = grains_on_square(1) - result == Ok(1) +expect { + result = grains_on_square(1) + result == Ok(1) +} # grains on square 2 -expect - result = grains_on_square(2) - result == Ok(2) +expect { + result = grains_on_square(2) + result == Ok(2) +} # grains on square 3 -expect - result = grains_on_square(3) - result == Ok(4) +expect { + result = grains_on_square(3) + result == Ok(4) +} # grains on square 4 -expect - result = grains_on_square(4) - result == Ok(8) +expect { + result = grains_on_square(4) + result == Ok(8) +} # grains on square 16 -expect - result = grains_on_square(16) - result == Ok(32768) +expect { + result = grains_on_square(16) + result == Ok(32768) +} # grains on square 32 -expect - result = grains_on_square(32) - result == Ok(2147483648) +expect { + result = grains_on_square(32) + result == Ok(2147483648) +} # grains on square 64 -expect - result = grains_on_square(64) - result == Ok(9223372036854775808) +expect { + result = grains_on_square(64) + result == Ok(9223372036854775808) +} # square 0 is invalid -expect - result = grains_on_square(0) - Result.is_err(result) +expect { + result = grains_on_square(0) + result.is_err() +} # square greater than 64 is invalid -expect - result = grains_on_square(65) - Result.is_err(result) +expect { + result = grains_on_square(65) + result.is_err() +} ## ## returns the total number of grains on the board ## -expect - result = total_grains - result == 18446744073709551615 +expect { + result = total_grains + result == 18446744073709551615 +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/grep/.meta/Example.roc b/exercises/practice/grep/.meta/Example.roc index 51a3a958..53522706 100644 --- a/exercises/practice/grep/.meta/Example.roc +++ b/exercises/practice/grep/.meta/Example.roc @@ -1,116 +1,157 @@ -module [grep] - import "iliad.txt" as iliad : Str import "midsummer-night.txt" as midsummer_night : Str import "paradise-lost.txt" as paradise_lost : Str -grep : Str, List Str, List Str -> Result Str _ -grep = |pattern, flags, file_names| - config = parse_flags(flags)? - files = collect_files(file_names)? - display_file_names = List.len(files) > 1 - List.join_map( - files, - |file| - when find_matches(config, pattern, file.text) is - [] -> [] - _ if config.display_file_names -> [file.name] - matches -> - List.map( - matches, - |{ line, index }| - line_number = - if config.display_line_numbers then - "${index + 1 |> Num.to_str}:" - else - "" - file_name = - if display_file_names then - "${file.name}:" - else - "" - "${file_name}${line_number}${line}", - ), - ) - |> Str.join_with("\n") - |> Ok +Grep :: {}.{ + grep : Str, List(Str), List(Str) -> Try(Str, _) + grep = |pattern, flags, file_names| { + config = parse_flags(flags)? + files = collect_files(file_names)? + display_file_names = files.len() > 1 + files + ->join_map( + |file| { + match find_matches(config, pattern, file.text) { + [] => [] + _ if config.display_file_names => [file.name] + matches => { + matches.map( + |match_info| { + { line, index } = match_info + line_number = + if config.display_line_numbers { + "${(index + 1).to_str()}:" + } else { + "" + } + file_name = + if display_file_names { + "${file.name}:" + } else { + "" + } + "${file_name}${line_number}${line}" + }, + ) + } + } + }, + ) + ->Str.join_with("\n") + ->Ok() + } +} -find_matches : Config, Str, Str -> List { line : Str, index : U64 } -find_matches = |config, pattern, text| - Str.split_on(text, "\n") - |> List.map_with_index( - |line, index| - { line, index }, - ) - |> List.keep_if( - |{ line }| - (line_to_match, pattern_to_match) = - if config.ignore_case then - (to_lower(line), to_lower(pattern)) - else - (line, pattern) +find_matches : Config, Str, Str -> List({ line : Str, index : U64 }) +find_matches = |config, pattern, text| { + text.split_on("\n") + .map_with_index(|line, index| { line, index }) + .keep_if( + |match_info| { + { line, index: _ } = match_info + (line_to_match, pattern_to_match) = + if config.ignore_case { + (to_lower(line), to_lower(pattern)) + } else { + (line, pattern) + } - matches = - if config.match_full_lines then - line_to_match == pattern_to_match - else - Str.contains(line_to_match, pattern_to_match) + matches = + if config.match_full_lines { + line_to_match == pattern_to_match + } else { + line_to_match.contains(pattern_to_match) + } - # Using != is equivalent to xor which inverts `matches` - config.invert_results != matches, - ) + # Using != is equivalent to xor which inverts `matches` + config.invert_results != matches + }, + ) +} to_lower : Str -> Str -to_lower = |str| - Str.to_utf8(str) - |> List.map( - |byte| - if 'A' <= byte and byte <= 'Z' then - byte - 'A' + 'a' - else - byte, - ) - |> Str.from_utf8 - |> Result.with_default("") +to_lower = |str| { + str.to_utf8() + .map( + |byte| { + if 'A' <= byte and byte <= 'Z' { + byte - 'A' + 'a' + } else { + byte + } + }, + ) + ->Str.from_utf8() + ?? "" +} Config : { - display_line_numbers : Bool, - display_file_names : Bool, - ignore_case : Bool, - match_full_lines : Bool, - invert_results : Bool, + display_line_numbers : Bool, + display_file_names : Bool, + ignore_case : Bool, + match_full_lines : Bool, + invert_results : Bool, +} + +parse_flags : List(Str) -> Try(Config, _) +parse_flags = |flags| { + default_config = { + display_line_numbers: Bool.False, + display_file_names: Bool.False, + ignore_case: Bool.False, + match_full_lines: Bool.False, + invert_results: Bool.False, + } + List.fold_until( + flags, + Ok(default_config), + |config_res, flag| { + match config_res { + Ok(config) => { + match flag { + "-l" => Continue(Ok({ ..config, display_file_names: Bool.True })) + "-n" => Continue(Ok({ ..config, display_line_numbers: Bool.True })) + "-i" => Continue(Ok({ ..config, ignore_case: Bool.True })) + "-x" => Continue(Ok({ ..config, match_full_lines: Bool.True })) + "-v" => Continue(Ok({ ..config, invert_results: Bool.True })) + _ => Break(Err(UnknownFlag(flag))) + } + } + Err(err) => Break(Err(err)) + } + }, + ) } -parse_flags : List Str -> Result Config _ -parse_flags = |flags| - default_config = { - display_line_numbers: Bool.false, - display_file_names: Bool.false, - ignore_case: Bool.false, - match_full_lines: Bool.false, - invert_results: Bool.false, - } - List.walk_try( - flags, - default_config, - |config, flag| - when flag is - "-l" -> Ok({ config & display_file_names: Bool.true }) - "-n" -> Ok({ config & display_line_numbers: Bool.true }) - "-i" -> Ok({ config & ignore_case: Bool.true }) - "-x" -> Ok({ config & match_full_lines: Bool.true }) - "-v" -> Ok({ config & invert_results: Bool.true }) - _ -> Err(UnknownFlag(flag)), - ) +collect_files : List(Str) -> Try(List({ name : Str, text : Str }), _) +collect_files = |names| { + names->map_try( + |name| { + match name { + "midsummer-night.txt" => Ok({ name: "midsummer-night.txt", text: midsummer_night }) + "iliad.txt" => Ok({ name: "iliad.txt", text: iliad }) + "paradise-lost.txt" => Ok({ name: "paradise-lost.txt", text: paradise_lost }) + _ => Err(FileNotFound(name)) + } + }, + ) +} -collect_files : List Str -> Result (List { name : Str, text : Str }) _ -collect_files = |names| - List.map_try( - names, - |name| - when name is - "midsummer-night.txt" -> Ok({ name: "midsummer-night.txt", text: midsummer_night }) - "iliad.txt" -> Ok({ name: "iliad.txt", text: iliad }) - "paradise-lost.txt" -> Ok({ name: "paradise-lost.txt", text: paradise_lost }) - _ -> Err(FileNotFound(name)), - ) +# The following functions should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} + +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/grep/.meta/template.j2 b/exercises/practice/grep/.meta/template.j2 index 77270f17..2a5023dc 100644 --- a/exercises/practice/grep/.meta/template.j2 +++ b/exercises/practice/grep/.meta/template.j2 @@ -7,9 +7,12 @@ import {{ exercise | to_pascal }} exposing [grep] {% for case in cases -%} {% for innerCase in case["cases"] -%} # {{ case["description"] }} - {{ innerCase["description"] }} -expect +expect { result = {{ innerCase["property"] | to_snake }}({{ innerCase["input"]["pattern"] | to_roc }}, {{ innerCase["input"]["flags"] | to_roc }}, {{ innerCase["input"]["files"] | to_roc }}) result == Ok({{ innerCase["expected"] | join('\n') | to_roc_multiline_string | indent(8) }}) +} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/grep/Grep.roc b/exercises/practice/grep/Grep.roc index dd939f61..aa298bad 100644 --- a/exercises/practice/grep/Grep.roc +++ b/exercises/practice/grep/Grep.roc @@ -1,9 +1,10 @@ -module [grep] - import "iliad.txt" as iliad : Str import "midsummer-night.txt" as midsummer_night : Str import "paradise-lost.txt" as paradise_lost : Str -grep : Str, List Str, List Str -> Result Str _ -grep = |pattern, flags, files| - crash("Please implement 'grep'") +Grep :: {}.{ + grep : Str, List(Str), List(Str) -> Try(Str, _) + grep = |pattern, flags, files| { + crash "Please implement the 'grep' function" + } +} diff --git a/exercises/practice/grep/grep-test.roc b/exercises/practice/grep/grep-test.roc index d9cb6b88..57f91018 100644 --- a/exercises/practice/grep/grep-test.roc +++ b/exercises/practice/grep/grep-test.roc @@ -1,255 +1,252 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/grep/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-21 import Grep exposing [grep] # Test grepping a single file - One file, one match, no flags -expect - result = grep("Agamemnon", [], ["iliad.txt"]) - result == Ok("Of Atreus, Agamemnon, King of men.") +expect { + result = grep("Agamemnon", [], ["iliad.txt"]) + result == Ok("Of Atreus, Agamemnon, King of men.") +} # Test grepping a single file - One file, one match, print line numbers flag -expect - result = grep("Forbidden", ["-n"], ["paradise-lost.txt"]) - result == Ok("2:Of that Forbidden Tree, whose mortal tast") +expect { + result = grep("Forbidden", ["-n"], ["paradise-lost.txt"]) + result == Ok("2:Of that Forbidden Tree, whose mortal tast") +} # Test grepping a single file - One file, one match, case-insensitive flag -expect - result = grep("FORBIDDEN", ["-i"], ["paradise-lost.txt"]) - result == Ok("Of that Forbidden Tree, whose mortal tast") +expect { + result = grep("FORBIDDEN", ["-i"], ["paradise-lost.txt"]) + result == Ok("Of that Forbidden Tree, whose mortal tast") +} # Test grepping a single file - One file, one match, print file names flag -expect - result = grep("Forbidden", ["-l"], ["paradise-lost.txt"]) - result == Ok("paradise-lost.txt") +expect { + result = grep("Forbidden", ["-l"], ["paradise-lost.txt"]) + result == Ok("paradise-lost.txt") +} # Test grepping a single file - One file, one match, match entire lines flag -expect - result = grep("With loss of Eden, till one greater Man", ["-x"], ["paradise-lost.txt"]) - result == Ok("With loss of Eden, till one greater Man") +expect { + result = grep("With loss of Eden, till one greater Man", ["-x"], ["paradise-lost.txt"]) + result == Ok("With loss of Eden, till one greater Man") +} # Test grepping a single file - One file, one match, multiple flags -expect - result = grep("OF ATREUS, Agamemnon, KIng of MEN.", ["-n", "-i", "-x"], ["iliad.txt"]) - result == Ok("9:Of Atreus, Agamemnon, King of men.") +expect { + result = grep("OF ATREUS, Agamemnon, KIng of MEN.", ["-n", "-i", "-x"], ["iliad.txt"]) + result == Ok("9:Of Atreus, Agamemnon, King of men.") +} # Test grepping a single file - One file, several matches, no flags -expect - result = grep("may", [], ["midsummer-night.txt"]) - result - == Ok( - """ - Nor how it may concern my modesty, - But I beseech your grace that I may know - The worst that may befall me in this case, - """, - ) +expect { + result = grep("may", [], ["midsummer-night.txt"]) + result == Ok( + \\Nor how it may concern my modesty, + \\But I beseech your grace that I may know + \\The worst that may befall me in this case, + , + ) +} # Test grepping a single file - One file, several matches, print line numbers flag -expect - result = grep("may", ["-n"], ["midsummer-night.txt"]) - result - == Ok( - """ - 3:Nor how it may concern my modesty, - 5:But I beseech your grace that I may know - 6:The worst that may befall me in this case, - """, - ) +expect { + result = grep("may", ["-n"], ["midsummer-night.txt"]) + result == Ok( + \\3:Nor how it may concern my modesty, + \\5:But I beseech your grace that I may know + \\6:The worst that may befall me in this case, + , + ) +} # Test grepping a single file - One file, several matches, match entire lines flag -expect - result = grep("may", ["-x"], ["midsummer-night.txt"]) - result == Ok("") +expect { + result = grep("may", ["-x"], ["midsummer-night.txt"]) + result == Ok("") +} # Test grepping a single file - One file, several matches, case-insensitive flag -expect - result = grep("ACHILLES", ["-i"], ["iliad.txt"]) - result - == Ok( - """ - Achilles sing, O Goddess! Peleus' son; - The noble Chief Achilles from the son - """, - ) +expect { + result = grep("ACHILLES", ["-i"], ["iliad.txt"]) + result == Ok( + \\Achilles sing, O Goddess! Peleus' son; + \\The noble Chief Achilles from the son + , + ) +} # Test grepping a single file - One file, several matches, inverted flag -expect - result = grep("Of", ["-v"], ["paradise-lost.txt"]) - result - == Ok( - """ - Brought Death into the World, and all our woe, - With loss of Eden, till one greater Man - Restore us, and regain the blissful Seat, - Sing Heav'nly Muse, that on the secret top - That Shepherd, who first taught the chosen Seed - """, - ) +expect { + result = grep("Of", ["-v"], ["paradise-lost.txt"]) + result == Ok( + \\Brought Death into the World, and all our woe, + \\With loss of Eden, till one greater Man + \\Restore us, and regain the blissful Seat, + \\Sing Heav'nly Muse, that on the secret top + \\That Shepherd, who first taught the chosen Seed + , + ) +} # Test grepping a single file - One file, no matches, various flags -expect - result = grep("Gandalf", ["-n", "-l", "-x", "-i"], ["iliad.txt"]) - result == Ok("") +expect { + result = grep("Gandalf", ["-n", "-l", "-x", "-i"], ["iliad.txt"]) + result == Ok("") +} # Test grepping a single file - One file, one match, file flag takes precedence over line flag -expect - result = grep("ten", ["-n", "-l"], ["iliad.txt"]) - result == Ok("iliad.txt") +expect { + result = grep("ten", ["-n", "-l"], ["iliad.txt"]) + result == Ok("iliad.txt") +} # Test grepping a single file - One file, several matches, inverted and match entire lines flags -expect - result = grep("Illustrious into Ades premature,", ["-x", "-v"], ["iliad.txt"]) - result - == Ok( - """ - Achilles sing, O Goddess! Peleus' son; - His wrath pernicious, who ten thousand woes - Caused to Achaia's host, sent many a soul - And Heroes gave (so stood the will of Jove) - To dogs and to all ravening fowls a prey, - When fierce dispute had separated once - The noble Chief Achilles from the son - Of Atreus, Agamemnon, King of men. - """, - ) +expect { + result = grep("Illustrious into Ades premature,", ["-x", "-v"], ["iliad.txt"]) + result == Ok( + \\Achilles sing, O Goddess! Peleus' son; + \\His wrath pernicious, who ten thousand woes + \\Caused to Achaia's host, sent many a soul + \\And Heroes gave (so stood the will of Jove) + \\To dogs and to all ravening fowls a prey, + \\When fierce dispute had separated once + \\The noble Chief Achilles from the son + \\Of Atreus, Agamemnon, King of men. + , + ) +} # Test grepping multiples files at once - Multiple files, one match, no flags -expect - result = grep("Agamemnon", [], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result == Ok("iliad.txt:Of Atreus, Agamemnon, King of men.") +expect { + result = grep("Agamemnon", [], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok("iliad.txt:Of Atreus, Agamemnon, King of men.") +} # Test grepping multiples files at once - Multiple files, several matches, no flags -expect - result = grep("may", [], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result - == Ok( - """ - midsummer-night.txt:Nor how it may concern my modesty, - midsummer-night.txt:But I beseech your grace that I may know - midsummer-night.txt:The worst that may befall me in this case, - """, - ) +expect { + result = grep("may", [], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok( + \\midsummer-night.txt:Nor how it may concern my modesty, + \\midsummer-night.txt:But I beseech your grace that I may know + \\midsummer-night.txt:The worst that may befall me in this case, + , + ) +} # Test grepping multiples files at once - Multiple files, several matches, print line numbers flag -expect - result = grep("that", ["-n"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result - == Ok( - """ - midsummer-night.txt:5:But I beseech your grace that I may know - midsummer-night.txt:6:The worst that may befall me in this case, - paradise-lost.txt:2:Of that Forbidden Tree, whose mortal tast - paradise-lost.txt:6:Sing Heav'nly Muse, that on the secret top - """, - ) +expect { + result = grep("that", ["-n"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok( + \\midsummer-night.txt:5:But I beseech your grace that I may know + \\midsummer-night.txt:6:The worst that may befall me in this case, + \\paradise-lost.txt:2:Of that Forbidden Tree, whose mortal tast + \\paradise-lost.txt:6:Sing Heav'nly Muse, that on the secret top + , + ) +} # Test grepping multiples files at once - Multiple files, one match, print file names flag -expect - result = grep("who", ["-l"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result - == Ok( - """ - iliad.txt - paradise-lost.txt - """, - ) +expect { + result = grep("who", ["-l"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok( + \\iliad.txt + \\paradise-lost.txt + , + ) +} # Test grepping multiples files at once - Multiple files, several matches, case-insensitive flag -expect - result = grep("TO", ["-i"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result - == Ok( - """ - iliad.txt:Caused to Achaia's host, sent many a soul - iliad.txt:Illustrious into Ades premature, - iliad.txt:And Heroes gave (so stood the will of Jove) - iliad.txt:To dogs and to all ravening fowls a prey, - midsummer-night.txt:I do entreat your grace to pardon me. - midsummer-night.txt:In such a presence here to plead my thoughts; - midsummer-night.txt:If I refuse to wed Demetrius. - paradise-lost.txt:Brought Death into the World, and all our woe, - paradise-lost.txt:Restore us, and regain the blissful Seat, - paradise-lost.txt:Sing Heav'nly Muse, that on the secret top - """, - ) +expect { + result = grep("TO", ["-i"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok( + \\iliad.txt:Caused to Achaia's host, sent many a soul + \\iliad.txt:Illustrious into Ades premature, + \\iliad.txt:And Heroes gave (so stood the will of Jove) + \\iliad.txt:To dogs and to all ravening fowls a prey, + \\midsummer-night.txt:I do entreat your grace to pardon me. + \\midsummer-night.txt:In such a presence here to plead my thoughts; + \\midsummer-night.txt:If I refuse to wed Demetrius. + \\paradise-lost.txt:Brought Death into the World, and all our woe, + \\paradise-lost.txt:Restore us, and regain the blissful Seat, + \\paradise-lost.txt:Sing Heav'nly Muse, that on the secret top + , + ) +} # Test grepping multiples files at once - Multiple files, several matches, inverted flag -expect - result = grep("a", ["-v"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result - == Ok( - """ - iliad.txt:Achilles sing, O Goddess! Peleus' son; - iliad.txt:The noble Chief Achilles from the son - midsummer-night.txt:If I refuse to wed Demetrius. - """, - ) +expect { + result = grep("a", ["-v"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok( + \\iliad.txt:Achilles sing, O Goddess! Peleus' son; + \\iliad.txt:The noble Chief Achilles from the son + \\midsummer-night.txt:If I refuse to wed Demetrius. + , + ) +} # Test grepping multiples files at once - Multiple files, one match, match entire lines flag -expect - result = grep("But I beseech your grace that I may know", ["-x"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result == Ok("midsummer-night.txt:But I beseech your grace that I may know") +expect { + result = grep("But I beseech your grace that I may know", ["-x"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok("midsummer-night.txt:But I beseech your grace that I may know") +} # Test grepping multiples files at once - Multiple files, one match, multiple flags -expect - result = grep("WITH LOSS OF EDEN, TILL ONE GREATER MAN", ["-n", "-i", "-x"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result == Ok("paradise-lost.txt:4:With loss of Eden, till one greater Man") +expect { + result = grep("WITH LOSS OF EDEN, TILL ONE GREATER MAN", ["-n", "-i", "-x"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok("paradise-lost.txt:4:With loss of Eden, till one greater Man") +} # Test grepping multiples files at once - Multiple files, no matches, various flags -expect - result = grep("Frodo", ["-n", "-l", "-x", "-i"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result == Ok("") +expect { + result = grep("Frodo", ["-n", "-l", "-x", "-i"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok("") +} # Test grepping multiples files at once - Multiple files, several matches, file flag takes precedence over line number flag -expect - result = grep("who", ["-n", "-l"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result - == Ok( - """ - iliad.txt - paradise-lost.txt - """, - ) +expect { + result = grep("who", ["-n", "-l"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok( + \\iliad.txt + \\paradise-lost.txt + , + ) +} # Test grepping multiples files at once - Multiple files, several matches, inverted and match entire lines flags -expect - result = grep("Illustrious into Ades premature,", ["-x", "-v"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) - result - == Ok( - """ - iliad.txt:Achilles sing, O Goddess! Peleus' son; - iliad.txt:His wrath pernicious, who ten thousand woes - iliad.txt:Caused to Achaia's host, sent many a soul - iliad.txt:And Heroes gave (so stood the will of Jove) - iliad.txt:To dogs and to all ravening fowls a prey, - iliad.txt:When fierce dispute had separated once - iliad.txt:The noble Chief Achilles from the son - iliad.txt:Of Atreus, Agamemnon, King of men. - midsummer-night.txt:I do entreat your grace to pardon me. - midsummer-night.txt:I know not by what power I am made bold, - midsummer-night.txt:Nor how it may concern my modesty, - midsummer-night.txt:In such a presence here to plead my thoughts; - midsummer-night.txt:But I beseech your grace that I may know - midsummer-night.txt:The worst that may befall me in this case, - midsummer-night.txt:If I refuse to wed Demetrius. - paradise-lost.txt:Of Mans First Disobedience, and the Fruit - paradise-lost.txt:Of that Forbidden Tree, whose mortal tast - paradise-lost.txt:Brought Death into the World, and all our woe, - paradise-lost.txt:With loss of Eden, till one greater Man - paradise-lost.txt:Restore us, and regain the blissful Seat, - paradise-lost.txt:Sing Heav'nly Muse, that on the secret top - paradise-lost.txt:Of Oreb, or of Sinai, didst inspire - paradise-lost.txt:That Shepherd, who first taught the chosen Seed - """, - ) +expect { + result = grep("Illustrious into Ades premature,", ["-x", "-v"], ["iliad.txt", "midsummer-night.txt", "paradise-lost.txt"]) + result == Ok( + \\iliad.txt:Achilles sing, O Goddess! Peleus' son; + \\iliad.txt:His wrath pernicious, who ten thousand woes + \\iliad.txt:Caused to Achaia's host, sent many a soul + \\iliad.txt:And Heroes gave (so stood the will of Jove) + \\iliad.txt:To dogs and to all ravening fowls a prey, + \\iliad.txt:When fierce dispute had separated once + \\iliad.txt:The noble Chief Achilles from the son + \\iliad.txt:Of Atreus, Agamemnon, King of men. + \\midsummer-night.txt:I do entreat your grace to pardon me. + \\midsummer-night.txt:I know not by what power I am made bold, + \\midsummer-night.txt:Nor how it may concern my modesty, + \\midsummer-night.txt:In such a presence here to plead my thoughts; + \\midsummer-night.txt:But I beseech your grace that I may know + \\midsummer-night.txt:The worst that may befall me in this case, + \\midsummer-night.txt:If I refuse to wed Demetrius. + \\paradise-lost.txt:Of Mans First Disobedience, and the Fruit + \\paradise-lost.txt:Of that Forbidden Tree, whose mortal tast + \\paradise-lost.txt:Brought Death into the World, and all our woe, + \\paradise-lost.txt:With loss of Eden, till one greater Man + \\paradise-lost.txt:Restore us, and regain the blissful Seat, + \\paradise-lost.txt:Sing Heav'nly Muse, that on the secret top + \\paradise-lost.txt:Of Oreb, or of Sinai, didst inspire + \\paradise-lost.txt:That Shepherd, who first taught the chosen Seed + , + ) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/hamming/.meta/Example.roc b/exercises/practice/hamming/.meta/Example.roc index eb3f83e0..cac1ed75 100644 --- a/exercises/practice/hamming/.meta/Example.roc +++ b/exercises/practice/hamming/.meta/Example.roc @@ -1,17 +1,25 @@ -module [distance] - -distance : Str, Str -> Result (Num *) [StrandArgsWereNotOfEqualLength Str Str] -distance = |strand1, strand2| - nucleotides1 = strand1 |> Str.to_utf8 - nucleotides2 = strand2 |> Str.to_utf8 - if List.len(nucleotides1) == List.len(nucleotides2) then - List.map2( - nucleotides1, - nucleotides2, - |n1, n2| - if n1 == n2 then 0 else 1, - ) - |> List.sum - |> Ok - else - Err(StrandArgsWereNotOfEqualLength(strand1, strand2)) +Hamming :: {}.{ + distance : Str, Str -> Try(U64, [StrandArgsWereNotOfEqualLength(Str, Str)]) + distance = |strand1, strand2| { + nucleotides1 = strand1.to_utf8() + nucleotides2 = strand2.to_utf8() + if nucleotides1.len() == nucleotides2.len() { + Ok( + List.map2( + nucleotides1, + nucleotides2, + |n1, n2| { + if n1 == n2 { + 0 + } else { + 1 + } + }, + ) + .sum(), + ) + } else { + Err(StrandArgsWereNotOfEqualLength(strand1, strand2)) + } + } +} diff --git a/exercises/practice/hamming/.meta/template.j2 b/exercises/practice/hamming/.meta/template.j2 index 3b6eb393..b03c2a16 100644 --- a/exercises/practice/hamming/.meta/template.j2 +++ b/exercises/practice/hamming/.meta/template.j2 @@ -6,12 +6,15 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["strand1"] | to_roc }}, {{ case["input"]["strand2"] | to_roc }}) {%- if case["expected"]["error"] %} - Result.is_err(result) + result.is_err() {%- else %} result == Ok({{ case["expected"] }}) {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/hamming/Hamming.roc b/exercises/practice/hamming/Hamming.roc index 55b5e20e..1756334b 100644 --- a/exercises/practice/hamming/Hamming.roc +++ b/exercises/practice/hamming/Hamming.roc @@ -1,5 +1,6 @@ -module [distance] - -distance : Str, Str -> Result (Num *) _ -distance = |strand1, strand2| - crash("Please implement the 'distance' function") +Hamming :: {}.{ + distance : Str, Str -> Try(U64, [StrandArgsWereNotOfEqualLength(Str, Str)]) + distance = |strand1, strand2| { + crash "Please implement the 'distance' function" + } +} diff --git a/exercises/practice/hamming/hamming-test.roc b/exercises/practice/hamming/hamming-test.roc index ee0da94b..6e1d81d7 100644 --- a/exercises/practice/hamming/hamming-test.roc +++ b/exercises/practice/hamming/hamming-test.roc @@ -1,59 +1,64 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/hamming/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Hamming exposing [distance] # empty strands -expect - result = distance("", "") - result == Ok(0) +expect { + result = distance("", "") + result == Ok(0) +} # single letter identical strands -expect - result = distance("A", "A") - result == Ok(0) +expect { + result = distance("A", "A") + result == Ok(0) +} # single letter different strands -expect - result = distance("G", "T") - result == Ok(1) +expect { + result = distance("G", "T") + result == Ok(1) +} # long identical strands -expect - result = distance("GGACTGAAATCTG", "GGACTGAAATCTG") - result == Ok(0) +expect { + result = distance("GGACTGAAATCTG", "GGACTGAAATCTG") + result == Ok(0) +} # long different strands -expect - result = distance("GGACGGATTCTG", "AGGACGGATTCT") - result == Ok(9) +expect { + result = distance("GGACGGATTCTG", "AGGACGGATTCT") + result == Ok(9) +} # disallow first strand longer -expect - result = distance("AATG", "AAA") - Result.is_err(result) +expect { + result = distance("AATG", "AAA") + result.is_err() +} # disallow second strand longer -expect - result = distance("ATA", "AGTG") - Result.is_err(result) +expect { + result = distance("ATA", "AGTG") + result.is_err() +} # disallow empty first strand -expect - result = distance("", "G") - Result.is_err(result) +expect { + result = distance("", "G") + result.is_err() +} # disallow empty second strand -expect - result = distance("G", "") - Result.is_err(result) +expect { + result = distance("G", "") + 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/hello-world/.meta/Example.roc b/exercises/practice/hello-world/.meta/Example.roc index 9804f555..ffd4c67c 100644 --- a/exercises/practice/hello-world/.meta/Example.roc +++ b/exercises/practice/hello-world/.meta/Example.roc @@ -1,4 +1,4 @@ -module [hello] - -hello : Str -hello = "Hello, World!" +HelloWorld :: {}.{ + hello : Str + hello = "Hello, World!" +} diff --git a/exercises/practice/hello-world/.meta/template.j2 b/exercises/practice/hello-world/.meta/template.j2 index ccfb51dc..1ac91021 100644 --- a/exercises/practice/hello-world/.meta/template.j2 +++ b/exercises/practice/hello-world/.meta/template.j2 @@ -7,8 +7,11 @@ import HelloWorld exposing [hello] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] }} result == {{ case ["expected"] | to_roc }} {% endfor %} +} + +{{ macros.footer() }} diff --git a/exercises/practice/hello-world/HelloWorld.roc b/exercises/practice/hello-world/HelloWorld.roc index 1aa1043d..1c6fe3bf 100644 --- a/exercises/practice/hello-world/HelloWorld.roc +++ b/exercises/practice/hello-world/HelloWorld.roc @@ -1,4 +1,4 @@ -module [hello] - -hello : Str -hello = "Goodbye, Mars!" +HelloWorld :: {}.{ + hello : Str + hello = "Goodbye, Mars!" +} diff --git a/exercises/practice/hello-world/hello-world-test.roc b/exercises/practice/hello-world/hello-world-test.roc index 8bc634c7..c7eb72a4 100644 --- a/exercises/practice/hello-world/hello-world-test.roc +++ b/exercises/practice/hello-world/hello-world-test.roc @@ -1,19 +1,17 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/hello-world/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import HelloWorld exposing [hello] # Say Hi! -expect - result = hello - result == "Hello, World!" +expect { + result = hello + result == "Hello, World!" +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/hexadecimal/.meta/Example.roc b/exercises/practice/hexadecimal/.meta/Example.roc index 529fe293..33f77213 100644 --- a/exercises/practice/hexadecimal/.meta/Example.roc +++ b/exercises/practice/hexadecimal/.meta/Example.roc @@ -1,28 +1,46 @@ -module [parse] +Hexadecimal :: {}.{ + parse : Str -> Try(U64, [InvalidNumStr]) + parse = |string| { + if string == "" { + Err(InvalidNumStr) + } else { + digits = string.to_utf8()->map_try(parse_nibble)? + digits.fold_until( + Ok(0), + |acc_res, nibble| { + match acc_res { + Ok(number) => { + if number > 0xfffffffffffffff { + Break(Err(InvalidNumStr)) + } else { + Continue(Ok(U64.shift_left_by(number, 4) + nibble)) + } + } + Err(err) => Break(Err(err)) + } + }, + ) + } + } +} -parse_nibble = |char| - if char >= '0' and char <= '9' then - Ok((char - '0' |> Num.to_u64)) - else if char >= 'A' and char <= 'F' then - Ok((char - 'A' + 10 |> Num.to_u64)) - else if char >= 'a' and char <= 'f' then - Ok((char - 'a' + 10 |> Num.to_u64)) - else - Err(InvalidNumStr) +parse_nibble : U8 -> Try(U64, _) +parse_nibble = |char| { + if char >= '0' and char <= '9' { + Ok((char - '0').to_u64()) + } else if char >= 'A' and char <= 'F' { + Ok((char - 'A' + 10).to_u64()) + } else if char >= 'a' and char <= 'f' { + Ok((char - 'a' + 10).to_u64()) + } else { + Err(InvalidNumStr) + } +} -parse : Str -> Result U64 _ -parse = |string| - if string == "" then - Err(InvalidNumStr) - else - string - |> Str.to_utf8 - |> List.walk_try( - 0, - |number, char| - nibble = parse_nibble(char)? - if number > 0xfffffffffffffff then - Err(InvalidNumStr) - else - number |> Num.shift_left_by(4) |> Num.add(nibble) |> Ok, - ) +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/hexadecimal/Hexadecimal.roc b/exercises/practice/hexadecimal/Hexadecimal.roc index e286f262..57958603 100644 --- a/exercises/practice/hexadecimal/Hexadecimal.roc +++ b/exercises/practice/hexadecimal/Hexadecimal.roc @@ -1,5 +1,6 @@ -module [parse] - -parse : Str -> Result U64 _ -parse = |string| - crash("Please implement the 'parse' function") +Hexadecimal :: {}.{ + parse : Str -> Try(U64, _) + parse = |string| { + crash "Please implement the 'parse' function" + } +} diff --git a/exercises/practice/hexadecimal/hexadecimal-test.roc b/exercises/practice/hexadecimal/hexadecimal-test.roc index 604d0177..1a002d31 100644 --- a/exercises/practice/hexadecimal/hexadecimal-test.roc +++ b/exercises/practice/hexadecimal/hexadecimal-test.roc @@ -1,241 +1,285 @@ -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-13 import Hexadecimal exposing [parse] # Parse "0" -expect - result = parse("0") - result == Ok(0) +expect { + result = parse("0") + result == Ok(0) +} # Parse "1" -expect - result = parse("1") - result == Ok(1) +expect { + result = parse("1") + result == Ok(1) +} # Parse "2" -expect - result = parse("2") - result == Ok(2) +expect { + result = parse("2") + result == Ok(2) +} # Parse "3" -expect - result = parse("3") - result == Ok(3) +expect { + result = parse("3") + result == Ok(3) +} # Parse "4" -expect - result = parse("4") - result == Ok(4) +expect { + result = parse("4") + result == Ok(4) +} # Parse "5" -expect - result = parse("5") - result == Ok(5) +expect { + result = parse("5") + result == Ok(5) +} # Parse "6" -expect - result = parse("6") - result == Ok(6) +expect { + result = parse("6") + result == Ok(6) +} # Parse "7" -expect - result = parse("7") - result == Ok(7) +expect { + result = parse("7") + result == Ok(7) +} # Parse "8" -expect - result = parse("8") - result == Ok(8) +expect { + result = parse("8") + result == Ok(8) +} # Parse "9" -expect - result = parse("9") - result == Ok(9) +expect { + result = parse("9") + result == Ok(9) +} # Parse "a" -expect - result = parse("a") - result == Ok(10) +expect { + result = parse("a") + result == Ok(10) +} # Parse "b" -expect - result = parse("b") - result == Ok(11) +expect { + result = parse("b") + result == Ok(11) +} # Parse "c" -expect - result = parse("c") - result == Ok(12) +expect { + result = parse("c") + result == Ok(12) +} # Parse "d" -expect - result = parse("d") - result == Ok(13) +expect { + result = parse("d") + result == Ok(13) +} # Parse "e" -expect - result = parse("e") - result == Ok(14) +expect { + result = parse("e") + result == Ok(14) +} # Parse "f" -expect - result = parse("f") - result == Ok(15) +expect { + result = parse("f") + result == Ok(15) +} # Parse "10" -expect - result = parse("10") - result == Ok(16) +expect { + result = parse("10") + result == Ok(16) +} # Parse "11" -expect - result = parse("11") - result == Ok(17) +expect { + result = parse("11") + result == Ok(17) +} # Parse "1f" -expect - result = parse("1f") - result == Ok(31) +expect { + result = parse("1f") + result == Ok(31) +} # Parse "ff" -expect - result = parse("ff") - result == Ok(255) +expect { + result = parse("ff") + result == Ok(255) +} # Parse "abc" -expect - result = parse("abc") - result == Ok(2748) +expect { + result = parse("abc") + result == Ok(2748) +} # Parse "cafe" -expect - result = parse("cafe") - result == Ok(51966) +expect { + result = parse("cafe") + result == Ok(51966) +} # Parse "deadbeef" -expect - result = parse("deadbeef") - result == Ok(3735928559) +expect { + result = parse("deadbeef") + result == Ok(3735928559) +} # Parse "123456789abcdef" -expect - result = parse("123456789abcdef") - result == Ok(81985529216486895) +expect { + result = parse("123456789abcdef") + result == Ok(81985529216486895) +} # Parse "ffffffffffffffff", the largest U64 value -expect - result = parse("ffffffffffffffff") - result == Ok(18446744073709551615) +expect { + result = parse("ffffffffffffffff") + result == Ok(18446744073709551615) +} # Ignore leading zeros in "00000" -expect - result = parse("00000") - result == Ok(0) +expect { + result = parse("00000") + result == Ok(0) +} # Ignore leading zeros in "00001" -expect - result = parse("00001") - result == Ok(1) +expect { + result = parse("00001") + result == Ok(1) +} # Ignore leading zeros in "0000a" -expect - result = parse("0000a") - result == Ok(10) +expect { + result = parse("0000a") + result == Ok(10) +} # Ignore leading zeros in "000010" -expect - result = parse("000010") - result == Ok(16) +expect { + result = parse("000010") + result == Ok(16) +} # Ignore leading zeros in "0000ff" -expect - result = parse("0000ff") - result == Ok(255) +expect { + result = parse("0000ff") + result == Ok(255) +} # Ignore leading zeros in "0000a0000" -expect - result = parse("0000a0000") - result == Ok(655360) +expect { + result = parse("0000a0000") + result == Ok(655360) +} # Ignore leading zeros even before the largest U64 value -expect - result = parse("0000ffffffffffffffff") - result == Ok(18446744073709551615) +expect { + result = parse("0000ffffffffffffffff") + result == Ok(18446744073709551615) +} # Accept upper case -expect - result = parse("ABCDEF") - result == Ok(11259375) +expect { + result = parse("ABCDEF") + result == Ok(11259375) +} # Accept mixed case -expect - result = parse("aAbBcCdDeEfF") - result == Ok(187723572702975) +expect { + result = parse("aAbBcCdDeEfF") + result == Ok(187723572702975) +} # Empty strings are invalid -expect - result = parse("") - result |> Result.is_err +expect { + result = parse("") + result.is_err() +} # A string with only spaces is invalid -expect - result = parse(" ") - result |> Result.is_err +expect { + result = parse(" ") + result.is_err() +} # Leading spaces are invalid -expect - result = parse(" 12ab") - result |> Result.is_err +expect { + result = parse(" 12ab") + result.is_err() +} # Trailing spaces are invalid -expect - result = parse("12ab ") - result |> Result.is_err +expect { + result = parse("12ab ") + result.is_err() +} # Spaces anywhere are invalid -expect - result = parse("12 ab") - result |> Result.is_err +expect { + result = parse("12 ab") + result.is_err() +} # Invalid character in "1*2ab" -expect - result = parse("1*2ab") - result |> Result.is_err +expect { + result = parse("1*2ab") + result.is_err() +} # Invalid character in "12fg" -expect - result = parse("12fg") - result |> Result.is_err +expect { + result = parse("12fg") + result.is_err() +} # Invalid character in "12/ab" -expect - result = parse("12/ab") - result |> Result.is_err +expect { + result = parse("12/ab") + result.is_err() +} # Invalid character in "12ab\n" -expect - result = parse("12ab\n") - result |> Result.is_err +expect { + result = parse("12ab\n") + result.is_err() +} # Invalid character in "12-ab" -expect - result = parse("12-ab") - result |> Result.is_err +expect { + result = parse("12-ab") + result.is_err() +} # Invalid character in "12+ab" -expect - result = parse("12+ab") - result |> Result.is_err +expect { + result = parse("12+ab") + result.is_err() +} # For numbers that don't fit in an U64, `parse` should return an error # instead of crashing -expect - result = parse("100000000000000000000000000000000") - result |> Result.is_err +expect { + result = parse("100000000000000000000000000000000") + 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/high-scores/.meta/Example.roc b/exercises/practice/high-scores/.meta/Example.roc index cb59ab58..1024f0c2 100644 --- a/exercises/practice/high-scores/.meta/Example.roc +++ b/exercises/practice/high-scores/.meta/Example.roc @@ -1,13 +1,29 @@ -module [latest, personal_best, personal_top_three] +HighScores :: {}.{ + latest : List(U64) -> Try(U64, [ListWasEmpty]) + latest = |scores| { + List.last(scores) + } -Score : U64 + personal_best : List(U64) -> Try(U64, [ListWasEmpty]) + personal_best = |scores| { + List.max(scores) + } -latest : List Score -> Result Score [ListWasEmpty] -latest = List.last + personal_top_three : List(U64) -> List(U64) + personal_top_three = |scores| { + scores->sort_desc().take_first(3) + } +} -personal_best : List Score -> Result Score [ListWasEmpty] -personal_best = List.max - -personal_top_three : List Score -> List Score -personal_top_three = |scores| - scores |> List.sort_desc |> List.take_first(3) +# The following functions should soon be available in Roc's builtins +sort_desc = |list| { + list.sort_with( + |a, b| if a < b { + GT + } else if a > b { + LT + } else { + EQ + }, + ) +} diff --git a/exercises/practice/high-scores/.meta/template.j2 b/exercises/practice/high-scores/.meta/template.j2 index 0c7b1cc5..86f7fad4 100644 --- a/exercises/practice/high-scores/.meta/template.j2 +++ b/exercises/practice/high-scores/.meta/template.j2 @@ -15,9 +15,12 @@ import {{ exercise | to_pascal }} exposing [latest, personal_best, personal_top_ {%- if case["description"] != supercase["description"] %} # {{ case["description"] }} {%- endif %} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["scores"] }}) result == {% if case["property"] != "personalTopThree" %}Ok({{ case["expected"] | to_roc }}){%- else -%}{{ case["expected"] | to_roc }}{% endif %} +} {% endfor -%} {%- endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/high-scores/HighScores.roc b/exercises/practice/high-scores/HighScores.roc index f3b4b2d4..e6753001 100644 --- a/exercises/practice/high-scores/HighScores.roc +++ b/exercises/practice/high-scores/HighScores.roc @@ -1,15 +1,16 @@ -module [latest, personal_best, personal_top_three] +HighScores :: {}.{ + latest : List(U64) -> Try(U64, _) + latest = |scores| { + crash "Please implement the 'latest' function" + } -Score : U64 + personal_best : List(U64) -> Try(U64, _) + personal_best = |scores| { + crash "Please implement the 'personal_best' function" + } -latest : List Score -> Result Score _ -latest = |scores| - crash("Please implement the 'latest' function") - -personal_best : List Score -> Result Score _ -personal_best = |scores| - crash("Please implement the 'personal_best' function") - -personal_top_three : List Score -> List Score -personal_top_three = |scores| - crash("Please implement the 'personal_top_three' function") + personal_top_three : List(U64) -> List(U64) + personal_top_three = |scores| { + crash "Please implement the 'personal_top_three' function" + } +} diff --git a/exercises/practice/high-scores/high-scores-test.roc b/exercises/practice/high-scores/high-scores-test.roc index 34630f97..c5e21867 100644 --- a/exercises/practice/high-scores/high-scores-test.roc +++ b/exercises/practice/high-scores/high-scores-test.roc @@ -1,50 +1,53 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/high-scores/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import HighScores exposing [latest, personal_best, personal_top_three] ## Latest score -expect - result = latest([100, 0, 90, 30]) - result == Ok(30) +expect { + result = latest([100, 0, 90, 30]) + result == Ok(30) +} ## Personal best -expect - result = personal_best([40, 100, 70]) - result == Ok(100) +expect { + result = personal_best([40, 100, 70]) + result == Ok(100) +} ## Top 3 scores # Personal top three from a list of scores -expect - result = personal_top_three([10, 30, 90, 30, 100, 20, 10, 0, 30, 40, 40, 70, 70]) - result == [100, 90, 70] +expect { + result = personal_top_three([10, 30, 90, 30, 100, 20, 10, 0, 30, 40, 40, 70, 70]) + result == [100, 90, 70] +} # Personal top highest to lowest -expect - result = personal_top_three([20, 10, 30]) - result == [30, 20, 10] +expect { + result = personal_top_three([20, 10, 30]) + result == [30, 20, 10] +} # Personal top when there is a tie -expect - result = personal_top_three([40, 20, 40, 30]) - result == [40, 40, 30] +expect { + result = personal_top_three([40, 20, 40, 30]) + result == [40, 40, 30] +} # Personal top when there are less than 3 -expect - result = personal_top_three([30, 70]) - result == [70, 30] +expect { + result = personal_top_three([30, 70]) + result == [70, 30] +} # Personal top when there is only one -expect - result = personal_top_three([40]) - result == [40] +expect { + result = personal_top_three([40]) + result == [40] +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/house/.meta/Example.roc b/exercises/practice/house/.meta/Example.roc index 11ec29e3..bce1cfc0 100644 --- a/exercises/practice/house/.meta/Example.roc +++ b/exercises/practice/house/.meta/Example.roc @@ -1,30 +1,33 @@ -module [recite] +House :: {}.{ + recite : U64, U64 -> Str + recite = |start_verse, end_verse| { + (start_verse..=end_verse) + .map(verse) + ->List.from_iter() + ->Str.join_with("\n") + } +} segments = [ - "house that Jack built.", - "malt that lay in", - "rat that ate", - "cat that killed", - "dog that worried", - "cow with the crumpled horn that tossed", - "maiden all forlorn that milked", - "man all tattered and torn that kissed", - "priest all shaven and shorn that married", - "rooster that crowed in the morn that woke", - "farmer sowing his corn that kept", - "horse and the hound and the horn that belonged to", + "house that Jack built.", + "malt that lay in", + "rat that ate", + "cat that killed", + "dog that worried", + "cow with the crumpled horn that tossed", + "maiden all forlorn that milked", + "man all tattered and torn that kissed", + "priest all shaven and shorn that married", + "rooster that crowed in the morn that woke", + "farmer sowing his corn that kept", + "horse and the hound and the horn that belonged to", ] -verse = |index| - blablabla = - segments - |> List.take_first(index) - |> List.reverse - |> Str.join_with(" the ") - "This is the ${blablabla}" - -recite : U64, U64 -> Str -recite = |start_verse, end_verse| - List.range({ start: At(start_verse), end: At(end_verse) }) - |> List.map(verse) - |> Str.join_with("\n") +verse = |index| { + blablabla = + segments + .take_first(index) + .rev() + ->Str.join_with(" the ") + "This is the ${blablabla}" +} diff --git a/exercises/practice/house/.meta/template.j2 b/exercises/practice/house/.meta/template.j2 index 1ad789e0..479d6242 100644 --- a/exercises/practice/house/.meta/template.j2 +++ b/exercises/practice/house/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["startVerse"] }}, {{ case["input"]["endVerse"] }}) result == {{ "\n".join(case["expected"]) | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/house/House.roc b/exercises/practice/house/House.roc index 90ae8aa3..2005c037 100644 --- a/exercises/practice/house/House.roc +++ b/exercises/practice/house/House.roc @@ -1,5 +1,6 @@ -module [recite] - -recite : U64, U64 -> Str -recite = |start_verse, end_verse| - crash("Please implement the 'recite' function") +House :: {}.{ + recite : U64, U64 -> Str + recite = |start_verse, end_verse| { + crash "Please implement the 'recite' function" + } +} diff --git a/exercises/practice/house/house-test.roc b/exercises/practice/house/house-test.roc index 5031a2cf..364edc22 100644 --- a/exercises/practice/house/house-test.roc +++ b/exercises/practice/house/house-test.roc @@ -1,84 +1,94 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/house/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import House exposing [recite] # verse one - the house that jack built -expect - result = recite(1, 1) - result == "This is the house that Jack built." +expect { + result = recite(1, 1) + result == "This is the house that Jack built." +} # verse two - the malt that lay -expect - result = recite(2, 2) - result == "This is the malt that lay in the house that Jack built." +expect { + result = recite(2, 2) + result == "This is the malt that lay in the house that Jack built." +} # verse three - the rat that ate -expect - result = recite(3, 3) - result == "This is the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(3, 3) + result == "This is the rat that ate the malt that lay in the house that Jack built." +} # verse four - the cat that killed -expect - result = recite(4, 4) - result == "This is the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(4, 4) + result == "This is the cat that killed the rat that ate the malt that lay in the house that Jack built." +} # verse five - the dog that worried -expect - result = recite(5, 5) - result == "This is the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(5, 5) + result == "This is the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +} # verse six - the cow with the crumpled horn -expect - result = recite(6, 6) - result == "This is the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(6, 6) + result == "This is the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +} # verse seven - the maiden all forlorn -expect - result = recite(7, 7) - result == "This is the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(7, 7) + result == "This is the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +} # verse eight - the man all tattered and torn -expect - result = recite(8, 8) - result == "This is the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(8, 8) + result == "This is the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +} # verse nine - the priest all shaven and shorn -expect - result = recite(9, 9) - result == "This is the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(9, 9) + result == "This is the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +} # verse 10 - the rooster that crowed in the morn -expect - result = recite(10, 10) - result == "This is the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(10, 10) + result == "This is the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +} # verse 11 - the farmer sowing his corn -expect - result = recite(11, 11) - result == "This is the farmer sowing his corn that kept the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(11, 11) + result == "This is the farmer sowing his corn that kept the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +} # verse 12 - the horse and the hound and the horn -expect - result = recite(12, 12) - result == "This is the horse and the hound and the horn that belonged to the farmer sowing his corn that kept the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(12, 12) + result == "This is the horse and the hound and the horn that belonged to the farmer sowing his corn that kept the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +} # multiple verses -expect - result = recite(4, 8) - result == "This is the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(4, 8) + result == "This is the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +} # full rhyme -expect - result = recite(1, 12) - result == "This is the house that Jack built.\nThis is the malt that lay in the house that Jack built.\nThis is the rat that ate the malt that lay in the house that Jack built.\nThis is the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the farmer sowing his corn that kept the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the horse and the hound and the horn that belonged to the farmer sowing his corn that kept the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +expect { + result = recite(1, 12) + result == "This is the house that Jack built.\nThis is the malt that lay in the house that Jack built.\nThis is the rat that ate the malt that lay in the house that Jack built.\nThis is the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the farmer sowing his corn that kept the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built.\nThis is the horse and the hound and the horn that belonged to the farmer sowing his corn that kept the rooster that crowed in the morn that woke the priest all shaven and shorn that married the man all tattered and torn that kissed the maiden all forlorn that milked the cow with the crumpled horn that tossed the dog that worried the cat that killed the rat that ate the malt that lay in the house that Jack built." +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/isbn-verifier/.meta/Example.roc b/exercises/practice/isbn-verifier/.meta/Example.roc index 802b842f..f3484493 100644 --- a/exercises/practice/isbn-verifier/.meta/Example.roc +++ b/exercises/practice/isbn-verifier/.meta/Example.roc @@ -1,27 +1,61 @@ -module [is_valid] +IsbnVerifier :: {}.{ + is_valid : Str -> Bool + is_valid = |isbn| { + chars = + isbn + .to_utf8() + .drop_if( + |char| char == '-', + ) + if chars.len() != 10 { + Bool.False + } else { + values : List(U64) + values = + chars + .map_with_index( + char_value, + ) + ->keep_oks(|v| v) + values.len() == 10 and (values.sum()) % 11 == 0 + } + } +} -char_value = |char, index| - if char == 'X' then - if index == 9 then - Ok(10) - else - Err(InvalidIsbnBadX) - else if char >= '0' and char <= '9' then - (10 - index) * (Num.int_cast((char - '0'))) |> Ok - else - Err(InvalidIsbnBadChar) +char_value : U8, U64 -> Try(U64, _) +char_value = |char, index| { + if char == 'X' { + if index == 9 { + Ok(10) + } else { + Err(InvalidIsbnBadX) + } + } else if char >= '0' and char <= '9' { + Ok((10 - index) * (char - '0').to_u64()) + } else { + Err(InvalidIsbnBadChar) + } +} -is_valid : Str -> Bool -is_valid = |isbn| - chars = - isbn - |> Str.to_utf8 - |> List.drop_if(|char| char == '-') - if List.len(chars) != 10 then - Bool.false - else - values = - chars - |> List.map_with_index(char_value) - |> List.keep_oks(|v| v) - List.len(values) == 10 and (List.sum(values)) % 11 == 0 +# The following functions should soon be available in Roc's builtins +keep_oks = |iter, func| { + iter + ->join_map( + |item| { + match func(item) { + Ok(result) => [result] + Err(_) => [] + } + }, + ) +} + +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} diff --git a/exercises/practice/isbn-verifier/.meta/template.j2 b/exercises/practice/isbn-verifier/.meta/template.j2 index 6cd86f30..bc198756 100644 --- a/exercises/practice/isbn-verifier/.meta/template.j2 +++ b/exercises/practice/isbn-verifier/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [is_valid] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["isbn"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/isbn-verifier/.meta/tests.toml b/exercises/practice/isbn-verifier/.meta/tests.toml index 6d5a8459..17e18d47 100644 --- a/exercises/practice/isbn-verifier/.meta/tests.toml +++ b/exercises/practice/isbn-verifier/.meta/tests.toml @@ -30,6 +30,12 @@ description = "invalid character in isbn is not treated as zero" [28025280-2c39-4092-9719-f3234b89c627] description = "X is only valid as a check digit" +[8005b57f-f194-44ee-88d2-a77ac4142591] +description = "only one check digit is allowed" + +[fdb14c99-4cf8-43c5-b06d-eb1638eff343] +description = "X is not substituted by the value 10" + [f6294e61-7e79-46b3-977b-f48789a4945b] description = "valid isbn without separating dashes" diff --git a/exercises/practice/isbn-verifier/IsbnVerifier.roc b/exercises/practice/isbn-verifier/IsbnVerifier.roc index 16c47251..12954087 100644 --- a/exercises/practice/isbn-verifier/IsbnVerifier.roc +++ b/exercises/practice/isbn-verifier/IsbnVerifier.roc @@ -1,5 +1,6 @@ -module [is_valid] - -is_valid : Str -> Bool -is_valid = |isbn| - crash("Please implement the 'is_valid' function") +IsbnVerifier :: {}.{ + is_valid : Str -> Bool + is_valid = |isbn| { + crash "Please implement the 'is_valid' function" + } +} diff --git a/exercises/practice/isbn-verifier/isbn-verifier-test.roc b/exercises/practice/isbn-verifier/isbn-verifier-test.roc index b7a53729..a6dff87a 100644 --- a/exercises/practice/isbn-verifier/isbn-verifier-test.roc +++ b/exercises/practice/isbn-verifier/isbn-verifier-test.roc @@ -1,109 +1,136 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/isbn-verifier/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import IsbnVerifier exposing [is_valid] # valid isbn -expect - result = is_valid("3-598-21508-8") - result == Bool.true +expect { + result = is_valid("3-598-21508-8") + result == Bool.True +} # invalid isbn check digit -expect - result = is_valid("3-598-21508-9") - result == Bool.false +expect { + result = is_valid("3-598-21508-9") + result == Bool.False +} # valid isbn with a check digit of 10 -expect - result = is_valid("3-598-21507-X") - result == Bool.true +expect { + result = is_valid("3-598-21507-X") + result == Bool.True +} # check digit is a character other than X -expect - result = is_valid("3-598-21507-A") - result == Bool.false +expect { + result = is_valid("3-598-21507-A") + result == Bool.False +} # invalid check digit in isbn is not treated as zero -expect - result = is_valid("4-598-21507-B") - result == Bool.false +expect { + result = is_valid("4-598-21507-B") + result == Bool.False +} # invalid character in isbn is not treated as zero -expect - result = is_valid("3-598-P1581-X") - result == Bool.false +expect { + result = is_valid("3-598-P1581-X") + result == Bool.False +} # X is only valid as a check digit -expect - result = is_valid("3-598-2X507-9") - result == Bool.false +expect { + result = is_valid("3-598-2X507-9") + result == Bool.False +} + +# only one check digit is allowed +expect { + result = is_valid("3-598-21508-96") + result == Bool.False +} + +# X is not substituted by the value 10 +expect { + result = is_valid("3-598-2X507-5") + result == Bool.False +} # valid isbn without separating dashes -expect - result = is_valid("3598215088") - result == Bool.true +expect { + result = is_valid("3598215088") + result == Bool.True +} # isbn without separating dashes and X as check digit -expect - result = is_valid("359821507X") - result == Bool.true +expect { + result = is_valid("359821507X") + result == Bool.True +} # isbn without check digit and dashes -expect - result = is_valid("359821507") - result == Bool.false +expect { + result = is_valid("359821507") + result == Bool.False +} # too long isbn and no dashes -expect - result = is_valid("3598215078X") - result == Bool.false +expect { + result = is_valid("3598215078X") + result == Bool.False +} # too short isbn -expect - result = is_valid("00") - result == Bool.false +expect { + result = is_valid("00") + result == Bool.False +} # isbn without check digit -expect - result = is_valid("3-598-21507") - result == Bool.false +expect { + result = is_valid("3-598-21507") + result == Bool.False +} # check digit of X should not be used for 0 -expect - result = is_valid("3-598-21515-X") - result == Bool.false +expect { + result = is_valid("3-598-21515-X") + result == Bool.False +} # empty isbn -expect - result = is_valid("") - result == Bool.false +expect { + result = is_valid("") + result == Bool.False +} # input is 9 characters -expect - result = is_valid("134456729") - result == Bool.false +expect { + result = is_valid("134456729") + result == Bool.False +} # invalid characters are not ignored after checking length -expect - result = is_valid("3132P34035") - result == Bool.false +expect { + result = is_valid("3132P34035") + result == Bool.False +} # invalid characters are not ignored before checking length -expect - result = is_valid("3598P215088") - result == Bool.false +expect { + result = is_valid("3598P215088") + result == Bool.False +} # input is too long but contains a valid isbn -expect - result = is_valid("98245726788") - result == Bool.false +expect { + result = is_valid("98245726788") + result == Bool.False +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/isogram/.meta/Example.roc b/exercises/practice/isogram/.meta/Example.roc index e8858127..437ea04d 100644 --- a/exercises/practice/isogram/.meta/Example.roc +++ b/exercises/practice/isogram/.meta/Example.roc @@ -1,12 +1,22 @@ -module [is_isogram] - -is_isogram : Str -> Bool -is_isogram = |phrase| - chars = - phrase - |> Str.to_utf8 - |> List.drop_if(|c| c == ' ' or c == '-') - |> List.map(|c| if c >= 'a' and c <= 'z' then c + 'A' - 'a' else c) # to uppercase - - (List.len(chars)) == Set.len(Set.from_list(chars)) +Isogram :: {}.{ + is_isogram : Str -> Bool + is_isogram = |phrase| { + chars = + phrase + .to_utf8() + .drop_if( + |c| c == ' ' or c == '-', + ) + .map( + |c| { + if c >= 'a' and c <= 'z' { + c + 'A' - 'a' + } else { + c + } + }, + ) # to uppercase + chars.len() == chars->Set.from_list().len() + } +} diff --git a/exercises/practice/isogram/.meta/template.j2 b/exercises/practice/isogram/.meta/template.j2 index 86dc2d01..e88e90c8 100644 --- a/exercises/practice/isogram/.meta/template.j2 +++ b/exercises/practice/isogram/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [is_isogram] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["phrase"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/isogram/Isogram.roc b/exercises/practice/isogram/Isogram.roc index fad2b384..e7f5e86f 100644 --- a/exercises/practice/isogram/Isogram.roc +++ b/exercises/practice/isogram/Isogram.roc @@ -1,5 +1,6 @@ -module [is_isogram] - -is_isogram : Str -> Bool -is_isogram = |phrase| - crash("Please implement the 'is_isogram' function") +Isogram :: {}.{ + is_isogram : Str -> Bool + is_isogram = |phrase| { + crash "Please implement the 'is_isogram' function" + } +} diff --git a/exercises/practice/isogram/isogram-test.roc b/exercises/practice/isogram/isogram-test.roc index 9e71ff4b..7c1710ed 100644 --- a/exercises/practice/isogram/isogram-test.roc +++ b/exercises/practice/isogram/isogram-test.roc @@ -1,84 +1,94 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/isogram/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Isogram exposing [is_isogram] # empty string -expect - result = is_isogram("") - result == Bool.true +expect { + result = is_isogram("") + result == Bool.True +} # isogram with only lower case characters -expect - result = is_isogram("isogram") - result == Bool.true +expect { + result = is_isogram("isogram") + result == Bool.True +} # word with one duplicated character -expect - result = is_isogram("eleven") - result == Bool.false +expect { + result = is_isogram("eleven") + result == Bool.False +} # word with one duplicated character from the end of the alphabet -expect - result = is_isogram("zzyzx") - result == Bool.false +expect { + result = is_isogram("zzyzx") + result == Bool.False +} # longest reported english isogram -expect - result = is_isogram("subdermatoglyphic") - result == Bool.true +expect { + result = is_isogram("subdermatoglyphic") + result == Bool.True +} # word with duplicated character in mixed case -expect - result = is_isogram("Alphabet") - result == Bool.false +expect { + result = is_isogram("Alphabet") + result == Bool.False +} # word with duplicated character in mixed case, lowercase first -expect - result = is_isogram("alphAbet") - result == Bool.false +expect { + result = is_isogram("alphAbet") + result == Bool.False +} # hypothetical isogrammic word with hyphen -expect - result = is_isogram("thumbscrew-japingly") - result == Bool.true +expect { + result = is_isogram("thumbscrew-japingly") + result == Bool.True +} # hypothetical word with duplicated character following hyphen -expect - result = is_isogram("thumbscrew-jappingly") - result == Bool.false +expect { + result = is_isogram("thumbscrew-jappingly") + result == Bool.False +} # isogram with duplicated hyphen -expect - result = is_isogram("six-year-old") - result == Bool.true +expect { + result = is_isogram("six-year-old") + result == Bool.True +} # made-up name that is an isogram -expect - result = is_isogram("Emily Jung Schwartzkopf") - result == Bool.true +expect { + result = is_isogram("Emily Jung Schwartzkopf") + result == Bool.True +} # duplicated character in the middle -expect - result = is_isogram("accentor") - result == Bool.false +expect { + result = is_isogram("accentor") + result == Bool.False +} # same first and last characters -expect - result = is_isogram("angola") - result == Bool.false +expect { + result = is_isogram("angola") + result == Bool.False +} # word with duplicated character and with two hyphens -expect - result = is_isogram("up-to-date") - result == Bool.false +expect { + result = is_isogram("up-to-date") + result == Bool.False +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/killer-sudoku-helper/.meta/Example.roc b/exercises/practice/killer-sudoku-helper/.meta/Example.roc index 305cb429..82465b4e 100644 --- a/exercises/practice/killer-sudoku-helper/.meta/Example.roc +++ b/exercises/practice/killer-sudoku-helper/.meta/Example.roc @@ -1,26 +1,46 @@ -module [combinations] +KillerSudokuHelper :: {}.{ + Combination : List(U8) -Combination : List U8 - -combinations : { sum : U8, size : U8, exclude ?? List U8 } -> List Combination -combinations = |{ sum, size, exclude ?? [] }| - help = |target, digits| - if target == 0 then - [[]] - else - when digits is - [] -> [] - [single] -> if single == target then [[single]] else [] - [first, .. as rest] -> - if first > target then - [] - else - help((target - first), rest) - |> List.map(|combi| combi |> List.append(first)) - |> List.concat(help(target, rest)) - available_digits = - [1, 2, 3, 4, 5, 6, 7, 8, 9] - |> List.drop_if(|digit| exclude |> List.contains(digit)) - help(sum, available_digits) - |> List.keep_if(|combi| List.len(combi) == size |> Num.to_u64) - |> List.map(List.reverse) + combinations : { sum : U8, size : U8, exclude : List(U8) } -> List(Combination) + combinations = |{ sum, size, exclude }| { + help = |target, digits| { + if target == 0 { + [[]] + } else { + match digits { + [] => [] + [single] => { + if single == target { + [[single]] + } else { + [] + } + } + [first, .. as rest] => { + if first > target { + [] + } else { + help((target - first), rest) + .map( + |combi| combi.append(first), + ) + .concat( + help(target, rest), + ) + } + } + } + } + } + available_digits = + [1, 2, 3, 4, 5, 6, 7, 8, 9] + .drop_if( + |digit| exclude.contains(digit), + ) + help(sum, available_digits) + .keep_if( + |combi| combi.len() == size.to_u64(), + ) + .map(List.rev) + } +} diff --git a/exercises/practice/killer-sudoku-helper/.meta/template.j2 b/exercises/practice/killer-sudoku-helper/.meta/template.j2 index 8d4e33e3..068045d8 100644 --- a/exercises/practice/killer-sudoku-helper/.meta/template.j2 +++ b/exercises/practice/killer-sudoku-helper/.meta/template.j2 @@ -5,10 +5,7 @@ import {{ exercise | to_pascal }} exposing [combinations] {% macro to_cage(cage) -%} -{ sum: {{ cage["sum"] }}, size: {{ cage["size"] }} -{%- if cage["exclude"] != [] -%} -, exclude: {{ cage["exclude"] | to_roc }} -{%- endif %} } +{ sum: {{ cage["sum"] }}, size: {{ cage["size"] }}, exclude: {{ cage["exclude"] | to_roc }} } {%- endmacro %} {% for supercase in cases %} @@ -22,10 +19,12 @@ import {{ exercise | to_pascal }} exposing [combinations] {%- if case["description"] != supercase["description"] %} # {{ case["description"] }} {%- endif %} -expect +expect { result = {{ case["property"] }}({{ to_cage(case["input"]["cage"]) }}) result == {{ case["expected"] | to_roc }} +} {% endfor -%} {%- endfor %} +{{ macros.footer() }} diff --git a/exercises/practice/killer-sudoku-helper/KillerSudokuHelper.roc b/exercises/practice/killer-sudoku-helper/KillerSudokuHelper.roc index 107376d0..be7f7c12 100644 --- a/exercises/practice/killer-sudoku-helper/KillerSudokuHelper.roc +++ b/exercises/practice/killer-sudoku-helper/KillerSudokuHelper.roc @@ -1,7 +1,8 @@ -module [combinations] +KillerSudokuHelper :: {}.{ + Combination : List(U8) -Combination : List U8 - -combinations : { sum : U8, size : U8, exclude ?? List U8 } -> List Combination -combinations = |{ sum, size, exclude ?? [] }| - crash("Please implement the 'combinations' function") + combinations : { sum : U8, size : U8, exclude : List(U8) } -> List(Combination) + combinations = |{ sum, size, exclude }| { + crash "Please implement the 'combinations' function" + } +} diff --git a/exercises/practice/killer-sudoku-helper/killer-sudoku-helper-test.roc b/exercises/practice/killer-sudoku-helper/killer-sudoku-helper-test.roc index e4f175d3..100c4b65 100644 --- a/exercises/practice/killer-sudoku-helper/killer-sudoku-helper-test.roc +++ b/exercises/practice/killer-sudoku-helper/killer-sudoku-helper-test.roc @@ -1,80 +1,89 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/killer-sudoku-helper/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import KillerSudokuHelper exposing [combinations] ## Trivial 1-digit cages # 1 -expect - result = combinations({ sum: 1, size: 1 }) - result == [[1]] +expect { + result = combinations({ sum: 1, size: 1, exclude: [] }) + result == [[1]] +} # 2 -expect - result = combinations({ sum: 2, size: 1 }) - result == [[2]] +expect { + result = combinations({ sum: 2, size: 1, exclude: [] }) + result == [[2]] +} # 3 -expect - result = combinations({ sum: 3, size: 1 }) - result == [[3]] +expect { + result = combinations({ sum: 3, size: 1, exclude: [] }) + result == [[3]] +} # 4 -expect - result = combinations({ sum: 4, size: 1 }) - result == [[4]] +expect { + result = combinations({ sum: 4, size: 1, exclude: [] }) + result == [[4]] +} # 5 -expect - result = combinations({ sum: 5, size: 1 }) - result == [[5]] +expect { + result = combinations({ sum: 5, size: 1, exclude: [] }) + result == [[5]] +} # 6 -expect - result = combinations({ sum: 6, size: 1 }) - result == [[6]] +expect { + result = combinations({ sum: 6, size: 1, exclude: [] }) + result == [[6]] +} # 7 -expect - result = combinations({ sum: 7, size: 1 }) - result == [[7]] +expect { + result = combinations({ sum: 7, size: 1, exclude: [] }) + result == [[7]] +} # 8 -expect - result = combinations({ sum: 8, size: 1 }) - result == [[8]] +expect { + result = combinations({ sum: 8, size: 1, exclude: [] }) + result == [[8]] +} # 9 -expect - result = combinations({ sum: 9, size: 1 }) - result == [[9]] +expect { + result = combinations({ sum: 9, size: 1, exclude: [] }) + result == [[9]] +} ## Cage with sum 45 contains all digits 1:9 -expect - result = combinations({ sum: 45, size: 9 }) - result == [[1, 2, 3, 4, 5, 6, 7, 8, 9]] +expect { + result = combinations({ sum: 45, size: 9, exclude: [] }) + result == [[1, 2, 3, 4, 5, 6, 7, 8, 9]] +} ## Cage with only 1 possible combination -expect - result = combinations({ sum: 7, size: 3 }) - result == [[1, 2, 4]] +expect { + result = combinations({ sum: 7, size: 3, exclude: [] }) + result == [[1, 2, 4]] +} ## Cage with several combinations -expect - result = combinations({ sum: 10, size: 2 }) - result == [[1, 9], [2, 8], [3, 7], [4, 6]] +expect { + result = combinations({ sum: 10, size: 2, exclude: [] }) + result == [[1, 9], [2, 8], [3, 7], [4, 6]] +} ## Cage with several combinations that is restricted -expect - result = combinations({ sum: 10, size: 2, exclude: [1, 4] }) - result == [[2, 8], [3, 7]] +expect { + result = combinations({ sum: 10, size: 2, exclude: [1, 4] }) + result == [[2, 8], [3, 7]] +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/kindergarten-garden/.meta/Example.roc b/exercises/practice/kindergarten-garden/.meta/Example.roc index 4a2d6e4b..a8d5ce32 100644 --- a/exercises/practice/kindergarten-garden/.meta/Example.roc +++ b/exercises/practice/kindergarten-garden/.meta/Example.roc @@ -1,36 +1,49 @@ -module [plants] +KindergartenGarden :: {}.{ + Student := [Alice, Bob, Charlie, David, Eve, Fred, Ginny, Harriet, Ileana, Joseph, Kincaid, Larry] + Plant := [Grass, Clover, Radishes, Violets] -Student : [Alice, Bob, Charlie, David, Eve, Fred, Ginny, Harriet, Ileana, Joseph, Kincaid, Larry] -Plant : [Grass, Clover, Radishes, Violets] + plants : Str, Student -> Try(List(Plant), [UnknownPlant(U8), OutOfBounds]) + plants = |diagram, student| { + start_index = 2 * student_index(student) + grid = diagram.to_utf8().split_on('\n') + [(0, 0), (0, 1), (1, 0), (1, 1)] + ->map_try( + |(row, column)| { + plant = grid.get(row)?.get(start_index + column)? + match plant { + 'G' => Ok(Grass) + 'C' => Ok(Clover) + 'R' => Ok(Radishes) + 'V' => Ok(Violets) + _ => Err(UnknownPlant(plant)) + } + }, + ) + } +} student_index : Student -> U64 -student_index = |student| - when student is - Alice -> 0 - Bob -> 1 - Charlie -> 2 - David -> 3 - Eve -> 4 - Fred -> 5 - Ginny -> 6 - Harriet -> 7 - Ileana -> 8 - Joseph -> 9 - Kincaid -> 10 - Larry -> 11 +student_index = |student| { + match student { + Alice => 0 + Bob => 1 + Charlie => 2 + David => 3 + Eve => 4 + Fred => 5 + Ginny => 6 + Harriet => 7 + Ileana => 8 + Joseph => 9 + Kincaid => 10 + Larry => 11 + } +} -plants : Str, Student -> Result (List Plant) _ -plants = |diagram, student| - start_index = 2 * student_index(student) - grid = diagram |> Str.to_utf8 |> List.split_on('\n') - [(0, 0), (0, 1), (1, 0), (1, 1)] - |> List.map_try( - |(row, column)| - plant = grid |> List.get(row)? |> List.get(start_index + column)? - when plant is - 'G' -> Ok(Grass) - 'C' -> Ok(Clover) - 'R' -> Ok(Radishes) - 'V' -> Ok(Violets) - _ -> Err(UnknownPlant(plant)), - ) +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/kindergarten-garden/.meta/template.j2 b/exercises/practice/kindergarten-garden/.meta/template.j2 index 2a2355d8..eb1bbad8 100644 --- a/exercises/practice/kindergarten-garden/.meta/template.j2 +++ b/exercises/practice/kindergarten-garden/.meta/template.j2 @@ -19,12 +19,15 @@ import {{ exercise | to_pascal }} exposing [plants] {% for subcase in subcases -%} # {{ subcase["description"] }} -expect +expect { diagram = {{ subcase["input"]["diagram"] | to_roc_multiline_string | indent(8) }} - result = diagram |> {{ subcase["property"] | to_snake }}({{ subcase["input"]["student"] | to_pascal }}) + result = diagram->{{ subcase["property"] | to_snake }}({{ subcase["input"]["student"] | to_pascal }}) result == Ok([{% for plant in subcase["expected"] %}{{ plant | to_pascal }}, {% endfor %}]) +} {% endfor %} {% endfor %} -{% endfor %} \ No newline at end of file +{% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/kindergarten-garden/KindergartenGarden.roc b/exercises/practice/kindergarten-garden/KindergartenGarden.roc index 0fe4c6bc..a0b2deab 100644 --- a/exercises/practice/kindergarten-garden/KindergartenGarden.roc +++ b/exercises/practice/kindergarten-garden/KindergartenGarden.roc @@ -1,8 +1,9 @@ -module [plants] +KindergartenGarden :: {}.{ + Student := [Alice, Bob, Charlie, David, Eve, Fred, Ginny, Harriet, Ileana, Joseph, Kincaid, Larry] + Plant := [Grass, Clover, Radishes, Violets] -Student : [Alice, Bob, Charlie, David, Eve, Fred, Ginny, Harriet, Ileana, Joseph, Kincaid, Larry] -Plant : [Grass, Clover, Radishes, Violets] - -plants : Str, Student -> Result (List Plant) _ -plants = |diagram, student| - crash("Please implement the 'plants' function") + plants : Str, Student -> Try(List(Plant), _) + plants = |diagram, student| { + crash "Please implement the 'plants' function" + } +} diff --git a/exercises/practice/kindergarten-garden/kindergarten-garden-test.roc b/exercises/practice/kindergarten-garden/kindergarten-garden-test.roc index 33a0f2bf..d190ae54 100644 --- a/exercises/practice/kindergarten-garden/kindergarten-garden-test.roc +++ b/exercises/practice/kindergarten-garden/kindergarten-garden-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/kindergarten-garden/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import KindergartenGarden exposing [plants] @@ -17,178 +9,301 @@ import KindergartenGarden exposing [plants] ### # garden with single student -expect - diagram = - """ - RC - GG - """ - result = diagram |> plants(Alice) - result == Ok([Radishes, Clover, Grass, Grass]) +expect { + diagram = + \\RC + \\GG + + result = diagram->plants(Alice) + result == Ok( + [ + Radishes, + Clover, + Grass, + Grass, + ], + ) +} # different garden with single student -expect - diagram = - """ - VC - RC - """ - result = diagram |> plants(Alice) - result == Ok([Violets, Clover, Radishes, Clover]) +expect { + diagram = + \\VC + \\RC + + result = diagram->plants(Alice) + result == Ok( + [ + Violets, + Clover, + Radishes, + Clover, + ], + ) +} # garden with two students -expect - diagram = - """ - VVCG - VVRC - """ - result = diagram |> plants(Bob) - result == Ok([Clover, Grass, Radishes, Clover]) +expect { + diagram = + \\VVCG + \\VVRC + + result = diagram->plants(Bob) + result == Ok( + [ + Clover, + Grass, + Radishes, + Clover, + ], + ) +} ## multiple students for the same garden with three students # second student's garden -expect - diagram = - """ - VVCCGG - VVCCGG - """ - result = diagram |> plants(Bob) - result == Ok([Clover, Clover, Clover, Clover]) +expect { + diagram = + \\VVCCGG + \\VVCCGG + + result = diagram->plants(Bob) + result == Ok( + [ + Clover, + Clover, + Clover, + Clover, + ], + ) +} # third student's garden -expect - diagram = - """ - VVCCGG - VVCCGG - """ - result = diagram |> plants(Charlie) - result == Ok([Grass, Grass, Grass, Grass]) +expect { + diagram = + \\VVCCGG + \\VVCCGG + + result = diagram->plants(Charlie) + result == Ok( + [ + Grass, + Grass, + Grass, + Grass, + ], + ) +} ### ### full garden ### # for Alice, first student's garden -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Alice) - result == Ok([Violets, Radishes, Violets, Radishes]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Alice) + result == Ok( + [ + Violets, + Radishes, + Violets, + Radishes, + ], + ) +} # for Bob, second student's garden -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Bob) - result == Ok([Clover, Grass, Clover, Clover]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Bob) + result == Ok( + [ + Clover, + Grass, + Clover, + Clover, + ], + ) +} # for Charlie -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Charlie) - result == Ok([Violets, Violets, Clover, Grass]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Charlie) + result == Ok( + [ + Violets, + Violets, + Clover, + Grass, + ], + ) +} # for David -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(David) - result == Ok([Radishes, Violets, Clover, Radishes]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(David) + result == Ok( + [ + Radishes, + Violets, + Clover, + Radishes, + ], + ) +} # for Eve -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Eve) - result == Ok([Clover, Grass, Radishes, Grass]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Eve) + result == Ok( + [ + Clover, + Grass, + Radishes, + Grass, + ], + ) +} # for Fred -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Fred) - result == Ok([Grass, Clover, Violets, Clover]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Fred) + result == Ok( + [ + Grass, + Clover, + Violets, + Clover, + ], + ) +} # for Ginny -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Ginny) - result == Ok([Clover, Grass, Grass, Clover]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Ginny) + result == Ok( + [ + Clover, + Grass, + Grass, + Clover, + ], + ) +} # for Harriet -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Harriet) - result == Ok([Violets, Radishes, Radishes, Violets]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Harriet) + result == Ok( + [ + Violets, + Radishes, + Radishes, + Violets, + ], + ) +} # for Ileana -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Ileana) - result == Ok([Grass, Clover, Violets, Clover]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Ileana) + result == Ok( + [ + Grass, + Clover, + Violets, + Clover, + ], + ) +} # for Joseph -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Joseph) - result == Ok([Violets, Clover, Violets, Grass]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Joseph) + result == Ok( + [ + Violets, + Clover, + Violets, + Grass, + ], + ) +} # for Kincaid, second to last student's garden -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Kincaid) - result == Ok([Grass, Clover, Clover, Grass]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Kincaid) + result == Ok( + [ + Grass, + Clover, + Clover, + Grass, + ], + ) +} # for Larry, last student's garden -expect - diagram = - """ - VRCGVVRVCGGCCGVRGCVCGCGV - VRCCCGCRRGVCGCRVVCVGCGCV - """ - result = diagram |> plants(Larry) - result == Ok([Grass, Violets, Clover, Violets]) +expect { + diagram = + \\VRCGVVRVCGGCCGVRGCVCGCGV + \\VRCCCGCRRGVCGCRVVCVGCGCV + + result = diagram->plants(Larry) + result == Ok( + [ + Grass, + Violets, + Clover, + Violets, + ], + ) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/knapsack/.meta/Example.roc b/exercises/practice/knapsack/.meta/Example.roc index d981c8dc..e8abb191 100644 --- a/exercises/practice/knapsack/.meta/Example.roc +++ b/exercises/practice/knapsack/.meta/Example.roc @@ -1,15 +1,20 @@ -module [maximum_value] +Knapsack :: {}.{ + Item : { weight : U64, value : U64 } -Item : { weight : U64, value : U64 } + maximum_value : { items : List(Item), maximum_weight : U64 } -> U64 + maximum_value = |{ items, maximum_weight }| { + match items { + [] => 0 + [item, .. as rest] => { + max_value_without_item = maximum_value({ items: rest, maximum_weight }) + if item.weight > maximum_weight { + max_value_without_item + } else { + max_value_with_item = item.value + maximum_value({ items: rest, maximum_weight: maximum_weight - item.weight }) + U64.max(max_value_without_item, max_value_with_item) + } + } + } + } +} -maximum_value : { items : List Item, maximum_weight : U64 } -> U64 -maximum_value = |{ items, maximum_weight }| - when items is - [] -> 0 - [item, .. as rest] -> - max_value_without_item = maximum_value({ items: rest, maximum_weight }) - if item.weight > maximum_weight then - max_value_without_item - else - max_value_with_item = item.value + maximum_value({ items: rest, maximum_weight: maximum_weight - item.weight }) - Num.max(max_value_without_item, max_value_with_item) diff --git a/exercises/practice/knapsack/.meta/template.j2 b/exercises/practice/knapsack/.meta/template.j2 index a7a2d839..a8df60f7 100644 --- a/exercises/practice/knapsack/.meta/template.j2 +++ b/exercises/practice/knapsack/.meta/template.j2 @@ -6,7 +6,7 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { {%- if case["input"]["items"] == [] %} items = [] {%- else %} @@ -18,5 +18,8 @@ expect {%- endif %} result = {{ case["property"] | to_snake }}({ items, maximum_weight: {{ case["input"]["maximumWeight"] }} }) result == {{ case["expected"] | to_roc }} +} -{% endfor %} \ No newline at end of file +{% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/knapsack/Knapsack.roc b/exercises/practice/knapsack/Knapsack.roc index 07aa9504..640bfd33 100644 --- a/exercises/practice/knapsack/Knapsack.roc +++ b/exercises/practice/knapsack/Knapsack.roc @@ -1,7 +1,9 @@ -module [maximum_value] +Knapsack :: {}.{ + Item : { weight : U64, value : U64 } -Item : { weight : U64, value : U64 } + maximum_value : { items : List(Item), maximum_weight : U64 } -> U64 + maximum_value = |{ items, maximum_weight }| { + crash "Please implement the 'maximum_value' function" + } +} -maximum_value : { items : List Item, maximum_weight : U64 } -> U64 -maximum_value = |{ items, maximum_weight }| - crash("Please implement the 'maximum_value' function") diff --git a/exercises/practice/knapsack/knapsack-test.roc b/exercises/practice/knapsack/knapsack-test.roc index 4b77d6f3..1c7ae31a 100644 --- a/exercises/practice/knapsack/knapsack-test.roc +++ b/exercises/practice/knapsack/knapsack-test.roc @@ -1,100 +1,103 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/knapsack/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Knapsack exposing [maximum_value] # no items -expect - items = [] - result = maximum_value({ items, maximum_weight: 100 }) - result == 0 +expect { + items = [] + result = maximum_value({ items, maximum_weight: 100 }) + result == 0 +} # one item, too heavy -expect - items = [ - { weight: 100, value: 1 }, - ] - result = maximum_value({ items, maximum_weight: 10 }) - result == 0 +expect { + items = [ + { weight: 100, value: 1 }, + ] + result = maximum_value({ items, maximum_weight: 10 }) + result == 0 +} # five items (cannot be greedy by weight) -expect - items = [ - { weight: 2, value: 5 }, - { weight: 2, value: 5 }, - { weight: 2, value: 5 }, - { weight: 2, value: 5 }, - { weight: 10, value: 21 }, - ] - result = maximum_value({ items, maximum_weight: 10 }) - result == 21 +expect { + items = [ + { weight: 2, value: 5 }, + { weight: 2, value: 5 }, + { weight: 2, value: 5 }, + { weight: 2, value: 5 }, + { weight: 10, value: 21 }, + ] + result = maximum_value({ items, maximum_weight: 10 }) + result == 21 +} # five items (cannot be greedy by value) -expect - items = [ - { weight: 2, value: 20 }, - { weight: 2, value: 20 }, - { weight: 2, value: 20 }, - { weight: 2, value: 20 }, - { weight: 10, value: 50 }, - ] - result = maximum_value({ items, maximum_weight: 10 }) - result == 80 +expect { + items = [ + { weight: 2, value: 20 }, + { weight: 2, value: 20 }, + { weight: 2, value: 20 }, + { weight: 2, value: 20 }, + { weight: 10, value: 50 }, + ] + result = maximum_value({ items, maximum_weight: 10 }) + result == 80 +} # example knapsack -expect - items = [ - { weight: 5, value: 10 }, - { weight: 4, value: 40 }, - { weight: 6, value: 30 }, - { weight: 4, value: 50 }, - ] - result = maximum_value({ items, maximum_weight: 10 }) - result == 90 +expect { + items = [ + { weight: 5, value: 10 }, + { weight: 4, value: 40 }, + { weight: 6, value: 30 }, + { weight: 4, value: 50 }, + ] + result = maximum_value({ items, maximum_weight: 10 }) + result == 90 +} # 8 items -expect - items = [ - { weight: 25, value: 350 }, - { weight: 35, value: 400 }, - { weight: 45, value: 450 }, - { weight: 5, value: 20 }, - { weight: 25, value: 70 }, - { weight: 3, value: 8 }, - { weight: 2, value: 5 }, - { weight: 2, value: 5 }, - ] - result = maximum_value({ items, maximum_weight: 104 }) - result == 900 +expect { + items = [ + { weight: 25, value: 350 }, + { weight: 35, value: 400 }, + { weight: 45, value: 450 }, + { weight: 5, value: 20 }, + { weight: 25, value: 70 }, + { weight: 3, value: 8 }, + { weight: 2, value: 5 }, + { weight: 2, value: 5 }, + ] + result = maximum_value({ items, maximum_weight: 104 }) + result == 900 +} # 15 items -expect - items = [ - { weight: 70, value: 135 }, - { weight: 73, value: 139 }, - { weight: 77, value: 149 }, - { weight: 80, value: 150 }, - { weight: 82, value: 156 }, - { weight: 87, value: 163 }, - { weight: 90, value: 173 }, - { weight: 94, value: 184 }, - { weight: 98, value: 192 }, - { weight: 106, value: 201 }, - { weight: 110, value: 210 }, - { weight: 113, value: 214 }, - { weight: 115, value: 221 }, - { weight: 118, value: 229 }, - { weight: 120, value: 240 }, - ] - result = maximum_value({ items, maximum_weight: 750 }) - result == 1458 +expect { + items = [ + { weight: 70, value: 135 }, + { weight: 73, value: 139 }, + { weight: 77, value: 149 }, + { weight: 80, value: 150 }, + { weight: 82, value: 156 }, + { weight: 87, value: 163 }, + { weight: 90, value: 173 }, + { weight: 94, value: 184 }, + { weight: 98, value: 192 }, + { weight: 106, value: 201 }, + { weight: 110, value: 210 }, + { weight: 113, value: 214 }, + { weight: 115, value: 221 }, + { weight: 118, value: 229 }, + { weight: 120, value: 240 }, + ] + result = maximum_value({ items, maximum_weight: 750 }) + result == 1458 +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/largest-series-product/.meta/Example.roc b/exercises/practice/largest-series-product/.meta/Example.roc index 2930e049..7e115041 100644 --- a/exercises/practice/largest-series-product/.meta/Example.roc +++ b/exercises/practice/largest-series-product/.meta/Example.roc @@ -1,27 +1,38 @@ -module [largest_product] - -largest_product : Str, U64 -> Result U64 [SpanWasTooLarge, InvalidDigit] -largest_product = |digits, span| - if span == 0 then - Ok(1) - else - chars = digits |> Str.to_utf8 - if List.len(chars) < span then - Err(SpanWasTooLarge) - else if chars |> List.any(|char| char < '0' or char > '9') then - Err(InvalidDigit) - else - List.range({ start: At(0), end: At((List.len(chars) - span)) }) - |> List.map( - |start_index| - chars - |> List.sublist({ start: start_index, len: span }) - |> List.walk( - 1, - |product, char| - product * (char - '0' |> Num.to_u64), - ), - ) - |> List.max - |> Result.on_err(|ListWasEmpty| crash("Unreachable: the list cannot be empty here")) - +LargestSeriesProduct :: {}.{ + largest_product : Str, U64 -> Try(U64, [SpanWasTooLarge, InvalidDigit]) + largest_product = |digits, span| { + if span == 0 { + Ok(1) + } else { + chars = digits.to_utf8() + if chars.len() < span { + Err(SpanWasTooLarge) + } else if chars.any(|char| char < '0' or char > '9') { + Err(InvalidDigit) + } else { + (0..=(chars.len() - span)) + .map( + |start_index| { + chars + .sublist( + { start: start_index, len: span }, + ) + .fold( + 1, + |product, char| { + product * (char - '0').to_u64() + }, + ) + }, + ) + ->List.from_iter() + .max() # TODO: replace with Iter.max when available + .map_err( + |_| { + crash "Unreachable: the list cannot be empty here" + }, + ) + } + } + } +} diff --git a/exercises/practice/largest-series-product/.meta/template.j2 b/exercises/practice/largest-series-product/.meta/template.j2 index c0cf1475..dec6c997 100644 --- a/exercises/practice/largest-series-product/.meta/template.j2 +++ b/exercises/practice/largest-series-product/.meta/template.j2 @@ -6,14 +6,17 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { digits = {{ case["input"]["digits"] | to_roc }} - result = digits |> {{ case["property"] | to_snake }}({{ case["input"]["span"] | to_roc }}) + result = digits -> {{ case["property"] | to_snake }}({{ case["input"]["span"] | to_roc }}) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} expected = Ok({{ case["expected"] | to_roc }}) result == expected {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/largest-series-product/LargestSeriesProduct.roc b/exercises/practice/largest-series-product/LargestSeriesProduct.roc index a2721951..a77afb7e 100644 --- a/exercises/practice/largest-series-product/LargestSeriesProduct.roc +++ b/exercises/practice/largest-series-product/LargestSeriesProduct.roc @@ -1,5 +1,6 @@ -module [largest_product] - -largest_product : Str, U64 -> Result U64 _ -largest_product = |digits, span| - crash("Please implement the 'largest_product' function") +LargestSeriesProduct :: {}.{ + largest_product : Str, U64 -> Try(U64, _) + largest_product = |digits, span| { + crash "Please implement the 'largest_product' function" + } +} diff --git a/exercises/practice/largest-series-product/largest-series-product-test.roc b/exercises/practice/largest-series-product/largest-series-product-test.roc index 2e5e1d70..d77b948d 100644 --- a/exercises/practice/largest-series-product/largest-series-product-test.roc +++ b/exercises/practice/largest-series-product/largest-series-product-test.roc @@ -1,109 +1,119 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/largest-series-product/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import LargestSeriesProduct exposing [largest_product] # finds the largest product if span equals length -expect - digits = "29" - result = digits |> largest_product(2) - expected = Ok(18) - result == expected +expect { + digits = "29" + result = digits->largest_product(2) + expected = Ok(18) + result == expected +} # can find the largest product of 2 with numbers in order -expect - digits = "0123456789" - result = digits |> largest_product(2) - expected = Ok(72) - result == expected +expect { + digits = "0123456789" + result = digits->largest_product(2) + expected = Ok(72) + result == expected +} # can find the largest product of 2 -expect - digits = "576802143" - result = digits |> largest_product(2) - expected = Ok(48) - result == expected +expect { + digits = "576802143" + result = digits->largest_product(2) + expected = Ok(48) + result == expected +} # can find the largest product of 3 with numbers in order -expect - digits = "0123456789" - result = digits |> largest_product(3) - expected = Ok(504) - result == expected +expect { + digits = "0123456789" + result = digits->largest_product(3) + expected = Ok(504) + result == expected +} # can find the largest product of 3 -expect - digits = "1027839564" - result = digits |> largest_product(3) - expected = Ok(270) - result == expected +expect { + digits = "1027839564" + result = digits->largest_product(3) + expected = Ok(270) + result == expected +} # can find the largest product of 5 with numbers in order -expect - digits = "0123456789" - result = digits |> largest_product(5) - expected = Ok(15120) - result == expected +expect { + digits = "0123456789" + result = digits->largest_product(5) + expected = Ok(15120) + result == expected +} # can get the largest product of a big number -expect - digits = "73167176531330624919225119674426574742355349194934" - result = digits |> largest_product(6) - expected = Ok(23520) - result == expected +expect { + digits = "73167176531330624919225119674426574742355349194934" + result = digits->largest_product(6) + expected = Ok(23520) + result == expected +} # reports zero if the only digits are zero -expect - digits = "0000" - result = digits |> largest_product(2) - expected = Ok(0) - result == expected +expect { + digits = "0000" + result = digits->largest_product(2) + expected = Ok(0) + result == expected +} # reports zero if all spans include zero -expect - digits = "99099" - result = digits |> largest_product(3) - expected = Ok(0) - result == expected +expect { + digits = "99099" + result = digits->largest_product(3) + expected = Ok(0) + result == expected +} # rejects span longer than string length -expect - digits = "123" - result = digits |> largest_product(4) - result |> Result.is_err +expect { + digits = "123" + result = digits->largest_product(4) + result.is_err() +} # reports 1 for empty string and empty product (0 span) -expect - digits = "" - result = digits |> largest_product(0) - expected = Ok(1) - result == expected +expect { + digits = "" + result = digits->largest_product(0) + expected = Ok(1) + result == expected +} # reports 1 for nonempty string and empty product (0 span) -expect - digits = "123" - result = digits |> largest_product(0) - expected = Ok(1) - result == expected +expect { + digits = "123" + result = digits->largest_product(0) + expected = Ok(1) + result == expected +} # rejects empty string and nonzero span -expect - digits = "" - result = digits |> largest_product(1) - result |> Result.is_err +expect { + digits = "" + result = digits->largest_product(1) + result.is_err() +} # rejects invalid character in digits -expect - digits = "1234a5" - result = digits |> largest_product(2) - result |> Result.is_err +expect { + digits = "1234a5" + result = digits->largest_product(2) + 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/leap/.meta/Example.roc b/exercises/practice/leap/.meta/Example.roc index e12fb53f..0a3f48e0 100644 --- a/exercises/practice/leap/.meta/Example.roc +++ b/exercises/practice/leap/.meta/Example.roc @@ -1,5 +1,5 @@ -module [is_leap_year] - -is_leap_year : I64 -> Bool -is_leap_year = |year| - (year % 4 == 0) and (year % 400 == 0 or year % 100 != 0) +Leap :: {}.{ + is_leap_year : I64 -> Bool + is_leap_year = |year| + (year % 4 == 0) and (year % 400 == 0 or year % 100 != 0) +} diff --git a/exercises/practice/leap/.meta/template.j2 b/exercises/practice/leap/.meta/template.j2 index c974c801..266616ec 100644 --- a/exercises/practice/leap/.meta/template.j2 +++ b/exercises/practice/leap/.meta/template.j2 @@ -7,8 +7,11 @@ import Leap exposing [is_leap_year] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = is_leap_year({{ case["input"]["year"] }}) result == {{ case ["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/leap/Leap.roc b/exercises/practice/leap/Leap.roc index 03943d22..eb44b513 100644 --- a/exercises/practice/leap/Leap.roc +++ b/exercises/practice/leap/Leap.roc @@ -1,5 +1,6 @@ -module [is_leap_year] - -is_leap_year : I64 -> Bool -is_leap_year = |year| - crash("Please implement the `is_leap_year` function") +Leap :: {}.{ + is_leap_year : I64 -> Bool + is_leap_year = |year| { + crash "Please implement the 'is_leap_year' function" + } +} diff --git a/exercises/practice/leap/leap-test.roc b/exercises/practice/leap/leap-test.roc index 1782ac93..67a5c22e 100644 --- a/exercises/practice/leap/leap-test.roc +++ b/exercises/practice/leap/leap-test.roc @@ -1,59 +1,64 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/leap/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Leap exposing [is_leap_year] # year not divisible by 4 in common year -expect - result = is_leap_year(2015) - result == Bool.false +expect { + result = is_leap_year(2015) + result == Bool.False +} # year divisible by 2, not divisible by 4 in common year -expect - result = is_leap_year(1970) - result == Bool.false +expect { + result = is_leap_year(1970) + result == Bool.False +} # year divisible by 4, not divisible by 100 in leap year -expect - result = is_leap_year(1996) - result == Bool.true +expect { + result = is_leap_year(1996) + result == Bool.True +} # year divisible by 4 and 5 is still a leap year -expect - result = is_leap_year(1960) - result == Bool.true +expect { + result = is_leap_year(1960) + result == Bool.True +} # year divisible by 100, not divisible by 400 in common year -expect - result = is_leap_year(2100) - result == Bool.false +expect { + result = is_leap_year(2100) + result == Bool.False +} # year divisible by 100 but not by 3 is still not a leap year -expect - result = is_leap_year(1900) - result == Bool.false +expect { + result = is_leap_year(1900) + result == Bool.False +} # year divisible by 400 is leap year -expect - result = is_leap_year(2000) - result == Bool.true +expect { + result = is_leap_year(2000) + result == Bool.True +} # year divisible by 400 but not by 125 is still a leap year -expect - result = is_leap_year(2400) - result == Bool.true +expect { + result = is_leap_year(2400) + result == Bool.True +} # year divisible by 200, not divisible by 400 in common year -expect - result = is_leap_year(1800) - result == Bool.false +expect { + result = is_leap_year(1800) + result == Bool.False +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/list-ops/.docs/instructions.append.md b/exercises/practice/list-ops/.docs/instructions.append.md index 56d270a1..1cabfb38 100644 --- a/exercises/practice/list-ops/.docs/instructions.append.md +++ b/exercises/practice/list-ops/.docs/instructions.append.md @@ -3,7 +3,7 @@ ## Wait, It's Impossible! Implementing these list operations without using _any_ built-in function is virtually impossible in Roc, because you need a way to append or prepend elements to a list. -In other languages you might use operators such as `:` in Haskell or `+=` in Python, but in Roc you have to use the [`List` functions](https://www.roc-lang.org/builtins/List). +In other languages you might use operators such as `:` in Haskell or `+=` in Python, but in Roc you have to use the [`List` functions](https://www.roc-lang.org/builtins/main/#Builtin.List). So for this exercise you're allowed to use `List.append` (but avoid using any other built-in function). @@ -13,18 +13,37 @@ In Roc however, a `List` is an array (a contiguous chunk of bytes). Arrays have different properties than linked lists like the ability to efficiently access elements by index and append new elements. Because of this, in Roc we use `List.append` often and rarely use `List.prepend`. -Hint: try using: +## Naming + +Roc uses slightly different names than the ones listed in the general instructions: + +- `concat` instead of `append` +- `join` instead of `concatenate` +- `len` instead of `length` +- `fold` instead of `foldl` +- `fold_rev` instead of `foldr` + +However, the following function names are standard in Roc: +- `filter` +- `map` +- `reverse` + +## Hint + +Try using: ```roc -when list is - [] -> ???? - [first, .. as rest] -> ??? +match list { + [] => ... + [first, .. as rest] => ... +} ``` or ```roc -when list is - [] -> ??? - [.. as rest, last] -> ??? +match list { + [] => ... + [.. as rest, last] => ... +} ``` diff --git a/exercises/practice/list-ops/.meta/Example.roc b/exercises/practice/list-ops/.meta/Example.roc index 723cd03f..04b71af8 100644 --- a/exercises/practice/list-ops/.meta/Example.roc +++ b/exercises/practice/list-ops/.meta/Example.roc @@ -1,71 +1,84 @@ -module [append, concat, filter, length, map, foldl, foldr, reverse] +ListOps :: {}.{ + concat : List(a), List(a) -> List(a) + concat = |list1, list2| { + match list2 { + [] => list1 + [first, .. as rest] => concat(list1.append(first), rest) + } + } -append : List a, List a -> List a -append = |list1, list2| - # Cheating: list1 |> List.concat list2 - when list2 is - [] -> list1 - [first, .. as rest] -> list1 |> List.append(first) |> append(rest) + join : List(List(a)) -> List(a) + join = |lists| { + match lists { + [] => [] + [first, .. as rest] => first->concat(join(rest)) + } + } -concat : List (List a) -> List a -concat = |lists| - # Cheating: list |> List.join - when lists is - [] -> [] - [one] -> one - [.. as rest, one, two] -> rest |> List.append(append(one, two)) |> concat + filter : List(a), (a -> Bool) -> List(a) + filter = |list, function| { + loop = |l, acc| { + match l { + [] => acc + [first, .. as rest] => { + if function(first) { + loop(rest, acc.append(first)) + } else { + loop(rest, acc) + } + } + } + } -filter : List a, (a -> Bool) -> List a -filter = |list, function| - # Cheating: list |> List.keep_if function - loop = |l, acc| - when l is - [] -> acc - [first, .. as rest] -> - if function(first) then - rest |> loop(List.append(acc, first)) - else - rest |> loop(acc) + loop(list, []) + } - loop(list, []) + len : List(a) -> U64 + len = |list| { + loop = |l, acc| { + match l { + [] => acc + [_, .. as rest] => loop(rest, (acc + 1)) + } + } + loop(list, 0) + } -length : List a -> U64 -length = |list| - # Cheating: list |> List.len - loop = |l, acc| - when l is - [] -> acc - [_, .. as rest] -> rest |> loop((acc + 1)) - loop(list, 0) + map : List(a), (a -> b) -> List(b) + map = |list, function| { + loop = |l, acc| { + match l { + [] => acc + [first, .. as rest] => loop(rest, acc.append(function(first))) + } + } + loop(list, []) + } -map : List a, (a -> b) -> List b -map = |list, function| - # Cheating: list |> List.map function - loop = |l, acc| - when l is - [] -> acc - [first, .. as rest] -> rest |> loop(List.append(acc, function(first))) - loop(list, []) + fold : List(a), b, (b, a -> b) -> b + fold = |list, initial, function| { + match list { + [] => initial + [first, .. as rest] => fold(rest, function(initial, first), function) + } + } -foldl : List a, b, (b, a -> b) -> b -foldl = |list, initial, function| - # Cheating: list |> List.walk initial function - when list is - [] -> initial - [first, .. as rest] -> rest |> foldl(function(initial, first), function) + fold_rev : List(a), b, (b, a -> b) -> b + fold_rev = |list, initial, function| { + match list { + [] => initial + [.. as rest, last] => fold_rev(rest, function(initial, last), function) + } + } -foldr : List a, b, (b, a -> b) -> b -foldr = |list, initial, function| - # Cheating: list |> List.walk_backwards initial function - when list is - [] -> initial - [.. as rest, last] -> rest |> foldr(function(initial, last), function) - -reverse : List a -> List a -reverse = |list| - # Cheating: list |> List.reverse - loop = |l, acc| - when l is - [] -> acc - [.. as rest, last] -> rest |> loop(List.append(acc, last)) - loop(list, []) + reverse : List(a) -> List(a) + reverse = |list| { + loop = |l, acc| { + match l { + [] => acc + [.. as rest, last] => loop(rest, acc.append(last)) + } + } + loop(list, []) + } +} diff --git a/exercises/practice/list-ops/.meta/template.j2 b/exercises/practice/list-ops/.meta/template.j2 index aaa0c48f..897bb299 100644 --- a/exercises/practice/list-ops/.meta/template.j2 +++ b/exercises/practice/list-ops/.meta/template.j2 @@ -2,14 +2,14 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [append, concat, filter, length, map, foldl, foldr, reverse] +import {{ exercise | to_pascal }} exposing [concat, join, filter, len, map, fold, fold_rev, reverse] {% set function_map = { - "(acc, el) -> el * acc": "\\acc, el -> el * acc", - "(acc, el) -> el / acc": "\\acc, el -> el / acc", - "(acc, el) -> el + acc": "\\acc, el -> el + acc", - "(x) -> x + 1": "\\x -> x + 1", - "(x) -> x modulo 2 == 1": "Num.is_odd", + "(acc, el) -> el * acc": "|acc, el| el * acc", + "(acc, el) -> el / acc": "|acc, el| el / acc", + "(acc, el) -> el + acc": "|acc, el| el + acc", + "(x) -> x + 1": "|x| x + 1", + "(x) -> x modulo 2 == 1": "|n| n % 2 == 1", } -%} {% for supercase in cases %} @@ -19,31 +19,48 @@ import {{ exercise | to_pascal }} exposing [append, concat, filter, length, map, {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect +expect { {%- if case["property"] == "append" %} - result = {{ case["property"] | to_snake }}({{ case["input"]["list1"] | to_roc }}, {{ case["input"]["list2"] | to_roc }}) + result = {{ case["input"]["list1"] | to_roc }}->concat({{ case["input"]["list2"] | to_roc }}) result == {{ case["expected"] | to_roc }} {%- elif case["property"] == "concat" %} - result = {{ case["property"] | to_snake }}({{ case["input"]["lists"] | to_roc }}) + result = {{ case["input"]["lists"] | to_roc }}->join() result == {{ case["expected"] | to_roc }} {%- elif case["property"] == "filter" %} - result = {{ case["property"] | to_snake }}({{ case["input"]["list"] | to_roc }}, {{ function_map[case["input"]["function"]] }}) + result = {{ case["input"]["list"] | to_roc }}->filter({{ function_map[case["input"]["function"]] }}) result == {{ case["expected"] | to_roc }} {%- elif case["property"] == "length" %} - result = {{ case["property"] | to_snake }}({{ case["input"]["list"] | to_roc }}) + result = {{ case["input"]["list"] | to_roc }}->len() result == {{ case["expected"] }} {%- elif case["property"] == "map" %} - result = {{ case["property"] | to_snake }}({{ case["input"]["list"] | to_roc }}, {{ function_map[case["input"]["function"]] }}) + result = {{ case["input"]["list"] | to_roc }}->map({{ function_map[case["input"]["function"]] }}) result == {{ case["expected"] }} -{%- elif case["property"] in ("foldl", "foldr") %} - result = {{ case["property"] | to_snake }}({{ case["input"]["list"] | to_roc }}, {{ case["input"]["initial"] }}, {{ function_map[case["input"]["function"]] }}){% if "/" in function_map[case["input"]["function"]] %}|> Num.round{% endif %} +{%- elif case["property"] == "foldl" %} + result = {{ case["input"]["list"] | to_roc }}->fold({{ case["input"]["initial"] }}, {{ function_map[case["input"]["function"]] }}){% if "/" in function_map[case["input"]["function"]] %} -> round(){% endif %} + result == {{ case["expected"] | to_roc }} +{%- elif case["property"] == "foldr" %} + result = {{ case["input"]["list"] | to_roc }}->fold_rev({{ case["input"]["initial"] }}, {{ function_map[case["input"]["function"]] }}){% if "/" in function_map[case["input"]["function"]] %} -> round(){% endif %} result == {{ case["expected"] | to_roc }} {%- elif case["property"] == "reverse" %} - result = {{ case["property"] | to_snake }}({{ case["input"]["list"] | to_roc }}) + result = {{ case["input"]["list"] | to_roc }}->reverse() result == {{ case["expected"] | to_roc }} {%- else %} - Bool.true # This test case is not yet implemented + Bool.True # This test case is not yet implemented {%- endif %} +} {% endfor -%} {% endfor -%} + +# The following function will soon be available in Roc's builtins +round : Dec -> Dec +round = |value| { + pow = 1000.0 + ( + (value * pow + 0.5).to_u64_try() ?? { + crash "Unreachable" + } + ).to_dec() / pow +} + +{{ macros.footer() }} diff --git a/exercises/practice/list-ops/ListOps.roc b/exercises/practice/list-ops/ListOps.roc index 3e5762a8..1a1411eb 100644 --- a/exercises/practice/list-ops/ListOps.roc +++ b/exercises/practice/list-ops/ListOps.roc @@ -1,33 +1,51 @@ -module [append, concat, filter, length, map, foldl, foldr, reverse] - -append : List a, List a -> List a -append = |list1, list2| - crash("Please implement the 'append' function") - -concat : List (List a) -> List a -concat = |lists| - crash("Please implement the 'concat' function") - -filter : List a, (a -> Bool) -> List a -filter = |list, function| - crash("Please implement the 'filter' function") - -length : List a -> U64 -length = |list| - crash("Please implement the 'length' function") - -map : List a, (a -> b) -> List b -map = |list, function| - crash("Please implement the 'map' function") - -foldl : List a, b, (b, a -> b) -> b -foldl = |list, initial, function| - crash("Please implement the 'foldl' function") - -foldr : List a, b, (b, a -> b) -> b -foldr = |list, initial, function| - crash("Please implement the 'foldr' function") - -reverse : List a -> List a -reverse = |list| - crash("Please implement the 'reverse' function") +ListOps :: {}.{ + # # Note: this function is named "append" in the exercise instructions + # # but Roc prefers to name this "concat" + concat : List(a), List(a) -> List(a) + concat = |list1, list2| { + crash "Please implement the 'concat' function" + } + + # # Note: this function is named "concatenate" in the exercise instructions + # # but Roc prefers to name this "join" + join : List(List(a)) -> List(a) + join = |lists| { + crash "Please implement the 'join' function" + } + + filter : List(a), (a -> Bool) -> List(a) + filter = |list, function| { + crash "Please implement the 'filter' function" + } + + # # Note: this function is named "length" in the exercise instructions + # # but Roc prefers to name this "len" + len : List(a) -> U64 + len = |list| { + crash "Please implement the 'len' function" + } + + map : List(a), (a -> b) -> List(b) + map = |list, function| { + crash "Please implement the 'map' function" + } + + # # Note: this function is named "foldl" in the exercise instructions + # # but Roc prefers to name this "fold" + fold : List(a), b, (b, a -> b) -> b + fold = |list, initial, function| { + crash "Please implement the 'fold' function" + } + + # # Note: this function is named "foldr" in the exercise instructions + # # but Roc prefers to name this "fold_rev" + fold_rev : List(a), b, (b, a -> b) -> b + fold_rev = |list, initial, function| { + crash "Please implement the 'fold_rev' function" + } + + reverse : List(a) -> List(a) + reverse = |list| { + crash "Please implement the 'reverse' function" + } +} diff --git a/exercises/practice/list-ops/list-ops-test.roc b/exercises/practice/list-ops/list-ops-test.roc index 44e890b1..1b835464 100644 --- a/exercises/practice/list-ops/list-ops-test.roc +++ b/exercises/practice/list-ops/list-ops-test.roc @@ -1,156 +1,185 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/list-ops/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout +# File last updated on 2026-06-22 -main! = |_args| - Stdout.line!("") - -import ListOps exposing [append, concat, filter, length, map, foldl, foldr, reverse] +import ListOps exposing [concat, join, filter, len, map, fold, fold_rev, reverse] ## ## append entries to a list and return the new list ## # empty lists -expect - result = append([], []) - result == [] +expect { + result = []->concat([]) + result == [] +} # list to empty list -expect - result = append([], [1, 2, 3, 4]) - result == [1, 2, 3, 4] +expect { + result = []->concat([1, 2, 3, 4]) + result == [1, 2, 3, 4] +} # empty list to list -expect - result = append([1, 2, 3, 4], []) - result == [1, 2, 3, 4] +expect { + result = [1, 2, 3, 4]->concat([]) + result == [1, 2, 3, 4] +} # non-empty lists -expect - result = append([1, 2], [2, 3, 4, 5]) - result == [1, 2, 2, 3, 4, 5] +expect { + result = [1, 2]->concat([2, 3, 4, 5]) + result == [1, 2, 2, 3, 4, 5] +} ## ## concatenate a list of lists ## # empty list -expect - result = concat([]) - result == [] +expect { + result = []->join() + result == [] +} # list of lists -expect - result = concat([[1, 2], [3], [], [4, 5, 6]]) - result == [1, 2, 3, 4, 5, 6] +expect { + result = [[1, 2], [3], [], [4, 5, 6]]->join() + result == [1, 2, 3, 4, 5, 6] +} # list of nested lists -expect - result = concat([[[1], [2]], [[3]], [[]], [[4, 5, 6]]]) - result == [[1], [2], [3], [], [4, 5, 6]] +expect { + result = [[[1], [2]], [[3]], [[]], [[4, 5, 6]]]->join() + result == [[1], [2], [3], [], [4, 5, 6]] +} ## ## filter list returning only values that satisfy the filter function ## # empty list -expect - result = filter([], Num.is_odd) - result == [] +expect { + result = []->filter(|n| n % 2 == 1) + result == [] +} # non-empty list -expect - result = filter([1, 2, 3, 5], Num.is_odd) - result == [1, 3, 5] +expect { + result = [1, 2, 3, 5]->filter(|n| n % 2 == 1) + result == [1, 3, 5] +} ## ## returns the length of a list ## # empty list -expect - result = length([]) - result == 0 +expect { + result = []->len() + result == 0 +} # non-empty list -expect - result = length([1, 2, 3, 4]) - result == 4 +expect { + result = [1, 2, 3, 4]->len() + result == 4 +} ## ## return a list of elements whose values equal the list value transformed by the mapping function ## # empty list -expect - result = map([], |x| x + 1) - result == [] +expect { + result = []->map(|x| x + 1) + result == [] +} # non-empty list -expect - result = map([1, 3, 5, 7], |x| x + 1) - result == [2, 4, 6, 8] +expect { + result = [1, 3, 5, 7]->map(|x| x + 1) + result == [2, 4, 6, 8] +} ## ## folds (reduces) the given list from the left with a function ## # empty list -expect - result = foldl([], 2, |acc, el| el * acc) - result == 2 +expect { + result = []->fold(2, |acc, el| el * acc) + result == 2 +} # direction independent function applied to non-empty list -expect - result = foldl([1, 2, 3, 4], 5, |acc, el| el + acc) - result == 15 +expect { + result = [1, 2, 3, 4]->fold(5, |acc, el| el + acc) + result == 15 +} # direction dependent function applied to non-empty list -expect - result = foldl([1, 2, 3, 4], 24, |acc, el| el / acc) |> Num.round - result == 64 +expect { + result = [1, 2, 3, 4]->fold(24, |acc, el| el / acc)->round() + result == 64 +} ## ## folds (reduces) the given list from the right with a function ## # empty list -expect - result = foldr([], 2, |acc, el| el * acc) - result == 2 +expect { + result = []->fold_rev(2, |acc, el| el * acc) + result == 2 +} # direction independent function applied to non-empty list -expect - result = foldr([1, 2, 3, 4], 5, |acc, el| el + acc) - result == 15 +expect { + result = [1, 2, 3, 4]->fold_rev(5, |acc, el| el + acc) + result == 15 +} # direction dependent function applied to non-empty list -expect - result = foldr([1, 2, 3, 4], 24, |acc, el| el / acc) |> Num.round - result == 9 +expect { + result = [1, 2, 3, 4]->fold_rev(24, |acc, el| el / acc)->round() + result == 9 +} ## ## reverse the elements of the list ## # empty list -expect - result = reverse([]) - result == [] +expect { + result = []->reverse() + result == [] +} # non-empty list -expect - result = reverse([1, 3, 5, 7]) - result == [7, 5, 3, 1] +expect { + result = [1, 3, 5, 7]->reverse() + result == [7, 5, 3, 1] +} # list of lists is not flattened -expect - result = reverse([[1, 2], [3], [], [4, 5, 6]]) - result == [[4, 5, 6], [], [3], [1, 2]] +expect { + result = [[1, 2], [3], [], [4, 5, 6]]->reverse() + result == [[4, 5, 6], [], [3], [1, 2]] +} +# The following function will soon be available in Roc's builtins +round : Dec -> Dec +round = |value| { + pow = 1000.0 + ( + (value * pow + 0.5).to_u64_try() ?? { + crash "Unreachable" + }, + ).to_dec() / pow +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/luhn/.meta/Example.roc b/exercises/practice/luhn/.meta/Example.roc index 707029c7..9f6edfd3 100644 --- a/exercises/practice/luhn/.meta/Example.roc +++ b/exercises/practice/luhn/.meta/Example.roc @@ -1,45 +1,57 @@ -module [valid] +Luhn :: {}.{ + valid : Str -> Bool + valid = |number| { + match to_digits(number) { + Ok(digits) if digits.len() > 1 => { + sum = map_every_other_backwards( + digits, + |digit| { + product = digit * 2 + if product < 10 { + product + } else { + product - 9 + } + }, + ).sum() + sum % 10 == 0 + } + _ => Bool.False + } + } +} -valid : Str -> Bool -valid = |number| - when to_digits(number) is - Ok(digits) if List.len(digits) > 1 -> - map_every_other_backwards( - digits, - |digit| - product = digit * 2 - if product < 10 then product else product - 9, - ) - |> List.sum - |> Num.is_multiple_of(10) +to_digits : Str -> Try(List(U16), [IllegalCharacter]) +to_digits = |number| { + help : List(U8), List(U16) -> Try(List(U16), [IllegalCharacter]) + help = |input, digits| { + match input { + [] => Ok(digits) + [byte, .. as rest] if byte == ' ' => help(rest, digits) + [byte, .. as rest] if '0' <= byte and byte <= '9' => { + digit = (byte - '0').to_u16() + help(rest, digits.append(digit)) + } + _ => Err(IllegalCharacter) + } + } + help(number.to_utf8(), []) +} - _ -> Bool.false +map_every_other_backwards : List(a), (a -> a) -> List(a) +map_every_other_backwards = |list, func| { + help = |state, input| { + match input { + [.. as rest, x, y] => { + state + .append(y) + .append(func(x)) + ->help(rest) + } -to_digits : Str -> Result (List U16) [IllegalCharacter] -to_digits = |number| - help = |input, digits| - when input is - [] -> Ok(digits) - [byte, .. as rest] if byte == ' ' -> help(rest, digits) - [byte, .. as rest] if '0' <= byte and byte <= '9' -> - # convert to U16 to prevent an overflow when summing up the digits - digit = byte - '0' |> Num.to_u16 - help(rest, List.append(digits, digit)) - - _ -> Err(IllegalCharacter) - help(Str.to_utf8(number), []) - -map_every_other_backwards : List a, (a -> a) -> List a -map_every_other_backwards = |list, func| - help = |state, input| - when input is - [.. as rest, x, y] -> - List.append(state, y) - |> List.append(func(x)) - |> help(rest) - - [x] -> List.append(state, x) - [] -> state - - help([], list) - |> List.reverse + [x] => state.append(x) + [] => state + } + } + help([], list).rev() +} diff --git a/exercises/practice/luhn/.meta/template.j2 b/exercises/practice/luhn/.meta/template.j2 index 2f2e4ef6..387605ab 100644 --- a/exercises/practice/luhn/.meta/template.j2 +++ b/exercises/practice/luhn/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["value"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/luhn/Luhn.roc b/exercises/practice/luhn/Luhn.roc index 4373355e..c90cb499 100644 --- a/exercises/practice/luhn/Luhn.roc +++ b/exercises/practice/luhn/Luhn.roc @@ -1,5 +1,6 @@ -module [valid] - -valid : Str -> Bool -valid = |digits| - crash("Please implement 'valid'") +Luhn :: {}.{ + valid : Str -> Bool + valid = |digits| { + crash "Please implement the 'valid' function" + } +} diff --git a/exercises/practice/luhn/luhn-test.roc b/exercises/practice/luhn/luhn-test.roc index 3ed18a32..10a0f0b2 100644 --- a/exercises/practice/luhn/luhn-test.roc +++ b/exercises/practice/luhn/luhn-test.roc @@ -1,124 +1,142 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/luhn/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Luhn exposing [valid] # single digit strings can not be valid -expect - result = valid("1") - result == Bool.false +expect { + result = valid("1") + result == Bool.False +} # a single zero is invalid -expect - result = valid("0") - result == Bool.false +expect { + result = valid("0") + result == Bool.False +} # a simple valid SIN that remains valid if reversed -expect - result = valid("059") - result == Bool.true +expect { + result = valid("059") + result == Bool.True +} # a simple valid SIN that becomes invalid if reversed -expect - result = valid("59") - result == Bool.true +expect { + result = valid("59") + result == Bool.True +} # a valid Canadian SIN -expect - result = valid("055 444 285") - result == Bool.true +expect { + result = valid("055 444 285") + result == Bool.True +} # invalid Canadian SIN -expect - result = valid("055 444 286") - result == Bool.false +expect { + result = valid("055 444 286") + result == Bool.False +} # invalid credit card -expect - result = valid("8273 1232 7352 0569") - result == Bool.false +expect { + result = valid("8273 1232 7352 0569") + result == Bool.False +} # invalid long number with an even remainder -expect - result = valid("1 2345 6789 1234 5678 9012") - result == Bool.false +expect { + result = valid("1 2345 6789 1234 5678 9012") + result == Bool.False +} # invalid long number with a remainder divisible by 5 -expect - result = valid("1 2345 6789 1234 5678 9013") - result == Bool.false +expect { + result = valid("1 2345 6789 1234 5678 9013") + result == Bool.False +} # valid number with an even number of digits -expect - result = valid("095 245 88") - result == Bool.true +expect { + result = valid("095 245 88") + result == Bool.True +} # valid number with an odd number of spaces -expect - result = valid("234 567 891 234") - result == Bool.true +expect { + result = valid("234 567 891 234") + result == Bool.True +} # valid strings with a non-digit added at the end become invalid -expect - result = valid("059a") - result == Bool.false +expect { + result = valid("059a") + result == Bool.False +} # valid strings with punctuation included become invalid -expect - result = valid("055-444-285") - result == Bool.false +expect { + result = valid("055-444-285") + result == Bool.False +} # valid strings with symbols included become invalid -expect - result = valid("055# 444$ 285") - result == Bool.false +expect { + result = valid("055# 444$ 285") + result == Bool.False +} # single zero with space is invalid -expect - result = valid(" 0") - result == Bool.false +expect { + result = valid(" 0") + result == Bool.False +} # more than a single zero is valid -expect - result = valid("0000 0") - result == Bool.true +expect { + result = valid("0000 0") + result == Bool.True +} # input digit 9 is correctly converted to output digit 9 -expect - result = valid("091") - result == Bool.true +expect { + result = valid("091") + result == Bool.True +} # very long input is valid -expect - result = valid("9999999999 9999999999 9999999999 9999999999") - result == Bool.true +expect { + result = valid("9999999999 9999999999 9999999999 9999999999") + result == Bool.True +} # valid luhn with an odd number of digits and non zero first digit -expect - result = valid("109") - result == Bool.true +expect { + result = valid("109") + result == Bool.True +} # using ascii value for non-doubled non-digit isn't allowed -expect - result = valid("055b 444 285") - result == Bool.false +expect { + result = valid("055b 444 285") + result == Bool.False +} # using ascii value for doubled non-digit isn't allowed -expect - result = valid(":9") - result == Bool.false +expect { + result = valid(":9") + result == Bool.False +} # non-numeric, non-space char in the middle with a sum that's divisible by 10 isn't allowed -expect - result = valid("59%59") - result == Bool.false +expect { + result = valid("59%59") + result == Bool.False +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/matching-brackets/.meta/Example.roc b/exercises/practice/matching-brackets/.meta/Example.roc index de43b274..70f6dfe1 100644 --- a/exercises/practice/matching-brackets/.meta/Example.roc +++ b/exercises/practice/matching-brackets/.meta/Example.roc @@ -1,25 +1,39 @@ -module [is_paired] +MatchingBrackets :: {}.{ + is_paired : Str -> Bool + is_paired = |string| { + is_open = |c| { + c == '[' or c == '(' or c == '{' + } + is_close = |c| { + c == ']' or c == ')' or c == '}' + } + is_match = |pair| { + pair == ('[', ']') or pair == ('(', ')') or pair == ('{', '}') + } + help = |open_brackets, remaining_chars| { + match remaining_chars { + [] => open_brackets.is_empty() + [next_char, .. as rest_chars] => { + if is_open(next_char) { + help(open_brackets.append(next_char), rest_chars) + } else if is_close(next_char) { + match open_brackets { + [] => Bool.False + [.. as previous_opens, last_open] => { + if is_match((last_open, next_char)) { + help(previous_opens, rest_chars) + } else { + Bool.False + } + } + } + } else { + help(open_brackets, rest_chars) + } + } + } + } -is_paired : Str -> Bool -is_paired = |string| - is_open = |c| c == '[' or c == '(' or c == '{' - is_close = |c| c == ']' or c == ')' or c == '}' - is_match = |pair| pair == ('[', ']') or pair == ('(', ')') or pair == ('{', '}') - help = |open_brackets, remaining_chars| - when remaining_chars is - [] -> List.is_empty(open_brackets) # ok or missing closing bracket - [next_char, .. as rest_chars] -> - if is_open(next_char) then - help((open_brackets |> List.append(next_char)), rest_chars) - else if is_close(next_char) then - when open_brackets is - [] -> Bool.false # missing opening bracket - [.. as previous_opens, last_open] -> - if is_match((last_open, next_char)) then - help(previous_opens, rest_chars) - else - Bool.false # mismatching brackets - else - help(open_brackets, rest_chars) - - help([], (string |> Str.to_utf8)) + help([], string.to_utf8()) + } +} diff --git a/exercises/practice/matching-brackets/.meta/template.j2 b/exercises/practice/matching-brackets/.meta/template.j2 index 0b4214c4..24e2106b 100644 --- a/exercises/practice/matching-brackets/.meta/template.j2 +++ b/exercises/practice/matching-brackets/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect - result = {{ case["input"]["value"] | to_roc }} |> {{ case["property"] | to_snake }} +expect { + result = {{ case["input"]["value"] | to_roc }} -> {{ case["property"] | to_snake }} result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/matching-brackets/MatchingBrackets.roc b/exercises/practice/matching-brackets/MatchingBrackets.roc index 0ea9898f..70c07a9a 100644 --- a/exercises/practice/matching-brackets/MatchingBrackets.roc +++ b/exercises/practice/matching-brackets/MatchingBrackets.roc @@ -1,5 +1,6 @@ -module [is_paired] - -is_paired : Str -> Bool -is_paired = |string| - crash("Please implement the 'is_paired' function") +MatchingBrackets :: {}.{ + is_paired : Str -> Bool + is_paired = |string| { + crash "Please implement the 'is_paired' function" + } +} diff --git a/exercises/practice/matching-brackets/matching-brackets-test.roc b/exercises/practice/matching-brackets/matching-brackets-test.roc index d68058fa..e8c237da 100644 --- a/exercises/practice/matching-brackets/matching-brackets-test.roc +++ b/exercises/practice/matching-brackets/matching-brackets-test.roc @@ -1,114 +1,130 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/matching-brackets/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import MatchingBrackets exposing [is_paired] # paired square brackets -expect - result = "[]" |> is_paired - result == Bool.true +expect { + result = "[]"->is_paired() + result == Bool.True +} # empty string -expect - result = "" |> is_paired - result == Bool.true +expect { + result = ""->is_paired() + result == Bool.True +} # unpaired brackets -expect - result = "[[" |> is_paired - result == Bool.false +expect { + result = "[["->is_paired() + result == Bool.False +} # wrong ordered brackets -expect - result = "}{" |> is_paired - result == Bool.false +expect { + result = "}{"->is_paired() + result == Bool.False +} # wrong closing bracket -expect - result = "{]" |> is_paired - result == Bool.false +expect { + result = "{]"->is_paired() + result == Bool.False +} # paired with whitespace -expect - result = "{ }" |> is_paired - result == Bool.true +expect { + result = "{ }"->is_paired() + result == Bool.True +} # partially paired brackets -expect - result = "{[])" |> is_paired - result == Bool.false +expect { + result = "{[])"->is_paired() + result == Bool.False +} # simple nested brackets -expect - result = "{[]}" |> is_paired - result == Bool.true +expect { + result = "{[]}"->is_paired() + result == Bool.True +} # several paired brackets -expect - result = "{}[]" |> is_paired - result == Bool.true +expect { + result = "{}[]"->is_paired() + result == Bool.True +} # paired and nested brackets -expect - result = "([{}({}[])])" |> is_paired - result == Bool.true +expect { + result = "([{}({}[])])"->is_paired() + result == Bool.True +} # unopened closing brackets -expect - result = "{[)][]}" |> is_paired - result == Bool.false +expect { + result = "{[)][]}"->is_paired() + result == Bool.False +} # unpaired and nested brackets -expect - result = "([{])" |> is_paired - result == Bool.false +expect { + result = "([{])"->is_paired() + result == Bool.False +} # paired and wrong nested brackets -expect - result = "[({]})" |> is_paired - result == Bool.false +expect { + result = "[({]})"->is_paired() + result == Bool.False +} # paired and wrong nested brackets but innermost are correct -expect - result = "[({}])" |> is_paired - result == Bool.false +expect { + result = "[({}])"->is_paired() + result == Bool.False +} # paired and incomplete brackets -expect - result = "{}[" |> is_paired - result == Bool.false +expect { + result = "{}["->is_paired() + result == Bool.False +} # too many closing brackets -expect - result = "[]]" |> is_paired - result == Bool.false +expect { + result = "[]]"->is_paired() + result == Bool.False +} # early unexpected brackets -expect - result = ")()" |> is_paired - result == Bool.false +expect { + result = ")()"->is_paired() + result == Bool.False +} # early mismatched brackets -expect - result = "{)()" |> is_paired - result == Bool.false +expect { + result = "{)()"->is_paired() + result == Bool.False +} # math expression -expect - result = "(((185 + 223.85) * 15) - 543)/2" |> is_paired - result == Bool.true +expect { + result = "(((185 + 223.85) * 15) - 543)/2"->is_paired() + result == Bool.True +} # complex latex expression -expect - result = "\\left(\\begin{array}{cc} \\frac{1}{3} & x\\\\ \\mathrm{e}^{x} &... x^2 \\end{array}\\right)" |> is_paired - result == Bool.true +expect { + result = "\\left(\\begin{array}{cc} \\frac{1}{3} & x\\\\ \\mathrm{e}^{x} &... x^2 \\end{array}\\right)"->is_paired() + result == Bool.True +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/matrix/.meta/Example.roc b/exercises/practice/matrix/.meta/Example.roc index 72692ef5..016e46b6 100644 --- a/exercises/practice/matrix/.meta/Example.roc +++ b/exercises/practice/matrix/.meta/Example.roc @@ -1,33 +1,48 @@ -module [row, column] +Matrix :: {}.{ + column : Str, U64 -> Try(List(I64), [BadNumStr, OutOfBounds, ..]) + column = |matrix_str, index| { + if index == 0 { + Err(OutOfBounds) + } else { + matrix = parse_matrix(matrix_str)? + matrix->map_try(|r| r.get(index - 1)) + } + } -parse_row : Str -> Result (List I64) [InvalidNumStr] -parse_row = |row_str| - row_str - |> Str.trim - |> Str.split_on(" ") - |> List.map(Str.trim) - |> List.drop_if(Str.is_empty) - |> List.map_try(Str.to_i64) + row : Str, U64 -> Try(List(I64), [BadNumStr, OutOfBounds, ..]) + row = |matrix_str, index| { + if index == 0 { + Err(OutOfBounds) + } else { + matrix = parse_matrix(matrix_str)? + result = matrix.get(index - 1)? + Ok(result) + } + } +} -parse_matrix : Str -> Result (List (List I64)) [InvalidNumStr] -parse_matrix = |matrix_str| - matrix_str - |> Str.split_on("\n") - |> List.map_try(parse_row) +parse_row : Str -> Try(List(I64), [BadNumStr, ..]) +parse_row = |row_str| { + row_str + .trim() + .split_on(" ") + .map(Str.trim) + .drop_if(Str.is_empty) + ->map_try(I64.from_str) +} -column : Str, U64 -> Result (List I64) [InvalidNumStr, OutOfBounds] -column = |matrix_str, index| - if index == 0 then - Err(OutOfBounds) - else - matrix = parse_matrix(matrix_str)? - matrix |> List.map_try(|r| r |> List.get((index - 1))) +parse_matrix : Str -> Try(List(List(I64)), [BadNumStr, ..]) +parse_matrix = |matrix_str| { + matrix_str + .split_on("\n") + ->map_try(parse_row) +} -row : Str, U64 -> Result (List I64) [InvalidNumStr, OutOfBounds] -row = |matrix_str, index| - if index == 0 then - Err(OutOfBounds) - else - matrix = parse_matrix(matrix_str)? - result = matrix |> List.get(index - 1)? - Ok(result) +# The following functions should soon be available in Roc's builtins +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/matrix/.meta/template.j2 b/exercises/practice/matrix/.meta/template.j2 index 465f298d..c6093f69 100644 --- a/exercises/practice/matrix/.meta/template.j2 +++ b/exercises/practice/matrix/.meta/template.j2 @@ -6,9 +6,12 @@ import {{ exercise | to_pascal }} exposing [row, column] {% for case in cases -%} # {{ case["description"] }} -expect +expect { matrix_str = {{ case["input"]["string"] | to_roc_multiline_string | indent(8) }} - result = matrix_str |> {{ case["property"] | to_snake }}({{ case["input"]["index"] | to_roc }}) + result = matrix_str -> {{ case["property"] | to_snake }}({{ case["input"]["index"] | to_roc }}) result == Ok({{ case["expected"] | to_roc }}) +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/matrix/Matrix.roc b/exercises/practice/matrix/Matrix.roc index c37de1b0..b69652f3 100644 --- a/exercises/practice/matrix/Matrix.roc +++ b/exercises/practice/matrix/Matrix.roc @@ -1,9 +1,11 @@ -module [row, column] +Matrix :: {}.{ + column : Str, U64 -> Try(List(I64), _) + column = |matrix_str, index| { + crash "Please implement the 'column' function" + } -column : Str, U64 -> Result (List I64) _ -column = |matrix_str, index| - crash("Please implement the 'column' function") - -row : Str, U64 -> Result (List I64) _ -row = |matrix_str, index| - crash("Please implement the 'row' function") + row : Str, U64 -> Try(List(I64), _) + row = |matrix_str, index| { + crash "Please implement the 'row' function" + } +} diff --git a/exercises/practice/matrix/matrix-test.roc b/exercises/practice/matrix/matrix-test.roc index 5ff6736f..c0d56e04 100644 --- a/exercises/practice/matrix/matrix-test.roc +++ b/exercises/practice/matrix/matrix-test.roc @@ -1,91 +1,89 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/matrix/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Matrix exposing [row, column] # extract row from one number matrix -expect - matrix_str = "1" - result = matrix_str |> row(1) - result == Ok([1]) +expect { + matrix_str = "1" + result = matrix_str->row(1) + result == Ok([1]) +} # can extract row -expect - matrix_str = - """ - 1 2 - 3 4 - """ - result = matrix_str |> row(2) - result == Ok([3, 4]) +expect { + matrix_str = + \\1 2 + \\3 4 + + result = matrix_str->row(2) + result == Ok([3, 4]) +} # extract row where numbers have different widths -expect - matrix_str = - """ - 1 2 - 10 20 - """ - result = matrix_str |> row(2) - result == Ok([10, 20]) +expect { + matrix_str = + \\1 2 + \\10 20 + + result = matrix_str->row(2) + result == Ok([10, 20]) +} # can extract row from non-square matrix with no corresponding column -expect - matrix_str = - """ - 1 2 3 - 4 5 6 - 7 8 9 - 8 7 6 - """ - result = matrix_str |> row(4) - result == Ok([8, 7, 6]) +expect { + matrix_str = + \\1 2 3 + \\4 5 6 + \\7 8 9 + \\8 7 6 + + result = matrix_str->row(4) + result == Ok([8, 7, 6]) +} # extract column from one number matrix -expect - matrix_str = "1" - result = matrix_str |> column(1) - result == Ok([1]) +expect { + matrix_str = "1" + result = matrix_str->column(1) + result == Ok([1]) +} # can extract column -expect - matrix_str = - """ - 1 2 3 - 4 5 6 - 7 8 9 - """ - result = matrix_str |> column(3) - result == Ok([3, 6, 9]) +expect { + matrix_str = + \\1 2 3 + \\4 5 6 + \\7 8 9 + + result = matrix_str->column(3) + result == Ok([3, 6, 9]) +} # can extract column from non-square matrix with no corresponding row -expect - matrix_str = - """ - 1 2 3 4 - 5 6 7 8 - 9 8 7 6 - """ - result = matrix_str |> column(4) - result == Ok([4, 8, 6]) +expect { + matrix_str = + \\1 2 3 4 + \\5 6 7 8 + \\9 8 7 6 + + result = matrix_str->column(4) + result == Ok([4, 8, 6]) +} # extract column where numbers have different widths -expect - matrix_str = - """ - 89 1903 3 - 18 3 1 - 9 4 800 - """ - result = matrix_str |> column(2) - result == Ok([1903, 3, 4]) +expect { + matrix_str = + \\89 1903 3 + \\18 3 1 + \\9 4 800 + + result = matrix_str->column(2) + result == Ok([1903, 3, 4]) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/meetup/.meta/Example.roc b/exercises/practice/meetup/.meta/Example.roc index 382010f6..f7cdabae 100644 --- a/exercises/practice/meetup/.meta/Example.roc +++ b/exercises/practice/meetup/.meta/Example.roc @@ -1,34 +1,35 @@ -module [meetup] + import isodate.Date +Meetup :: {}.{ + meetup : { year : I64, month : U8, week : Week, day_of_week : DayOfWeek } -> Result Str [InvalidMonth, InvalidYear] + meetup = |{ year, month, week, day_of_week }| + if month == 0 or month > 12 then + Err(InvalidMonth) + else + first_day = Date.from_ymd(year, month, 1) + first_weekday = Date.weekday(first_day.year, first_day.month, first_day.day_of_month) + first_time = (7 + day_of_week_number(day_of_week) - first_weekday) % 7 + 1 + day_of_month = + when week is + First -> first_time + Second -> first_time + 7 + Third -> first_time + 14 + Fourth -> first_time + 21 + Last -> + if first_time + 28 > Date.days_in_month(year, month) then + first_time + 21 + else + first_time + 28 + + Teenth -> + if first_time == 6 then 13 else first_time + 14 + Ok("${year |> pad_number(4)}-${month |> pad_number(2)}-${day_of_month |> pad_number(2)}") +} Week : [First, Second, Third, Fourth, Last, Teenth] DayOfWeek : [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] -meetup : { year : I64, month : U8, week : Week, day_of_week : DayOfWeek } -> Result Str [InvalidMonth, InvalidYear] -meetup = |{ year, month, week, day_of_week }| - if month == 0 or month > 12 then - Err(InvalidMonth) - else - first_day = Date.from_ymd(year, month, 1) - first_weekday = Date.weekday(first_day.year, first_day.month, first_day.day_of_month) - first_time = (7 + day_of_week_number(day_of_week) - first_weekday) % 7 + 1 - day_of_month = - when week is - First -> first_time - Second -> first_time + 7 - Third -> first_time + 14 - Fourth -> first_time + 21 - Last -> - if first_time + 28 > Date.days_in_month(year, month) then - first_time + 21 - else - first_time + 28 - - Teenth -> - if first_time == 6 then 13 else first_time + 14 - Ok("${year |> pad_number(4)}-${month |> pad_number(2)}-${day_of_month |> pad_number(2)}") - pad_number : Num *, U64 -> Str pad_number = |number, pad| number_str = number |> Num.to_str diff --git a/exercises/practice/meetup/.meta/template.j2 b/exercises/practice/meetup/.meta/template.j2 index 0519cd73..22d2f277 100644 --- a/exercises/practice/meetup/.meta/template.j2 +++ b/exercises/practice/meetup/.meta/template.j2 @@ -6,7 +6,7 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({ year: {{ case["input"]["year"] | to_roc }}, month: {{ case["input"]["month"] | to_roc }}, @@ -15,5 +15,8 @@ expect }) expected = Ok({{ case["expected"] | to_roc }}) result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/meetup/Meetup.roc b/exercises/practice/meetup/Meetup.roc index 4132bf1b..1929622c 100644 --- a/exercises/practice/meetup/Meetup.roc +++ b/exercises/practice/meetup/Meetup.roc @@ -1,12 +1,14 @@ -module [meetup] +Meetup :: {}.{ + meetup : { year : I64, month : U8, week : Week, day_of_week : DayOfWeek } -> Result Str _ + meetup = |{ year, month, week, day_of_week }| { + crash "Please implement the 'meetup' function" + } -Week : [First, Second, Third, Fourth, Last, Teenth] -DayOfWeek : [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] + # HINT: we have added the `roc-isodate` package to the app's header in + # meetup-test.roc, so you can use it here if you need to. + # For example, you could import isodate.Date, just sayin'. +} -meetup : { year : I64, month : U8, week : Week, day_of_week : DayOfWeek } -> Result Str _ -meetup = |{ year, month, week, day_of_week }| - crash("Please implement the 'meetup' function") -# HINT: we have added the `roc-isodate` package to the app's header in -# meetup-test.roc, so you can use it here if you need to. -# For example, you could import isodate.Date, just sayin'. +Week : [First, Second, Third, Fourth, Last, Teenth] +DayOfWeek : [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] diff --git a/exercises/practice/meetup/meetup-test.roc b/exercises/practice/meetup/meetup-test.roc index 373c0878..d7039b46 100644 --- a/exercises/practice/meetup/meetup-test.roc +++ b/exercises/practice/meetup/meetup-test.roc @@ -1,1250 +1,1346 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/meetup/canonical-data.json -# File last updated on 2025-09-15 +# File last updated on 2026-06-21 app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", - isodate: "https://github.com/imclerran/roc-isodate/releases/download/v0.6.2/73w_H-aSJNcWqtXvMG4JQw_HoaApMBLnE92XD4OcVGU.tar.br", + pf: platform "https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/download/0.9/8GdFEvQYS3TeAZxKvTzCLVdQiomweGtXcdZkXNDEeABq.tar.zst", + isodate: "https://github.com/imclerran/roc-isodate/...", # TODO: update when a zig-compatible release is available } import pf.Stdout -main! = |_args| - Stdout.line!("") - import Meetup exposing [meetup] # when teenth Monday is the 13th, the first day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 5, - week: Teenth, - day_of_week: Monday, - }, - ) - expected = Ok("2013-05-13") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 5, + week: Teenth, + day_of_week: Monday, + }, + ) + expected = Ok("2013-05-13") + result == expected +} # when teenth Monday is the 19th, the last day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 8, - week: Teenth, - day_of_week: Monday, - }, - ) - expected = Ok("2013-08-19") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 8, + week: Teenth, + day_of_week: Monday, + }, + ) + expected = Ok("2013-08-19") + result == expected +} # when teenth Monday is some day in the middle of the teenth week -expect - result = meetup( - { - year: 2013, - month: 9, - week: Teenth, - day_of_week: Monday, - }, - ) - expected = Ok("2013-09-16") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 9, + week: Teenth, + day_of_week: Monday, + }, + ) + expected = Ok("2013-09-16") + result == expected +} # when teenth Tuesday is the 19th, the last day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 3, - week: Teenth, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-03-19") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: Teenth, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-03-19") + result == expected +} # when teenth Tuesday is some day in the middle of the teenth week -expect - result = meetup( - { - year: 2013, - month: 4, - week: Teenth, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-04-16") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Teenth, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-04-16") + result == expected +} # when teenth Tuesday is the 13th, the first day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 8, - week: Teenth, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-08-13") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 8, + week: Teenth, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-08-13") + result == expected +} # when teenth Wednesday is some day in the middle of the teenth week -expect - result = meetup( - { - year: 2013, - month: 1, - week: Teenth, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-01-16") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 1, + week: Teenth, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-01-16") + result == expected +} # when teenth Wednesday is the 13th, the first day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 2, - week: Teenth, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-02-13") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 2, + week: Teenth, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-02-13") + result == expected +} # when teenth Wednesday is the 19th, the last day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 6, - week: Teenth, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-06-19") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 6, + week: Teenth, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-06-19") + result == expected +} # when teenth Thursday is some day in the middle of the teenth week -expect - result = meetup( - { - year: 2013, - month: 5, - week: Teenth, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-05-16") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 5, + week: Teenth, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-05-16") + result == expected +} # when teenth Thursday is the 13th, the first day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 6, - week: Teenth, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-06-13") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 6, + week: Teenth, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-06-13") + result == expected +} # when teenth Thursday is the 19th, the last day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 9, - week: Teenth, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-09-19") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 9, + week: Teenth, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-09-19") + result == expected +} # when teenth Friday is the 19th, the last day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 4, - week: Teenth, - day_of_week: Friday, - }, - ) - expected = Ok("2013-04-19") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Teenth, + day_of_week: Friday, + }, + ) + expected = Ok("2013-04-19") + result == expected +} # when teenth Friday is some day in the middle of the teenth week -expect - result = meetup( - { - year: 2013, - month: 8, - week: Teenth, - day_of_week: Friday, - }, - ) - expected = Ok("2013-08-16") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 8, + week: Teenth, + day_of_week: Friday, + }, + ) + expected = Ok("2013-08-16") + result == expected +} # when teenth Friday is the 13th, the first day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 9, - week: Teenth, - day_of_week: Friday, - }, - ) - expected = Ok("2013-09-13") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 9, + week: Teenth, + day_of_week: Friday, + }, + ) + expected = Ok("2013-09-13") + result == expected +} # when teenth Saturday is some day in the middle of the teenth week -expect - result = meetup( - { - year: 2013, - month: 2, - week: Teenth, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-02-16") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 2, + week: Teenth, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-02-16") + result == expected +} # when teenth Saturday is the 13th, the first day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 4, - week: Teenth, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-04-13") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Teenth, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-04-13") + result == expected +} # when teenth Saturday is the 19th, the last day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 10, - week: Teenth, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-10-19") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 10, + week: Teenth, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-10-19") + result == expected +} # when teenth Sunday is the 19th, the last day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 5, - week: Teenth, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-05-19") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 5, + week: Teenth, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-05-19") + result == expected +} # when teenth Sunday is some day in the middle of the teenth week -expect - result = meetup( - { - year: 2013, - month: 6, - week: Teenth, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-06-16") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 6, + week: Teenth, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-06-16") + result == expected +} # when teenth Sunday is the 13th, the first day of the teenth week -expect - result = meetup( - { - year: 2013, - month: 10, - week: Teenth, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-10-13") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 10, + week: Teenth, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-10-13") + result == expected +} # when first Monday is some day in the middle of the first week -expect - result = meetup( - { - year: 2013, - month: 3, - week: First, - day_of_week: Monday, - }, - ) - expected = Ok("2013-03-04") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: First, + day_of_week: Monday, + }, + ) + expected = Ok("2013-03-04") + result == expected +} # when first Monday is the 1st, the first day of the first week -expect - result = meetup( - { - year: 2013, - month: 4, - week: First, - day_of_week: Monday, - }, - ) - expected = Ok("2013-04-01") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: First, + day_of_week: Monday, + }, + ) + expected = Ok("2013-04-01") + result == expected +} # when first Tuesday is the 7th, the last day of the first week -expect - result = meetup( - { - year: 2013, - month: 5, - week: First, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-05-07") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 5, + week: First, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-05-07") + result == expected +} # when first Tuesday is some day in the middle of the first week -expect - result = meetup( - { - year: 2013, - month: 6, - week: First, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-06-04") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 6, + week: First, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-06-04") + result == expected +} # when first Wednesday is some day in the middle of the first week -expect - result = meetup( - { - year: 2013, - month: 7, - week: First, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-07-03") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 7, + week: First, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-07-03") + result == expected +} # when first Wednesday is the 7th, the last day of the first week -expect - result = meetup( - { - year: 2013, - month: 8, - week: First, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-08-07") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 8, + week: First, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-08-07") + result == expected +} # when first Thursday is some day in the middle of the first week -expect - result = meetup( - { - year: 2013, - month: 9, - week: First, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-09-05") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 9, + week: First, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-09-05") + result == expected +} # when first Thursday is another day in the middle of the first week -expect - result = meetup( - { - year: 2013, - month: 10, - week: First, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-10-03") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 10, + week: First, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-10-03") + result == expected +} # when first Friday is the 1st, the first day of the first week -expect - result = meetup( - { - year: 2013, - month: 11, - week: First, - day_of_week: Friday, - }, - ) - expected = Ok("2013-11-01") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 11, + week: First, + day_of_week: Friday, + }, + ) + expected = Ok("2013-11-01") + result == expected +} # when first Friday is some day in the middle of the first week -expect - result = meetup( - { - year: 2013, - month: 12, - week: First, - day_of_week: Friday, - }, - ) - expected = Ok("2013-12-06") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 12, + week: First, + day_of_week: Friday, + }, + ) + expected = Ok("2013-12-06") + result == expected +} # when first Saturday is some day in the middle of the first week -expect - result = meetup( - { - year: 2013, - month: 1, - week: First, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-01-05") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 1, + week: First, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-01-05") + result == expected +} # when first Saturday is another day in the middle of the first week -expect - result = meetup( - { - year: 2013, - month: 2, - week: First, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-02-02") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 2, + week: First, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-02-02") + result == expected +} # when first Sunday is some day in the middle of the first week -expect - result = meetup( - { - year: 2013, - month: 3, - week: First, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-03-03") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: First, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-03-03") + result == expected +} # when first Sunday is the 7th, the last day of the first week -expect - result = meetup( - { - year: 2013, - month: 4, - week: First, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-04-07") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: First, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-04-07") + result == expected +} # when second Monday is some day in the middle of the second week -expect - result = meetup( - { - year: 2013, - month: 3, - week: Second, - day_of_week: Monday, - }, - ) - expected = Ok("2013-03-11") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: Second, + day_of_week: Monday, + }, + ) + expected = Ok("2013-03-11") + result == expected +} # when second Monday is the 8th, the first day of the second week -expect - result = meetup( - { - year: 2013, - month: 4, - week: Second, - day_of_week: Monday, - }, - ) - expected = Ok("2013-04-08") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Second, + day_of_week: Monday, + }, + ) + expected = Ok("2013-04-08") + result == expected +} # when second Tuesday is the 14th, the last day of the second week -expect - result = meetup( - { - year: 2013, - month: 5, - week: Second, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-05-14") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 5, + week: Second, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-05-14") + result == expected +} # when second Tuesday is some day in the middle of the second week -expect - result = meetup( - { - year: 2013, - month: 6, - week: Second, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-06-11") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 6, + week: Second, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-06-11") + result == expected +} # when second Wednesday is some day in the middle of the second week -expect - result = meetup( - { - year: 2013, - month: 7, - week: Second, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-07-10") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 7, + week: Second, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-07-10") + result == expected +} # when second Wednesday is the 14th, the last day of the second week -expect - result = meetup( - { - year: 2013, - month: 8, - week: Second, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-08-14") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 8, + week: Second, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-08-14") + result == expected +} # when second Thursday is some day in the middle of the second week -expect - result = meetup( - { - year: 2013, - month: 9, - week: Second, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-09-12") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 9, + week: Second, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-09-12") + result == expected +} # when second Thursday is another day in the middle of the second week -expect - result = meetup( - { - year: 2013, - month: 10, - week: Second, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-10-10") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 10, + week: Second, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-10-10") + result == expected +} # when second Friday is the 8th, the first day of the second week -expect - result = meetup( - { - year: 2013, - month: 11, - week: Second, - day_of_week: Friday, - }, - ) - expected = Ok("2013-11-08") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 11, + week: Second, + day_of_week: Friday, + }, + ) + expected = Ok("2013-11-08") + result == expected +} # when second Friday is some day in the middle of the second week -expect - result = meetup( - { - year: 2013, - month: 12, - week: Second, - day_of_week: Friday, - }, - ) - expected = Ok("2013-12-13") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 12, + week: Second, + day_of_week: Friday, + }, + ) + expected = Ok("2013-12-13") + result == expected +} # when second Saturday is some day in the middle of the second week -expect - result = meetup( - { - year: 2013, - month: 1, - week: Second, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-01-12") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 1, + week: Second, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-01-12") + result == expected +} # when second Saturday is another day in the middle of the second week -expect - result = meetup( - { - year: 2013, - month: 2, - week: Second, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-02-09") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 2, + week: Second, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-02-09") + result == expected +} # when second Sunday is some day in the middle of the second week -expect - result = meetup( - { - year: 2013, - month: 3, - week: Second, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-03-10") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: Second, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-03-10") + result == expected +} # when second Sunday is the 14th, the last day of the second week -expect - result = meetup( - { - year: 2013, - month: 4, - week: Second, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-04-14") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Second, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-04-14") + result == expected +} # when third Monday is some day in the middle of the third week -expect - result = meetup( - { - year: 2013, - month: 3, - week: Third, - day_of_week: Monday, - }, - ) - expected = Ok("2013-03-18") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: Third, + day_of_week: Monday, + }, + ) + expected = Ok("2013-03-18") + result == expected +} # when third Monday is the 15th, the first day of the third week -expect - result = meetup( - { - year: 2013, - month: 4, - week: Third, - day_of_week: Monday, - }, - ) - expected = Ok("2013-04-15") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Third, + day_of_week: Monday, + }, + ) + expected = Ok("2013-04-15") + result == expected +} # when third Tuesday is the 21st, the last day of the third week -expect - result = meetup( - { - year: 2013, - month: 5, - week: Third, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-05-21") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 5, + week: Third, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-05-21") + result == expected +} # when third Tuesday is some day in the middle of the third week -expect - result = meetup( - { - year: 2013, - month: 6, - week: Third, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-06-18") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 6, + week: Third, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-06-18") + result == expected +} # when third Wednesday is some day in the middle of the third week -expect - result = meetup( - { - year: 2013, - month: 7, - week: Third, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-07-17") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 7, + week: Third, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-07-17") + result == expected +} # when third Wednesday is the 21st, the last day of the third week -expect - result = meetup( - { - year: 2013, - month: 8, - week: Third, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-08-21") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 8, + week: Third, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-08-21") + result == expected +} # when third Thursday is some day in the middle of the third week -expect - result = meetup( - { - year: 2013, - month: 9, - week: Third, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-09-19") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 9, + week: Third, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-09-19") + result == expected +} # when third Thursday is another day in the middle of the third week -expect - result = meetup( - { - year: 2013, - month: 10, - week: Third, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-10-17") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 10, + week: Third, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-10-17") + result == expected +} # when third Friday is the 15th, the first day of the third week -expect - result = meetup( - { - year: 2013, - month: 11, - week: Third, - day_of_week: Friday, - }, - ) - expected = Ok("2013-11-15") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 11, + week: Third, + day_of_week: Friday, + }, + ) + expected = Ok("2013-11-15") + result == expected +} # when third Friday is some day in the middle of the third week -expect - result = meetup( - { - year: 2013, - month: 12, - week: Third, - day_of_week: Friday, - }, - ) - expected = Ok("2013-12-20") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 12, + week: Third, + day_of_week: Friday, + }, + ) + expected = Ok("2013-12-20") + result == expected +} # when third Saturday is some day in the middle of the third week -expect - result = meetup( - { - year: 2013, - month: 1, - week: Third, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-01-19") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 1, + week: Third, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-01-19") + result == expected +} # when third Saturday is another day in the middle of the third week -expect - result = meetup( - { - year: 2013, - month: 2, - week: Third, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-02-16") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 2, + week: Third, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-02-16") + result == expected +} # when third Sunday is some day in the middle of the third week -expect - result = meetup( - { - year: 2013, - month: 3, - week: Third, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-03-17") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: Third, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-03-17") + result == expected +} # when third Sunday is the 21st, the last day of the third week -expect - result = meetup( - { - year: 2013, - month: 4, - week: Third, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-04-21") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Third, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-04-21") + result == expected +} # when fourth Monday is some day in the middle of the fourth week -expect - result = meetup( - { - year: 2013, - month: 3, - week: Fourth, - day_of_week: Monday, - }, - ) - expected = Ok("2013-03-25") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: Fourth, + day_of_week: Monday, + }, + ) + expected = Ok("2013-03-25") + result == expected +} # when fourth Monday is the 22nd, the first day of the fourth week -expect - result = meetup( - { - year: 2013, - month: 4, - week: Fourth, - day_of_week: Monday, - }, - ) - expected = Ok("2013-04-22") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Fourth, + day_of_week: Monday, + }, + ) + expected = Ok("2013-04-22") + result == expected +} # when fourth Tuesday is the 28th, the last day of the fourth week -expect - result = meetup( - { - year: 2013, - month: 5, - week: Fourth, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-05-28") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 5, + week: Fourth, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-05-28") + result == expected +} # when fourth Tuesday is some day in the middle of the fourth week -expect - result = meetup( - { - year: 2013, - month: 6, - week: Fourth, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-06-25") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 6, + week: Fourth, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-06-25") + result == expected +} # when fourth Wednesday is some day in the middle of the fourth week -expect - result = meetup( - { - year: 2013, - month: 7, - week: Fourth, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-07-24") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 7, + week: Fourth, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-07-24") + result == expected +} # when fourth Wednesday is the 28th, the last day of the fourth week -expect - result = meetup( - { - year: 2013, - month: 8, - week: Fourth, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-08-28") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 8, + week: Fourth, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-08-28") + result == expected +} # when fourth Thursday is some day in the middle of the fourth week -expect - result = meetup( - { - year: 2013, - month: 9, - week: Fourth, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-09-26") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 9, + week: Fourth, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-09-26") + result == expected +} # when fourth Thursday is another day in the middle of the fourth week -expect - result = meetup( - { - year: 2013, - month: 10, - week: Fourth, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-10-24") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 10, + week: Fourth, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-10-24") + result == expected +} # when fourth Friday is the 22nd, the first day of the fourth week -expect - result = meetup( - { - year: 2013, - month: 11, - week: Fourth, - day_of_week: Friday, - }, - ) - expected = Ok("2013-11-22") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 11, + week: Fourth, + day_of_week: Friday, + }, + ) + expected = Ok("2013-11-22") + result == expected +} # when fourth Friday is some day in the middle of the fourth week -expect - result = meetup( - { - year: 2013, - month: 12, - week: Fourth, - day_of_week: Friday, - }, - ) - expected = Ok("2013-12-27") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 12, + week: Fourth, + day_of_week: Friday, + }, + ) + expected = Ok("2013-12-27") + result == expected +} # when fourth Saturday is some day in the middle of the fourth week -expect - result = meetup( - { - year: 2013, - month: 1, - week: Fourth, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-01-26") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 1, + week: Fourth, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-01-26") + result == expected +} # when fourth Saturday is another day in the middle of the fourth week -expect - result = meetup( - { - year: 2013, - month: 2, - week: Fourth, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-02-23") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 2, + week: Fourth, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-02-23") + result == expected +} # when fourth Sunday is some day in the middle of the fourth week -expect - result = meetup( - { - year: 2013, - month: 3, - week: Fourth, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-03-24") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: Fourth, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-03-24") + result == expected +} # when fourth Sunday is the 28th, the last day of the fourth week -expect - result = meetup( - { - year: 2013, - month: 4, - week: Fourth, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-04-28") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Fourth, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-04-28") + result == expected +} # last Monday in a month with four Mondays -expect - result = meetup( - { - year: 2013, - month: 3, - week: Last, - day_of_week: Monday, - }, - ) - expected = Ok("2013-03-25") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: Last, + day_of_week: Monday, + }, + ) + expected = Ok("2013-03-25") + result == expected +} # last Monday in a month with five Mondays -expect - result = meetup( - { - year: 2013, - month: 4, - week: Last, - day_of_week: Monday, - }, - ) - expected = Ok("2013-04-29") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Last, + day_of_week: Monday, + }, + ) + expected = Ok("2013-04-29") + result == expected +} # last Tuesday in a month with four Tuesdays -expect - result = meetup( - { - year: 2013, - month: 5, - week: Last, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-05-28") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 5, + week: Last, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-05-28") + result == expected +} # last Tuesday in another month with four Tuesdays -expect - result = meetup( - { - year: 2013, - month: 6, - week: Last, - day_of_week: Tuesday, - }, - ) - expected = Ok("2013-06-25") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 6, + week: Last, + day_of_week: Tuesday, + }, + ) + expected = Ok("2013-06-25") + result == expected +} # last Wednesday in a month with five Wednesdays -expect - result = meetup( - { - year: 2013, - month: 7, - week: Last, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-07-31") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 7, + week: Last, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-07-31") + result == expected +} # last Wednesday in a month with four Wednesdays -expect - result = meetup( - { - year: 2013, - month: 8, - week: Last, - day_of_week: Wednesday, - }, - ) - expected = Ok("2013-08-28") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 8, + week: Last, + day_of_week: Wednesday, + }, + ) + expected = Ok("2013-08-28") + result == expected +} # last Thursday in a month with four Thursdays -expect - result = meetup( - { - year: 2013, - month: 9, - week: Last, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-09-26") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 9, + week: Last, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-09-26") + result == expected +} # last Thursday in a month with five Thursdays -expect - result = meetup( - { - year: 2013, - month: 10, - week: Last, - day_of_week: Thursday, - }, - ) - expected = Ok("2013-10-31") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 10, + week: Last, + day_of_week: Thursday, + }, + ) + expected = Ok("2013-10-31") + result == expected +} # last Friday in a month with five Fridays -expect - result = meetup( - { - year: 2013, - month: 11, - week: Last, - day_of_week: Friday, - }, - ) - expected = Ok("2013-11-29") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 11, + week: Last, + day_of_week: Friday, + }, + ) + expected = Ok("2013-11-29") + result == expected +} # last Friday in a month with four Fridays -expect - result = meetup( - { - year: 2013, - month: 12, - week: Last, - day_of_week: Friday, - }, - ) - expected = Ok("2013-12-27") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 12, + week: Last, + day_of_week: Friday, + }, + ) + expected = Ok("2013-12-27") + result == expected +} # last Saturday in a month with four Saturdays -expect - result = meetup( - { - year: 2013, - month: 1, - week: Last, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-01-26") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 1, + week: Last, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-01-26") + result == expected +} # last Saturday in another month with four Saturdays -expect - result = meetup( - { - year: 2013, - month: 2, - week: Last, - day_of_week: Saturday, - }, - ) - expected = Ok("2013-02-23") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 2, + week: Last, + day_of_week: Saturday, + }, + ) + expected = Ok("2013-02-23") + result == expected +} # last Sunday in a month with five Sundays -expect - result = meetup( - { - year: 2013, - month: 3, - week: Last, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-03-31") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 3, + week: Last, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-03-31") + result == expected +} # last Sunday in a month with four Sundays -expect - result = meetup( - { - year: 2013, - month: 4, - week: Last, - day_of_week: Sunday, - }, - ) - expected = Ok("2013-04-28") - result == expected +expect { + result = meetup( + { + year: 2013, + month: 4, + week: Last, + day_of_week: Sunday, + }, + ) + expected = Ok("2013-04-28") + result == expected +} # when last Wednesday in February in a leap year is the 29th -expect - result = meetup( - { - year: 2012, - month: 2, - week: Last, - day_of_week: Wednesday, - }, - ) - expected = Ok("2012-02-29") - result == expected +expect { + result = meetup( + { + year: 2012, + month: 2, + week: Last, + day_of_week: Wednesday, + }, + ) + expected = Ok("2012-02-29") + result == expected +} # last Wednesday in December that is also the last day of the year -expect - result = meetup( - { - year: 2014, - month: 12, - week: Last, - day_of_week: Wednesday, - }, - ) - expected = Ok("2014-12-31") - result == expected +expect { + result = meetup( + { + year: 2014, + month: 12, + week: Last, + day_of_week: Wednesday, + }, + ) + expected = Ok("2014-12-31") + result == expected +} # when last Sunday in February in a non-leap year is not the 29th -expect - result = meetup( - { - year: 2015, - month: 2, - week: Last, - day_of_week: Sunday, - }, - ) - expected = Ok("2015-02-22") - result == expected +expect { + result = meetup( + { + year: 2015, + month: 2, + week: Last, + day_of_week: Sunday, + }, + ) + expected = Ok("2015-02-22") + result == expected +} # when first Friday is the 7th, the last day of the first week -expect - result = meetup( - { - year: 2012, - month: 12, - week: First, - day_of_week: Friday, - }, - ) - expected = Ok("2012-12-07") - result == expected +expect { + result = meetup( + { + year: 2012, + month: 12, + week: First, + day_of_week: Friday, + }, + ) + expected = Ok("2012-12-07") + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/micro-blog/.meta/Example.roc b/exercises/practice/micro-blog/.meta/Example.roc index 8c5d2260..e9be92f8 100644 --- a/exercises/practice/micro-blog/.meta/Example.roc +++ b/exercises/practice/micro-blog/.meta/Example.roc @@ -1,6 +1,15 @@ -module [truncate] + import unicode.Grapheme +MicroBlog :: {}.{ + truncate : Str -> Result Str GraphemeErrors + truncate = |input| + input + |> Grapheme.split? + |> List.take_first(5) + |> Str.join_with("") + |> Ok +} GraphemeErrors : [ CodepointTooLarge, @@ -10,11 +19,3 @@ GraphemeErrors : [ ListWasEmpty, OverlongEncoding, ] - -truncate : Str -> Result Str GraphemeErrors -truncate = |input| - input - |> Grapheme.split? - |> List.take_first(5) - |> Str.join_with("") - |> Ok diff --git a/exercises/practice/micro-blog/.meta/template.j2 b/exercises/practice/micro-blog/.meta/template.j2 index 32cb0476..4c6ab6a5 100644 --- a/exercises/practice/micro-blog/.meta/template.j2 +++ b/exercises/practice/micro-blog/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["phrase"] | to_roc }}) result == Ok({{ case["expected"] | to_roc }}) +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/micro-blog/MicroBlog.roc b/exercises/practice/micro-blog/MicroBlog.roc index f8b7118c..96d1c3c8 100644 --- a/exercises/practice/micro-blog/MicroBlog.roc +++ b/exercises/practice/micro-blog/MicroBlog.roc @@ -1,7 +1,10 @@ -module [truncate] + import unicode.Grapheme -truncate : Str -> Result Str _ -truncate = |input| - crash("Please implement 'truncate'") +MicroBlog :: {}.{ + truncate : Str -> Try(Str, _) + truncate = |input| { + crash "Please implement the 'truncate' function" + } +} diff --git a/exercises/practice/micro-blog/micro-blog-test.roc b/exercises/practice/micro-blog/micro-blog-test.roc index 04dbf0dd..cb0515a0 100644 --- a/exercises/practice/micro-blog/micro-blog-test.roc +++ b/exercises/practice/micro-blog/micro-blog-test.roc @@ -1,75 +1,88 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/micro-blog/canonical-data.json -# File last updated on 2025-09-15 +# File last updated on 2026-06-21 app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", - unicode: "https://github.com/roc-lang/unicode/releases/download/0.3.0/9KKFsA4CdOz0JIOL7iBSI_2jGIXQ6TsFBXgd086idpY.tar.br", + pf: platform "https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/download/0.9/8GdFEvQYS3TeAZxKvTzCLVdQiomweGtXcdZkXNDEeABq.tar.zst", + unicode: "https://github.com/roc-lang/unicode/...", # TODO: update when a zig-compatible release is available } import pf.Stdout -main! = |_args| - Stdout.line!("") - import MicroBlog exposing [truncate] # English language short -expect - result = truncate("Hi") - result == Ok("Hi") +expect { + result = truncate("Hi") + result == Ok("Hi") +} # English language long -expect - result = truncate("Hello there") - result == Ok("Hello") +expect { + result = truncate("Hello there") + result == Ok("Hello") +} # German language short (broth) -expect - result = truncate("brühe") - result == Ok("brühe") +expect { + result = truncate("brühe") + result == Ok("brühe") +} # German language long (bear carpet → beards) -expect - result = truncate("Bärteppich") - result == Ok("Bärte") +expect { + result = truncate("Bärteppich") + result == Ok("Bärte") +} # Bulgarian language short (good) -expect - result = truncate("Добър") - result == Ok("Добър") +expect { + result = truncate("Добър") + result == Ok("Добър") +} # Greek language short (health) -expect - result = truncate("υγειά") - result == Ok("υγειά") +expect { + result = truncate("υγειά") + result == Ok("υγειά") +} # Maths short -expect - result = truncate("a=πr²") - result == Ok("a=πr²") +expect { + result = truncate("a=πr²") + result == Ok("a=πr²") +} # Maths long -expect - result = truncate("∅⊊ℕ⊊ℤ⊊ℚ⊊ℝ⊊ℂ") - result == Ok("∅⊊ℕ⊊ℤ") +expect { + result = truncate("∅⊊ℕ⊊ℤ⊊ℚ⊊ℝ⊊ℂ") + result == Ok("∅⊊ℕ⊊ℤ") +} # English and emoji short -expect - result = truncate("Fly 🛫") - result == Ok("Fly 🛫") +expect { + result = truncate("Fly 🛫") + result == Ok("Fly 🛫") +} # Emoji short -expect - result = truncate("💇") - result == Ok("💇") +expect { + result = truncate("💇") + result == Ok("💇") +} # Emoji long -expect - result = truncate("❄🌡🤧🤒🏥🕰😀") - result == Ok("❄🌡🤧🤒🏥") +expect { + result = truncate("❄🌡🤧🤒🏥🕰😀") + result == Ok("❄🌡🤧🤒🏥") +} # Royal Flush? -expect - result = truncate("🃎🂸🃅🃋🃍🃁🃊") - result == Ok("🃎🂸🃅🃋🃍") +expect { + result = truncate("🃎🂸🃅🃋🃍🃁🃊") + result == Ok("🃎🂸🃅🃋🃍") +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/minesweeper/.meta/Example.roc b/exercises/practice/minesweeper/.meta/Example.roc index 15c2b2ad..1a9693a9 100644 --- a/exercises/practice/minesweeper/.meta/Example.roc +++ b/exercises/practice/minesweeper/.meta/Example.roc @@ -1,56 +1,74 @@ -module [annotate] +Minesweeper :: {}.{ + annotate : Str -> Str + annotate = |minefield| { + rows = minefield.to_utf8().split_on('\n') + annotated = + rows.map_with_index( + |row, y| { + row.map_with_index( + |cell, x| { + if cell == '*' { + '*' + } else { + match count_neighbors(rows, x, y) { + 0 => ' ' + n => '0' + n + } + } + }, + ) + ->Str.from_utf8() + }, + ) -is_bomb : List (List U8), I64, I64 -> Result Bool [OutOfBounds] -is_bomb = |rows, nx, ny| - x = Num.to_u64_checked(nx)? - y = Num.to_u64_checked(ny)? - rows - |> List.get(y)? - |> List.get(x)? - |> Bool.is_eq('*') - |> Ok + annotated + .map( + |maybe_row| { + match maybe_row { + Ok(row) => row + Err(_) => { + crash "Unreachable" + } + } + }, + ) + ->Str.join_with("\n") + } +} -count_neighbors : List (List U8), U64, U64 -> Num * -count_neighbors = |rows, x, y| - [-1, 0, 1] - |> List.map( - |dy| - [-1, 0, 1] - |> List.map( - |dx| - when is_bomb(rows, (Num.to_i64(x) + dx), (Num.to_i64(y) + dy)) is - Ok(bomb) -> if bomb then 1 else 0 - Err(OutOfBounds) -> 0, - ) - |> List.sum, - ) - |> List.sum +is_bomb : List(List(U8)), I64, I64 -> Try(Bool, [OutOfBounds]) +is_bomb = |rows, nx, ny| { + if nx < 0 or ny < 0 { + Err(OutOfBounds) + } else { + x = nx.to_u64_try() ?? 0 + y = ny.to_u64_try() ?? 0 + row = rows.get(y)? + cell = row.get(x)? + Ok(cell == '*') + } +} -annotate : Str -> Str -annotate = |minefield| - rows = minefield |> Str.to_utf8 |> List.split_on('\n') - annotated = - rows - |> List.map_with_index( - |row, y| - row - |> List.map_with_index( - |cell, x| - if cell == '*' then - '*' - else - when count_neighbors(rows, x, y) is - 0 -> ' ' - n -> '0' + n, - ) - |> Str.from_utf8, - ) - - annotated - |> List.map( - |maybe_row| - when maybe_row is - Ok(row) -> row - Err(_) -> crash("Unreachable"), - ) # fromUtf8 cannot fail in the code above - |> Str.join_with("\n") +count_neighbors : List(List(U8)), U64, U64 -> U8 +count_neighbors = |rows, x, y| { + [-1, 0, 1] + .map( + |dy| { + [-1, 0, 1] + .map( + |dx| { + match is_bomb(rows, ((x.to_i64_try() ?? 0) + dx), ((y.to_i64_try() ?? 0) + dy)) { + Ok(bomb) => if bomb { + 1 + } else { + 0 + } + Err(OutOfBounds) => 0 + } + }, + ) + .sum() + }, + ) + .sum() +} diff --git a/exercises/practice/minesweeper/.meta/template.j2 b/exercises/practice/minesweeper/.meta/template.j2 index 7e04ffab..9c8e1820 100644 --- a/exercises/practice/minesweeper/.meta/template.j2 +++ b/exercises/practice/minesweeper/.meta/template.j2 @@ -6,10 +6,13 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect - minefield = {{ case["input"]["minefield"] | to_roc_multiline_string | replace(" ", "·") | indent(8) }} |> Str.replace_each("·", " ") +expect { + minefield = {{ case["input"]["minefield"] | to_roc_multiline_string | indent(8) }} result = {{ case["property"] | to_snake }}(minefield) - expected = {{ case["expected"] | to_roc_multiline_string | replace(" ", "·") | indent(8) }} |> Str.replace_each("·", " ") + expected = {{ case["expected"] | to_roc_multiline_string | indent(8) }} result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/minesweeper/Minesweeper.roc b/exercises/practice/minesweeper/Minesweeper.roc index d965e113..bec7ed30 100644 --- a/exercises/practice/minesweeper/Minesweeper.roc +++ b/exercises/practice/minesweeper/Minesweeper.roc @@ -1,5 +1,6 @@ -module [annotate] - -annotate : Str -> Str -annotate = |minefield| - crash("Please implement the 'annotate' function") +Minesweeper :: {}.{ + annotate : Str -> Str + annotate = |minefield| { + crash "Please implement the 'annotate' function" + } +} diff --git a/exercises/practice/minesweeper/minesweeper-test.roc b/exercises/practice/minesweeper/minesweeper-test.roc index 80a7e710..412ba338 100644 --- a/exercises/practice/minesweeper/minesweeper-test.roc +++ b/exercises/practice/minesweeper/minesweeper-test.roc @@ -1,212 +1,188 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/minesweeper/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Minesweeper exposing [annotate] # no rows -expect - minefield = "" |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = "" |> Str.replace_each("·", " ") - result == expected +expect { + minefield = "" + result = annotate(minefield) + expected = "" + result == expected +} # no columns -expect - minefield = "" |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = "" |> Str.replace_each("·", " ") - result == expected +expect { + minefield = "" + result = annotate(minefield) + expected = "" + result == expected +} # no mines -expect - minefield = - """ - ··· - ··· - ··· - """ - |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = - """ - ··· - ··· - ··· - """ - |> Str.replace_each("·", " ") - result == expected +expect { + minefield = + \\ + \\ + \\ + + result = annotate(minefield) + expected = + \\ + \\ + \\ + + result == expected +} # minefield with only mines -expect - minefield = - """ - *** - *** - *** - """ - |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = - """ - *** - *** - *** - """ - |> Str.replace_each("·", " ") - result == expected +expect { + minefield = + \\*** + \\*** + \\*** + + result = annotate(minefield) + expected = + \\*** + \\*** + \\*** + + result == expected +} # mine surrounded by spaces -expect - minefield = - """ - ··· - ·*· - ··· - """ - |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = - """ - 111 - 1*1 - 111 - """ - |> Str.replace_each("·", " ") - result == expected +expect { + minefield = + \\ + \\ * + \\ + + result = annotate(minefield) + expected = + \\111 + \\1*1 + \\111 + + result == expected +} # space surrounded by mines -expect - minefield = - """ - *** - *·* - *** - """ - |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = - """ - *** - *8* - *** - """ - |> Str.replace_each("·", " ") - result == expected +expect { + minefield = + \\*** + \\* * + \\*** + + result = annotate(minefield) + expected = + \\*** + \\*8* + \\*** + + result == expected +} # horizontal line -expect - minefield = "·*·*·" |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = "1*2*1" |> Str.replace_each("·", " ") - result == expected +expect { + minefield = " * * " + result = annotate(minefield) + expected = "1*2*1" + result == expected +} # horizontal line, mines at edges -expect - minefield = "*···*" |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = "*1·1*" |> Str.replace_each("·", " ") - result == expected +expect { + minefield = "* *" + result = annotate(minefield) + expected = "*1 1*" + result == expected +} # vertical line -expect - minefield = - """ - · - * - · - * - · - """ - |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = - """ - 1 - * - 2 - * - 1 - """ - |> Str.replace_each("·", " ") - result == expected +expect { + minefield = + \\ + \\* + \\ + \\* + \\ + + result = annotate(minefield) + expected = + \\1 + \\* + \\2 + \\* + \\1 + + result == expected +} # vertical line, mines at edges -expect - minefield = - """ - * - · - · - · - * - """ - |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = - """ - * - 1 - · - 1 - * - """ - |> Str.replace_each("·", " ") - result == expected +expect { + minefield = + \\* + \\ + \\ + \\ + \\* + + result = annotate(minefield) + expected = + \\* + \\1 + \\ + \\1 + \\* + + result == expected +} # cross -expect - minefield = - """ - ··*·· - ··*·· - ***** - ··*·· - ··*·· - """ - |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = - """ - ·2*2· - 25*52 - ***** - 25*52 - ·2*2· - """ - |> Str.replace_each("·", " ") - result == expected +expect { + minefield = + \\ * + \\ * + \\***** + \\ * + \\ * + + result = annotate(minefield) + expected = + \\ 2*2 + \\25*52 + \\***** + \\25*52 + \\ 2*2 + + result == expected +} # large minefield -expect - minefield = - """ - ·*··*· - ··*··· - ····*· - ···*·* - ·*··*· - ······ - """ - |> Str.replace_each("·", " ") - result = annotate(minefield) - expected = - """ - 1*22*1 - 12*322 - ·123*2 - 112*4* - 1*22*2 - 111111 - """ - |> Str.replace_each("·", " ") - result == expected +expect { + minefield = + \\ * * + \\ * + \\ * + \\ * * + \\ * * + \\ + + result = annotate(minefield) + expected = + \\1*22*1 + \\12*322 + \\ 123*2 + \\112*4* + \\1*22*2 + \\111111 + + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/nth-prime/.meta/Example.roc b/exercises/practice/nth-prime/.meta/Example.roc index 60dd0e3a..096a0e5c 100644 --- a/exercises/practice/nth-prime/.meta/Example.roc +++ b/exercises/practice/nth-prime/.meta/Example.roc @@ -1,28 +1,33 @@ -module [prime] - -prime : U64 -> Result U64 [NoPrime0] -prime = |number| - if number == 0 then - Err(NoPrime0) - else if number == 1 then - Ok(2) - else if number == 2 then - Ok(3) - else - find_prime = |primes, index| - if List.len(primes) == number then - primes - else - next_index = index + 2 - new_primes = - if primes |> List.any(|p| next_index % p == 0) then - primes - else - primes |> List.append(next_index) - find_prime(new_primes, next_index) - find_prime([2, 3, 5], 5) - |> List.last - |> Result.map_err( - |_| - crash("Unreachable: list cannot be empty"), - ) +NthPrime :: {}.{ + prime : U64 -> Try(U64, [NoPrime0]) + prime = |number| { + if number == 0 { + Err(NoPrime0) + } else if number == 1 { + Ok(2) + } else { + find_prime = |primes, index| { + if primes.len() == number { + primes + } else { + next_index = index + 2 + new_primes = { + if primes.any(|p| next_index % p == 0) { + primes + } else { + primes.append(next_index) + } + } + find_prime(new_primes, next_index) + } + } + find_prime([2, 3], 3) + .last() + .map_err( + |_| { + crash "Unreachable: list cannot be empty" + }, + ) + } + } +} diff --git a/exercises/practice/nth-prime/.meta/template.j2 b/exercises/practice/nth-prime/.meta/template.j2 index e815e36b..a90aa4ec 100644 --- a/exercises/practice/nth-prime/.meta/template.j2 +++ b/exercises/practice/nth-prime/.meta/template.j2 @@ -6,12 +6,15 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["number"] }}) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} result == Ok({{ case["expected"] }}) {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/nth-prime/NthPrime.roc b/exercises/practice/nth-prime/NthPrime.roc index 51865d64..7673807b 100644 --- a/exercises/practice/nth-prime/NthPrime.roc +++ b/exercises/practice/nth-prime/NthPrime.roc @@ -1,5 +1,6 @@ -module [prime] - -prime : U64 -> Result U64 _ -prime = |number| - crash("Please implement the 'prime' function") +NthPrime :: {}.{ + prime : U64 -> Try(U64, _) + prime = |number| { + crash "Please implement the 'prime' function" + } +} diff --git a/exercises/practice/nth-prime/nth-prime-test.roc b/exercises/practice/nth-prime/nth-prime-test.roc index 1d6c324c..dff8432d 100644 --- a/exercises/practice/nth-prime/nth-prime-test.roc +++ b/exercises/practice/nth-prime/nth-prime-test.roc @@ -1,39 +1,40 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/nth-prime/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import NthPrime exposing [prime] # first prime -expect - result = prime(1) - result == Ok(2) +expect { + result = prime(1) + result == Ok(2) +} # second prime -expect - result = prime(2) - result == Ok(3) +expect { + result = prime(2) + result == Ok(3) +} -# sixth prime -expect - result = prime(6) - result == Ok(13) +# sixth primes +expect { + result = prime(6) + result == Ok(13) +} # big prime -expect - result = prime(10001) - result == Ok(104743) +expect { + result = prime(10001) + result == Ok(104743) +} # there is no zeroth prime -expect - result = prime(0) - result |> Result.is_err +expect { + result = prime(0) + 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/nucleotide-count/.meta/Example.roc b/exercises/practice/nucleotide-count/.meta/Example.roc index ca48c467..07a5c335 100644 --- a/exercises/practice/nucleotide-count/.meta/Example.roc +++ b/exercises/practice/nucleotide-count/.meta/Example.roc @@ -1,15 +1,24 @@ -module [nucleotide_counts] - -nucleotide_counts : Str -> Result { a : U64, c : U64, g : U64, t : U64 } _ -nucleotide_counts = |input| - Str.to_utf8(input) - |> List.walk_try( - { a: 0, c: 0, g: 0, t: 0 }, - |state, elem| - when elem is - 'A' -> Ok({ state & a: state.a + 1 }) - 'C' -> Ok({ state & c: state.c + 1 }) - 'G' -> Ok({ state & g: state.g + 1 }) - 'T' -> Ok({ state & t: state.t + 1 }) - _ -> Err(InvalidNucleotide(elem)), - ) +NucleotideCount :: {}.{ + nucleotide_counts : Str -> Try({ a : U64, c : U64, g : U64, t : U64 }, [InvalidNucleotide(U8)]) + nucleotide_counts = |input| { + input + .to_utf8() + .fold_until( + Ok({ a: 0, c: 0, g: 0, t: 0 }), + |state_res, elem| { + match state_res { + Ok(state) => { + match elem { + 'A' => Continue(Ok({ ..state, a: state.a + 1 })) + 'C' => Continue(Ok({ ..state, c: state.c + 1 })) + 'G' => Continue(Ok({ ..state, g: state.g + 1 })) + 'T' => Continue(Ok({ ..state, t: state.t + 1 })) + _ => Break(Err(InvalidNucleotide(elem))) + } + } + Err(err) => Break(Err(err)) + } + }, + ) + } +} diff --git a/exercises/practice/nucleotide-count/.meta/template.j2 b/exercises/practice/nucleotide-count/.meta/template.j2 index 0594f9d9..4cbccb9f 100644 --- a/exercises/practice/nucleotide-count/.meta/template.j2 +++ b/exercises/practice/nucleotide-count/.meta/template.j2 @@ -6,12 +6,15 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["strand"] | to_roc }}) {%- if case["expected"]["error"] %} - Result.is_err(result) + result.is_err() {%- else %} result == Ok({{ case["expected"] | to_roc }}) {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/nucleotide-count/NucleotideCount.roc b/exercises/practice/nucleotide-count/NucleotideCount.roc index cb009330..2b1b0f90 100644 --- a/exercises/practice/nucleotide-count/NucleotideCount.roc +++ b/exercises/practice/nucleotide-count/NucleotideCount.roc @@ -1,5 +1,6 @@ -module [nucleotide_counts] - -nucleotide_counts : Str -> Result { a : U64, c : U64, g : U64, t : U64 } _ -nucleotide_counts = |input| - crash("Please implement 'nucleotide_counts'") +NucleotideCount :: {}.{ + nucleotide_counts : Str -> Try({ a : U64, c : U64, g : U64, t : U64 }, [InvalidNucleotide(U8)]) + nucleotide_counts = |input| { + crash "Please implement the 'nucleotide_counts' function" + } +} diff --git a/exercises/practice/nucleotide-count/nucleotide-count-test.roc b/exercises/practice/nucleotide-count/nucleotide-count-test.roc index fcb82444..f38fa125 100644 --- a/exercises/practice/nucleotide-count/nucleotide-count-test.roc +++ b/exercises/practice/nucleotide-count/nucleotide-count-test.roc @@ -1,39 +1,40 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/nucleotide-count/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import NucleotideCount exposing [nucleotide_counts] # empty strand -expect - result = nucleotide_counts("") - result == Ok({ a: 0, c: 0, g: 0, t: 0 }) +expect { + result = nucleotide_counts("") + result == Ok({ a: 0, c: 0, g: 0, t: 0 }) +} # can count one nucleotide in single-character input -expect - result = nucleotide_counts("G") - result == Ok({ a: 0, c: 0, g: 1, t: 0 }) +expect { + result = nucleotide_counts("G") + result == Ok({ a: 0, c: 0, g: 1, t: 0 }) +} # strand with repeated nucleotide -expect - result = nucleotide_counts("GGGGGGG") - result == Ok({ a: 0, c: 0, g: 7, t: 0 }) +expect { + result = nucleotide_counts("GGGGGGG") + result == Ok({ a: 0, c: 0, g: 7, t: 0 }) +} # strand with multiple nucleotides -expect - result = nucleotide_counts("AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC") - result == Ok({ a: 20, c: 12, g: 17, t: 21 }) +expect { + result = nucleotide_counts("AGCTTTTCATTCTGACTGCAACGGGCAATATGTCTCTGTGTGGATTAAAAAAAGAGTGTCTGATAGCAGC") + result == Ok({ a: 20, c: 12, g: 17, t: 21 }) +} # strand with invalid nucleotides -expect - result = nucleotide_counts("AGXXACT") - Result.is_err(result) +expect { + result = nucleotide_counts("AGXXACT") + 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/ocr-numbers/.meta/Example.roc b/exercises/practice/ocr-numbers/.meta/Example.roc index e87e926f..7b51a707 100644 --- a/exercises/practice/ocr-numbers/.meta/Example.roc +++ b/exercises/practice/ocr-numbers/.meta/Example.roc @@ -1,77 +1,110 @@ -module [convert] +OcrNumbers :: {}.{ + convert : Str -> Try(Str, BadGridSize) + convert = |grid| { + if grid == "" { + Ok("") + } else { + grid_chars = grid.to_utf8().split_on('\n') + size = check_size(grid_chars)? + digits_str = + grid_chars + ->chunks_of(4) + .map( + |row_group| { + get_digit_grids(row_group, size.width) + .map(identify_digit) + ->Str.join_with("") + }, + ) + ->Str.join_with(",") + Ok(digits_str) + } + } +} BadGridSize : [HeightWasNotAMultipleOf4, WidthWasNotAMultipleOf3, GridShapeWasNotRectangular] -convert : Str -> Result Str BadGridSize -convert = |grid| - if grid == "" then - Ok("") - else - grid_chars = grid |> Str.to_utf8 |> List.split_on('\n') - size = check_size(grid_chars)? - grid_chars - |> List.chunks_of(4) # split vertically into groups of 4 rows - |> List.map( - |row_group| - get_digit_grids(row_group, size.width) - |> List.map(identify_digit) - |> Str.join_with(""), - ) - |> Str.join_with(",") - |> Ok - -check_size : List (List U8) -> Result { height : U64, width : U64 } BadGridSize -check_size = |grid_chars| - height = List.len(grid_chars) - if height % 4 != 0 then - Err(HeightWasNotAMultipleOf4) - else - width = grid_chars |> List.map(List.len) |> List.max |> Result.with_default(0) - if width % 3 != 0 then - Err(WidthWasNotAMultipleOf3) - else - is_rectangular = grid_chars |> List.all(|row| List.len(row) == width) - if is_rectangular then - Ok({ height, width }) - else - Err(GridShapeWasNotRectangular) +check_size : List(List(U8)) -> Try({ height : U64, width : U64 }, BadGridSize) +check_size = |grid_chars| { + height = grid_chars.len() + if height % 4 != 0 { + Err(HeightWasNotAMultipleOf4) + } else { + width = grid_chars.map(List.len).fold( + 0, + |max_val, len| if len > max_val { + len + } else { + max_val + }, + ) + if width % 3 != 0 { + Err(WidthWasNotAMultipleOf3) + } else { + is_rectangular = grid_chars.all(|row| row.len() == width) + if is_rectangular { + Ok({ height, width }) + } else { + Err(GridShapeWasNotRectangular) + } + } + } +} ## Given four rows from the full grid, return the 3x4 grid for each digit -get_digit_grids : List (List U8), U64 -> List (List (List U8)) -get_digit_grids = |row_group, full_grid_width| - chunked_rows = - row_group - |> List.map( - |row| - row |> List.chunks_of(3), - ) - num_horizontal_chunks = full_grid_width // 3 - List.range({ start: At(0), end: Before(num_horizontal_chunks) }) - |> List.map( - |chunk_index| - chunked_rows - |> List.map( - |chunked_row| - when chunked_row |> List.get(chunk_index) is - Ok(chunk) -> chunk - Err(OutOfBounds) -> crash("Unreachable: we checked the grid size"), - ), - ) +get_digit_grids : List(List(U8)), U64 -> List(List(List(U8))) +get_digit_grids = |row_group, full_grid_width| { + chunked_rows = row_group.map(|row| row->chunks_of(3)) + num_horizontal_chunks = full_grid_width // 3 + (0.. chunk + Err(OutOfBounds) => { + crash "Unreachable: we checked the grid size" + } + } + }, + ) + }, + ) + ->List.from_iter() +} + +identify_digit : List(List(U8)) -> Str +identify_digit = |digit_grid| { + match digit_grid { + [[' ', '_', ' '], ['|', ' ', '|'], ['|', '_', '|'], [' ', ' ', ' ']] => "0" + [[' ', ' ', ' '], [' ', ' ', '|'], [' ', ' ', '|'], [' ', ' ', ' ']] => "1" + [[' ', '_', ' '], [' ', '_', '|'], ['|', '_', ' '], [' ', ' ', ' ']] => "2" + [[' ', '_', ' '], [' ', '_', '|'], [' ', '_', '|'], [' ', ' ', ' ']] => "3" + [[' ', ' ', ' '], ['|', '_', '|'], [' ', ' ', '|'], [' ', ' ', ' ']] => "4" + [[' ', '_', ' '], ['|', '_', ' '], [' ', '_', '|'], [' ', ' ', ' ']] => "5" + [[' ', '_', ' '], ['|', '_', ' '], ['|', '_', '|'], [' ', ' ', ' ']] => "6" + [[' ', '_', ' '], [' ', ' ', '|'], [' ', ' ', '|'], [' ', ' ', ' ']] => "7" + [[' ', '_', ' '], ['|', '_', '|'], ['|', '_', '|'], [' ', ' ', ' ']] => "8" + [[' ', '_', ' '], ['|', '_', '|'], [' ', '_', '|'], [' ', ' ', ' ']] => "9" + _ => "?" + } +} -identify_digit : List (List U8) -> Str -identify_digit = |digit_grid| - # _ _ _ _ _ _ _ _ - # | | | _| _||_||_ |_ ||_||_| - # |_| ||_ _| | _||_| ||_| _| - when digit_grid is - [[' ', '_', ' '], ['|', ' ', '|'], ['|', '_', '|'], [' ', ' ', ' ']] -> "0" - [[' ', ' ', ' '], [' ', ' ', '|'], [' ', ' ', '|'], [' ', ' ', ' ']] -> "1" - [[' ', '_', ' '], [' ', '_', '|'], ['|', '_', ' '], [' ', ' ', ' ']] -> "2" - [[' ', '_', ' '], [' ', '_', '|'], [' ', '_', '|'], [' ', ' ', ' ']] -> "3" - [[' ', ' ', ' '], ['|', '_', '|'], [' ', ' ', '|'], [' ', ' ', ' ']] -> "4" - [[' ', '_', ' '], ['|', '_', ' '], [' ', '_', '|'], [' ', ' ', ' ']] -> "5" - [[' ', '_', ' '], ['|', '_', ' '], ['|', '_', '|'], [' ', ' ', ' ']] -> "6" - [[' ', '_', ' '], [' ', ' ', '|'], [' ', ' ', '|'], [' ', ' ', ' ']] -> "7" - [[' ', '_', ' '], ['|', '_', '|'], ['|', '_', '|'], [' ', ' ', ' ']] -> "8" - [[' ', '_', ' '], ['|', '_', '|'], [' ', '_', '|'], [' ', ' ', ' ']] -> "9" - _ -> "?" +# The following functions should soon be available in Roc's builtins +chunks_of = |iter, size| { + var $state = [] + var $chunk = [] + for item in iter { + $chunk = $chunk.append(item) + if $chunk.len() == size { + $state = $state.append($chunk) + $chunk = [] + } + } + if $chunk.len() > 0 { + $state = $state.append($chunk) + } + $state +} diff --git a/exercises/practice/ocr-numbers/.meta/template.j2 b/exercises/practice/ocr-numbers/.meta/template.j2 index 85b49b0b..b2419365 100644 --- a/exercises/practice/ocr-numbers/.meta/template.j2 +++ b/exercises/practice/ocr-numbers/.meta/template.j2 @@ -6,14 +6,17 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { grid = {{ case["input"]["rows"] | to_roc_multiline_string | indent(8) }} result = {{ case["property"] | to_snake }}(grid) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} expected = Ok({{ case["expected"] | to_roc }}) result == expected {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/ocr-numbers/OcrNumbers.roc b/exercises/practice/ocr-numbers/OcrNumbers.roc index dcabe3b7..0e5de8f9 100644 --- a/exercises/practice/ocr-numbers/OcrNumbers.roc +++ b/exercises/practice/ocr-numbers/OcrNumbers.roc @@ -1,5 +1,6 @@ -module [convert] - -convert : Str -> Result Str _ -convert = |grid| - crash("Please implement the 'convert' function") +OcrNumbers :: {}.{ + convert : Str -> Try(Str, _) + convert = |grid| { + crash "Please implement the 'convert' function" + } +} diff --git a/exercises/practice/ocr-numbers/ocr-numbers-test.roc b/exercises/practice/ocr-numbers/ocr-numbers-test.roc index 0151351b..29941e49 100644 --- a/exercises/practice/ocr-numbers/ocr-numbers-test.roc +++ b/exercises/practice/ocr-numbers/ocr-numbers-test.roc @@ -1,240 +1,236 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/ocr-numbers/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import OcrNumbers exposing [convert] # Recognizes 0 -expect - grid = - """ - _ - | | - |_| - - """ - result = convert(grid) - expected = Ok("0") - result == expected +expect { + grid = + \\ _ + \\| | + \\|_| + \\ + + result = convert(grid) + expected = Ok("0") + result == expected +} # Recognizes 1 -expect - grid = - """ - - | - | - - """ - result = convert(grid) - expected = Ok("1") - result == expected +expect { + grid = + \\ + \\ | + \\ | + \\ + + result = convert(grid) + expected = Ok("1") + result == expected +} # Unreadable but correctly sized inputs return ? -expect - grid = - """ - - _ - | - - """ - result = convert(grid) - expected = Ok("?") - result == expected +expect { + grid = + \\ + \\ _ + \\ | + \\ + + result = convert(grid) + expected = Ok("?") + result == expected +} # Input with a number of lines that is not a multiple of four raises an error -expect - grid = - """ - _ - | | - - """ - result = convert(grid) - result |> Result.is_err +expect { + grid = + \\ _ + \\| | + \\ + + result = convert(grid) + result.is_err() +} # Input with a number of columns that is not a multiple of three raises an error -expect - grid = - """ - - | - | - - """ - result = convert(grid) - result |> Result.is_err +expect { + grid = + \\ + \\ | + \\ | + \\ + + result = convert(grid) + result.is_err() +} # Recognizes 110101100 -expect - grid = - """ - _ _ _ _ - | || | || | | || || | - | ||_| ||_| | ||_||_| - - """ - result = convert(grid) - expected = Ok("110101100") - result == expected +expect { + grid = + \\ _ _ _ _ + \\ | || | || | | || || | + \\ | ||_| ||_| | ||_||_| + \\ + + result = convert(grid) + expected = Ok("110101100") + result == expected +} # Garbled numbers in a string are replaced with ? -expect - grid = - """ - _ _ _ - | || | || | || || | - | | _| ||_| | ||_||_| - - """ - result = convert(grid) - expected = Ok("11?10?1?0") - result == expected +expect { + grid = + \\ _ _ _ + \\ | || | || | || || | + \\ | | _| ||_| | ||_||_| + \\ + + result = convert(grid) + expected = Ok("11?10?1?0") + result == expected +} # Recognizes 2 -expect - grid = - """ - _ - _| - |_ - - """ - result = convert(grid) - expected = Ok("2") - result == expected +expect { + grid = + \\ _ + \\ _| + \\|_ + \\ + + result = convert(grid) + expected = Ok("2") + result == expected +} # Recognizes 3 -expect - grid = - """ - _ - _| - _| - - """ - result = convert(grid) - expected = Ok("3") - result == expected +expect { + grid = + \\ _ + \\ _| + \\ _| + \\ + + result = convert(grid) + expected = Ok("3") + result == expected +} # Recognizes 4 -expect - grid = - """ - - |_| - | - - """ - result = convert(grid) - expected = Ok("4") - result == expected +expect { + grid = + \\ + \\|_| + \\ | + \\ + + result = convert(grid) + expected = Ok("4") + result == expected +} # Recognizes 5 -expect - grid = - """ - _ - |_ - _| - - """ - result = convert(grid) - expected = Ok("5") - result == expected +expect { + grid = + \\ _ + \\|_ + \\ _| + \\ + + result = convert(grid) + expected = Ok("5") + result == expected +} # Recognizes 6 -expect - grid = - """ - _ - |_ - |_| - - """ - result = convert(grid) - expected = Ok("6") - result == expected +expect { + grid = + \\ _ + \\|_ + \\|_| + \\ + + result = convert(grid) + expected = Ok("6") + result == expected +} # Recognizes 7 -expect - grid = - """ - _ - | - | - - """ - result = convert(grid) - expected = Ok("7") - result == expected +expect { + grid = + \\ _ + \\ | + \\ | + \\ + + result = convert(grid) + expected = Ok("7") + result == expected +} # Recognizes 8 -expect - grid = - """ - _ - |_| - |_| - - """ - result = convert(grid) - expected = Ok("8") - result == expected +expect { + grid = + \\ _ + \\|_| + \\|_| + \\ + + result = convert(grid) + expected = Ok("8") + result == expected +} # Recognizes 9 -expect - grid = - """ - _ - |_| - _| - - """ - result = convert(grid) - expected = Ok("9") - result == expected +expect { + grid = + \\ _ + \\|_| + \\ _| + \\ + + result = convert(grid) + expected = Ok("9") + result == expected +} # Recognizes string of decimal numbers -expect - grid = - """ - _ _ _ _ _ _ _ _ - | _| _||_||_ |_ ||_||_|| | - ||_ _| | _||_| ||_| _||_| - - """ - result = convert(grid) - expected = Ok("1234567890") - result == expected +expect { + grid = + \\ _ _ _ _ _ _ _ _ + \\ | _| _||_||_ |_ ||_||_|| | + \\ ||_ _| | _||_| ||_| _||_| + \\ + + result = convert(grid) + expected = Ok("1234567890") + result == expected +} # Numbers separated by empty lines are recognized. Lines are joined by commas. -expect - grid = - """ - _ _ - | _| _| - ||_ _| - - _ _ - |_||_ |_ - | _||_| - - _ _ _ - ||_||_| - ||_| _| - - """ - result = convert(grid) - expected = Ok("123,456,789") - result == expected +expect { + grid = + \\ _ _ + \\ | _| _| + \\ ||_ _| + \\ + \\ _ _ + \\|_||_ |_ + \\ | _||_| + \\ + \\ _ _ _ + \\ ||_||_| + \\ ||_| _| + \\ + + result = convert(grid) + expected = Ok("123,456,789") + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/octal/.meta/Example.roc b/exercises/practice/octal/.meta/Example.roc index df228124..4c6f7ce6 100644 --- a/exercises/practice/octal/.meta/Example.roc +++ b/exercises/practice/octal/.meta/Example.roc @@ -1,24 +1,43 @@ -module [parse] +Octal :: {}.{ + parse : Str -> Try(U64, [InvalidNumStr]) + parse = |string| { + if string == "" { + Err(InvalidNumStr) + } else { + digits = string.to_utf8()->map_try(parse_octal_digit)? + digits.fold_until( + Ok(0), + |acc_res, octal_digit| { + match acc_res { + Ok(number) => { + if number > 0x1fffffffffffffff { + Break(Err(InvalidNumStr)) + } else { + Continue(Ok(U64.shift_left_by(number, 3) + octal_digit)) + } + } + Err(err) => Break(Err(err)) + } + }, + ) + } + } +} -parse_octal_digit = |char| - if char >= '0' and char <= '7' then - Ok((char - '0' |> Num.to_u64)) - else - Err(InvalidNumStr) +parse_octal_digit : U8 -> Try(U64, [InvalidNumStr]) +parse_octal_digit = |char| { + if char >= '0' and char <= '7' { + Ok((char - '0').to_u64()) + } else { + Err(InvalidNumStr) + } +} -parse : Str -> Result U64 _ -parse = |string| - if string == "" then - Err(InvalidNumStr) - else - string - |> Str.to_utf8 - |> List.walk_try( - 0, - |number, char| - octal_digit = parse_octal_digit(char)? - if number > 0x1fffffffffffffff then - Err(InvalidNumStr) - else - number |> Num.shift_left_by(3) |> Num.add(octal_digit) |> Ok, - ) +# The following function should soon be available in Roc's builtins +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/octal/Octal.roc b/exercises/practice/octal/Octal.roc index e286f262..b6e2eeb7 100644 --- a/exercises/practice/octal/Octal.roc +++ b/exercises/practice/octal/Octal.roc @@ -1,5 +1,6 @@ -module [parse] - -parse : Str -> Result U64 _ -parse = |string| - crash("Please implement the 'parse' function") +Octal :: {}.{ + parse : Str -> Try(U64, _) + parse = |string| { + crash "Please implement the 'parse' function" + } +} diff --git a/exercises/practice/octal/octal-test.roc b/exercises/practice/octal/octal-test.roc index 2127ca2c..2c22c787 100644 --- a/exercises/practice/octal/octal-test.roc +++ b/exercises/practice/octal/octal-test.roc @@ -1,221 +1,261 @@ -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-13 import Octal exposing [parse] # Parse "0" -expect - result = parse("0") - result == Ok(0) +expect { + result = parse("0") + result == Ok(0) +} # Parse "1" -expect - result = parse("1") - result == Ok(1) +expect { + result = parse("1") + result == Ok(1) +} # Parse "2" -expect - result = parse("2") - result == Ok(2) +expect { + result = parse("2") + result == Ok(2) +} # Parse "3" -expect - result = parse("3") - result == Ok(3) +expect { + result = parse("3") + result == Ok(3) +} # Parse "4" -expect - result = parse("4") - result == Ok(4) +expect { + result = parse("4") + result == Ok(4) +} # Parse "5" -expect - result = parse("5") - result == Ok(5) +expect { + result = parse("5") + result == Ok(5) +} # Parse "6" -expect - result = parse("6") - result == Ok(6) +expect { + result = parse("6") + result == Ok(6) +} # Parse "7" -expect - result = parse("7") - result == Ok(7) +expect { + result = parse("7") + result == Ok(7) +} # Parse "10" -expect - result = parse("10") - result == Ok(8) +expect { + result = parse("10") + result == Ok(8) +} # Parse "11" -expect - result = parse("11") - result == Ok(9) +expect { + result = parse("11") + result == Ok(9) +} # Parse "12" -expect - result = parse("12") - result == Ok(10) +expect { + result = parse("12") + result == Ok(10) +} # Parse "13" -expect - result = parse("13") - result == Ok(11) +expect { + result = parse("13") + result == Ok(11) +} # Parse "14" -expect - result = parse("14") - result == Ok(12) +expect { + result = parse("14") + result == Ok(12) +} # Parse "15" -expect - result = parse("15") - result == Ok(13) +expect { + result = parse("15") + result == Ok(13) +} # Parse "16" -expect - result = parse("16") - result == Ok(14) +expect { + result = parse("16") + result == Ok(14) +} # Parse "17" -expect - result = parse("17") - result == Ok(15) +expect { + result = parse("17") + result == Ok(15) +} # Parse "20" -expect - result = parse("20") - result == Ok(16) +expect { + result = parse("20") + result == Ok(16) +} # Parse "21" -expect - result = parse("21") - result == Ok(17) +expect { + result = parse("21") + result == Ok(17) +} # Parse "22" -expect - result = parse("22") - result == Ok(18) +expect { + result = parse("22") + result == Ok(18) +} # Parse "77" -expect - result = parse("77") - result == Ok(63) +expect { + result = parse("77") + result == Ok(63) +} # Parse "100" -expect - result = parse("100") - result == Ok(64) +expect { + result = parse("100") + result == Ok(64) +} # Parse "1234567654321" -expect - result = parse("1234567654321") - result == Ok(89755965649) +expect { + result = parse("1234567654321") + result == Ok(89755965649) +} # Parse "1777777777777777777777", the largest U64 value -expect - result = parse("1777777777777777777777") - result == Ok(18446744073709551615) +expect { + result = parse("1777777777777777777777") + result == Ok(18446744073709551615) +} # Ignore leading zeros in "00000" -expect - result = parse("00000") - result == Ok(0) +expect { + result = parse("00000") + result == Ok(0) +} # Ignore leading zeros in "00001" -expect - result = parse("00001") - result == Ok(1) +expect { + result = parse("00001") + result == Ok(1) +} # Ignore leading zeros in "00007" -expect - result = parse("00007") - result == Ok(7) +expect { + result = parse("00007") + result == Ok(7) +} # Ignore leading zeros in "000010" -expect - result = parse("000010") - result == Ok(8) +expect { + result = parse("000010") + result == Ok(8) +} # Ignore leading zeros in "000077" -expect - result = parse("000077") - result == Ok(63) +expect { + result = parse("000077") + result == Ok(63) +} # Ignore leading zeros in "000070000" -expect - result = parse("000070000") - result == Ok(28672) +expect { + result = parse("000070000") + result == Ok(28672) +} # Ignore leading zeros even before the largest U64 value -expect - result = parse("00001777777777777777777777") - result == Ok(18446744073709551615) +expect { + result = parse("00001777777777777777777777") + result == Ok(18446744073709551615) +} # Empty strings are invalid -expect - result = parse("") - result |> Result.is_err +expect { + result = parse("") + result.is_err() +} # A string with only spaces is invalid -expect - result = parse(" ") - result |> Result.is_err +expect { + result = parse(" ") + result.is_err() +} # Leading spaces are invalid -expect - result = parse(" 1234567") - result |> Result.is_err +expect { + result = parse(" 1234567") + result.is_err() +} # Trailing spaces are invalid -expect - result = parse("1234567 ") - result |> Result.is_err +expect { + result = parse("1234567 ") + result.is_err() +} # Spaces anywhere are invalid -expect - result = parse("123 4567") - result |> Result.is_err +expect { + result = parse("123 4567") + result.is_err() +} # Invalid character in "1*234" -expect - result = parse("1*234") - result |> Result.is_err +expect { + result = parse("1*234") + result.is_err() +} # Invalid character in "12abc" -expect - result = parse("12abc") - result |> Result.is_err +expect { + result = parse("12abc") + result.is_err() +} # Invalid character in "12/34" -expect - result = parse("12/34") - result |> Result.is_err +expect { + result = parse("12/34") + result.is_err() +} # Invalid character in "1234\n" -expect - result = parse("1234\n") - result |> Result.is_err +expect { + result = parse("1234\n") + result.is_err() +} # Invalid character in "-1234" -expect - result = parse("-1234") - result |> Result.is_err +expect { + result = parse("-1234") + result.is_err() +} # Invalid character in "+1234" -expect - result = parse("+1234") - result |> Result.is_err +expect { + result = parse("+1234") + result.is_err() +} # For numbers that don't fit in an U64, `parse` should return an error # instead of crashing -expect - result = parse("2000000000000000000000") - result |> Result.is_err +expect { + result = parse("2000000000000000000000") + 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/palindrome-products/.meta/Example.roc b/exercises/practice/palindrome-products/.meta/Example.roc index 640e84f4..e1819863 100644 --- a/exercises/practice/palindrome-products/.meta/Example.roc +++ b/exercises/practice/palindrome-products/.meta/Example.roc @@ -1,60 +1,81 @@ -module [smallest, largest] +PalindromeProducts :: {}.{ + smallest : { min : U64, max : U64 } -> Try({ value : U64, factors : Set((U64, U64)) }, [MinWasLargerThanMax, ..]) + smallest = |{ min, max }| { + if min > max { + Err(MinWasLargerThanMax) + } else { + help = |factor1, factor2, best_value, factors| { + if factor1 > max { + final_value = if factors == [] { + 0 + } else { + best_value + } + Ok({ value: final_value, factors: Set.from_list(factors) }) + } else if factor2 == max + 1 { + help((factor1 + 1), (factor1 + 1), best_value, factors) + } else { + value = factor1 * factor2 + if value > best_value { + help((factor1 + 1), (factor1 + 1), best_value, factors) + } else { + next_factor2 = factor2 + 1 + if value == best_value { + new_factors = factors.append((factor1, factor2)) + help(factor1, next_factor2, best_value, new_factors) + } else if is_palindrome(value) { + help(factor1, next_factor2, value, [(factor1, factor2)]) + } else { + help(factor1, next_factor2, best_value, factors) + } + } + } + } -smallest : { min : U64, max : U64 } -> Result { value : U64, factors : Set (U64, U64) } [MinWasLargerThanMax] -smallest = |{ min, max }| - if min > max then - Err(MinWasLargerThanMax) - else - help = |factor1, factor2, best_value, factors| - if factor1 > max then - final_value = if factors == [] then 0 else best_value - Ok({ value: final_value, factors: Set.from_list(factors) }) - else if factor2 == max + 1 then - help((factor1 + 1), (factor1 + 1), best_value, factors) - else - value = factor1 * factor2 - if value > best_value then - help((factor1 + 1), (factor1 + 1), best_value, factors) - else - next_factor2 = factor2 + 1 - if value == best_value then - new_factors = factors |> List.append((factor1, factor2)) - help(factor1, next_factor2, best_value, new_factors) - else if value |> is_palindrome then - help(factor1, next_factor2, value, [(factor1, factor2)]) - else - help(factor1, next_factor2, best_value, factors) + help(min, min, U64.highest, []) + } + } - help(min, min, Num.max_u64, []) + largest : { min : U64, max : U64 } -> Try({ value : U64, factors : Set((U64, U64)) }, [MinWasLargerThanMax, ..]) + largest = |{ min, max }| { + if min > max { + Err(MinWasLargerThanMax) + } else { + help = |factor1, factor2, best_value, factors| { + if factor1 < min { + final_value = if factors == [] { + 0 + } else { + best_value + } + Ok({ value: final_value, factors: Set.from_list(factors) }) + } else if factor2 < factor1 { + help((factor1 - 1), max, best_value, factors) + } else { + value = factor1 * factor2 + if value < best_value { + help((factor1 - 1), max, best_value, factors) + } else { + next_factor2 = factor2 - 1 + if value == best_value { + new_factors = factors.append((factor1, factor2)) + help(factor1, next_factor2, best_value, new_factors) + } else if is_palindrome(value) { + help(factor1, next_factor2, value, [(factor1, factor2)]) + } else { + help(factor1, next_factor2, best_value, factors) + } + } + } + } -largest : { min : U64, max : U64 } -> Result { value : U64, factors : Set (U64, U64) } [MinWasLargerThanMax] -largest = |{ min, max }| - if min > max then - Err(MinWasLargerThanMax) - else - help = |factor1, factor2, best_value, factors| - if factor1 < min then - final_value = if factors == [] then 0 else best_value - Ok({ value: final_value, factors: Set.from_list(factors) }) - else if factor2 < factor1 then - help((factor1 - 1), max, best_value, factors) - else - value = factor1 * factor2 - if value < best_value then - help((factor1 - 1), max, best_value, factors) - else - next_factor2 = factor2 - 1 - if value == best_value then - new_factors = factors |> List.append((factor1, factor2)) - help(factor1, next_factor2, best_value, new_factors) - else if value |> is_palindrome then - help(factor1, next_factor2, value, [(factor1, factor2)]) - else - help(factor1, next_factor2, best_value, factors) - - help(max, max, 0, []) + help(max, max, 0, []) + } + } +} is_palindrome : U64 -> Bool -is_palindrome = |number| - digits = number |> Num.to_str |> Str.to_utf8 - digits == digits |> List.reverse +is_palindrome = |number| { + digits = number.to_str().to_utf8() + digits == digits.rev() +} diff --git a/exercises/practice/palindrome-products/.meta/template.j2 b/exercises/practice/palindrome-products/.meta/template.j2 index 7c80bd28..fe14ce8d 100644 --- a/exercises/practice/palindrome-products/.meta/template.j2 +++ b/exercises/practice/palindrome-products/.meta/template.j2 @@ -4,18 +4,21 @@ import {{ exercise | to_pascal }} exposing [smallest, largest] -is_eq = |result, expected| - when (result, expected) is - (Ok({value, factors}), Ok({value: expected_value, factors: expected_factors})) -> - value == expected_value && factors == expected_factors - _ -> Bool.false +is_eq = |result, expected| { + match (result, expected) { + (Ok({value, factors}), Ok({value: expected_value, factors: expected_factors})) => { + value == expected_value and factors == expected_factors + } + _ => Bool.False + } +} {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"] | to_roc }}) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} expected = Ok({ value: {% if case["expected"]["value"] is none %}0{% else %}{{ case["expected"]["value"] }}{% endif %}, @@ -24,8 +27,10 @@ expect ({{ pair[0] }}, {{ pair[1] }}),{% endfor %} ]) }) - result |> is_eq(expected) + result->is_eq(expected) {%- endif %} +} {% endfor %} +{{ macros.footer() }} diff --git a/exercises/practice/palindrome-products/PalindromeProducts.roc b/exercises/practice/palindrome-products/PalindromeProducts.roc index ccdf41ed..1f8e3be2 100644 --- a/exercises/practice/palindrome-products/PalindromeProducts.roc +++ b/exercises/practice/palindrome-products/PalindromeProducts.roc @@ -1,9 +1,11 @@ -module [smallest, largest] +PalindromeProducts :: {}.{ + smallest : { min : U64, max : U64 } -> Try({ value : U64, factors : Set((U64, U64)) }, _) + smallest = |{ min, max }| { + crash "Please implement the 'smallest' function" + } -smallest : { min : U64, max : U64 } -> Result { value : U64, factors : Set (U64, U64) } _ -smallest = |{ min, max }| - crash("Please implement the 'smallest' function") - -largest : { min : U64, max : U64 } -> Result { value : U64, factors : Set (U64, U64) } _ -largest = |{ min, max }| - crash("Please implement the 'largest' function") + largest : { min : U64, max : U64 } -> Try({ value : U64, factors : Set((U64, U64)) }, _) + largest = |{ min, max }| { + crash "Please implement the 'largest' function" + } +} diff --git a/exercises/practice/palindrome-products/palindrome-products-test.roc b/exercises/practice/palindrome-products/palindrome-products-test.roc index 3af510f5..0b54925d 100644 --- a/exercises/practice/palindrome-products/palindrome-products-test.roc +++ b/exercises/practice/palindrome-products/palindrome-products-test.roc @@ -1,195 +1,204 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/palindrome-products/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import PalindromeProducts exposing [smallest, largest] -is_eq = |result, expected| - when (result, expected) is - (Ok({ value, factors }), Ok({ value: expected_value, factors: expected_factors })) -> - value == expected_value and factors == expected_factors - - _ -> Bool.false +is_eq = |result, expected| { + match (result, expected) { + (Ok({ value, factors }), Ok({ value: expected_value, factors: expected_factors })) => { + value == expected_value and factors == expected_factors + } + _ => Bool.False + } +} # find the smallest palindrome from single digit factors -expect - result = smallest({ min: 1, max: 9 }) - expected = Ok( - { - value: 1, - factors: Set.from_list( - [ - (1, 1), - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = smallest({ min: 1, max: 9 }) + expected = Ok( + { + value: 1, + factors: Set.from_list( + [ + (1, 1), + ], + ), + }, + ) + result->is_eq(expected) +} # find the largest palindrome from single digit factors -expect - result = largest({ min: 1, max: 9 }) - expected = Ok( - { - value: 9, - factors: Set.from_list( - [ - (1, 9), - (3, 3), - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = largest({ min: 1, max: 9 }) + expected = Ok( + { + value: 9, + factors: Set.from_list( + [ + (1, 9), + (3, 3), + ], + ), + }, + ) + result->is_eq(expected) +} # find the smallest palindrome from double digit factors -expect - result = smallest({ min: 10, max: 99 }) - expected = Ok( - { - value: 121, - factors: Set.from_list( - [ - (11, 11), - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = smallest({ min: 10, max: 99 }) + expected = Ok( + { + value: 121, + factors: Set.from_list( + [ + (11, 11), + ], + ), + }, + ) + result->is_eq(expected) +} # find the largest palindrome from double digit factors -expect - result = largest({ min: 10, max: 99 }) - expected = Ok( - { - value: 9009, - factors: Set.from_list( - [ - (91, 99), - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = largest({ min: 10, max: 99 }) + expected = Ok( + { + value: 9009, + factors: Set.from_list( + [ + (91, 99), + ], + ), + }, + ) + result->is_eq(expected) +} # find the smallest palindrome from triple digit factors -expect - result = smallest({ min: 100, max: 999 }) - expected = Ok( - { - value: 10201, - factors: Set.from_list( - [ - (101, 101), - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = smallest({ min: 100, max: 999 }) + expected = Ok( + { + value: 10201, + factors: Set.from_list( + [ + (101, 101), + ], + ), + }, + ) + result->is_eq(expected) +} # find the largest palindrome from triple digit factors -expect - result = largest({ min: 100, max: 999 }) - expected = Ok( - { - value: 906609, - factors: Set.from_list( - [ - (913, 993), - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = largest({ min: 100, max: 999 }) + expected = Ok( + { + value: 906609, + factors: Set.from_list( + [ + (913, 993), + ], + ), + }, + ) + result->is_eq(expected) +} # find the smallest palindrome from four digit factors -expect - result = smallest({ min: 1000, max: 9999 }) - expected = Ok( - { - value: 1002001, - factors: Set.from_list( - [ - (1001, 1001), - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = smallest({ min: 1000, max: 9999 }) + expected = Ok( + { + value: 1002001, + factors: Set.from_list( + [ + (1001, 1001), + ], + ), + }, + ) + result->is_eq(expected) +} # find the largest palindrome from four digit factors -expect - result = largest({ min: 1000, max: 9999 }) - expected = Ok( - { - value: 99000099, - factors: Set.from_list( - [ - (9901, 9999), - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = largest({ min: 1000, max: 9999 }) + expected = Ok( + { + value: 99000099, + factors: Set.from_list( + [ + (9901, 9999), + ], + ), + }, + ) + result->is_eq(expected) +} # empty result for smallest if no palindrome in the range -expect - result = smallest({ min: 1002, max: 1003 }) - expected = Ok( - { - value: 0, - factors: Set.from_list( - [ - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = smallest({ min: 1002, max: 1003 }) + expected = Ok( + { + value: 0, + factors: Set.from_list( + [], + ), + }, + ) + result->is_eq(expected) +} # empty result for largest if no palindrome in the range -expect - result = largest({ min: 15, max: 15 }) - expected = Ok( - { - value: 0, - factors: Set.from_list( - [ - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = largest({ min: 15, max: 15 }) + expected = Ok( + { + value: 0, + factors: Set.from_list( + [], + ), + }, + ) + result->is_eq(expected) +} # error result for smallest if min is more than max -expect - result = smallest({ min: 10000, max: 1 }) - result |> Result.is_err +expect { + result = smallest({ min: 10000, max: 1 }) + result.is_err() +} # error result for largest if min is more than max -expect - result = largest({ min: 2, max: 1 }) - result |> Result.is_err +expect { + result = largest({ min: 2, max: 1 }) + result.is_err() +} # smallest product does not use the smallest factor -expect - result = smallest({ min: 3215, max: 4000 }) - expected = Ok( - { - value: 10988901, - factors: Set.from_list( - [ - (3297, 3333), - ], - ), - }, - ) - result |> is_eq(expected) +expect { + result = smallest({ min: 3215, max: 4000 }) + expected = Ok( + { + value: 10988901, + factors: Set.from_list( + [ + (3297, 3333), + ], + ), + }, + ) + result->is_eq(expected) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/pangram/.meta/Example.roc b/exercises/practice/pangram/.meta/Example.roc index 671c9d9f..17ac6623 100644 --- a/exercises/practice/pangram/.meta/Example.roc +++ b/exercises/practice/pangram/.meta/Example.roc @@ -1,13 +1,26 @@ -module [is_pangram] +Pangram :: {}.{ + is_pangram : Str -> Bool + is_pangram = |sentence| { + sentence + .to_utf8() + ->Set.from_list() + .map( + |c| if c >= 'a' and c <= 'z' { + c + 'A' - 'a' + } else { + c + }, + ) + .keep_if( + |c| c >= 'A' and c <= 'Z', + ) + == alphabet + } +} -alphabet = - List.range({ start: At('A'), end: At('Z') }) |> Set.from_list - -is_pangram : Str -> Bool -is_pangram = |sentence| - sentence - |> Str.to_utf8 - |> Set.from_list - |> Set.map(|c| if c >= 'a' and c <= 'z' then c + 'A' - 'a' else c) # to uppercase - |> Set.keep_if(|c| c >= 'A' and c <= 'Z') - |> Bool.is_eq(alphabet) +alphabet = + ('A'..='Z') + .fold( + Set.empty(), + |set, c| set.insert(c), + ) diff --git a/exercises/practice/pangram/.meta/template.j2 b/exercises/practice/pangram/.meta/template.j2 index c0511911..b4be700c 100644 --- a/exercises/practice/pangram/.meta/template.j2 +++ b/exercises/practice/pangram/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [is_pangram] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["sentence"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/pangram/Pangram.roc b/exercises/practice/pangram/Pangram.roc index 004f9c57..457d609c 100644 --- a/exercises/practice/pangram/Pangram.roc +++ b/exercises/practice/pangram/Pangram.roc @@ -1,5 +1,6 @@ -module [is_pangram] - -is_pangram : Str -> Bool -is_pangram = |sentence| - crash("Please implement the 'is_pangram' function") +Pangram :: {}.{ + is_pangram : Str -> Bool + is_pangram = |sentence| { + crash "Please implement the 'is_pangram' function" + } +} diff --git a/exercises/practice/pangram/pangram-test.roc b/exercises/practice/pangram/pangram-test.roc index 1348a315..d61ce420 100644 --- a/exercises/practice/pangram/pangram-test.roc +++ b/exercises/practice/pangram/pangram-test.roc @@ -1,64 +1,70 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/pangram/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Pangram exposing [is_pangram] # empty sentence -expect - result = is_pangram("") - result == Bool.false +expect { + result = is_pangram("") + result == Bool.False +} # perfect lower case -expect - result = is_pangram("abcdefghijklmnopqrstuvwxyz") - result == Bool.true +expect { + result = is_pangram("abcdefghijklmnopqrstuvwxyz") + result == Bool.True +} # only lower case -expect - result = is_pangram("the quick brown fox jumps over the lazy dog") - result == Bool.true +expect { + result = is_pangram("the quick brown fox jumps over the lazy dog") + result == Bool.True +} # missing the letter 'x' -expect - result = is_pangram("a quick movement of the enemy will jeopardize five gunboats") - result == Bool.false +expect { + result = is_pangram("a quick movement of the enemy will jeopardize five gunboats") + result == Bool.False +} # missing the letter 'h' -expect - result = is_pangram("five boxing wizards jump quickly at it") - result == Bool.false +expect { + result = is_pangram("five boxing wizards jump quickly at it") + result == Bool.False +} # with underscores -expect - result = is_pangram("the_quick_brown_fox_jumps_over_the_lazy_dog") - result == Bool.true +expect { + result = is_pangram("the_quick_brown_fox_jumps_over_the_lazy_dog") + result == Bool.True +} # with numbers -expect - result = is_pangram("the 1 quick brown fox jumps over the 2 lazy dogs") - result == Bool.true +expect { + result = is_pangram("the 1 quick brown fox jumps over the 2 lazy dogs") + result == Bool.True +} # missing letters replaced by numbers -expect - result = is_pangram("7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog") - result == Bool.false +expect { + result = is_pangram("7h3 qu1ck brown fox jumps ov3r 7h3 lazy dog") + result == Bool.False +} # mixed case and punctuation -expect - result = is_pangram("\"Five quacking Zephyrs jolt my wax bed.\"") - result == Bool.true +expect { + result = is_pangram("\"Five quacking Zephyrs jolt my wax bed.\"") + result == Bool.True +} # a-m and A-M are 26 different characters but not a pangram -expect - result = is_pangram("abcdefghijklm ABCDEFGHIJKLM") - result == Bool.false +expect { + result = is_pangram("abcdefghijklm ABCDEFGHIJKLM") + result == Bool.False +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/pascals-triangle/.meta/Example.roc b/exercises/practice/pascals-triangle/.meta/Example.roc index bc219678..7a241ca2 100644 --- a/exercises/practice/pascals-triangle/.meta/Example.roc +++ b/exercises/practice/pascals-triangle/.meta/Example.roc @@ -1,23 +1,37 @@ -module [pascals_triangle] - -pascals_triangle : U64 -> List (List U64) -pascals_triangle = |count| - List.range({ start: At(0), end: Before(count) }) - |> List.map( - |row| - List.range({ start: At(0), end: At(row) }) - |> List.map(|column| binomial_coefficient(row, column)), - ) +PascalsTriangle :: {}.{ + pascals_triangle : U64 -> List(List(U64)) + pascals_triangle = |count| { + (0..List.from_iter() + }, + ) + ->List.from_iter() + } +} binomial_coefficient : U64, U64 -> U64 -binomial_coefficient = |n, k| - if k == 0 or k == n then - 1 - else - numerator = - List.range({ start: At((n + 1 - k)), end: At(n) }) - |> List.walk(1, |product, value| product * value) - denominator = - List.range({ start: At(1), end: At(k) }) - |> List.walk(1, |product, value| product * value) - numerator // denominator +binomial_coefficient = |n, k| { + if k == 0 or k == n { + 1 + } else { + numerator = + ((n + 1 - k)..=n) + .fold( + 1, + |product, value| product * value, + ) + denominator = + (1..=k) + .fold( + 1, + |product, value| product * value, + ) + numerator // denominator + } +} diff --git a/exercises/practice/pascals-triangle/.meta/template.j2 b/exercises/practice/pascals-triangle/.meta/template.j2 index db72aca9..ecf09c1e 100644 --- a/exercises/practice/pascals-triangle/.meta/template.j2 +++ b/exercises/practice/pascals-triangle/.meta/template.j2 @@ -6,11 +6,14 @@ import {{ exercise | to_pascal }} exposing [pascals_triangle] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = pascals_triangle({{ case["input"]["count"] | to_roc }}) expected = [{%- for row in case["expected"] %} {{ row | to_roc }}, {%- endfor %}] result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/pascals-triangle/PascalsTriangle.roc b/exercises/practice/pascals-triangle/PascalsTriangle.roc index 07593063..62e8cc15 100644 --- a/exercises/practice/pascals-triangle/PascalsTriangle.roc +++ b/exercises/practice/pascals-triangle/PascalsTriangle.roc @@ -1,5 +1,6 @@ -module [pascals_triangle] - -pascals_triangle : U64 -> List (List U64) -pascals_triangle = |count| - crash("Please implement the 'pascals_triangle' function") +PascalsTriangle :: {}.{ + pascals_triangle : U64 -> List(List(U64)) + pascals_triangle = |count| { + crash "Please implement the 'pascals_triangle' function" + } +} diff --git a/exercises/practice/pascals-triangle/pascals-triangle-test.roc b/exercises/practice/pascals-triangle/pascals-triangle-test.roc index d46b38f2..795e34d0 100644 --- a/exercises/practice/pascals-triangle/pascals-triangle-test.roc +++ b/exercises/practice/pascals-triangle/pascals-triangle-test.roc @@ -1,100 +1,104 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/pascals-triangle/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import PascalsTriangle exposing [pascals_triangle] # zero rows -expect - result = pascals_triangle(0) - expected = [] - result == expected +expect { + result = pascals_triangle(0) + expected = [] + result == expected +} # single row -expect - result = pascals_triangle(1) - expected = [ - [1], - ] - result == expected +expect { + result = pascals_triangle(1) + expected = [ + [1], + ] + result == expected +} # two rows -expect - result = pascals_triangle(2) - expected = [ - [1], - [1, 1], - ] - result == expected +expect { + result = pascals_triangle(2) + expected = [ + [1], + [1, 1], + ] + result == expected +} # three rows -expect - result = pascals_triangle(3) - expected = [ - [1], - [1, 1], - [1, 2, 1], - ] - result == expected +expect { + result = pascals_triangle(3) + expected = [ + [1], + [1, 1], + [1, 2, 1], + ] + result == expected +} # four rows -expect - result = pascals_triangle(4) - expected = [ - [1], - [1, 1], - [1, 2, 1], - [1, 3, 3, 1], - ] - result == expected +expect { + result = pascals_triangle(4) + expected = [ + [1], + [1, 1], + [1, 2, 1], + [1, 3, 3, 1], + ] + result == expected +} # five rows -expect - result = pascals_triangle(5) - expected = [ - [1], - [1, 1], - [1, 2, 1], - [1, 3, 3, 1], - [1, 4, 6, 4, 1], - ] - result == expected +expect { + result = pascals_triangle(5) + expected = [ + [1], + [1, 1], + [1, 2, 1], + [1, 3, 3, 1], + [1, 4, 6, 4, 1], + ] + result == expected +} # six rows -expect - result = pascals_triangle(6) - expected = [ - [1], - [1, 1], - [1, 2, 1], - [1, 3, 3, 1], - [1, 4, 6, 4, 1], - [1, 5, 10, 10, 5, 1], - ] - result == expected +expect { + result = pascals_triangle(6) + expected = [ + [1], + [1, 1], + [1, 2, 1], + [1, 3, 3, 1], + [1, 4, 6, 4, 1], + [1, 5, 10, 10, 5, 1], + ] + result == expected +} # ten rows -expect - result = pascals_triangle(10) - expected = [ - [1], - [1, 1], - [1, 2, 1], - [1, 3, 3, 1], - [1, 4, 6, 4, 1], - [1, 5, 10, 10, 5, 1], - [1, 6, 15, 20, 15, 6, 1], - [1, 7, 21, 35, 35, 21, 7, 1], - [1, 8, 28, 56, 70, 56, 28, 8, 1], - [1, 9, 36, 84, 126, 126, 84, 36, 9, 1], - ] - result == expected +expect { + result = pascals_triangle(10) + expected = [ + [1], + [1, 1], + [1, 2, 1], + [1, 3, 3, 1], + [1, 4, 6, 4, 1], + [1, 5, 10, 10, 5, 1], + [1, 6, 15, 20, 15, 6, 1], + [1, 7, 21, 35, 35, 21, 7, 1], + [1, 8, 28, 56, 70, 56, 28, 8, 1], + [1, 9, 36, 84, 126, 126, 84, 36, 9, 1], + ] + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/perfect-numbers/.meta/Example.roc b/exercises/practice/perfect-numbers/.meta/Example.roc index 09ba58da..45c43f31 100644 --- a/exercises/practice/perfect-numbers/.meta/Example.roc +++ b/exercises/practice/perfect-numbers/.meta/Example.roc @@ -1,27 +1,33 @@ -module [classify] +PerfectNumbers :: {}.{ + classify : U64 -> Try([Abundant, Deficient, Perfect], [NumberArgIsZero]) + classify = |number| { + sum = aliquot_sum(number)? + if sum == number { + Ok(Perfect) + } else if sum > number { + Ok(Abundant) + } else { + Ok(Deficient) + } + } +} -aliquot_sum : U64 -> Result U64 [NumberArgIsZero] -aliquot_sum = |number| - if number == 0 then - Err(NumberArgIsZero) - else if number == 1 then - Ok(0) # edge case - else - Ok( - ( - # There are more efficient algorithms, but this is the simplest - List.range({ start: At(1), end: At((number // 2)) }) - |> List.keep_if(|d| number % d == 0) - |> List.sum - ), - ) - -classify : U64 -> Result [Abundant, Deficient, Perfect] [NumberArgIsZero] -classify = |number| - sum = aliquot_sum(number)? - if sum == number then - Ok(Perfect) - else if sum > number then - Ok(Abundant) - else - Ok(Deficient) +aliquot_sum : U64 -> Try(U64, [NumberArgIsZero]) +aliquot_sum = |number| { + if number == 0 { + Err(NumberArgIsZero) + } else if number == 1 { + Ok(0) + } else { + (1..=(number // 2)) + .fold( + 0, + |acc, d| if number % d == 0 { + acc + d + } else { + acc + }, + ) + ->Ok() + } +} diff --git a/exercises/practice/perfect-numbers/.meta/template.j2 b/exercises/practice/perfect-numbers/.meta/template.j2 index ec51e9f2..d822bce0 100644 --- a/exercises/practice/perfect-numbers/.meta/template.j2 +++ b/exercises/practice/perfect-numbers/.meta/template.j2 @@ -11,13 +11,16 @@ import {{ exercise | to_pascal }} exposing [classify] {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["number"] }}) {%- if case["expected"]["error"] %} - Result.is_err(result) + result.is_err() {%- else %} result == Ok({{ case["expected"] | to_pascal }}) {%- endif %} +} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/perfect-numbers/.meta/tests.toml b/exercises/practice/perfect-numbers/.meta/tests.toml index 1dbd4976..96656996 100644 --- a/exercises/practice/perfect-numbers/.meta/tests.toml +++ b/exercises/practice/perfect-numbers/.meta/tests.toml @@ -27,6 +27,9 @@ description = "Abundant numbers -> Medium abundant number is classified correctl [ec7792e6-8786-449c-b005-ce6dd89a772b] description = "Abundant numbers -> Large abundant number is classified correctly" +[05f15b93-849c-45e9-9c7d-1ea131ef7d10] +description = "Abundant numbers -> Perfect square abundant number is classified correctly" + [e610fdc7-2b6e-43c3-a51c-b70fb37413ba] description = "Deficient numbers -> Smallest prime deficient number is classified correctly" diff --git a/exercises/practice/perfect-numbers/PerfectNumbers.roc b/exercises/practice/perfect-numbers/PerfectNumbers.roc index 508d38f0..eaee1196 100644 --- a/exercises/practice/perfect-numbers/PerfectNumbers.roc +++ b/exercises/practice/perfect-numbers/PerfectNumbers.roc @@ -1,5 +1,6 @@ -module [classify] - -classify : U64 -> Result [Abundant, Deficient, Perfect] _ -classify = |number| - crash("Please implement the 'classify' function") +PerfectNumbers :: {}.{ + classify : U64 -> Try([Abundant, Deficient, Perfect], _) + classify = |number| { + crash "Please implement the 'classify' function" + } +} diff --git a/exercises/practice/perfect-numbers/perfect-numbers-test.roc b/exercises/practice/perfect-numbers/perfect-numbers-test.roc index cd821008..c6060e48 100644 --- a/exercises/practice/perfect-numbers/perfect-numbers-test.roc +++ b/exercises/practice/perfect-numbers/perfect-numbers-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/perfect-numbers/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import PerfectNumbers exposing [classify] @@ -17,74 +9,96 @@ import PerfectNumbers exposing [classify] ## # Smallest perfect number is classified correctly -expect - result = classify(6) - result == Ok(Perfect) +expect { + result = classify(6) + result == Ok(Perfect) +} # Medium perfect number is classified correctly -expect - result = classify(28) - result == Ok(Perfect) +expect { + result = classify(28) + result == Ok(Perfect) +} # Large perfect number is classified correctly -expect - result = classify(33550336) - result == Ok(Perfect) +expect { + result = classify(33550336) + result == Ok(Perfect) +} ## ## Abundant numbers ## # Smallest abundant number is classified correctly -expect - result = classify(12) - result == Ok(Abundant) +expect { + result = classify(12) + result == Ok(Abundant) +} # Medium abundant number is classified correctly -expect - result = classify(30) - result == Ok(Abundant) +expect { + result = classify(30) + result == Ok(Abundant) +} # Large abundant number is classified correctly -expect - result = classify(33550335) - result == Ok(Abundant) +expect { + result = classify(33550335) + result == Ok(Abundant) +} + +# Perfect square abundant number is classified correctly +expect { + result = classify(196) + result == Ok(Abundant) +} ## ## Deficient numbers ## # Smallest prime deficient number is classified correctly -expect - result = classify(2) - result == Ok(Deficient) +expect { + result = classify(2) + result == Ok(Deficient) +} # Smallest non-prime deficient number is classified correctly -expect - result = classify(4) - result == Ok(Deficient) +expect { + result = classify(4) + result == Ok(Deficient) +} # Medium deficient number is classified correctly -expect - result = classify(32) - result == Ok(Deficient) +expect { + result = classify(32) + result == Ok(Deficient) +} # Large deficient number is classified correctly -expect - result = classify(33550337) - result == Ok(Deficient) +expect { + result = classify(33550337) + result == Ok(Deficient) +} # Edge case (no factors other than itself) is classified correctly -expect - result = classify(1) - result == Ok(Deficient) +expect { + result = classify(1) + result == Ok(Deficient) +} ## ## Invalid inputs ## # Zero is rejected (as it is not a positive integer) -expect - result = classify(0) - Result.is_err(result) +expect { + result = classify(0) + 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/phone-number/.meta/Example.roc b/exercises/practice/phone-number/.meta/Example.roc index c923336d..8cb4d9a5 100644 --- a/exercises/practice/phone-number/.meta/Example.roc +++ b/exercises/practice/phone-number/.meta/Example.roc @@ -1,29 +1,34 @@ -module [clean] +PhoneNumber :: {}.{ + clean : Str -> Try(Str, [InvalidNumber]) + clean = |phone_number| { + digits = + phone_number + .to_utf8() + .keep_if(|c| c >= '0' and c <= '9') -clean : Str -> Result Str [InvalidNumber] -clean = |phone_number| - digits = - phone_number - |> Str.to_utf8 - |> List.keep_if(|c| c >= '0' and c <= '9') + num_digits = digits.len() + if num_digits == 10 { + check_number(digits) + } else if num_digits == 11 { + match digits { + ['1', .. as rest] => check_number(rest) + _ => Err(InvalidNumber) + } + } else { + Err(InvalidNumber) + } + } +} - num_digits = List.len(digits) - if num_digits == 10 then - digits |> check_number - else if num_digits == 11 then - when digits is - ['1', .. as rest] -> rest |> check_number - _ -> Err(InvalidNumber) # only country code 1 is supported - else - Err(InvalidNumber) - -check_number : List U8 -> Result Str [InvalidNumber] -check_number = |digits| - when digits is - [a1, _, _, e1, ..] if a1 == '0' or a1 == '1' or e1 == '0' or e1 == '1' -> - Err(InvalidNumber) # area code and exchange must not start with 0 or 1 - - _ -> - when digits |> Str.from_utf8 is - Ok(number) -> Ok(number) - Err(BadUtf8(_)) -> crash("Unreachable: a string of digits is valid UTF8") +check_number : List(U8) -> Try(Str, [InvalidNumber]) +check_number = |digits| { + match digits { + [a1, _, _, e1, ..] if a1 == '0' or a1 == '1' or e1 == '0' or e1 == '1' => Err(InvalidNumber) + _ => match Str.from_utf8(digits) { + Ok(number) => Ok(number) + Err(BadUtf8(_)) => { + crash "Unreachable: a string of digits is valid UTF8" + } + } + } +} diff --git a/exercises/practice/phone-number/.meta/template.j2 b/exercises/practice/phone-number/.meta/template.j2 index 45f4dec9..dec60079 100644 --- a/exercises/practice/phone-number/.meta/template.j2 +++ b/exercises/practice/phone-number/.meta/template.j2 @@ -6,13 +6,16 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["phrase"] | to_roc }}) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} expected = Ok({{ case["expected"] | to_roc }}) result == expected {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/phone-number/PhoneNumber.roc b/exercises/practice/phone-number/PhoneNumber.roc index 7f05b445..8d9f86f2 100644 --- a/exercises/practice/phone-number/PhoneNumber.roc +++ b/exercises/practice/phone-number/PhoneNumber.roc @@ -1,5 +1,6 @@ -module [clean] - -clean : Str -> Result Str _ -clean = |phone_number| - crash("Please implement the 'clean' function") +PhoneNumber :: {}.{ + clean : Str -> Try(Str, _) + clean = |phone_number| { + crash "Please implement the 'clean' function" + } +} diff --git a/exercises/practice/phone-number/phone-number-test.roc b/exercises/practice/phone-number/phone-number-test.roc index f66146da..6853fd6e 100644 --- a/exercises/practice/phone-number/phone-number-test.roc +++ b/exercises/practice/phone-number/phone-number-test.roc @@ -1,109 +1,123 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/phone-number/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import PhoneNumber exposing [clean] # cleans the number -expect - result = clean("(223) 456-7890") - expected = Ok("2234567890") - result == expected +expect { + result = clean("(223) 456-7890") + expected = Ok("2234567890") + result == expected +} # cleans numbers with dots -expect - result = clean("223.456.7890") - expected = Ok("2234567890") - result == expected +expect { + result = clean("223.456.7890") + expected = Ok("2234567890") + result == expected +} # cleans numbers with multiple spaces -expect - result = clean("223 456 7890 ") - expected = Ok("2234567890") - result == expected +expect { + result = clean("223 456 7890 ") + expected = Ok("2234567890") + result == expected +} # invalid when 9 digits -expect - result = clean("123456789") - result |> Result.is_err +expect { + result = clean("123456789") + result.is_err() +} # invalid when 11 digits does not start with a 1 -expect - result = clean("22234567890") - result |> Result.is_err +expect { + result = clean("22234567890") + result.is_err() +} # valid when 11 digits and starting with 1 -expect - result = clean("12234567890") - expected = Ok("2234567890") - result == expected +expect { + result = clean("12234567890") + expected = Ok("2234567890") + result == expected +} # valid when 11 digits and starting with 1 even with punctuation -expect - result = clean("+1 (223) 456-7890") - expected = Ok("2234567890") - result == expected +expect { + result = clean("+1 (223) 456-7890") + expected = Ok("2234567890") + result == expected +} # invalid when more than 11 digits -expect - result = clean("321234567890") - result |> Result.is_err +expect { + result = clean("321234567890") + result.is_err() +} # invalid with letters -expect - result = clean("523-abc-7890") - result |> Result.is_err +expect { + result = clean("523-abc-7890") + result.is_err() +} # invalid with punctuations -expect - result = clean("523-@:!-7890") - result |> Result.is_err +expect { + result = clean("523-@:!-7890") + result.is_err() +} # invalid if area code starts with 0 -expect - result = clean("(023) 456-7890") - result |> Result.is_err +expect { + result = clean("(023) 456-7890") + result.is_err() +} # invalid if area code starts with 1 -expect - result = clean("(123) 456-7890") - result |> Result.is_err +expect { + result = clean("(123) 456-7890") + result.is_err() +} # invalid if exchange code starts with 0 -expect - result = clean("(223) 056-7890") - result |> Result.is_err +expect { + result = clean("(223) 056-7890") + result.is_err() +} # invalid if exchange code starts with 1 -expect - result = clean("(223) 156-7890") - result |> Result.is_err +expect { + result = clean("(223) 156-7890") + result.is_err() +} # invalid if area code starts with 0 on valid 11-digit number -expect - result = clean("1 (023) 456-7890") - result |> Result.is_err +expect { + result = clean("1 (023) 456-7890") + result.is_err() +} # invalid if area code starts with 1 on valid 11-digit number -expect - result = clean("1 (123) 456-7890") - result |> Result.is_err +expect { + result = clean("1 (123) 456-7890") + result.is_err() +} # invalid if exchange code starts with 0 on valid 11-digit number -expect - result = clean("1 (223) 056-7890") - result |> Result.is_err +expect { + result = clean("1 (223) 056-7890") + result.is_err() +} # invalid if exchange code starts with 1 on valid 11-digit number -expect - result = clean("1 (223) 156-7890") - result |> Result.is_err +expect { + result = clean("1 (223) 156-7890") + 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/pig-latin/.meta/Example.roc b/exercises/practice/pig-latin/.meta/Example.roc index 83010251..e2e69d69 100644 --- a/exercises/practice/pig-latin/.meta/Example.roc +++ b/exercises/practice/pig-latin/.meta/Example.roc @@ -1,48 +1,60 @@ -module [translate] +PigLatin :: {}.{ + translate : Str -> Str + translate = |phrase| { + phrase + .split_on(" ") + .map(translate_word) + ->Str.join_with(" ") + } +} -is_vowel = |char| - ['a', 'e', 'i', 'o', 'u'] |> List.contains(char) +is_vowel = |char| { + ['a', 'e', 'i', 'o', 'u'].contains(char) +} -rule1_applies = |chars| - when chars is - [c, ..] if is_vowel(c) -> Bool.true - ['x', 'r', ..] -> Bool.true - ['y', 't', ..] -> Bool.true - _ -> Bool.false +rule1_applies = |chars| { + match chars { + [c, ..] if is_vowel(c) => Bool.True + ['x', 'r', ..] => Bool.True + ['y', 't', ..] => Bool.True + _ => Bool.False + } +} -pig_latin_swap = |chars| - if rule1_applies(chars) then - chars - else - (_, split_index) = - chars - |> List.walk_until( - (0, 0), - |(previous_char, index), char| - when (previous_char, char) is - ('q', 'u') -> Break((0, index + 1)) # rule 3 - (_, 'y') if index > 0 -> Break((0, index)) # rule 4 - (_, c) if is_vowel(c) -> Break((0, index)) # rule 2 - _ -> Continue((char, index + 1)), - ) - { before, others } = chars |> List.split_at(split_index) - others |> List.concat(before) +pig_latin_swap = |chars| { + if rule1_applies(chars) { + chars + } else { + (_, split_index) = + chars + .fold_until( + (0, 0), + |(previous_char, index), char| { + match (previous_char, char) { + ('q', 'u') => Break((0, index + 1)) + (_, 'y') if index > 0 => Break((0, index)) + (_, c) if is_vowel(c) => Break((0, index)) + _ => Continue((char, index + 1)) + } + }, + ) + { before, others } = chars.split_at(split_index) + others.concat(before) + } +} translate_word : Str -> Str -translate_word = |word| - maybe_result = - word - |> Str.to_utf8 - |> pig_latin_swap - |> List.concat(['a', 'y']) - |> Str.from_utf8 - when maybe_result is - Ok(result) -> result - Err(_) -> crash("Unreachable") - -translate : Str -> Str -translate = |phrase| - phrase - |> Str.split_on(" ") - |> List.map(translate_word) - |> Str.join_with(" ") +translate_word = |word| { + maybe_result = + word + .to_utf8() + ->pig_latin_swap() + .concat(['a', 'y']) + ->Str.from_utf8() + match maybe_result { + Ok(result) => result + Err(_) => { + crash "Unreachable" + } + } +} diff --git a/exercises/practice/pig-latin/.meta/template.j2 b/exercises/practice/pig-latin/.meta/template.j2 index b4ae2f04..ab6d4470 100644 --- a/exercises/practice/pig-latin/.meta/template.j2 +++ b/exercises/practice/pig-latin/.meta/template.j2 @@ -11,9 +11,12 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["cases"][0]["property"] {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["phrase"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/pig-latin/PigLatin.roc b/exercises/practice/pig-latin/PigLatin.roc index 1ad8dbfd..18b3cb15 100644 --- a/exercises/practice/pig-latin/PigLatin.roc +++ b/exercises/practice/pig-latin/PigLatin.roc @@ -1,5 +1,6 @@ -module [translate] - -translate : Str -> Str -translate = |phrase| - crash("Please implement the 'translate' function") +PigLatin :: {}.{ + translate : Str -> Str + translate = |phrase| { + crash "Please implement the 'translate' function" + } +} diff --git a/exercises/practice/pig-latin/pig-latin-test.roc b/exercises/practice/pig-latin/pig-latin-test.roc index a2483268..ed46c2f0 100644 --- a/exercises/practice/pig-latin/pig-latin-test.roc +++ b/exercises/practice/pig-latin/pig-latin-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/pig-latin/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import PigLatin exposing [translate] @@ -17,137 +9,164 @@ import PigLatin exposing [translate] ## # word beginning with a -expect - result = translate("apple") - result == "appleay" +expect { + result = translate("apple") + result == "appleay" +} # word beginning with e -expect - result = translate("ear") - result == "earay" +expect { + result = translate("ear") + result == "earay" +} # word beginning with i -expect - result = translate("igloo") - result == "iglooay" +expect { + result = translate("igloo") + result == "iglooay" +} # word beginning with o -expect - result = translate("object") - result == "objectay" +expect { + result = translate("object") + result == "objectay" +} # word beginning with u -expect - result = translate("under") - result == "underay" +expect { + result = translate("under") + result == "underay" +} # word beginning with a vowel and followed by a qu -expect - result = translate("equal") - result == "equalay" +expect { + result = translate("equal") + result == "equalay" +} ## ## first letter and ay are moved to the end of words that start with consonants ## # word beginning with p -expect - result = translate("pig") - result == "igpay" +expect { + result = translate("pig") + result == "igpay" +} # word beginning with k -expect - result = translate("koala") - result == "oalakay" +expect { + result = translate("koala") + result == "oalakay" +} # word beginning with x -expect - result = translate("xenon") - result == "enonxay" +expect { + result = translate("xenon") + result == "enonxay" +} # word beginning with q without a following u -expect - result = translate("qat") - result == "atqay" +expect { + result = translate("qat") + result == "atqay" +} # word beginning with consonant and vowel containing qu -expect - result = translate("liquid") - result == "iquidlay" +expect { + result = translate("liquid") + result == "iquidlay" +} ## ## some letter clusters are treated like a single consonant ## # word beginning with ch -expect - result = translate("chair") - result == "airchay" +expect { + result = translate("chair") + result == "airchay" +} # word beginning with qu -expect - result = translate("queen") - result == "eenquay" +expect { + result = translate("queen") + result == "eenquay" +} # word beginning with qu and a preceding consonant -expect - result = translate("square") - result == "aresquay" +expect { + result = translate("square") + result == "aresquay" +} # word beginning with th -expect - result = translate("therapy") - result == "erapythay" +expect { + result = translate("therapy") + result == "erapythay" +} # word beginning with thr -expect - result = translate("thrush") - result == "ushthray" +expect { + result = translate("thrush") + result == "ushthray" +} # word beginning with sch -expect - result = translate("school") - result == "oolschay" +expect { + result = translate("school") + result == "oolschay" +} ## ## some letter clusters are treated like a single vowel ## # word beginning with yt -expect - result = translate("yttria") - result == "yttriaay" +expect { + result = translate("yttria") + result == "yttriaay" +} # word beginning with xr -expect - result = translate("xray") - result == "xrayay" +expect { + result = translate("xray") + result == "xrayay" +} ## ## position of y in a word determines if it is a consonant or a vowel ## # y is treated like a consonant at the beginning of a word -expect - result = translate("yellow") - result == "ellowyay" +expect { + result = translate("yellow") + result == "ellowyay" +} # y is treated like a vowel at the end of a consonant cluster -expect - result = translate("rhythm") - result == "ythmrhay" +expect { + result = translate("rhythm") + result == "ythmrhay" +} # y as second letter in two letter word -expect - result = translate("my") - result == "ymay" +expect { + result = translate("my") + result == "ymay" +} ## ## phrases are translated ## # a whole phrase -expect - result = translate("quick fast run") - result == "ickquay astfay unray" +expect { + result = translate("quick fast run") + result == "ickquay astfay unray" +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/poker/.meta/Example.roc b/exercises/practice/poker/.meta/Example.roc index 96503495..ceb9420f 100644 --- a/exercises/practice/poker/.meta/Example.roc +++ b/exercises/practice/poker/.meta/Example.roc @@ -1,115 +1,218 @@ -module [best_hands] +Poker :: {}.{ + HandParsingError : [InvalidNumberOfCards(U64), CardWasEmpty, InvalidCardValue(List(U8)), InvalidCardSuit(U8)] + + best_hands : List(Str) -> Try(List(Str), HandParsingError) + best_hands = |hands| { + parsed_hands = hands->map_try(parse_hand)? + ranks = parsed_hands.map(get_rank) + top_rank = ranks.fold( + 0, + |max_val, val| if val > max_val { + val + } else { + max_val + }, + ) + List.map2(hands, ranks, |hand, rank| { hand, rank }) + ->join_map( + |res| { + { hand, rank } = res + if rank == top_rank { + [hand] + } else { + [] + } + }, + ) + ->Ok() + } +} Value : U8 + Suit : [Spades, Hearts, Diamonds, Clubs] + Card : { value : Value, suit : Suit } -Hand : List Card -HandParsingError : [InvalidNumberOfCards U64, CardWasEmpty, InvalidCardValue (List U8), InvalidCardSuit U8] - -best_hands : List Str -> Result (List Str) HandParsingError -best_hands = |hands| - parsed_hands = hands |> List.map_try(parse_hand)? - ranks = parsed_hands |> List.map(get_rank) - top_rank = ranks |> List.max |> Result.with_default(0) - List.map2(hands, ranks, |hand, rank| { hand, rank }) - |> List.join_map( - |{ hand, rank }| - if rank == top_rank then [hand] else [], - ) - |> Ok - -parse_hand : Str -> Result Hand HandParsingError -parse_hand = |hand_str| - cards = hand_str |> Str.split_on(" ") - num_cards = List.len(cards) - if num_cards != 5 then - Err(InvalidNumberOfCards(num_cards)) - else - cards |> List.map_try(parse_card) - -parse_card : Str -> Result Card [CardWasEmpty, InvalidCardValue (List U8), InvalidCardSuit U8] -parse_card = |card_str| - when card_str |> Str.to_utf8 is - [] -> Err(CardWasEmpty) - [.. as value_chars, suit_char] -> - value = parse_value(value_chars)? - suit = parse_suit(suit_char)? - Ok({ value, suit }) - -parse_value : List U8 -> Result Value [InvalidCardValue (List U8)] -parse_value = |chars| - when chars is - [val] if val >= '2' and val <= '9' -> Ok((val - '0')) - ['1', '0'] -> Ok(10) - ['J'] -> Ok(11) - ['Q'] -> Ok(12) - ['K'] -> Ok(13) - ['A'] -> Ok(14) - _ -> Err(InvalidCardValue(chars)) - -parse_suit : U8 -> Result Suit [InvalidCardSuit U8] -parse_suit = |char| - when char is - 'S' -> Ok(Spades) - 'H' -> Ok(Hearts) - 'D' -> Ok(Diamonds) - 'C' -> Ok(Clubs) - _ -> Err(InvalidCardSuit(char)) + +Hand : List(Card) + +parse_hand : Str -> Try(Hand, HandParsingError) +parse_hand = |hand_str| { + cards = hand_str.split_on(" ") + num_cards = cards.len() + if num_cards != 5 { + Err(InvalidNumberOfCards(num_cards)) + } else { + cards->map_try(parse_card) + } +} + +parse_card : Str -> Try(Card, [CardWasEmpty, InvalidCardValue(List(U8)), InvalidCardSuit(U8), ..]) +parse_card = |card_str| { + match card_str.to_utf8() { + [] => Err(CardWasEmpty) + [.. as value_chars, suit_char] => { + value = parse_value(value_chars)? + suit = parse_suit(suit_char)? + Ok({ value, suit }) + } + } +} + +parse_value : List(U8) -> Try(Value, [InvalidCardValue(List(U8)), ..]) +parse_value = |chars| { + match chars { + [val] if val >= '2' and val <= '9' => Ok((val - '0')) + ['1', '0'] => Ok(10) + ['J'] => Ok(11) + ['Q'] => Ok(12) + ['K'] => Ok(13) + ['A'] => Ok(14) + _ => Err(InvalidCardValue(chars)) + } +} + +parse_suit : U8 -> Try(Suit, [InvalidCardSuit(U8), ..]) +parse_suit = |char| { + match char { + 'S' => Ok(Spades) + 'H' => Ok(Hearts) + 'D' => Ok(Diamonds) + 'C' => Ok(Clubs) + _ => Err(InvalidCardSuit(char)) + } +} get_rank : Hand -> U64 -get_rank = |hand| - card_values = hand |> List.map(.value) |> List.map(Num.to_u64) |> List.sort_asc - is_consecutive = - List.map2(card_values, (card_values |> List.take_last(4)), |card1, card2| (card1, card2)) - |> List.all(|(card1, card2)| card2 - card1 == 1) - is_special_straight = card_values == [2, 3, 4, 5, 14] # straight starting with Ace - is_straight = is_consecutive or is_special_straight - is_flush = (hand |> List.map(.suit) |> Set.from_list |> Set.len) == 1 - - value_groups = - # Example: [4, 4, 4, 7, 7] -> [{size: 3, value: 4}, {size: 2, value: 7}] - card_values - |> List.walk( - List.repeat(0, 13), - |counters, card_value| - counters |> List.update((card_value - 2), |group_size| group_size + 1), - ) - |> List.map_with_index(|counter, value| counter * 13 + value) - |> List.sort_desc - |> List.map(|group_rank| { size: group_rank // 13, value: group_rank % 13 + 2 }) - |> List.drop_if(|{ size }| size == 0) - - group_sizes = value_groups |> List.map(.size) - - category = - if is_flush and is_straight then - 8 # Straight flush - else if group_sizes == [4, 1] then - 7 # Four of a kind - else if group_sizes == [3, 2] then - 6 # Full house - else if is_flush then - 5 # Flush - else if is_straight then - 4 # Straight - else if group_sizes == [3, 1, 1] then - 3 # Three of a kind - else if group_sizes == [2, 2, 1] then - 2 # Two pairs - else if group_sizes == [2, 1, 1, 1] then - 1 # One pair - else - 0 # High card - - rank_within_category = - if is_special_straight then - 0 # the straight starting with an Ace is the smallest straight - else - value_groups - |> List.walk( - 0, - |rank, { value }| - rank * 13 + value - 2, - ) - - category * Num.pow_int(13, 5) + rank_within_category +get_rank = |hand| { + card_values = sort_asc(hand.map(|c| U8.to_u64(c.value))) + is_consecutive = + List.map2(card_values, take_last(card_values, 4), |card1, card2| (card1, card2)) + .all( + |pair| { + (card1, card2) = pair + card2 - card1 == 1 + }, + ) + is_special_straight = card_values == [2, 3, 4, 5, 14] # straight starting with Ace + is_straight = is_consecutive or is_special_straight + is_flush = (hand.map(|c| c.suit)->Set.from_list().len()) == 1 + + value_groups = + card_values + .fold( + List.repeat(0, 13), + |counters, card_value| { + counters.update((card_value - 2), |group_size| group_size + 1) ?? counters + }, + ) + .map_with_index(|counter, value| counter * 13 + value) + ->sort_desc() + .map(|group_rank| { size: group_rank // 13, value: group_rank % 13 + 2 }) + .drop_if( + |group| { + { size, value: _ } = group + size == 0 + }, + ) + + group_sizes = value_groups.map(|g| g.size) + + category = + if is_flush and is_straight { + 8 # Straight flush + } else if group_sizes == [4, 1] { + 7 # Four of a kind + } else if group_sizes == [3, 2] { + 6 # Full house + } else if is_flush { + 5 # Flush + } else if is_straight { + 4 # Straight + } else if group_sizes == [3, 1, 1] { + 3 # Three of a kind + } else if group_sizes == [2, 2, 1] { + 2 # Two pairs + } else if group_sizes == [2, 1, 1, 1] { + 1 # One pair + } else { + 0 # High card + } + + rank_within_category = + if is_special_straight { + 0 # the straight starting with an Ace is the smallest straight + } else { + value_groups + .fold( + 0, + |rank, group| { + { value, size: _ } = group + rank * 13 + value - 2 + }, + ) + } + + category * pow_int(13, 5) + rank_within_category +} + +pow_int : U64, U64 -> U64 +pow_int = |number, pow| { + (1..=pow).fold( + 1, + |acc, _| { + acc * number + }, + ) +} + +sort_asc = |list| { + list.sort_with( + |a, b| { + if a < b { + LT + } else if a > b { + GT + } else { + EQ + } + }, + ) +} + +sort_desc = |list| { + list.sort_with( + |a, b| { + if a > b { + LT + } else if a < b { + GT + } else { + EQ + } + }, + ) +} + +take_last = |list, n| { + list.sublist({ start: list.len() - n, len: n }) +} + +# The following functions should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} + +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/poker/.meta/template.j2 b/exercises/practice/poker/.meta/template.j2 index b3dcde9d..3ab96f53 100644 --- a/exercises/practice/poker/.meta/template.j2 +++ b/exercises/practice/poker/.meta/template.j2 @@ -6,9 +6,12 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { hands = {{ case["input"]["hands"] | to_roc }} result = {{ case["property"] | to_snake }}(hands) result == Ok({{ case["expected"] | to_roc }}) +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/poker/Poker.roc b/exercises/practice/poker/Poker.roc index d3445344..8359fdc0 100644 --- a/exercises/practice/poker/Poker.roc +++ b/exercises/practice/poker/Poker.roc @@ -1,5 +1,6 @@ -module [best_hands] - -best_hands : List Str -> Result (List Str) _ -best_hands = |hands| - crash("Please implement the 'best_hands' function") +Poker :: {}.{ + best_hands : List(Str) -> Try(List(Str), _) + best_hands = |hands| { + crash "Please implement the 'best_hands' function" + } +} diff --git a/exercises/practice/poker/poker-test.roc b/exercises/practice/poker/poker-test.roc index aae1b2c7..8c8bb88b 100644 --- a/exercises/practice/poker/poker-test.roc +++ b/exercises/practice/poker/poker-test.roc @@ -1,236 +1,269 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/poker/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-13 import Poker exposing [best_hands] # single hand always wins -expect - hands = ["4S 5S 7H 8D JC"] - result = best_hands(hands) - result == Ok(["4S 5S 7H 8D JC"]) +expect { + hands = ["4S 5S 7H 8D JC"] + result = best_hands(hands) + result == Ok(["4S 5S 7H 8D JC"]) +} # highest card out of all hands wins -expect - hands = ["4D 5S 6S 8D 3C", "2S 4C 7S 9H 10H", "3S 4S 5D 6H JH"] - result = best_hands(hands) - result == Ok(["3S 4S 5D 6H JH"]) +expect { + hands = ["4D 5S 6S 8D 3C", "2S 4C 7S 9H 10H", "3S 4S 5D 6H JH"] + result = best_hands(hands) + result == Ok(["3S 4S 5D 6H JH"]) +} # a tie has multiple winners -expect - hands = ["4D 5S 6S 8D 3C", "2S 4C 7S 9H 10H", "3S 4S 5D 6H JH", "3H 4H 5C 6C JD"] - result = best_hands(hands) - result == Ok(["3S 4S 5D 6H JH", "3H 4H 5C 6C JD"]) +expect { + hands = ["4D 5S 6S 8D 3C", "2S 4C 7S 9H 10H", "3S 4S 5D 6H JH", "3H 4H 5C 6C JD"] + result = best_hands(hands) + result == Ok(["3S 4S 5D 6H JH", "3H 4H 5C 6C JD"]) +} # multiple hands with the same high cards, tie compares next highest ranked, down to last card -expect - hands = ["3S 5H 6S 8D 7H", "2S 5D 6D 8C 7S"] - result = best_hands(hands) - result == Ok(["3S 5H 6S 8D 7H"]) +expect { + hands = ["3S 5H 6S 8D 7H", "2S 5D 6D 8C 7S"] + result = best_hands(hands) + result == Ok(["3S 5H 6S 8D 7H"]) +} # winning high card hand also has the lowest card -expect - hands = ["2S 5H 6S 8D 7H", "3S 4D 6D 8C 7S"] - result = best_hands(hands) - result == Ok(["2S 5H 6S 8D 7H"]) +expect { + hands = ["2S 5H 6S 8D 7H", "3S 4D 6D 8C 7S"] + result = best_hands(hands) + result == Ok(["2S 5H 6S 8D 7H"]) +} # one pair beats high card -expect - hands = ["4S 5H 6C 8D KH", "2S 4H 6S 4D JH"] - result = best_hands(hands) - result == Ok(["2S 4H 6S 4D JH"]) +expect { + hands = ["4S 5H 6C 8D KH", "2S 4H 6S 4D JH"] + result = best_hands(hands) + result == Ok(["2S 4H 6S 4D JH"]) +} # highest pair wins -expect - hands = ["4S 2H 6S 2D JH", "2S 4H 6C 4D JD"] - result = best_hands(hands) - result == Ok(["2S 4H 6C 4D JD"]) +expect { + hands = ["4S 2H 6S 2D JH", "2S 4H 6C 4D JD"] + result = best_hands(hands) + result == Ok(["2S 4H 6C 4D JD"]) +} # both hands have the same pair, high card wins -expect - hands = ["4H 4S AH JC 3D", "4C 4D AS 5D 6C"] - result = best_hands(hands) - result == Ok(["4H 4S AH JC 3D"]) +expect { + hands = ["4H 4S AH JC 3D", "4C 4D AS 5D 6C"] + result = best_hands(hands) + result == Ok(["4H 4S AH JC 3D"]) +} # two pairs beats one pair -expect - hands = ["2S 8H 6S 8D JH", "4S 5H 4C 8C 5C"] - result = best_hands(hands) - result == Ok(["4S 5H 4C 8C 5C"]) +expect { + hands = ["2S 8H 6S 8D JH", "4S 5H 4C 8C 5C"] + result = best_hands(hands) + result == Ok(["4S 5H 4C 8C 5C"]) +} # both hands have two pairs, highest ranked pair wins -expect - hands = ["2S 8H 2D 8D 3H", "4S 5H 4C 8S 5D"] - result = best_hands(hands) - result == Ok(["2S 8H 2D 8D 3H"]) +expect { + hands = ["2S 8H 2D 8D 3H", "4S 5H 4C 8S 5D"] + result = best_hands(hands) + result == Ok(["2S 8H 2D 8D 3H"]) +} # both hands have two pairs, with the same highest ranked pair, tie goes to low pair -expect - hands = ["2S QS 2C QD JH", "JD QH JS 8D QC"] - result = best_hands(hands) - result == Ok(["JD QH JS 8D QC"]) +expect { + hands = ["2S QS 2C QD JH", "JD QH JS 8D QC"] + result = best_hands(hands) + result == Ok(["JD QH JS 8D QC"]) +} # both hands have two identically ranked pairs, tie goes to remaining card (kicker) -expect - hands = ["JD QH JS 8D QC", "JS QS JC 2D QD"] - result = best_hands(hands) - result == Ok(["JD QH JS 8D QC"]) +expect { + hands = ["JD QH JS 8D QC", "JS QS JC 2D QD"] + result = best_hands(hands) + result == Ok(["JD QH JS 8D QC"]) +} # both hands have two pairs that add to the same value, win goes to highest pair -expect - hands = ["6S 6H 3S 3H AS", "7H 7S 2H 2S AC"] - result = best_hands(hands) - result == Ok(["7H 7S 2H 2S AC"]) +expect { + hands = ["6S 6H 3S 3H AS", "7H 7S 2H 2S AC"] + result = best_hands(hands) + result == Ok(["7H 7S 2H 2S AC"]) +} # two pairs first ranked by largest pair -expect - hands = ["5C 2S 5S 4H 4C", "6S 2S 6H 7C 2C"] - result = best_hands(hands) - result == Ok(["6S 2S 6H 7C 2C"]) +expect { + hands = ["5C 2S 5S 4H 4C", "6S 2S 6H 7C 2C"] + result = best_hands(hands) + result == Ok(["6S 2S 6H 7C 2C"]) +} # three of a kind beats two pair -expect - hands = ["2S 8H 2H 8D JH", "4S 5H 4C 8S 4H"] - result = best_hands(hands) - result == Ok(["4S 5H 4C 8S 4H"]) +expect { + hands = ["2S 8H 2H 8D JH", "4S 5H 4C 8S 4H"] + result = best_hands(hands) + result == Ok(["4S 5H 4C 8S 4H"]) +} # both hands have three of a kind, tie goes to highest ranked triplet -expect - hands = ["2S 2H 2C 8D JH", "4S AH AS 8C AD"] - result = best_hands(hands) - result == Ok(["4S AH AS 8C AD"]) +expect { + hands = ["2S 2H 2C 8D JH", "4S AH AS 8C AD"] + result = best_hands(hands) + result == Ok(["4S AH AS 8C AD"]) +} # with multiple decks, two players can have same three of a kind, ties go to highest remaining cards -expect - hands = ["5S AH AS 7C AD", "4S AH AS 8C AD"] - result = best_hands(hands) - result == Ok(["4S AH AS 8C AD"]) +expect { + hands = ["5S AH AS 7C AD", "4S AH AS 8C AD"] + result = best_hands(hands) + result == Ok(["4S AH AS 8C AD"]) +} # a straight beats three of a kind -expect - hands = ["4S 5H 4C 8D 4H", "3S 4D 2S 6D 5C"] - result = best_hands(hands) - result == Ok(["3S 4D 2S 6D 5C"]) +expect { + hands = ["4S 5H 4C 8D 4H", "3S 4D 2S 6D 5C"] + result = best_hands(hands) + result == Ok(["3S 4D 2S 6D 5C"]) +} # aces can end a straight (10 J Q K A) -expect - hands = ["4S 5H 4C 8D 4H", "10D JH QS KD AC"] - result = best_hands(hands) - result == Ok(["10D JH QS KD AC"]) +expect { + hands = ["4S 5H 4C 8D 4H", "10D JH QS KD AC"] + result = best_hands(hands) + result == Ok(["10D JH QS KD AC"]) +} # aces can start a straight (A 2 3 4 5) -expect - hands = ["4S 5H 4C 8D 4H", "4D AH 3S 2D 5C"] - result = best_hands(hands) - result == Ok(["4D AH 3S 2D 5C"]) +expect { + hands = ["4S 5H 4C 8D 4H", "4D AH 3S 2D 5C"] + result = best_hands(hands) + result == Ok(["4D AH 3S 2D 5C"]) +} # aces cannot be in the middle of a straight (Q K A 2 3) -expect - hands = ["2C 3D 7H 5H 2S", "QS KH AC 2D 3S"] - result = best_hands(hands) - result == Ok(["2C 3D 7H 5H 2S"]) +expect { + hands = ["2C 3D 7H 5H 2S", "QS KH AC 2D 3S"] + result = best_hands(hands) + result == Ok(["2C 3D 7H 5H 2S"]) +} # both hands with a straight, tie goes to highest ranked card -expect - hands = ["4S 6C 7S 8D 5H", "5S 7H 8S 9D 6H"] - result = best_hands(hands) - result == Ok(["5S 7H 8S 9D 6H"]) +expect { + hands = ["4S 6C 7S 8D 5H", "5S 7H 8S 9D 6H"] + result = best_hands(hands) + result == Ok(["5S 7H 8S 9D 6H"]) +} # even though an ace is usually high, a 5-high straight is the lowest-scoring straight -expect - hands = ["2H 3C 4D 5D 6H", "4S AH 3S 2D 5H"] - result = best_hands(hands) - result == Ok(["2H 3C 4D 5D 6H"]) +expect { + hands = ["2H 3C 4D 5D 6H", "4S AH 3S 2D 5H"] + result = best_hands(hands) + result == Ok(["2H 3C 4D 5D 6H"]) +} # flush beats a straight -expect - hands = ["4C 6H 7D 8D 5H", "2S 4S 5S 6S 7S"] - result = best_hands(hands) - result == Ok(["2S 4S 5S 6S 7S"]) +expect { + hands = ["4C 6H 7D 8D 5H", "2S 4S 5S 6S 7S"] + result = best_hands(hands) + result == Ok(["2S 4S 5S 6S 7S"]) +} # both hands have a flush, tie goes to high card, down to the last one if necessary -expect - hands = ["2H 7H 8H 9H 6H", "3S 5S 6S 7S 8S"] - result = best_hands(hands) - result == Ok(["2H 7H 8H 9H 6H"]) +expect { + hands = ["2H 7H 8H 9H 6H", "3S 5S 6S 7S 8S"] + result = best_hands(hands) + result == Ok(["2H 7H 8H 9H 6H"]) +} # full house beats a flush -expect - hands = ["3H 6H 7H 8H 5H", "4S 5H 4C 5D 4H"] - result = best_hands(hands) - result == Ok(["4S 5H 4C 5D 4H"]) +expect { + hands = ["3H 6H 7H 8H 5H", "4S 5H 4C 5D 4H"] + result = best_hands(hands) + result == Ok(["4S 5H 4C 5D 4H"]) +} # both hands have a full house, tie goes to highest-ranked triplet -expect - hands = ["4H 4S 4D 9S 9D", "5H 5S 5D 8S 8D"] - result = best_hands(hands) - result == Ok(["5H 5S 5D 8S 8D"]) +expect { + hands = ["4H 4S 4D 9S 9D", "5H 5S 5D 8S 8D"] + result = best_hands(hands) + result == Ok(["5H 5S 5D 8S 8D"]) +} # with multiple decks, both hands have a full house with the same triplet, tie goes to the pair -expect - hands = ["5H 5S 5D 9S 9D", "5H 5S 5D 8S 8D"] - result = best_hands(hands) - result == Ok(["5H 5S 5D 9S 9D"]) +expect { + hands = ["5H 5S 5D 9S 9D", "5H 5S 5D 8S 8D"] + result = best_hands(hands) + result == Ok(["5H 5S 5D 9S 9D"]) +} # four of a kind beats a full house -expect - hands = ["4S 5H 4D 5D 4H", "3S 3H 2S 3D 3C"] - result = best_hands(hands) - result == Ok(["3S 3H 2S 3D 3C"]) +expect { + hands = ["4S 5H 4D 5D 4H", "3S 3H 2S 3D 3C"] + result = best_hands(hands) + result == Ok(["3S 3H 2S 3D 3C"]) +} # both hands have four of a kind, tie goes to high quad -expect - hands = ["2S 2H 2C 8D 2D", "4S 5H 5S 5D 5C"] - result = best_hands(hands) - result == Ok(["4S 5H 5S 5D 5C"]) +expect { + hands = ["2S 2H 2C 8D 2D", "4S 5H 5S 5D 5C"] + result = best_hands(hands) + result == Ok(["4S 5H 5S 5D 5C"]) +} # with multiple decks, both hands with identical four of a kind, tie determined by kicker -expect - hands = ["3S 3H 2S 3D 3C", "3S 3H 4S 3D 3C"] - result = best_hands(hands) - result == Ok(["3S 3H 4S 3D 3C"]) +expect { + hands = ["3S 3H 2S 3D 3C", "3S 3H 4S 3D 3C"] + result = best_hands(hands) + result == Ok(["3S 3H 4S 3D 3C"]) +} # straight flush beats four of a kind -expect - hands = ["4S 5H 5S 5D 5C", "7S 8S 9S 6S 10S"] - result = best_hands(hands) - result == Ok(["7S 8S 9S 6S 10S"]) +expect { + hands = ["4S 5H 5S 5D 5C", "7S 8S 9S 6S 10S"] + result = best_hands(hands) + result == Ok(["7S 8S 9S 6S 10S"]) +} # aces can end a straight flush (10 J Q K A) -expect - hands = ["KC AH AS AD AC", "10C JC QC KC AC"] - result = best_hands(hands) - result == Ok(["10C JC QC KC AC"]) +expect { + hands = ["KC AH AS AD AC", "10C JC QC KC AC"] + result = best_hands(hands) + result == Ok(["10C JC QC KC AC"]) +} # aces can start a straight flush (A 2 3 4 5) -expect - hands = ["KS AH AS AD AC", "4H AH 3H 2H 5H"] - result = best_hands(hands) - result == Ok(["4H AH 3H 2H 5H"]) +expect { + hands = ["KS AH AS AD AC", "4H AH 3H 2H 5H"] + result = best_hands(hands) + result == Ok(["4H AH 3H 2H 5H"]) +} # aces cannot be in the middle of a straight flush (Q K A 2 3) -expect - hands = ["2C AC QC 10C KC", "QH KH AH 2H 3H"] - result = best_hands(hands) - result == Ok(["2C AC QC 10C KC"]) +expect { + hands = ["2C AC QC 10C KC", "QH KH AH 2H 3H"] + result = best_hands(hands) + result == Ok(["2C AC QC 10C KC"]) +} # both hands have a straight flush, tie goes to highest-ranked card -expect - hands = ["4H 6H 7H 8H 5H", "5S 7S 8S 9S 6S"] - result = best_hands(hands) - result == Ok(["5S 7S 8S 9S 6S"]) +expect { + hands = ["4H 6H 7H 8H 5H", "5S 7S 8S 9S 6S"] + result = best_hands(hands) + result == Ok(["5S 7S 8S 9S 6S"]) +} # even though an ace is usually high, a 5-high straight flush is the lowest-scoring straight flush -expect - hands = ["2H 3H 4H 5H 6H", "4D AD 3D 2D 5D"] - result = best_hands(hands) - result == Ok(["2H 3H 4H 5H 6H"]) +expect { + hands = ["2H 3H 4H 5H 6H", "4D AD 3D 2D 5D"] + result = best_hands(hands) + result == Ok(["2H 3H 4H 5H 6H"]) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/prime-factors/.meta/Example.roc b/exercises/practice/prime-factors/.meta/Example.roc index 9b2fce2b..1a67c9ad 100644 --- a/exercises/practice/prime-factors/.meta/Example.roc +++ b/exercises/practice/prime-factors/.meta/Example.roc @@ -1,16 +1,25 @@ -module [prime_factors] +PrimeFactors :: {}.{ + prime_factors : U64 -> List(U64) + prime_factors = |value| { + find_prime_factors = |factors, n, p| { + if n < 2 { + factors + } else if n->is_multiple_of(p) { + find_prime_factors(factors.append(p), (n // p), p) + } else if p * p < n { + next_p = if p == 2 { + 3 + } else { + p + 2 + } + find_prime_factors(factors, n, next_p) + } else { + factors.append(n) + } + } -prime_factors : U64 -> List U64 -prime_factors = |value| - find_prime_factors = |factors, n, p| - if n < 2 then - factors - else if n |> Num.is_multiple_of(p) then - find_prime_factors(List.append(factors, p), (n // p), p) - else if p * p < n then - next_p = if p == 2 then 3 else p + 2 - find_prime_factors(factors, n, next_p) - else - List.append(factors, n) + find_prime_factors([], value, 2) + } +} - find_prime_factors([], value, 2) +is_multiple_of = |n, p| n % p == 0 diff --git a/exercises/practice/prime-factors/.meta/template.j2 b/exercises/practice/prime-factors/.meta/template.j2 index b4b90411..08c00775 100644 --- a/exercises/practice/prime-factors/.meta/template.j2 +++ b/exercises/practice/prime-factors/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ exercise | to_snake }}] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ exercise | to_snake }}({{ case["input"]["value"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/prime-factors/PrimeFactors.roc b/exercises/practice/prime-factors/PrimeFactors.roc index f8df4a49..9b9dcc9a 100644 --- a/exercises/practice/prime-factors/PrimeFactors.roc +++ b/exercises/practice/prime-factors/PrimeFactors.roc @@ -1,5 +1,6 @@ -module [prime_factors] - -prime_factors : U64 -> List U64 -prime_factors = |value| - crash("Please implement the 'prime_factors' function") +PrimeFactors :: {}.{ + prime_factors : U64 -> List(U64) + prime_factors = |value| { + crash "Please implement the 'prime_factors' function" + } +} diff --git a/exercises/practice/prime-factors/prime-factors-test.roc b/exercises/practice/prime-factors/prime-factors-test.roc index 6febf001..457d1699 100644 --- a/exercises/practice/prime-factors/prime-factors-test.roc +++ b/exercises/practice/prime-factors/prime-factors-test.roc @@ -1,74 +1,82 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/prime-factors/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import PrimeFactors exposing [prime_factors] # no factors -expect - result = prime_factors(1) - result == [] +expect { + result = prime_factors(1) + result == [] +} # prime number -expect - result = prime_factors(2) - result == [2] +expect { + result = prime_factors(2) + result == [2] +} # another prime number -expect - result = prime_factors(3) - result == [3] +expect { + result = prime_factors(3) + result == [3] +} # square of a prime -expect - result = prime_factors(9) - result == [3, 3] +expect { + result = prime_factors(9) + result == [3, 3] +} # product of first prime -expect - result = prime_factors(4) - result == [2, 2] +expect { + result = prime_factors(4) + result == [2, 2] +} # cube of a prime -expect - result = prime_factors(8) - result == [2, 2, 2] +expect { + result = prime_factors(8) + result == [2, 2, 2] +} # product of second prime -expect - result = prime_factors(27) - result == [3, 3, 3] +expect { + result = prime_factors(27) + result == [3, 3, 3] +} # product of third prime -expect - result = prime_factors(625) - result == [5, 5, 5, 5] +expect { + result = prime_factors(625) + result == [5, 5, 5, 5] +} # product of first and second prime -expect - result = prime_factors(6) - result == [2, 3] +expect { + result = prime_factors(6) + result == [2, 3] +} # product of primes and non-primes -expect - result = prime_factors(12) - result == [2, 2, 3] +expect { + result = prime_factors(12) + result == [2, 2, 3] +} # product of primes -expect - result = prime_factors(901255) - result == [5, 17, 23, 461] +expect { + result = prime_factors(901255) + result == [5, 17, 23, 461] +} # factors include a large prime -expect - result = prime_factors(93819012551) - result == [11, 9539, 894119] +expect { + result = prime_factors(93819012551) + result == [11, 9539, 894119] +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/protein-translation/.meta/Example.roc b/exercises/practice/protein-translation/.meta/Example.roc index 836b06af..260b5e8b 100644 --- a/exercises/practice/protein-translation/.meta/Example.roc +++ b/exercises/practice/protein-translation/.meta/Example.roc @@ -1,39 +1,63 @@ -module [to_protein] +ProteinTranslation :: {}.{ + Codon : List(U8) + AminoAcid : [Cysteine, Leucine, Methionine, Phenylalanine, Serine, Tryptophan, Tyrosine] + Protein : List(AminoAcid) -Codon : List U8 -AminoAcid : [Cysteine, Leucine, Methionine, Phenylalanine, Serine, Tryptophan, Tyrosine] -Protein : List AminoAcid + to_protein : Str -> Try(Protein, [InvalidCodon(Codon)]) + to_protein = |rna| { + help : Protein, List(Codon) -> Try(Protein, [InvalidCodon(Codon)]) + help = |protein, codons| { + match codons { + [] => Ok(protein) + [codon, .. as rest] => { + match to_instruction(codon)? { + Append(amino_acid) => protein.append(amino_acid)->help(rest) + Stop => Ok(protein) + } + } + } + } + help([], rna.to_utf8()->chunks_of(3)) + } +} -to_instruction : Codon -> Result [Append AminoAcid, Stop] [InvalidCodon Codon] -to_instruction = |codon| - when codon is - ['A', 'U', 'G'] -> Ok(Append(Methionine)) - ['U', 'U', 'U'] -> Ok(Append(Phenylalanine)) - ['U', 'U', 'C'] -> Ok(Append(Phenylalanine)) - ['U', 'U', 'A'] -> Ok(Append(Leucine)) - ['U', 'U', 'G'] -> Ok(Append(Leucine)) - ['U', 'C', 'U'] -> Ok(Append(Serine)) - ['U', 'C', 'C'] -> Ok(Append(Serine)) - ['U', 'C', 'A'] -> Ok(Append(Serine)) - ['U', 'C', 'G'] -> Ok(Append(Serine)) - ['U', 'A', 'U'] -> Ok(Append(Tyrosine)) - ['U', 'A', 'C'] -> Ok(Append(Tyrosine)) - ['U', 'G', 'U'] -> Ok(Append(Cysteine)) - ['U', 'G', 'C'] -> Ok(Append(Cysteine)) - ['U', 'G', 'G'] -> Ok(Append(Tryptophan)) - ['U', 'A', 'A'] -> Ok(Stop) - ['U', 'A', 'G'] -> Ok(Stop) - ['U', 'G', 'A'] -> Ok(Stop) - _ -> Err(InvalidCodon(codon)) +to_instruction : Codon -> Try([Append(AminoAcid), Stop], [InvalidCodon(Codon)]) +to_instruction = |codon| { + match codon { + ['A', 'U', 'G'] => Ok(Append(Methionine)) + ['U', 'U', 'U'] => Ok(Append(Phenylalanine)) + ['U', 'U', 'C'] => Ok(Append(Phenylalanine)) + ['U', 'U', 'A'] => Ok(Append(Leucine)) + ['U', 'U', 'G'] => Ok(Append(Leucine)) + ['U', 'C', 'U'] => Ok(Append(Serine)) + ['U', 'C', 'C'] => Ok(Append(Serine)) + ['U', 'C', 'A'] => Ok(Append(Serine)) + ['U', 'C', 'G'] => Ok(Append(Serine)) + ['U', 'A', 'U'] => Ok(Append(Tyrosine)) + ['U', 'A', 'C'] => Ok(Append(Tyrosine)) + ['U', 'G', 'U'] => Ok(Append(Cysteine)) + ['U', 'G', 'C'] => Ok(Append(Cysteine)) + ['U', 'G', 'G'] => Ok(Append(Tryptophan)) + ['U', 'A', 'A'] => Ok(Stop) + ['U', 'A', 'G'] => Ok(Stop) + ['U', 'G', 'A'] => Ok(Stop) + _ => Err(InvalidCodon(codon)) + } +} -to_protein : Str -> Result Protein [InvalidCodon Codon] -to_protein = |rna| - help = |protein, codons| - when codons is - [] -> Ok(protein) - [codon, .. as rest] -> - when codon |> to_instruction is - Ok(Append(amino_acid)) -> protein |> List.append(amino_acid) |> help(rest) - Ok(Stop) -> Ok(protein) - Err(err) -> Err(err) - help([], (rna |> Str.to_utf8 |> List.chunks_of(3))) +# The following functions should soon be available in Roc's builtins +chunks_of = |iter, size| { + var $state = [] + var $chunk = [] + for item in iter { + $chunk = $chunk.append(item) + if $chunk.len() == size { + $state = $state.append($chunk) + $chunk = [] + } + } + if $chunk.len() > 0 { + $state = $state.append($chunk) + } + $state +} diff --git a/exercises/practice/protein-translation/.meta/template.j2 b/exercises/practice/protein-translation/.meta/template.j2 index 9b602769..3eb89911 100644 --- a/exercises/practice/protein-translation/.meta/template.j2 +++ b/exercises/practice/protein-translation/.meta/template.j2 @@ -6,14 +6,16 @@ import {{ exercise | to_pascal }} exposing [to_protein] {% for case in cases -%} # {{ case["description"] }} -expect +expect { rna = {{ case["input"]["strand"] | to_roc }} - result = rna |> to_protein + result = rna -> to_protein() {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} result == Ok([{%- for aminoAcid in case["expected"] -%}{{ aminoAcid | to_pascal }}, {%- endfor %}]) {%- endif %} +} {% endfor %} +{{ macros.footer() }} diff --git a/exercises/practice/protein-translation/ProteinTranslation.roc b/exercises/practice/protein-translation/ProteinTranslation.roc index 8a61979f..392cf5bf 100644 --- a/exercises/practice/protein-translation/ProteinTranslation.roc +++ b/exercises/practice/protein-translation/ProteinTranslation.roc @@ -1,8 +1,10 @@ -module [to_protein] +ProteinTranslation :: {}.{ + Codon : List(U8) + AminoAcid : [Cysteine, Leucine, Methionine, Phenylalanine, Serine, Tryptophan, Tyrosine] + Protein : List(AminoAcid) -AminoAcid : [Cysteine, Leucine, Methionine, Phenylalanine, Serine, Tryptophan, Tyrosine] -Protein : List AminoAcid - -to_protein : Str -> Result Protein _ -to_protein = |rna| - crash("Please implement the 'to_protein' function") + to_protein : Str -> Try(Protein, _) + to_protein = |rna| { + crash "Please implement the 'to_protein' function" + } +} diff --git a/exercises/practice/protein-translation/protein-translation-test.roc b/exercises/practice/protein-translation/protein-translation-test.roc index a6b589ce..815504b6 100644 --- a/exercises/practice/protein-translation/protein-translation-test.roc +++ b/exercises/practice/protein-translation/protein-translation-test.roc @@ -1,194 +1,325 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/protein-translation/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import ProteinTranslation exposing [to_protein] # Empty RNA sequence results in no proteins -expect - rna = "" - result = rna |> to_protein - result == Ok([]) +expect { + rna = "" + result = rna->to_protein() + result == Ok([]) +} # Methionine RNA sequence -expect - rna = "AUG" - result = rna |> to_protein - result == Ok([Methionine]) +expect { + rna = "AUG" + result = rna->to_protein() + result == Ok( + [ + Methionine, + ], + ) +} # Phenylalanine RNA sequence 1 -expect - rna = "UUU" - result = rna |> to_protein - result == Ok([Phenylalanine]) +expect { + rna = "UUU" + result = rna->to_protein() + result == Ok( + [ + Phenylalanine, + ], + ) +} # Phenylalanine RNA sequence 2 -expect - rna = "UUC" - result = rna |> to_protein - result == Ok([Phenylalanine]) +expect { + rna = "UUC" + result = rna->to_protein() + result == Ok( + [ + Phenylalanine, + ], + ) +} # Leucine RNA sequence 1 -expect - rna = "UUA" - result = rna |> to_protein - result == Ok([Leucine]) +expect { + rna = "UUA" + result = rna->to_protein() + result == Ok( + [ + Leucine, + ], + ) +} # Leucine RNA sequence 2 -expect - rna = "UUG" - result = rna |> to_protein - result == Ok([Leucine]) +expect { + rna = "UUG" + result = rna->to_protein() + result == Ok( + [ + Leucine, + ], + ) +} # Serine RNA sequence 1 -expect - rna = "UCU" - result = rna |> to_protein - result == Ok([Serine]) +expect { + rna = "UCU" + result = rna->to_protein() + result == Ok( + [ + Serine, + ], + ) +} # Serine RNA sequence 2 -expect - rna = "UCC" - result = rna |> to_protein - result == Ok([Serine]) +expect { + rna = "UCC" + result = rna->to_protein() + result == Ok( + [ + Serine, + ], + ) +} # Serine RNA sequence 3 -expect - rna = "UCA" - result = rna |> to_protein - result == Ok([Serine]) +expect { + rna = "UCA" + result = rna->to_protein() + result == Ok( + [ + Serine, + ], + ) +} # Serine RNA sequence 4 -expect - rna = "UCG" - result = rna |> to_protein - result == Ok([Serine]) +expect { + rna = "UCG" + result = rna->to_protein() + result == Ok( + [ + Serine, + ], + ) +} # Tyrosine RNA sequence 1 -expect - rna = "UAU" - result = rna |> to_protein - result == Ok([Tyrosine]) +expect { + rna = "UAU" + result = rna->to_protein() + result == Ok( + [ + Tyrosine, + ], + ) +} # Tyrosine RNA sequence 2 -expect - rna = "UAC" - result = rna |> to_protein - result == Ok([Tyrosine]) +expect { + rna = "UAC" + result = rna->to_protein() + result == Ok( + [ + Tyrosine, + ], + ) +} # Cysteine RNA sequence 1 -expect - rna = "UGU" - result = rna |> to_protein - result == Ok([Cysteine]) +expect { + rna = "UGU" + result = rna->to_protein() + result == Ok( + [ + Cysteine, + ], + ) +} # Cysteine RNA sequence 2 -expect - rna = "UGC" - result = rna |> to_protein - result == Ok([Cysteine]) +expect { + rna = "UGC" + result = rna->to_protein() + result == Ok( + [ + Cysteine, + ], + ) +} # Tryptophan RNA sequence -expect - rna = "UGG" - result = rna |> to_protein - result == Ok([Tryptophan]) +expect { + rna = "UGG" + result = rna->to_protein() + result == Ok( + [ + Tryptophan, + ], + ) +} # STOP codon RNA sequence 1 -expect - rna = "UAA" - result = rna |> to_protein - result == Ok([]) +expect { + rna = "UAA" + result = rna->to_protein() + result == Ok([]) +} # STOP codon RNA sequence 2 -expect - rna = "UAG" - result = rna |> to_protein - result == Ok([]) +expect { + rna = "UAG" + result = rna->to_protein() + result == Ok([]) +} # STOP codon RNA sequence 3 -expect - rna = "UGA" - result = rna |> to_protein - result == Ok([]) +expect { + rna = "UGA" + result = rna->to_protein() + result == Ok([]) +} # Sequence of two protein codons translates into proteins -expect - rna = "UUUUUU" - result = rna |> to_protein - result == Ok([Phenylalanine, Phenylalanine]) +expect { + rna = "UUUUUU" + result = rna->to_protein() + result == Ok( + [ + Phenylalanine, + Phenylalanine, + ], + ) +} # Sequence of two different protein codons translates into proteins -expect - rna = "UUAUUG" - result = rna |> to_protein - result == Ok([Leucine, Leucine]) +expect { + rna = "UUAUUG" + result = rna->to_protein() + result == Ok( + [ + Leucine, + Leucine, + ], + ) +} # Translate RNA strand into correct protein list -expect - rna = "AUGUUUUGG" - result = rna |> to_protein - result == Ok([Methionine, Phenylalanine, Tryptophan]) +expect { + rna = "AUGUUUUGG" + result = rna->to_protein() + result == Ok( + [ + Methionine, + Phenylalanine, + Tryptophan, + ], + ) +} # Translation stops if STOP codon at beginning of sequence -expect - rna = "UAGUGG" - result = rna |> to_protein - result == Ok([]) +expect { + rna = "UAGUGG" + result = rna->to_protein() + result == Ok([]) +} # Translation stops if STOP codon at end of two-codon sequence -expect - rna = "UGGUAG" - result = rna |> to_protein - result == Ok([Tryptophan]) +expect { + rna = "UGGUAG" + result = rna->to_protein() + result == Ok( + [ + Tryptophan, + ], + ) +} # Translation stops if STOP codon at end of three-codon sequence -expect - rna = "AUGUUUUAA" - result = rna |> to_protein - result == Ok([Methionine, Phenylalanine]) +expect { + rna = "AUGUUUUAA" + result = rna->to_protein() + result == Ok( + [ + Methionine, + Phenylalanine, + ], + ) +} # Translation stops if STOP codon in middle of three-codon sequence -expect - rna = "UGGUAGUGG" - result = rna |> to_protein - result == Ok([Tryptophan]) +expect { + rna = "UGGUAGUGG" + result = rna->to_protein() + result == Ok( + [ + Tryptophan, + ], + ) +} # Translation stops if STOP codon in middle of six-codon sequence -expect - rna = "UGGUGUUAUUAAUGGUUU" - result = rna |> to_protein - result == Ok([Tryptophan, Cysteine, Tyrosine]) +expect { + rna = "UGGUGUUAUUAAUGGUUU" + result = rna->to_protein() + result == Ok( + [ + Tryptophan, + Cysteine, + Tyrosine, + ], + ) +} # Sequence of two non-STOP codons does not translate to a STOP codon -expect - rna = "AUGAUG" - result = rna |> to_protein - result == Ok([Methionine, Methionine]) +expect { + rna = "AUGAUG" + result = rna->to_protein() + result == Ok( + [ + Methionine, + Methionine, + ], + ) +} # Unknown amino acids, not part of a codon, can't translate -expect - rna = "XYZ" - result = rna |> to_protein - result |> Result.is_err +expect { + rna = "XYZ" + result = rna->to_protein() + result.is_err() +} # Incomplete RNA sequence can't translate -expect - rna = "AUGU" - result = rna |> to_protein - result |> Result.is_err - -# Incomplete RNA sequence can translate if valid until a STOP codon -expect - rna = "UUCUUCUAAUGGU" - result = rna |> to_protein - result == Ok([Phenylalanine, Phenylalanine]) +expect { + rna = "AUGU" + result = rna->to_protein() + result.is_err() +} +# The following test is commented out for now because it causes a segfault +# See https://github.com/roc-lang/roc/issues/9753 +# TODO: uncomment once the issue is resolved + +## Incomplete RNA sequence can translate if valid until a STOP codon +#expect { +# rna = "UUCUUCUAAUGGU" +# result = rna->to_protein() +# result == Ok( +# [ +# Phenylalanine, +# Phenylalanine, +# ], +# ) +#} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/proverb/.meta/Example.roc b/exercises/practice/proverb/.meta/Example.roc index 55079566..b7716851 100644 --- a/exercises/practice/proverb/.meta/Example.roc +++ b/exercises/practice/proverb/.meta/Example.roc @@ -1,16 +1,23 @@ -module [recite] +Proverb :: {}.{ + recite : List(Str) -> Str + recite = |strings| { + match strings { + [] => "" + [first_thing, .. as rest] => { + (_, lines) = + rest.fold( + (first_thing, []), + |(thing1, acc_lines), thing2| { + (thing2, acc_lines.append("For want of a ${thing1} the ${thing2} was lost.")) + }, + ) -recite : List Str -> Str -recite = |strings| - when strings is - [] -> "" - [firtst_thing, .. as rest] -> - rest - |> List.walk( - (firtst_thing, []), - |(thing1, lines), thing2| - (thing2, lines |> List.append("For want of a ${thing1} the ${thing2} was lost.")), - ) - |> .1 - |> List.append("And all for the want of a ${firtst_thing}.") - |> Str.join_with("\n") + lines + .append( + "And all for the want of a ${first_thing}.", + ) + ->Str.join_with("\n") + } + } + } +} diff --git a/exercises/practice/proverb/.meta/template.j2 b/exercises/practice/proverb/.meta/template.j2 index 5c274011..3875a281 100644 --- a/exercises/practice/proverb/.meta/template.j2 +++ b/exercises/practice/proverb/.meta/template.j2 @@ -6,10 +6,12 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["strings"] | to_roc }}) expected = {{ case["expected"] | to_roc_multiline_string | indent(8) }} result == expected +} {% endfor %} +{{ macros.footer() }} diff --git a/exercises/practice/proverb/Proverb.roc b/exercises/practice/proverb/Proverb.roc index faa3131f..9d6f03f8 100644 --- a/exercises/practice/proverb/Proverb.roc +++ b/exercises/practice/proverb/Proverb.roc @@ -1,5 +1,6 @@ -module [recite] - -recite : List Str -> Str -recite = |strings| - crash("Please implement the 'recite' function") +Proverb :: {}.{ + recite : List(Str) -> Str + recite = |strings| { + crash "Please implement the 'recite' function" + } +} diff --git a/exercises/practice/proverb/proverb-test.roc b/exercises/practice/proverb/proverb-test.roc index 0d39d912..3c203b86 100644 --- a/exercises/practice/proverb/proverb-test.roc +++ b/exercises/practice/proverb/proverb-test.roc @@ -1,74 +1,72 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/proverb/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Proverb exposing [recite] # zero pieces -expect - result = recite([]) - expected = "" - result == expected +expect { + result = recite([]) + expected = "" + result == expected +} # one piece -expect - result = recite(["nail"]) - expected = "And all for the want of a nail." - result == expected +expect { + result = recite(["nail"]) + expected = "And all for the want of a nail." + result == expected +} # two pieces -expect - result = recite(["nail", "shoe"]) - expected = - """ - For want of a nail the shoe was lost. - And all for the want of a nail. - """ - result == expected +expect { + result = recite(["nail", "shoe"]) + expected = + \\For want of a nail the shoe was lost. + \\And all for the want of a nail. + + result == expected +} # three pieces -expect - result = recite(["nail", "shoe", "horse"]) - expected = - """ - For want of a nail the shoe was lost. - For want of a shoe the horse was lost. - And all for the want of a nail. - """ - result == expected +expect { + result = recite(["nail", "shoe", "horse"]) + expected = + \\For want of a nail the shoe was lost. + \\For want of a shoe the horse was lost. + \\And all for the want of a nail. + + result == expected +} # full proverb -expect - result = recite(["nail", "shoe", "horse", "rider", "message", "battle", "kingdom"]) - expected = - """ - For want of a nail the shoe was lost. - For want of a shoe the horse was lost. - For want of a horse the rider was lost. - For want of a rider the message was lost. - For want of a message the battle was lost. - For want of a battle the kingdom was lost. - And all for the want of a nail. - """ - result == expected +expect { + result = recite(["nail", "shoe", "horse", "rider", "message", "battle", "kingdom"]) + expected = + \\For want of a nail the shoe was lost. + \\For want of a shoe the horse was lost. + \\For want of a horse the rider was lost. + \\For want of a rider the message was lost. + \\For want of a message the battle was lost. + \\For want of a battle the kingdom was lost. + \\And all for the want of a nail. + + result == expected +} # four pieces modernized -expect - result = recite(["pin", "gun", "soldier", "battle"]) - expected = - """ - For want of a pin the gun was lost. - For want of a gun the soldier was lost. - For want of a soldier the battle was lost. - And all for the want of a pin. - """ - result == expected +expect { + result = recite(["pin", "gun", "soldier", "battle"]) + expected = + \\For want of a pin the gun was lost. + \\For want of a gun the soldier was lost. + \\For want of a soldier the battle was lost. + \\And all for the want of a pin. + result == expected +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/pythagorean-triplet/.meta/Example.roc b/exercises/practice/pythagorean-triplet/.meta/Example.roc index e0466f4e..6f9df885 100644 --- a/exercises/practice/pythagorean-triplet/.meta/Example.roc +++ b/exercises/practice/pythagorean-triplet/.meta/Example.roc @@ -1,23 +1,30 @@ -module [triplets_with_sum] +PythagoreanTriplet :: {}.{ + Triplet : (U64, U64, U64) -Triplet : (U64, U64, U64) + triplets_with_sum : U64 -> Set(Triplet) + triplets_with_sum = |sum| { + help = |triplets, a, b, sum2| { + if a + b + b + 1 > sum2 { + # c would have to be too small (≤ b) + if 3 * (a + 1) > sum2 { + # we can't increment a so we're done + triplets + } else { + help(triplets, (a + 1), (a + 2), sum2) # increment a + } + } else { + c = sum2 - a - b + new_triplets = + if a * a + b * b == c * c { + triplets.append((a, b, c)) # success! + } else { + triplets + } + help(new_triplets, a, (b + 1), sum2) # increment b + } + } + help([], 1, 2, sum)->Set.from_list() + } +} -triplets_with_sum : U64 -> Set Triplet -triplets_with_sum = |sum| - help = |triplets, a, b| - if a + b + b + 1 > sum then - # c would have to be too small (≤ b) - if 3 * (a + 1) > sum then - # we can't increment a so we're done - triplets - else - help(triplets, (a + 1), (a + 2)) # increment a - else - c = sum - a - b - new_triplets = - if a * a + b * b == c * c then - triplets |> List.append((a, b, c)) # success! - else - triplets - help(new_triplets, a, (b + 1)) # increment b - help([], 1, 2) |> Set.from_list +# TODO: update once https://github.com/roc-lang/roc/issues/9690 is fixed diff --git a/exercises/practice/pythagorean-triplet/.meta/template.j2 b/exercises/practice/pythagorean-triplet/.meta/template.j2 index e7b51fc9..40413365 100644 --- a/exercises/practice/pythagorean-triplet/.meta/template.j2 +++ b/exercises/practice/pythagorean-triplet/.meta/template.j2 @@ -6,11 +6,14 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["n"] }}) expected = Set.from_list([{%- for triplet in case["expected"] %} ({{ triplet[0] }}, {{ triplet[1] }}, {{ triplet[2] }}), {%- endfor %}]) result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/pythagorean-triplet/PythagoreanTriplet.roc b/exercises/practice/pythagorean-triplet/PythagoreanTriplet.roc index d5a6cfdb..7756ee36 100644 --- a/exercises/practice/pythagorean-triplet/PythagoreanTriplet.roc +++ b/exercises/practice/pythagorean-triplet/PythagoreanTriplet.roc @@ -1,7 +1,8 @@ -module [triplets_with_sum] +PythagoreanTriplet :: {}.{ + Triplet : (U64, U64, U64) -Triplet : (U64, U64, U64) - -triplets_with_sum : U64 -> Set Triplet -triplets_with_sum = |sum| - crash("Please implement the 'triplets_with_sum' function") + triplets_with_sum : U64 -> Set(Triplet) + triplets_with_sum = |sum| { + crash "Please implement the 'triplets_with_sum' function" + } +} diff --git a/exercises/practice/pythagorean-triplet/pythagorean-triplet-test.roc b/exercises/practice/pythagorean-triplet/pythagorean-triplet-test.roc index 44ae295b..0a68c527 100644 --- a/exercises/practice/pythagorean-triplet/pythagorean-triplet-test.roc +++ b/exercises/practice/pythagorean-triplet/pythagorean-triplet-test.roc @@ -1,92 +1,95 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/pythagorean-triplet/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import PythagoreanTriplet exposing [triplets_with_sum] # triplets whose sum is 12 -expect - result = triplets_with_sum(12) - expected = Set.from_list( - [ - (3, 4, 5), - ], - ) - result == expected +expect { + result = triplets_with_sum(12) + expected = Set.from_list( + [ + (3, 4, 5), + ], + ) + result == expected +} # triplets whose sum is 108 -expect - result = triplets_with_sum(108) - expected = Set.from_list( - [ - (27, 36, 45), - ], - ) - result == expected +expect { + result = triplets_with_sum(108) + expected = Set.from_list( + [ + (27, 36, 45), + ], + ) + result == expected +} # triplets whose sum is 1000 -expect - result = triplets_with_sum(1000) - expected = Set.from_list( - [ - (200, 375, 425), - ], - ) - result == expected +expect { + result = triplets_with_sum(1000) + expected = Set.from_list( + [ + (200, 375, 425), + ], + ) + result == expected +} # no matching triplets for 1001 -expect - result = triplets_with_sum(1001) - expected = Set.from_list([]) - result == expected +expect { + result = triplets_with_sum(1001) + expected = Set.from_list([]) + result == expected +} # returns all matching triplets -expect - result = triplets_with_sum(90) - expected = Set.from_list( - [ - (9, 40, 41), - (15, 36, 39), - ], - ) - result == expected +expect { + result = triplets_with_sum(90) + expected = Set.from_list( + [ + (9, 40, 41), + (15, 36, 39), + ], + ) + result == expected +} # several matching triplets -expect - result = triplets_with_sum(840) - expected = Set.from_list( - [ - (40, 399, 401), - (56, 390, 394), - (105, 360, 375), - (120, 350, 370), - (140, 336, 364), - (168, 315, 357), - (210, 280, 350), - (240, 252, 348), - ], - ) - result == expected +expect { + result = triplets_with_sum(840) + expected = Set.from_list( + [ + (40, 399, 401), + (56, 390, 394), + (105, 360, 375), + (120, 350, 370), + (140, 336, 364), + (168, 315, 357), + (210, 280, 350), + (240, 252, 348), + ], + ) + result == expected +} # triplets for large number -expect - result = triplets_with_sum(30000) - expected = Set.from_list( - [ - (1200, 14375, 14425), - (1875, 14000, 14125), - (5000, 12000, 13000), - (6000, 11250, 12750), - (7500, 10000, 12500), - ], - ) - result == expected +expect { + result = triplets_with_sum(30000) + expected = Set.from_list( + [ + (1200, 14375, 14425), + (1875, 14000, 14125), + (5000, 12000, 13000), + (6000, 11250, 12750), + (7500, 10000, 12500), + ], + ) + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/queen-attack/.docs/instructions.append.md b/exercises/practice/queen-attack/.docs/instructions.append.md index 39447833..dea983c8 100644 --- a/exercises/practice/queen-attack/.docs/instructions.append.md +++ b/exercises/practice/queen-attack/.docs/instructions.append.md @@ -6,14 +6,14 @@ In Chess, rows are called "ranks", and columns are called "files". Ranks range from 1 to 8, while files range from A to H. In this exercise, `Square` is an opaque type which represents a square on a chessboard. -It uses 0-indexed `row` & `column` fields internally, and it guarantees that they are always valid (i.e., from 0 to 7). +It uses 0-indexed `row` and `column` fields internally, and it guarantees that they are always valid (i.e., from 0 to 7). The `create` function takes a `Str` as input, representing a valid square on the chessboard using the official notation, such as `"C5"`. The `create` function must parse this `Str`, verify that it represents a valid square, and if so it must return a `Square` value containing the appropriate `row` and `column` fields. Rank 1 corresponds to row 0, and rank 8 corresponds to row 7. -For example, `create "C5"` must return `@Square { row : 2, column : 3 }`. +For example, `create("C5")` must return a square with row 4 and column 2. -The `QueenAttack` module also exposes handy `rank` & `file` functions. +The `Square` type also exposes handy `rank` and `file` methods. In the example above, `rank` must return the integer `5`, and `file` must return the character `'C'`. Lastly, the `queen_can_attack` function takes two different `Square` values and checks whether or not queens placed on these squares can attack each other. diff --git a/exercises/practice/queen-attack/.meta/Example.roc b/exercises/practice/queen-attack/.meta/Example.roc index eb779b49..79a70585 100644 --- a/exercises/practice/queen-attack/.meta/Example.roc +++ b/exercises/practice/queen-attack/.meta/Example.roc @@ -1,29 +1,37 @@ -module [create, rank, file, queen_can_attack] +ChessSquare :: { row : U8, column : U8 }.{ + rank : ChessSquare -> U8 + rank = |{ row, column: _ }| row + 1 -Square := { row : U8, column : U8 } + file : ChessSquare -> U8 + file = |{ row: _, column }| column + 'A' -rank : Square -> U8 -rank = |@Square({ row, column: _ })| 8 - row + create : Str -> Try(ChessSquare, [InvalidSquare]) + create = |square_str| { + chars = square_str.to_utf8() + if chars.len() != 2 { + Err(InvalidSquare) + } else { + file_char = chars.get(0).map_err(|OutOfBounds| InvalidSquare)? + rank_char = chars.get(1).map_err(|OutOfBounds| InvalidSquare)? + if file_char < 'A' or file_char > 'H' or rank_char < '1' or rank_char > '8' { + Err(InvalidSquare) + } else { + Ok({ row: rank_char - '1', column: file_char - 'A' }) + } + } + } -file : Square -> U8 -file = |@Square({ row: _, column })| column + 'A' - -create : Str -> Result Square [InvalidSquare] -create = |square_str| - chars = square_str |> Str.to_utf8 - if List.len(chars) != 2 then - Err(InvalidSquare) - else - file_char = chars |> List.get(0) |> Result.map_err(|OutOfBounds| InvalidSquare)? - rank_char = chars |> List.get(1) |> Result.map_err(|OutOfBounds| InvalidSquare)? - if file_char < 'A' or file_char > 'H' or rank_char < '1' or rank_char > '8' then - Err(InvalidSquare) - else - Ok(@Square({ row: 7 - (rank_char - '1'), column: file_char - 'A' })) - -queen_can_attack : Square, Square -> Bool -queen_can_attack = |@Square({ row: r1, column: c1 }), @Square({ row: r2, column: c2 })| - abs_diff = |u, v| if u < v then v - u else u - v - row_diff = abs_diff(r1, r2) - column_diff = abs_diff(c1, c2) - row_diff == 0 or column_diff == 0 or row_diff == column_diff + queen_can_attack : ChessSquare, ChessSquare -> Bool + queen_can_attack = |{ row: r1, column: c1 }, { row: r2, column: c2 }| { + abs_diff = |u, v| { + if u < v { + v - u + } else { + u - v + } + } + rank_diff = abs_diff(r1, r2) + file_diff = abs_diff(c1, c2) + rank_diff == 0 or file_diff == 0 or rank_diff == file_diff + } +} diff --git a/exercises/practice/queen-attack/.meta/config.json b/exercises/practice/queen-attack/.meta/config.json index d9da1735..65e0ce42 100644 --- a/exercises/practice/queen-attack/.meta/config.json +++ b/exercises/practice/queen-attack/.meta/config.json @@ -4,7 +4,7 @@ ], "files": { "solution": [ - "QueenAttack.roc" + "ChessSquare.roc" ], "test": [ "queen-attack-test.roc" diff --git a/exercises/practice/queen-attack/.meta/plugins.py b/exercises/practice/queen-attack/.meta/plugins.py index 11b94b37..f31d9c62 100644 --- a/exercises/practice/queen-attack/.meta/plugins.py +++ b/exercises/practice/queen-attack/.meta/plugins.py @@ -5,7 +5,7 @@ def to_square(queen): def to_rank(row): - return 8 - row + return row + 1 def to_file(column): diff --git a/exercises/practice/queen-attack/.meta/template.j2 b/exercises/practice/queen-attack/.meta/template.j2 index 34720f73..f895dde1 100644 --- a/exercises/practice/queen-attack/.meta/template.j2 +++ b/exercises/practice/queen-attack/.meta/template.j2 @@ -2,7 +2,7 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [create, rank, file, queen_can_attack] +import ChessSquare {% for supercase in cases %} ## @@ -13,30 +13,33 @@ import {{ exercise | to_pascal }} exposing [create, rank, file, queen_can_attack # {{ case["description"] }} {%- if case["property"] == "create" %} {%- if case["expected"] == 0 %} -expect - maybe_square = create("{{ plugins.to_square(case["input"]["queen"]) }}") - result = maybe_square |> Result.try( |square| Ok(rank square) ) - result == Ok({{ plugins.to_rank(case["input"]["queen"]["position"]["row"]) }}) +expect { + square = ChessSquare.create("{{ plugins.to_square(case["input"]["queen"]) }}")? + result = square.rank() + result == {{ plugins.to_rank(case["input"]["queen"]["position"]["row"]) }} +} -expect - maybe_square = create("{{ plugins.to_square(case["input"]["queen"]) }}") - result = maybe_square |> Result.try( |square| Ok(file square) ) - result == Ok('{{ plugins.to_file(case["input"]["queen"]["position"]["column"]) }}') +expect { + square = ChessSquare.create("{{ plugins.to_square(case["input"]["queen"]) }}")? + result = square.file() + result == '{{ plugins.to_file(case["input"]["queen"]["position"]["column"]) }}' +} {%- else %} -expect - result = create("{{ plugins.to_square(case["input"]["queen"]) }}") - result |> Result.is_err +expect { + result = ChessSquare.create("{{ plugins.to_square(case["input"]["queen"]) }}") + result.is_err() +} {%- endif %} {%- elif case["property"] == "canAttack" %} -expect - maybe_square1 = create("{{ plugins.to_square(case["input"]["white_queen"]) }}") - maybe_square2 = create("{{ plugins.to_square(case["input"]["black_queen"]) }}") - result = when (maybe_square1, maybe_square2) is - (Ok(square1), Ok(square2)) -> - square1 |> queen_can_attack(square2) - _ -> crash "Unreachable: {{ plugins.to_square(case["input"]["white_queen"]) }} and {{ plugins.to_square(case["input"]["black_queen"]) }} are both valid squares" +expect { + square1 = ChessSquare.create("{{ plugins.to_square(case["input"]["white_queen"]) }}")? + square2 = ChessSquare.create("{{ plugins.to_square(case["input"]["black_queen"]) }}")? + result = square1.queen_can_attack(square2) result == {{ case["expected"] | to_roc }} +} {%- endif %} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/queen-attack/ChessSquare.roc b/exercises/practice/queen-attack/ChessSquare.roc new file mode 100644 index 00000000..129a7bac --- /dev/null +++ b/exercises/practice/queen-attack/ChessSquare.roc @@ -0,0 +1,27 @@ +ChessSquare :: { + # TODO: change this opaque type however you need + todo1 : U64, + todo2 : U64, + todo3 : U64, + # etc. +}.{ + create : Str -> Try(ChessSquare, _) + create = |square_str| { + crash "Please implement the 'create' function" + } + + rank : ChessSquare -> U8 + rank = |square| { + crash "Please implement the 'rank' function" + } + + file : ChessSquare -> U8 + file = |square| { + crash "Please implement the 'file' function" + } + + queen_can_attack : ChessSquare, ChessSquare -> Bool + queen_can_attack = |square1, square2| { + crash "Please implement the 'queen_can_attack' function" + } +} diff --git a/exercises/practice/queen-attack/QueenAttack.roc b/exercises/practice/queen-attack/QueenAttack.roc deleted file mode 100644 index 861512dc..00000000 --- a/exercises/practice/queen-attack/QueenAttack.roc +++ /dev/null @@ -1,19 +0,0 @@ -module [create, rank, file, queen_can_attack] - -Square := { row : U8, column : U8 } - -rank : Square -> U8 -rank = |@Square({ row, column })| - crash("Please implement the 'rank' function") - -file : Square -> U8 -file = |@Square({ row, column })| - crash("Please implement the 'file' function") - -create : Str -> Result Square _ -create = |square_str| - crash("Please implement the 'create' function") - -queen_can_attack : Square, Square -> Bool -queen_can_attack = |square1, square2| - crash("Please implement the 'queen_can_attack' function") diff --git a/exercises/practice/queen-attack/queen-attack-test.roc b/exercises/practice/queen-attack/queen-attack-test.roc index 383e4983..0b56b61f 100644 --- a/exercises/practice/queen-attack/queen-attack-test.roc +++ b/exercises/practice/queen-attack/queen-attack-test.roc @@ -1,139 +1,107 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/queen-attack/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-23 -import QueenAttack exposing [create, rank, file, queen_can_attack] +import ChessSquare ## ## Test creation of Queens with valid and invalid positions ## # queen with a valid position -expect - maybe_square = create("C6") - result = maybe_square |> Result.try(|square| Ok(rank square)) - result == Ok(6) +expect { + square = ChessSquare.create("C3")? + result = square.rank() + result == 3 +} -expect - maybe_square = create("C6") - result = maybe_square |> Result.try(|square| Ok(file square)) - result == Ok('C') +expect { + square = ChessSquare.create("C3")? + result = square.file() + result == 'C' +} # queen must have row on board -expect - result = create("E0") - result |> Result.is_err +expect { + result = ChessSquare.create("E9") + result.is_err() +} # queen must have column on board -expect - result = create("I4") - result |> Result.is_err +expect { + result = ChessSquare.create("I5") + result.is_err() +} ## ## Test the ability of one queen to attack another ## # cannot attack -expect - maybe_square1 = create("E6") - maybe_square2 = create("G2") - result = - when (maybe_square1, maybe_square2) is - (Ok(square1), Ok(square2)) -> - square1 |> queen_can_attack(square2) - - _ -> crash "Unreachable: E6 and G2 are both valid squares" - result == Bool.false +expect { + square1 = ChessSquare.create("E3")? + square2 = ChessSquare.create("G7")? + result = square1.queen_can_attack(square2) + result == Bool.False +} # can attack on same row -expect - maybe_square1 = create("E6") - maybe_square2 = create("G6") - result = - when (maybe_square1, maybe_square2) is - (Ok(square1), Ok(square2)) -> - square1 |> queen_can_attack(square2) - - _ -> crash "Unreachable: E6 and G6 are both valid squares" - result == Bool.true +expect { + square1 = ChessSquare.create("E3")? + square2 = ChessSquare.create("G3")? + result = square1.queen_can_attack(square2) + result == Bool.True +} # can attack on same column -expect - maybe_square1 = create("F4") - maybe_square2 = create("F6") - result = - when (maybe_square1, maybe_square2) is - (Ok(square1), Ok(square2)) -> - square1 |> queen_can_attack(square2) - - _ -> crash "Unreachable: F4 and F6 are both valid squares" - result == Bool.true +expect { + square1 = ChessSquare.create("F5")? + square2 = ChessSquare.create("F3")? + result = square1.queen_can_attack(square2) + result == Bool.True +} # can attack on first diagonal -expect - maybe_square1 = create("C6") - maybe_square2 = create("E8") - result = - when (maybe_square1, maybe_square2) is - (Ok(square1), Ok(square2)) -> - square1 |> queen_can_attack(square2) - - _ -> crash "Unreachable: C6 and E8 are both valid squares" - result == Bool.true +expect { + square1 = ChessSquare.create("C3")? + square2 = ChessSquare.create("E1")? + result = square1.queen_can_attack(square2) + result == Bool.True +} # can attack on second diagonal -expect - maybe_square1 = create("C6") - maybe_square2 = create("B5") - result = - when (maybe_square1, maybe_square2) is - (Ok(square1), Ok(square2)) -> - square1 |> queen_can_attack(square2) - - _ -> crash "Unreachable: C6 and B5 are both valid squares" - result == Bool.true +expect { + square1 = ChessSquare.create("C3")? + square2 = ChessSquare.create("B4")? + result = square1.queen_can_attack(square2) + result == Bool.True +} # can attack on third diagonal -expect - maybe_square1 = create("C6") - maybe_square2 = create("B7") - result = - when (maybe_square1, maybe_square2) is - (Ok(square1), Ok(square2)) -> - square1 |> queen_can_attack(square2) - - _ -> crash "Unreachable: C6 and B7 are both valid squares" - result == Bool.true +expect { + square1 = ChessSquare.create("C3")? + square2 = ChessSquare.create("B2")? + result = square1.queen_can_attack(square2) + result == Bool.True +} # can attack on fourth diagonal -expect - maybe_square1 = create("H7") - maybe_square2 = create("G8") - result = - when (maybe_square1, maybe_square2) is - (Ok(square1), Ok(square2)) -> - square1 |> queen_can_attack(square2) - - _ -> crash "Unreachable: H7 and G8 are both valid squares" - result == Bool.true +expect { + square1 = ChessSquare.create("H2")? + square2 = ChessSquare.create("G1")? + result = square1.queen_can_attack(square2) + result == Bool.True +} # cannot attack if falling diagonals are only the same when reflected across the longest falling diagonal -expect - maybe_square1 = create("B4") - maybe_square2 = create("F6") - result = - when (maybe_square1, maybe_square2) is - (Ok(square1), Ok(square2)) -> - square1 |> queen_can_attack(square2) - - _ -> crash "Unreachable: B4 and F6 are both valid squares" - result == Bool.false +expect { + square1 = ChessSquare.create("B5")? + square2 = ChessSquare.create("F3")? + result = square1.queen_can_attack(square2) + result == Bool.False +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/rail-fence-cipher/.meta/Example.roc b/exercises/practice/rail-fence-cipher/.meta/Example.roc index bece776e..e55b8ecd 100644 --- a/exercises/practice/rail-fence-cipher/.meta/Example.roc +++ b/exercises/practice/rail-fence-cipher/.meta/Example.roc @@ -1,54 +1,88 @@ -module [encode, decode] +RailFenceCipher :: {}.{ + encode : Str, U64 -> Try(Str, [ZeroRails, BadUtf8(_), ..]) + encode = |message, rails| { + reorder_with(message, encoded_indices, rails) + } -encoded_indices : U64, U64 -> List U64 -encoded_indices = |len, rails| - indices = List.range({ start: At(0), end: Before(len) }) - List.range({ start: At(0), end: Before(rails) }) - |> List.map( - |rail| - period = 2 * (rails - 1) - indices - |> List.map_with_index( - |index, original_index| - to_rail = original_index % period - if to_rail == rail or to_rail == period - rail then - [index] - else - [], - ) - |> List.join, - ) - |> List.join + decode : Str, U64 -> Try(Str, [ZeroRails, BadUtf8(_), ..]) + decode = |encrypted, rails| { + reorder_with(encrypted, decoded_indices, rails) + } +} -decoded_indices : U64, U64 -> List U64 -decoded_indices = |len, rails| - encoded_indices(len, rails) - |> List.map_with_index(|encoded, decoded| { encoded, decoded }) - |> List.sort_with( - |{ encoded: encoded1 }, { encoded: encoded2 }| - Num.compare(encoded1, encoded2), - ) - |> List.map(.decoded) +encoded_indices : U64, U64 -> List(U64) +encoded_indices = |len, rails| { + indices = (0..List.from_iter() + (0..join_map( + |rail| { + period = 2 * (rails - 1) + indices->join_map( + |index| { + to_rail = index % period + if to_rail == rail or to_rail == period - rail { + [index] + } else { + [] + } + }, + ) + }, + ) +} -encode : Str, U64 -> Result Str [ZeroRails, BadUtf8 _] -encode = |message, rails| - message |> reorder_with(encoded_indices, rails) +decoded_indices : U64, U64 -> List(U64) +decoded_indices = |len, rails| { + encoded_indices(len, rails) + .map_with_index(|encoded, decoded| { encoded, decoded }) + .sort_with( + |{ encoded: encoded1, decoded: _ }, { encoded: encoded2, decoded: _ }| { + if encoded1 < encoded2 { + LT + } else if encoded1 > encoded2 { + GT + } else { + EQ + } + }, + ) + .map(|r| r.decoded) +} -decode : Str, U64 -> Result Str [ZeroRails, BadUtf8 _] -decode = |encrypted, rails| - encrypted |> reorder_with(decoded_indices, rails) +reorder_with : Str, (U64, U64 -> List(U64)), U64 -> Try(Str, [ZeroRails, BadUtf8(_), ..]) +reorder_with = |message, get_indices, rails| { + if rails == 0 { + Err(ZeroRails) + } else if rails == 1 { + Ok(message) + } else { + chars = message.to_utf8() + indices = get_indices(chars.len(), rails) + result = indices->map_try(|index| chars.get(index)) + match result { + Ok(encrypted_chars) => Str.from_utf8(encrypted_chars) + Err(OutOfBounds) => { + crash "Unreachable: indices cannot be out of bounds here" + } + } + } +} -reorder_with : Str, (U64, U64 -> List U64), U64 -> Result Str [ZeroRails, BadUtf8 _] -reorder_with = |message, get_indices, rails| - if rails == 0 then - Err(ZeroRails) - else if rails == 1 then - Ok(message) - else - chars = message |> Str.to_utf8 - result = - get_indices(List.len(chars), rails) - |> List.map_try(|index| chars |> List.get(index)) - when result is - Ok(encrypted_chars) -> encrypted_chars |> Str.from_utf8 - Err(OutOfBounds) -> crash("Unreachable: indices cannot be out of bounds here") +# The following functions should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} + +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/rail-fence-cipher/.meta/template.j2 b/exercises/practice/rail-fence-cipher/.meta/template.j2 index c05a1a87..8a3d715f 100644 --- a/exercises/practice/rail-fence-cipher/.meta/template.j2 +++ b/exercises/practice/rail-fence-cipher/.meta/template.j2 @@ -11,11 +11,14 @@ import {{ exercise | to_pascal }} exposing [encode, decode] {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect +expect { message = {{ case["input"]["msg"] | to_roc }} - result = message |> {{ case["property"] | to_snake }}({{ case["input"]["rails"] | to_roc }}) + result = message -> {{ case["property"] | to_snake }}({{ case["input"]["rails"] | to_roc }}) expected = Ok({{ case["expected"] | to_roc }}) result == expected +} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/rail-fence-cipher/RailFenceCipher.roc b/exercises/practice/rail-fence-cipher/RailFenceCipher.roc index 7801c11e..c5625610 100644 --- a/exercises/practice/rail-fence-cipher/RailFenceCipher.roc +++ b/exercises/practice/rail-fence-cipher/RailFenceCipher.roc @@ -1,9 +1,11 @@ -module [encode, decode] +RailFenceCipher :: {}.{ + encode : Str, U64 -> Try(Str, _) + encode = |message, rails| { + crash "Please implement the 'encode' function" + } -encode : Str, U64 -> Result Str _ -encode = |message, rails| - crash("Please implement the 'encode' function") - -decode : Str, U64 -> Result Str _ -decode = |encrypted, rails| - crash("Please implement the 'decode' function") + decode : Str, U64 -> Try(Str, _) + decode = |encrypted, rails| { + crash "Please implement the 'decode' function" + } +} diff --git a/exercises/practice/rail-fence-cipher/rail-fence-cipher-test.roc b/exercises/practice/rail-fence-cipher/rail-fence-cipher-test.roc index f200ba3e..4e952fb8 100644 --- a/exercises/practice/rail-fence-cipher/rail-fence-cipher-test.roc +++ b/exercises/practice/rail-fence-cipher/rail-fence-cipher-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/rail-fence-cipher/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import RailFenceCipher exposing [encode, decode] @@ -17,48 +9,58 @@ import RailFenceCipher exposing [encode, decode] ## # encode with two rails -expect - message = "XOXOXOXOXOXOXOXOXO" - result = message |> encode(2) - expected = Ok("XXXXXXXXXOOOOOOOOO") - result == expected +expect { + message = "XOXOXOXOXOXOXOXOXO" + result = message->encode(2) + expected = Ok("XXXXXXXXXOOOOOOOOO") + result == expected +} # encode with three rails -expect - message = "WEAREDISCOVEREDFLEEATONCE" - result = message |> encode(3) - expected = Ok("WECRLTEERDSOEEFEAOCAIVDEN") - result == expected +expect { + message = "WEAREDISCOVEREDFLEEATONCE" + result = message->encode(3) + expected = Ok("WECRLTEERDSOEEFEAOCAIVDEN") + result == expected +} # encode with ending in the middle -expect - message = "EXERCISES" - result = message |> encode(4) - expected = Ok("ESXIEECSR") - result == expected +expect { + message = "EXERCISES" + result = message->encode(4) + expected = Ok("ESXIEECSR") + result == expected +} ## ## decode ## # decode with three rails -expect - message = "TEITELHDVLSNHDTISEIIEA" - result = message |> decode(3) - expected = Ok("THEDEVILISINTHEDETAILS") - result == expected +expect { + message = "TEITELHDVLSNHDTISEIIEA" + result = message->decode(3) + expected = Ok("THEDEVILISINTHEDETAILS") + result == expected +} # decode with five rails -expect - message = "EIEXMSMESAORIWSCE" - result = message |> decode(5) - expected = Ok("EXERCISMISAWESOME") - result == expected +expect { + message = "EIEXMSMESAORIWSCE" + result = message->decode(5) + expected = Ok("EXERCISMISAWESOME") + result == expected +} # decode with six rails -expect - message = "133714114238148966225439541018335470986172518171757571896261" - result = message |> decode(6) - expected = Ok("112358132134558914423337761098715972584418167651094617711286") - result == expected +expect { + message = "133714114238148966225439541018335470986172518171757571896261" + result = message->decode(6) + expected = Ok("112358132134558914423337761098715972584418167651094617711286") + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/raindrops/.meta/Example.roc b/exercises/practice/raindrops/.meta/Example.roc index fc9c3d0f..927f627d 100644 --- a/exercises/practice/raindrops/.meta/Example.roc +++ b/exercises/practice/raindrops/.meta/Example.roc @@ -1,12 +1,26 @@ -module [convert] - -convert : U64 -> Str -convert = |number| - pling = if number % 3 == 0 then "Pling" else "" - plang = if number % 5 == 0 then "Plang" else "" - plong = if number % 7 == 0 then "Plong" else "" - result = "${pling}${plang}${plong}" - if result == "" then - Num.to_str(number) - else - result +Raindrops :: {}.{ + convert : U64 -> Str + convert = |number| { + pling = if number % 3 == 0 { + "Pling" + } else { + "" + } + plang = if number % 5 == 0 { + "Plang" + } else { + "" + } + plong = if number % 7 == 0 { + "Plong" + } else { + "" + } + result = "${pling}${plang}${plong}" + if result == "" { + U64.to_str(number) + } else { + result + } + } +} diff --git a/exercises/practice/raindrops/.meta/template.j2 b/exercises/practice/raindrops/.meta/template.j2 index e8b3010e..326c5375 100644 --- a/exercises/practice/raindrops/.meta/template.j2 +++ b/exercises/practice/raindrops/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [convert] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["number"] }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/raindrops/Raindrops.roc b/exercises/practice/raindrops/Raindrops.roc index fc9884ba..52e28608 100644 --- a/exercises/practice/raindrops/Raindrops.roc +++ b/exercises/practice/raindrops/Raindrops.roc @@ -1,5 +1,6 @@ -module [convert] - -convert : U64 -> Str -convert = |number| - crash("Please implement the 'convert' function") +Raindrops :: {}.{ + convert : U64 -> Str + convert = |number| { + crash "Please implement the 'convert' function" + } +} diff --git a/exercises/practice/raindrops/raindrops-test.roc b/exercises/practice/raindrops/raindrops-test.roc index bc7cc9c9..c2c73d32 100644 --- a/exercises/practice/raindrops/raindrops-test.roc +++ b/exercises/practice/raindrops/raindrops-test.roc @@ -1,104 +1,118 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/raindrops/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Raindrops exposing [convert] # the sound for 1 is 1 -expect - result = convert(1) - result == "1" +expect { + result = convert(1) + result == "1" +} # the sound for 3 is Pling -expect - result = convert(3) - result == "Pling" +expect { + result = convert(3) + result == "Pling" +} # the sound for 5 is Plang -expect - result = convert(5) - result == "Plang" +expect { + result = convert(5) + result == "Plang" +} # the sound for 7 is Plong -expect - result = convert(7) - result == "Plong" +expect { + result = convert(7) + result == "Plong" +} # the sound for 6 is Pling as it has a factor 3 -expect - result = convert(6) - result == "Pling" +expect { + result = convert(6) + result == "Pling" +} # 2 to the power 3 does not make a raindrop sound as 3 is the exponent not the base -expect - result = convert(8) - result == "8" +expect { + result = convert(8) + result == "8" +} # the sound for 9 is Pling as it has a factor 3 -expect - result = convert(9) - result == "Pling" +expect { + result = convert(9) + result == "Pling" +} # the sound for 10 is Plang as it has a factor 5 -expect - result = convert(10) - result == "Plang" +expect { + result = convert(10) + result == "Plang" +} # the sound for 14 is Plong as it has a factor of 7 -expect - result = convert(14) - result == "Plong" +expect { + result = convert(14) + result == "Plong" +} # the sound for 15 is PlingPlang as it has factors 3 and 5 -expect - result = convert(15) - result == "PlingPlang" +expect { + result = convert(15) + result == "PlingPlang" +} # the sound for 21 is PlingPlong as it has factors 3 and 7 -expect - result = convert(21) - result == "PlingPlong" +expect { + result = convert(21) + result == "PlingPlong" +} # the sound for 25 is Plang as it has a factor 5 -expect - result = convert(25) - result == "Plang" +expect { + result = convert(25) + result == "Plang" +} # the sound for 27 is Pling as it has a factor 3 -expect - result = convert(27) - result == "Pling" +expect { + result = convert(27) + result == "Pling" +} # the sound for 35 is PlangPlong as it has factors 5 and 7 -expect - result = convert(35) - result == "PlangPlong" +expect { + result = convert(35) + result == "PlangPlong" +} # the sound for 49 is Plong as it has a factor 7 -expect - result = convert(49) - result == "Plong" +expect { + result = convert(49) + result == "Plong" +} # the sound for 52 is 52 -expect - result = convert(52) - result == "52" +expect { + result = convert(52) + result == "52" +} # the sound for 105 is PlingPlangPlong as it has factors 3, 5 and 7 -expect - result = convert(105) - result == "PlingPlangPlong" +expect { + result = convert(105) + result == "PlingPlangPlong" +} # the sound for 3125 is Plang as it has a factor 5 -expect - result = convert(3125) - result == "Plang" +expect { + result = convert(3125) + result == "Plang" +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/rational-numbers/.meta/Example.roc b/exercises/practice/rational-numbers/.meta/Example.roc index 070f10cd..e1340b6d 100644 --- a/exercises/practice/rational-numbers/.meta/Example.roc +++ b/exercises/practice/rational-numbers/.meta/Example.roc @@ -1,55 +1,180 @@ -module [add, sub, mul, div, abs, exp, exp_real, reduce] - -add : [Rational (Int a) (Int a)], [Rational (Int a) (Int a)] -> [Rational (Int a) (Int a)] -add = |r1, r2| - Rational(a1, b1) = r1 - Rational(a2, b2) = r2 - Rational((a1 * b2 + a2 * b1), (b1 * b2)) |> reduce - -sub : [Rational (Int a) (Int a)], [Rational (Int a) (Int a)] -> [Rational (Int a) (Int a)] -sub = |r1, r2| - Rational(a1, b1) = r1 - Rational(a2, b2) = r2 - Rational((a1 * b2 - a2 * b1), (b1 * b2)) |> reduce - -mul : [Rational (Int a) (Int a)], [Rational (Int a) (Int a)] -> [Rational (Int a) (Int a)] -mul = |r1, r2| - Rational(a1, b1) = r1 - Rational(a2, b2) = r2 - Rational((a1 * a2), (b1 * b2)) |> reduce - -div : [Rational (Int a) (Int a)], [Rational (Int a) (Int a)] -> [Rational (Int a) (Int a)] -div = |r1, r2| - Rational(a1, b1) = r1 - Rational(a2, b2) = r2 - Rational((a1 * b2), (a2 * b1)) |> reduce - -abs : [Rational (Int a) (Int a)] -> [Rational (Int a) (Int a)] -abs = |r| - Rational(a, b) = r - Rational(Num.abs(a), Num.abs(b)) |> reduce - -exp : [Rational (Int a) (Int a)], Int a -> [Rational (Int a) (Int a)] -exp = |r, n| - Rational(a, b) = r - when n is - 0 -> Rational(1, 1) - pos if pos > 0 -> Rational((a |> Num.pow_int(pos)), (b |> Num.pow_int(pos))) |> reduce - neg -> - m = Num.abs(neg) - Rational((b |> Num.pow_int(m)), (a |> Num.pow_int(m))) |> reduce - -exp_real : Frac a, [Rational (Int b) (Int b)] -> Frac a -exp_real = |x, r| - Rational(a, b) = r - x |> Num.pow((Num.to_frac(a) / Num.to_frac(b))) - -reduce : [Rational (Int b) (Int b)] -> [Rational (Int b) (Int b)] -reduce = |r| - Rational(a, b) = r - gcd = |m, n| if n == 0 then m else gcd(n, (m % n)) - sign = |n| if n < 0 then -1 else 1 - abs_a = Num.abs(a) - abs_b = Num.abs(b) - d = gcd(abs_a, abs_b) - Rational((sign(a) * sign(b) * abs_a // d), (abs_b // d)) +Rational :: { num : I64, den : I64 }.{ + new : { num : I64, den : I64 } -> Rational + new = |{ num, den }| { + { num, den }->reduce() + } + + # # The user can write plus(r1, r2), r1.plus(r2), or simply r1 + r2 + plus : Rational, Rational -> Rational + plus = |{ num: num1, den: den1 }, { num: num2, den: den2 }| { + { num: num1 * den2 + num2 * den1, den: den1 * den2 }->reduce() + } + + # # The user can write minus(r1, r2), r1.minus(r2), or simply r1 - r2 + minus : Rational, Rational -> Rational + minus = |{ num: num1, den: den1 }, { num: num2, den: den2 }| { + { num: num1 * den2 - num2 * den1, den: den1 * den2 }->reduce() + } + + # # The user can write times(r1, r2), r1.times(r2), or simply r1 * r2 + times : Rational, Rational -> Rational + times = |{ num: num1, den: den1 }, { num: num2, den: den2 }| { + { num: num1 * num2, den: den1 * den2 }->reduce() + } + + # # The user can write div_by(r1, r2), r1.div_by(r2), or simply r1 / r2 + div_by : Rational, Rational -> Rational + div_by = |{ num: num1, den: den1 }, { num: num2, den: den2 }| { + { num: num1 * den2, den: num2 * den1 }->reduce() + } + + abs : Rational -> Rational + abs = |{ num, den }| { + { num: num.abs(), den: den.abs() }->reduce() + } + + exp : Rational, I64 -> Rational + exp = |{ num, den }, n| { + match n { + 0 => { num: 1, den: 1 } + pos if pos > 0 => { num: pow_int(num, pos), den: pow_int(den, pos) }->reduce() + neg => { + m = neg.abs() + { num: pow_int(den, m), den: pow_int(num, m) }->reduce() + } + } + } + + exp_real : F64, Rational -> F64 + exp_real = |x, { num, den }| { + f : F64 + f = num.to_f64() / den.to_f64() + pow_f64(x, f) + } + + # # Reduce a rational number to its lowest terms, e.g., 6 / 8 --> 3 / 4 + reduce : Rational -> Rational + reduce = |{ num, den }| { + gcd = |m, n| if n == 0 { + m + } else { + gcd(n, (m % n)) + } + sign = |n| if n < 0 { + -1 + } else { + 1 + } + abs_num = num.abs() + abs_den = den.abs() + d = gcd(abs_num, abs_den) + { num: sign(num) * sign(den) * abs_num // d, den: abs_den // d } + } + + # TODO: remove this function once https://github.com/roc-lang/roc/issues/9769 is resolved + is_eq : Rational, Rational -> Bool + is_eq = |{ num: num1, den: den1 }, { num: num2, den: den2 }| { + (num1 == num2) and (den1 == den2) + } +} + +pow_int : I64, I64 -> I64 +pow_int = |number, pow| { + (1..=pow).fold( + 1, + |acc, _| { + acc * number + }, + ) +} + +# The following functions should soon be available in Roc's builtins + +e = 2.718281828459045.F64 + +# Calculates the natural logarithm of x, ln(x). +ln_f64 : F64 -> F64 +ln_f64 = |x| { + if x <= 0.0 { + # Natural log is undefined for zero and negative numbers + crash "ln is undefined for zero or negative numbers" + } else { + var $norm_x = x + var $log_offset = 0.0 + + # Range reduction: keep x between [1/e, e] + while $norm_x > e { + $norm_x = $norm_x / e + $log_offset = $log_offset + 1.0 + } + while $norm_x < 1 / e { + $norm_x = $norm_x * e + $log_offset = $log_offset - 1.0 + } + + # Area hyperbolic tangent series + z = ($norm_x - 1.0) / ($norm_x + 1.0) + z2 = z * z + var $z_term = z + var $ln_sum = 0.0 + var $ln_n = 1.0 + + for _ in 0..<30.U8 { + $ln_sum = $ln_sum + ($z_term / $ln_n) + $z_term = $z_term * z2 + $ln_n = $ln_n + 2.0 + } + + (2.0 * $ln_sum) + $log_offset + } +} + +# Calculates e^x using the Taylor series. +exp_f64 : F64 -> F64 +exp_f64 = |x| { + var $norm_x = x + var $exp_mult = 1.0 + + # Range reduction: keep x between [-1.0, 1.0] + while $norm_x > 1.0 { + $norm_x = $norm_x - 1.0 + $exp_mult = $exp_mult * e + } + while $norm_x < -1.0 { + $norm_x = $norm_x + 1.0 + $exp_mult = $exp_mult / e + } + + # Taylor series + var $exp_term = 1.0 + var $exp_sum = 1.0 + var $exp_n = 1.0 + + for _ in 0..<25.U8 { + $exp_term = $exp_term * $norm_x / $exp_n + $exp_sum = $exp_sum + $exp_term + $exp_n = $exp_n + 1.0 + } + + $exp_sum * $exp_mult +} + +# Calculates x^p for 64-bit values using the newly separated functions. +pow_f64 : F64, F64 -> F64 +pow_f64 = |x, p| { + if x == 0.0 { + if p == 0.0 { + 1.0 + } else { + 0.0 + } + } else if x < 0.0 { + # Fractional powers of negative numbers are undefined in pure real 64-bit math + crash "Raising a negative number to a fractional power is undefined" + } else if p == 0.0 { + 1.0 + } else { + # The core mathematical calculation + exp_f64(p * ln_f64(x)) + } +} diff --git a/exercises/practice/rational-numbers/.meta/config.json b/exercises/practice/rational-numbers/.meta/config.json index 26387c00..4fa5b763 100644 --- a/exercises/practice/rational-numbers/.meta/config.json +++ b/exercises/practice/rational-numbers/.meta/config.json @@ -4,7 +4,7 @@ ], "files": { "solution": [ - "RationalNumbers.roc" + "Rational.roc" ], "test": [ "rational-numbers-test.roc" diff --git a/exercises/practice/rational-numbers/.meta/plugins.py b/exercises/practice/rational-numbers/.meta/plugins.py index 26d36cee..cde472a2 100644 --- a/exercises/practice/rational-numbers/.meta/plugins.py +++ b/exercises/practice/rational-numbers/.meta/plugins.py @@ -1,2 +1,2 @@ def to_roc_rational(r): - return f"Rational({r[0]}, {r[1]})" + return f"Rational.new({{num: {r[0]}, den: {r[1]}}})" diff --git a/exercises/practice/rational-numbers/.meta/template.j2 b/exercises/practice/rational-numbers/.meta/template.j2 index 6d0855cf..34171890 100644 --- a/exercises/practice/rational-numbers/.meta/template.j2 +++ b/exercises/practice/rational-numbers/.meta/template.j2 @@ -2,32 +2,39 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [add, sub, mul, div, abs, exp, exp_real, reduce] - -{% set property_map = { - "abs": "abs", - "add": "add", - "div": "div", - "exprational": "exp", - "expreal": "exp_real", - "mul": "mul", - "reduce": "reduce", - "sub": "sub", +import Rational + +{% set equations = { + "add": "r1 + r2", + "sub": "r1 - r2", + "mul": "r1 * r2", + "div": "r1 / r2", + "abs": "r.abs()", + "reduce": "r.reduce()", + "exprational": "r.exp(n)", + "expreal": "Rational.exp_real(x, r)", } %} {% macro test_case(case) -%} # {{ case["description"] }} -expect +expect { {%- if "r1" in case["input"] %} - result = {{ plugins.to_roc_rational(case["input"]["r1"]) }} |> {{ property_map[case["property"]] | to_snake }}(({{ plugins.to_roc_rational(case["input"]["r2"]) }})) + r1 = {{ plugins.to_roc_rational(case["input"]["r1"]) }} + r2 = {{ plugins.to_roc_rational(case["input"]["r2"]) }} + result = {{ equations[case["property"]] }} {%- elif "r" in case["input"] %} {%- if "x" in case["input"] %} - result = {{ case["input"]["x"] | to_roc }} |> {{ property_map[case["property"]] | to_snake }}(({{ plugins.to_roc_rational(case["input"]["r"]) }})) + r = {{ plugins.to_roc_rational(case["input"]["r"]) }} + x = {{ case["input"]["x"] | to_roc }} + result = {{ equations[case["property"]] }} {%- elif "n" in case["input"] %} - result = {{ plugins.to_roc_rational(case["input"]["r"]) }} |> {{ property_map[case["property"]] | to_snake }}({{ case["input"]["n"] | to_roc }}) + r = {{ plugins.to_roc_rational(case["input"]["r"]) }} + n = {{ case["input"]["n"] | to_roc }} + result = {{ equations[case["property"]] }} {%- else %} - result = {{ plugins.to_roc_rational(case["input"]["r"]) }} |> {{ property_map[case["property"]] | to_snake }} + r = {{ plugins.to_roc_rational(case["input"]["r"]) }} + result = {{ equations[case["property"]] }} {%- endif %} {%- else %} crash "This test case is not implemented yet." @@ -35,8 +42,9 @@ expect {%- if case["expected"] is iterable %} result == {{ plugins.to_roc_rational(case["expected"]) }} {%- else %} - result |> Num.is_approx_eq({{ case["expected"] | to_roc }}, {}) + result->is_approx_eq({{ case["expected"] | to_roc }}) {%- endif %} +} {% endmacro %} @@ -55,3 +63,9 @@ expect {%- endif %} {% endfor %} {% endfor %} + +is_approx_eq = |f1, f2| { + (f1 * 1e9 + 0.5).to_u64_wrap() == (f2 * 1e9 + 0.5).to_u64_wrap() +} + +{{ macros.footer() }} diff --git a/exercises/practice/rational-numbers/Rational.roc b/exercises/practice/rational-numbers/Rational.roc new file mode 100644 index 00000000..512a7dc5 --- /dev/null +++ b/exercises/practice/rational-numbers/Rational.roc @@ -0,0 +1,51 @@ +Rational :: { num : I64, den : I64 }.{ + new : { num : I64, den : I64 } -> Rational + new = |{ num, den }| { + crash "Please implement the 'new' function" + } + + # # The user can write plus(r1, r2), r1.plus(r2), or simply r1 + r2 + plus : Rational, Rational -> Rational + plus = |r1, r2| { + crash "Please implement the 'plus' function" + } + + # # The user can write minus(r1, r2), r1.minus(r2), or simply r1 - r2 + minus : Rational, Rational -> Rational + minus = |r1, r2| { + crash "Please implement the 'minus' function" + } + + # # The user can write times(r1, r2), r1.times(r2), or simply r1 * r2 + times : Rational, Rational -> Rational + times = |r1, r2| { + crash "Please implement the 'times' function" + } + + # # The user can write div_by(r1, r2), r1.div_by(r2), or simply r1 / r2 + div_by : Rational, Rational -> Rational + div_by = |r1, r2| { + crash "Please implement the 'div_by' function" + } + + abs : Rational -> Rational + abs = |r| { + crash "Please implement the 'abs' function" + } + + exp : Rational, I64 -> Rational + exp = |r, n| { + crash "Please implement the 'exp' function" + } + + exp_real : F64, Rational -> F64 + exp_real = |x, r| { + crash "Please implement the 'exp_real' function" + } + + # # Reduce a rational number to its lowest terms, e.g., 6 / 8 --> 3 / 4 + reduce : Rational -> Rational + reduce = |r| { + crash "Please implement the 'reduce' function" + } +} diff --git a/exercises/practice/rational-numbers/RationalNumbers.roc b/exercises/practice/rational-numbers/RationalNumbers.roc deleted file mode 100644 index 5d6012ef..00000000 --- a/exercises/practice/rational-numbers/RationalNumbers.roc +++ /dev/null @@ -1,35 +0,0 @@ -module [add, sub, mul, div, abs, exp, exp_real, reduce] - -add : [Rational (Int a) (Int a)], [Rational (Int a) (Int a)] -> [Rational (Int a) (Int a)] -add = |r1, r2| - crash("Please implement the 'add' function") - -sub : [Rational (Int a) (Int a)], [Rational (Int a) (Int a)] -> [Rational (Int a) (Int a)] -sub = |r1, r2| - crash("Please implement the 'sub' function") - -mul : [Rational (Int a) (Int a)], [Rational (Int a) (Int a)] -> [Rational (Int a) (Int a)] -mul = |r1, r2| - crash("Please implement the 'mul' function") - -div : [Rational (Int a) (Int a)], [Rational (Int a) (Int a)] -> [Rational (Int a) (Int a)] -div = |r1, r2| - crash("Please implement the 'div' function") - -abs : [Rational (Int a) (Int a)] -> [Rational (Int a) (Int a)] -abs = |r| - crash("Please implement the 'abs' function") - -exp : [Rational (Int a) (Int a)], Int a -> [Rational (Int a) (Int a)] -exp = |r, n| - crash("Please implement the 'exp' function") - -exp_real : Frac a, [Rational (Int b) (Int b)] -> Frac a -exp_real = |x, r| - crash("Please implement the 'exp_real' function") - -## Reduce a rational number to its lowest terms, e.g., 6 / 8 --> 3 / 4 -reduce : [Rational (Int b) (Int b)] -> [Rational (Int b) (Int b)] -reduce = |r| - crash("Please implement the 'reduce' function") - diff --git a/exercises/practice/rational-numbers/rational-numbers-test.roc b/exercises/practice/rational-numbers/rational-numbers-test.roc index d3d9c7a9..e0b0dead 100644 --- a/exercises/practice/rational-numbers/rational-numbers-test.roc +++ b/exercises/practice/rational-numbers/rational-numbers-test.roc @@ -1,249 +1,365 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/rational-numbers/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout +# File last updated on 2026-06-23 -main! = |_args| - Stdout.line!("") - -import RationalNumbers exposing [add, sub, mul, div, abs, exp, exp_real, reduce] +import Rational ## ## Arithmetic ## # Add two positive rational numbers -expect - result = Rational(1, 2) |> add(Rational(2, 3)) - result == Rational(7, 6) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: 2, den: 3 }) + result = r1 + r2 + result == Rational.new({ num: 7, den: 6 }) +} # Add a positive rational number and a negative rational number -expect - result = Rational(1, 2) |> add(Rational(-2, 3)) - result == Rational(-1, 6) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: -2, den: 3 }) + result = r1 + r2 + result == Rational.new({ num: -1, den: 6 }) +} # Add two negative rational numbers -expect - result = Rational(-1, 2) |> add(Rational(-2, 3)) - result == Rational(-7, 6) +expect { + r1 = Rational.new({ num: -1, den: 2 }) + r2 = Rational.new({ num: -2, den: 3 }) + result = r1 + r2 + result == Rational.new({ num: -7, den: 6 }) +} # Add a rational number to its additive inverse -expect - result = Rational(1, 2) |> add(Rational(-1, 2)) - result == Rational(0, 1) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: -1, den: 2 }) + result = r1 + r2 + result == Rational.new({ num: 0, den: 1 }) +} # Subtract two positive rational numbers -expect - result = Rational(1, 2) |> sub(Rational(2, 3)) - result == Rational(-1, 6) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: 2, den: 3 }) + result = r1 - r2 + result == Rational.new({ num: -1, den: 6 }) +} # Subtract a positive rational number and a negative rational number -expect - result = Rational(1, 2) |> sub(Rational(-2, 3)) - result == Rational(7, 6) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: -2, den: 3 }) + result = r1 - r2 + result == Rational.new({ num: 7, den: 6 }) +} # Subtract two negative rational numbers -expect - result = Rational(-1, 2) |> sub(Rational(-2, 3)) - result == Rational(1, 6) +expect { + r1 = Rational.new({ num: -1, den: 2 }) + r2 = Rational.new({ num: -2, den: 3 }) + result = r1 - r2 + result == Rational.new({ num: 1, den: 6 }) +} # Subtract a rational number from itself -expect - result = Rational(1, 2) |> sub(Rational(1, 2)) - result == Rational(0, 1) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: 1, den: 2 }) + result = r1 - r2 + result == Rational.new({ num: 0, den: 1 }) +} # Multiply two positive rational numbers -expect - result = Rational(1, 2) |> mul(Rational(2, 3)) - result == Rational(1, 3) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: 2, den: 3 }) + result = r1 * r2 + result == Rational.new({ num: 1, den: 3 }) +} # Multiply a negative rational number by a positive rational number -expect - result = Rational(-1, 2) |> mul(Rational(2, 3)) - result == Rational(-1, 3) +expect { + r1 = Rational.new({ num: -1, den: 2 }) + r2 = Rational.new({ num: 2, den: 3 }) + result = r1 * r2 + result == Rational.new({ num: -1, den: 3 }) +} # Multiply two negative rational numbers -expect - result = Rational(-1, 2) |> mul(Rational(-2, 3)) - result == Rational(1, 3) +expect { + r1 = Rational.new({ num: -1, den: 2 }) + r2 = Rational.new({ num: -2, den: 3 }) + result = r1 * r2 + result == Rational.new({ num: 1, den: 3 }) +} # Multiply a rational number by its reciprocal -expect - result = Rational(1, 2) |> mul(Rational(2, 1)) - result == Rational(1, 1) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: 2, den: 1 }) + result = r1 * r2 + result == Rational.new({ num: 1, den: 1 }) +} # Multiply a rational number by 1 -expect - result = Rational(1, 2) |> mul(Rational(1, 1)) - result == Rational(1, 2) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: 1, den: 1 }) + result = r1 * r2 + result == Rational.new({ num: 1, den: 2 }) +} # Multiply a rational number by 0 -expect - result = Rational(1, 2) |> mul(Rational(0, 1)) - result == Rational(0, 1) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: 0, den: 1 }) + result = r1 * r2 + result == Rational.new({ num: 0, den: 1 }) +} # Divide two positive rational numbers -expect - result = Rational(1, 2) |> div(Rational(2, 3)) - result == Rational(3, 4) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: 2, den: 3 }) + result = r1 / r2 + result == Rational.new({ num: 3, den: 4 }) +} # Divide a positive rational number by a negative rational number -expect - result = Rational(1, 2) |> div(Rational(-2, 3)) - result == Rational(-3, 4) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: -2, den: 3 }) + result = r1 / r2 + result == Rational.new({ num: -3, den: 4 }) +} # Divide two negative rational numbers -expect - result = Rational(-1, 2) |> div(Rational(-2, 3)) - result == Rational(3, 4) +expect { + r1 = Rational.new({ num: -1, den: 2 }) + r2 = Rational.new({ num: -2, den: 3 }) + result = r1 / r2 + result == Rational.new({ num: 3, den: 4 }) +} # Divide a rational number by 1 -expect - result = Rational(1, 2) |> div(Rational(1, 1)) - result == Rational(1, 2) +expect { + r1 = Rational.new({ num: 1, den: 2 }) + r2 = Rational.new({ num: 1, den: 1 }) + result = r1 / r2 + result == Rational.new({ num: 1, den: 2 }) +} ## ## Absolute value ## # Absolute value of a positive rational number -expect - result = Rational(1, 2) |> abs - result == Rational(1, 2) +expect { + r = Rational.new({ num: 1, den: 2 }) + result = r.abs() + result == Rational.new({ num: 1, den: 2 }) +} # Absolute value of a positive rational number with negative numerator and denominator -expect - result = Rational(-1, -2) |> abs - result == Rational(1, 2) +expect { + r = Rational.new({ num: -1, den: -2 }) + result = r.abs() + result == Rational.new({ num: 1, den: 2 }) +} # Absolute value of a negative rational number -expect - result = Rational(-1, 2) |> abs - result == Rational(1, 2) +expect { + r = Rational.new({ num: -1, den: 2 }) + result = r.abs() + result == Rational.new({ num: 1, den: 2 }) +} # Absolute value of a negative rational number with negative denominator -expect - result = Rational(1, -2) |> abs - result == Rational(1, 2) +expect { + r = Rational.new({ num: 1, den: -2 }) + result = r.abs() + result == Rational.new({ num: 1, den: 2 }) +} # Absolute value of zero -expect - result = Rational(0, 1) |> abs - result == Rational(0, 1) +expect { + r = Rational.new({ num: 0, den: 1 }) + result = r.abs() + result == Rational.new({ num: 0, den: 1 }) +} # Absolute value of a rational number is reduced to lowest terms -expect - result = Rational(2, 4) |> abs - result == Rational(1, 2) +expect { + r = Rational.new({ num: 2, den: 4 }) + result = r.abs() + result == Rational.new({ num: 1, den: 2 }) +} ## ## Exponentiation of a rational number ## # Raise a positive rational number to a positive integer power -expect - result = Rational(1, 2) |> exp(3) - result == Rational(1, 8) +expect { + r = Rational.new({ num: 1, den: 2 }) + n = 3 + result = r.exp(n) + result == Rational.new({ num: 1, den: 8 }) +} # Raise a negative rational number to a positive integer power -expect - result = Rational(-1, 2) |> exp(3) - result == Rational(-1, 8) +expect { + r = Rational.new({ num: -1, den: 2 }) + n = 3 + result = r.exp(n) + result == Rational.new({ num: -1, den: 8 }) +} # Raise a positive rational number to a negative integer power -expect - result = Rational(3, 5) |> exp(-2) - result == Rational(25, 9) +expect { + r = Rational.new({ num: 3, den: 5 }) + n = -2 + result = r.exp(n) + result == Rational.new({ num: 25, den: 9 }) +} # Raise a negative rational number to an even negative integer power -expect - result = Rational(-3, 5) |> exp(-2) - result == Rational(25, 9) +expect { + r = Rational.new({ num: -3, den: 5 }) + n = -2 + result = r.exp(n) + result == Rational.new({ num: 25, den: 9 }) +} # Raise a negative rational number to an odd negative integer power -expect - result = Rational(-3, 5) |> exp(-3) - result == Rational(-125, 27) +expect { + r = Rational.new({ num: -3, den: 5 }) + n = -3 + result = r.exp(n) + result == Rational.new({ num: -125, den: 27 }) +} # Raise zero to an integer power -expect - result = Rational(0, 1) |> exp(5) - result == Rational(0, 1) +expect { + r = Rational.new({ num: 0, den: 1 }) + n = 5 + result = r.exp(n) + result == Rational.new({ num: 0, den: 1 }) +} # Raise one to an integer power -expect - result = Rational(1, 1) |> exp(4) - result == Rational(1, 1) +expect { + r = Rational.new({ num: 1, den: 1 }) + n = 4 + result = r.exp(n) + result == Rational.new({ num: 1, den: 1 }) +} # Raise a positive rational number to the power of zero -expect - result = Rational(1, 2) |> exp(0) - result == Rational(1, 1) +expect { + r = Rational.new({ num: 1, den: 2 }) + n = 0 + result = r.exp(n) + result == Rational.new({ num: 1, den: 1 }) +} # Raise a negative rational number to the power of zero -expect - result = Rational(-1, 2) |> exp(0) - result == Rational(1, 1) +expect { + r = Rational.new({ num: -1, den: 2 }) + n = 0 + result = r.exp(n) + result == Rational.new({ num: 1, den: 1 }) +} ## ## Exponentiation of a real number to a rational number ## # Raise a real number to a positive rational number -expect - result = 8 |> exp_real(Rational(4, 3)) - result |> Num.is_approx_eq(16.0f64, {}) +expect { + r = Rational.new({ num: 4, den: 3 }) + x = 8 + result = Rational.exp_real(x, r) + result->is_approx_eq(16.0.F64) +} # Raise a real number to a negative rational number -expect - result = 9 |> exp_real(Rational(-1, 2)) - result |> Num.is_approx_eq(0.3333333333333333f64, {}) +expect { + r = Rational.new({ num: -1, den: 2 }) + x = 9 + result = Rational.exp_real(x, r) + result->is_approx_eq(0.3333333333333333.F64) +} # Raise a real number to a zero rational number -expect - result = 2 |> exp_real(Rational(0, 1)) - result |> Num.is_approx_eq(1.0f64, {}) +expect { + r = Rational.new({ num: 0, den: 1 }) + x = 2 + result = Rational.exp_real(x, r) + result->is_approx_eq(1.0.F64) +} ## ## Reduction to lowest terms ## # Reduce a positive rational number to lowest terms -expect - result = Rational(2, 4) |> reduce - result == Rational(1, 2) +expect { + r = Rational.new({ num: 2, den: 4 }) + result = r.reduce() + result == Rational.new({ num: 1, den: 2 }) +} # Reduce places the minus sign on the numerator -expect - result = Rational(3, -4) |> reduce - result == Rational(-3, 4) +expect { + r = Rational.new({ num: 3, den: -4 }) + result = r.reduce() + result == Rational.new({ num: -3, den: 4 }) +} # Reduce a negative rational number to lowest terms -expect - result = Rational(-4, 6) |> reduce - result == Rational(-2, 3) +expect { + r = Rational.new({ num: -4, den: 6 }) + result = r.reduce() + result == Rational.new({ num: -2, den: 3 }) +} # Reduce a rational number with a negative denominator to lowest terms -expect - result = Rational(3, -9) |> reduce - result == Rational(-1, 3) +expect { + r = Rational.new({ num: 3, den: -9 }) + result = r.reduce() + result == Rational.new({ num: -1, den: 3 }) +} # Reduce zero to lowest terms -expect - result = Rational(0, 6) |> reduce - result == Rational(0, 1) +expect { + r = Rational.new({ num: 0, den: 6 }) + result = r.reduce() + result == Rational.new({ num: 0, den: 1 }) +} # Reduce an integer to lowest terms -expect - result = Rational(-14, 7) |> reduce - result == Rational(-2, 1) +expect { + r = Rational.new({ num: -14, den: 7 }) + result = r.reduce() + result == Rational.new({ num: -2, den: 1 }) +} # Reduce one to lowest terms -expect - result = Rational(13, 13) |> reduce - result == Rational(1, 1) +expect { + r = Rational.new({ num: 13, den: 13 }) + result = r.reduce() + result == Rational.new({ num: 1, den: 1 }) +} +is_approx_eq = |f1, f2| { + (f1 * 1e9 + 0.5).to_u64_wrap() == (f2 * 1e9 + 0.5).to_u64_wrap() +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/rectangles/.meta/Example.roc b/exercises/practice/rectangles/.meta/Example.roc index dfe2189d..62f38a76 100644 --- a/exercises/practice/rectangles/.meta/Example.roc +++ b/exercises/practice/rectangles/.meta/Example.roc @@ -1,57 +1,75 @@ -module [rectangles] +Rectangles :: {}.{ + rectangles : Str -> U64 + rectangles = |diagram| { + grid = + diagram + .split_on("\n") + .map(|s| s.to_utf8()) + height = grid.len() + grid + .map_with_index( + |row, y1| { + row + .map_with_index( + |_char, x1| { + y2s = ((y1 + 1).. Bool -is_rectangle = |{ grid, x1, y1, x2, y2 }| - get_cell = - |(x, y)| - grid - |> List.get(y)? - |> List.get(x) +is_rectangle : { grid : List(List(U8)), x1 : U64, y1 : U64, x2 : U64, y2 : U64 } -> Bool +is_rectangle = |{ grid, x1, y1, x2, y2 }| { + get_cell = |pos| { + (x, y) = pos + grid.get(y)?.get(x) + } - is_corner = |pos| get_cell(pos) == Ok('+') - is_horizontal = |pos| is_corner(pos) or get_cell(pos) == Ok('-') - is_vertical = |pos| is_corner(pos) or get_cell(pos) == Ok('|') + is_corner = |pos| { + get_cell(pos) == Ok('+') + } + is_horizontal = |pos| { + is_corner(pos) or get_cell(pos) == Ok('-') + } + is_vertical = |pos| { + is_corner(pos) or get_cell(pos) == Ok('|') + } - has_horizontal_border = |y| - List.range({ start: At(x1), end: At(x2) }) - |> List.all(|x| is_horizontal((x, y))) - - has_vertical_border = |x| - List.range({ start: At(y1), end: At(y2) }) - |> List.all(|y| is_vertical((x, y))) - ( - ([(x1, y1), (x2, y1), (x1, y2), (x2, y2)] |> List.all(is_corner)) - and ([y1, y2] |> List.all(has_horizontal_border)) - and ([x1, x2] |> List.all(has_vertical_border)) - ) + has_horizontal_border = |y| { + xs = (x1..=x2).fold([], |acc, x| acc.append(x)) + xs.all(|x| is_horizontal((x, y))) + } -rectangles : Str -> U64 -rectangles = |diagram| - grid = - diagram - |> Str.split_on("\n") - |> List.map(Str.to_utf8) - height = grid |> List.len - grid - |> List.map_with_index( - |row, y1| # number of rectangles with top on this row - row - |> List.map_with_index( - |_char, x1| # number with top-left on this column - List.range({ start: After(y1), end: Before(height) }) - |> List.map( - |y2| # number of rectangles with bottom on this row - List.range({ start: After(x1), end: Before(List.len(row)) }) - |> List.map( - |x2| # number with bottom-right on this column - if is_rectangle({ grid, x1, y1, x2, y2 }) then 1 else 0, - ) - |> List.sum, - ) - |> List.sum, - ) - |> List.sum, - ) - |> List.sum + has_vertical_border = |x| { + ys = (y1..=y2).fold([], |acc, y| acc.append(y)) + ys.all(|y| is_vertical((x, y))) + } + + ( + [(x1, y1), (x2, y1), (x1, y2), (x2, y2)].all(is_corner) + and [y1, y2].all(has_horizontal_border) + and [x1, x2].all(has_vertical_border), + ) +} diff --git a/exercises/practice/rectangles/.meta/template.j2 b/exercises/practice/rectangles/.meta/template.j2 index 53918371..b0918e38 100644 --- a/exercises/practice/rectangles/.meta/template.j2 +++ b/exercises/practice/rectangles/.meta/template.j2 @@ -6,9 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["strings"] | to_roc_multiline_string | indent(8) }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} +{{ macros.footer() }} diff --git a/exercises/practice/rectangles/Rectangles.roc b/exercises/practice/rectangles/Rectangles.roc index 415700ec..16633cb8 100644 --- a/exercises/practice/rectangles/Rectangles.roc +++ b/exercises/practice/rectangles/Rectangles.roc @@ -1,5 +1,6 @@ -module [rectangles] - -rectangles : Str -> U64 -rectangles = |diagram| - crash("Please implement the 'rectangles' function") +Rectangles :: {}.{ + rectangles : Str -> U64 + rectangles = |diagram| { + crash "Please implement the 'rectangles' function" + } +} diff --git a/exercises/practice/rectangles/rectangles-test.roc b/exercises/practice/rectangles/rectangles-test.roc index 1279b25d..cf493bdb 100644 --- a/exercises/practice/rectangles/rectangles-test.roc +++ b/exercises/practice/rectangles/rectangles-test.roc @@ -1,167 +1,166 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/rectangles/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Rectangles exposing [rectangles] # no rows -expect - result = rectangles("") - result == 0 +expect { + result = rectangles("") + result == 0 +} # no columns -expect - result = rectangles("") - result == 0 +expect { + result = rectangles("") + result == 0 +} # no rectangles -expect - result = rectangles(" ") - result == 0 +expect { + result = rectangles(" ") + result == 0 +} # one rectangle -expect - result = rectangles( - """ - +-+ - | | - +-+ - """, - ) - result == 1 +expect { + result = rectangles( + \\+-+ + \\| | + \\+-+ + , + ) + result == 1 +} # two rectangles without shared parts -expect - result = rectangles( - """ - +-+ - | | - +-+-+ - | | - +-+ - """, - ) - result == 2 +expect { + result = rectangles( + \\ +-+ + \\ | | + \\+-+-+ + \\| | + \\+-+ + , + ) + result == 2 +} # five rectangles with shared parts -expect - result = rectangles( - """ - +-+ - | | - +-+-+ - | | | - +-+-+ - """, - ) - result == 5 +expect { + result = rectangles( + \\ +-+ + \\ | | + \\+-+-+ + \\| | | + \\+-+-+ + , + ) + result == 5 +} # rectangle of height 1 is counted -expect - result = rectangles( - """ - +--+ - +--+ - """, - ) - result == 1 +expect { + result = rectangles( + \\+--+ + \\+--+ + , + ) + result == 1 +} # rectangle of width 1 is counted -expect - result = rectangles( - """ - ++ - || - ++ - """, - ) - result == 1 +expect { + result = rectangles( + \\++ + \\|| + \\++ + , + ) + result == 1 +} # 1x1 square is counted -expect - result = rectangles( - """ - ++ - ++ - """, - ) - result == 1 +expect { + result = rectangles( + \\++ + \\++ + , + ) + result == 1 +} # only complete rectangles are counted -expect - result = rectangles( - """ - +-+ - | - +-+-+ - | | - - +-+-+ - """, - ) - result == 1 +expect { + result = rectangles( + \\ +-+ + \\ | + \\+-+-+ + \\| | - + \\+-+-+ + , + ) + result == 1 +} # rectangles can be of different sizes -expect - result = rectangles( - """ - +------+----+ - | | | - +---+--+ | - | | | - +---+-------+ - """, - ) - result == 3 +expect { + result = rectangles( + \\+------+----+ + \\| | | + \\+---+--+ | + \\| | | + \\+---+-------+ + , + ) + result == 3 +} # corner is required for a rectangle to be complete -expect - result = rectangles( - """ - +------+----+ - | | | - +------+ | - | | | - +---+-------+ - """, - ) - result == 2 +expect { + result = rectangles( + \\+------+----+ + \\| | | + \\+------+ | + \\| | | + \\+---+-------+ + , + ) + result == 2 +} # large input with many rectangles -expect - result = rectangles( - """ - +---+--+----+ - | +--+----+ - +---+--+ | - | +--+----+ - +---+--+--+-+ - +---+--+--+-+ - +------+ | | - +-+ - """, - ) - result == 60 +expect { + result = rectangles( + \\+---+--+----+ + \\| +--+----+ + \\+---+--+ | + \\| +--+----+ + \\+---+--+--+-+ + \\+---+--+--+-+ + \\+------+ | | + \\ +-+ + , + ) + result == 60 +} # rectangles must have four sides -expect - result = rectangles( - """ - +-+ +-+ - | | | | - +-+-+-+ - | | - +-+-+-+ - | | | | - +-+ +-+ - """, - ) - result == 5 +expect { + result = rectangles( + \\+-+ +-+ + \\| | | | + \\+-+-+-+ + \\ | | + \\+-+-+-+ + \\| | | | + \\+-+ +-+ + , + ) + result == 5 +} +# 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-duo/.meta/Example.roc b/exercises/practice/resistor-color-duo/.meta/Example.roc index ef94d0ff..832c8ca7 100644 --- a/exercises/practice/resistor-color-duo/.meta/Example.roc +++ b/exercises/practice/resistor-color-duo/.meta/Example.roc @@ -1,32 +1,35 @@ -module [value] +ResistorColorDuo :: {}.{ + Color := [ + Black, + Brown, + Red, + Orange, + Yellow, + Green, + Blue, + Violet, + Grey, + White, + ] -Color : [ - Black, - Brown, - Red, - Orange, - Yellow, - Green, - Blue, - Violet, - Grey, - White, -] - -value : Color, Color -> U8 -value = |first, second| - 10 * get_code(first) + get_code(second) + value : Color, Color -> U8 + value = |first, second| { + 10 * get_code(first) + get_code(second) + } +} get_code : Color -> U8 -get_code = |color| - when color is - Black -> 0 - Brown -> 1 - Red -> 2 - Orange -> 3 - Yellow -> 4 - Green -> 5 - Blue -> 6 - Violet -> 7 - Grey -> 8 - White -> 9 +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-duo/.meta/template.j2 b/exercises/practice/resistor-color-duo/.meta/template.j2 index b548a358..b6333171 100644 --- a/exercises/practice/resistor-color-duo/.meta/template.j2 +++ b/exercises/practice/resistor-color-duo/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [value] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["colors"][0] | to_pascal }}, {{ case["input"]["colors"][1] | to_pascal }}) result == {{ case["expected"] }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/resistor-color-duo/ResistorColorDuo.roc b/exercises/practice/resistor-color-duo/ResistorColorDuo.roc index 389ec2d1..7f842b26 100644 --- a/exercises/practice/resistor-color-duo/ResistorColorDuo.roc +++ b/exercises/practice/resistor-color-duo/ResistorColorDuo.roc @@ -1,18 +1,19 @@ -module [value] +ResistorColorDuo :: {}.{ + Color := [ + Black, + Brown, + Red, + Orange, + Yellow, + Green, + Blue, + Violet, + Grey, + White, + ] -Color : [ - Black, - Brown, - Red, - Orange, - Yellow, - Green, - Blue, - Violet, - Grey, - White, -] - -value : Color, Color -> U8 -value = |first, second| - crash("Please implement the 'value' function") + value : Color, Color -> U8 + value = |first, second| { + crash "Please implement the 'value' function" + } +} diff --git a/exercises/practice/resistor-color-duo/resistor-color-duo-test.roc b/exercises/practice/resistor-color-duo/resistor-color-duo-test.roc index 131175f0..be93d99c 100644 --- a/exercises/practice/resistor-color-duo/resistor-color-duo-test.roc +++ b/exercises/practice/resistor-color-duo/resistor-color-duo-test.roc @@ -1,44 +1,46 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/resistor-color-duo/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import ResistorColorDuo exposing [value] # Brown and black -expect - result = value(Brown, Black) - result == 10 +expect { + result = value(Brown, Black) + result == 10 +} # Blue and grey -expect - result = value(Blue, Grey) - result == 68 +expect { + result = value(Blue, Grey) + result == 68 +} # Yellow and violet -expect - result = value(Yellow, Violet) - result == 47 +expect { + result = value(Yellow, Violet) + result == 47 +} # White and red -expect - result = value(White, Red) - result == 92 +expect { + result = value(White, Red) + result == 92 +} # Orange and orange -expect - result = value(Orange, Orange) - result == 33 +expect { + result = value(Orange, Orange) + result == 33 +} # Black and brown, one-digit -expect - result = value(Black, Brown) - result == 1 +expect { + result = value(Black, Brown) + result == 1 +} +# 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/.meta/Example.roc b/exercises/practice/resistor-color/.meta/Example.roc index e964ae08..2c974e09 100644 --- a/exercises/practice/resistor-color/.meta/Example.roc +++ b/exercises/practice/resistor-color/.meta/Example.roc @@ -1,20 +1,9 @@ -module [color_code, color_code_from_dict, colors] - -colors : List Str -colors = - ["black", "brown", "red", "orange", "yellow", "green", "blue", "violet", "grey", "white"] - -color_code : Str -> Result U64 [NotFound] -color_code = |color| - colors |> List.find_first_index(|elem| elem == color) - -# Since the list of colors is quite small, representing it as a List is simple -# and efficient. However, if there were many more colors, it would make sense -# to use a Dict instead to avoid traversing the list at each lookup: -colors_dict = - colors - |> List.map_with_index(|color, index| (color, index)) - |> Dict.from_list - -color_code_from_dict = |color| - colors_dict |> Dict.get(color) +ResistorColor :: {}.{ + colors : List(Str) + colors = ["black", "brown", "red", "orange", "yellow", "green", "blue", "violet", "grey", "white"] + + color_code : Str -> Try(U64, [NotFound]) + color_code = |color| { + colors.find_first_index(|elem| elem == color) + } +} diff --git a/exercises/practice/resistor-color/.meta/template.j2 b/exercises/practice/resistor-color/.meta/template.j2 index c8e32f9f..ece97f25 100644 --- a/exercises/practice/resistor-color/.meta/template.j2 +++ b/exercises/practice/resistor-color/.meta/template.j2 @@ -9,20 +9,23 @@ import {{ exercise | to_pascal }} exposing [color_code, colors] ## {{ case["description"] }} ## {% if case["property"] == "colors" %} -expect - result = {{ case["property"] | to_snake }} - result == [ +expect { + {{ case["property"] | to_snake }} == [ {%- for color in case["expected"] %} {{ color | to_roc }}, {%- endfor %} ] +} {% else %} {% for subcase in case["cases"] -%} # {{ subcase["description"] }} -expect +expect { result = {{ subcase["property"] | to_snake }}({{ subcase["input"]["color"] | to_roc }}) result == Ok({{ subcase["expected"] }}) +} {% endfor %} {%- endif -%} -{%- endfor -%} +{%- endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/resistor-color/ResistorColor.roc b/exercises/practice/resistor-color/ResistorColor.roc index 48130358..6aecdb76 100644 --- a/exercises/practice/resistor-color/ResistorColor.roc +++ b/exercises/practice/resistor-color/ResistorColor.roc @@ -1,9 +1,11 @@ -module [color_code, colors] +ResistorColor :: {}.{ + color_code : Str -> Try(U64, [NotFound]) + color_code = |color| { + crash "Please implement the 'color_code' function" + } -color_code : Str -> Result U64 _ -color_code = |color| - crash("Please implement the 'color_code' function") - -colors : List Str -colors = - crash("Please implement the 'colors' function") + colors : List(Str) + colors = { + crash "Please implement the 'colors' function" + } +} diff --git a/exercises/practice/resistor-color/resistor-color-test.roc b/exercises/practice/resistor-color/resistor-color-test.roc index b1a35811..f43d1945 100644 --- a/exercises/practice/resistor-color/resistor-color-test.roc +++ b/exercises/practice/resistor-color/resistor-color-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/resistor-color/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import ResistorColor exposing [color_code, colors] @@ -17,36 +9,43 @@ import ResistorColor exposing [color_code, colors] ## # Black -expect - result = color_code("black") - result == Ok(0) +expect { + result = color_code("black") + result == Ok(0) +} # White -expect - result = color_code("white") - result == Ok(9) +expect { + result = color_code("white") + result == Ok(9) +} # Orange -expect - result = color_code("orange") - result == Ok(3) +expect { + result = color_code("orange") + result == Ok(3) +} ## ## Colors ## -expect - result = colors - result - == [ - "black", - "brown", - "red", - "orange", - "yellow", - "green", - "blue", - "violet", - "grey", - "white", - ] +expect { + colors == [ + "black", + "brown", + "red", + "orange", + "yellow", + "green", + "blue", + "violet", + "grey", + "white", + ] +} + +# 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 46309eef..51f9bc00 100644 --- a/exercises/practice/rest-api/.meta/Example.roc +++ b/exercises/practice/rest-api/.meta/Example.roc @@ -1,6 +1,28 @@ -module [get, post] + import json.Json +RestApi :: {}.{ + get : Database, { url : Str, payload ?? Str } -> Result Str [Http404 Str, Http422 Str] + get = |database, { url, payload ?? "" }| + when url is + "/users" -> + database + |> get_users(payload) + |> Result.map_err(|InvalidJson| Http422(payload)) + + bad_url -> Err(Http404(bad_url)) + + post : Database, { url : Str, payload ?? Str } -> Result Str [Http404 Str, Http422 Str] + post = |database, { url, payload ?? "" }| + handle_error = |err| + when err is + InvalidJson -> Http422(payload) + NotFound -> Http404(payload) + when url is + "/add" -> database |> add_user(payload) |> Result.map_err(handle_error) + "/iou" -> database |> add_loan(payload) |> Result.map_err(handle_error) + bad_url -> Err(Http404(bad_url)) +} User : { name : Str, @@ -13,16 +35,6 @@ Database : { users : List User } Loan : { lender : Str, borrower : Str, amount : F64 } -get : Database, { url : Str, payload ?? Str } -> Result Str [Http404 Str, Http422 Str] -get = |database, { url, payload ?? "" }| - when url is - "/users" -> - database - |> get_users(payload) - |> Result.map_err(|InvalidJson| Http422(payload)) - - bad_url -> Err(Http404(bad_url)) - compare_strings : Str, Str -> [LT, EQ, GT] compare_strings = |string1, string2| b1 = string1 |> Str.to_utf8 @@ -119,17 +131,6 @@ parse_json_loan = |payload| maybe_loan = Decode.from_bytes_partial(bytes, Json.utf8) maybe_loan.result |> Result.map_err(|_| InvalidJson) -post : Database, { url : Str, payload ?? Str } -> Result Str [Http404 Str, Http422 Str] -post = |database, { url, payload ?? "" }| - handle_error = |err| - when err is - InvalidJson -> Http422(payload) - NotFound -> Http404(payload) - when url is - "/add" -> database |> add_user(payload) |> Result.map_err(handle_error) - "/iou" -> database |> add_loan(payload) |> Result.map_err(handle_error) - bad_url -> Err(Http404(bad_url)) - add_user : Database, Str -> Result Str [InvalidJson] add_user = |_database, payload| user_payload = parse_json_user(payload)? diff --git a/exercises/practice/rest-api/.meta/template.j2 b/exercises/practice/rest-api/.meta/template.j2 index 109b5ee3..255952af 100644 --- a/exercises/practice/rest-api/.meta/template.j2 +++ b/exercises/practice/rest-api/.meta/template.j2 @@ -4,17 +4,19 @@ import {{ exercise | to_pascal }} exposing [get, post] -standardize_result = |result| +standardize_result = |result| { result - |> Result.try( - |string| + -> Result.try( + |string| { string - |> Str.replace_each(".0,", ",") - |> Str.replace_each(".0}", "}") - |> Str.to_utf8 - |> List.drop_if |c| [' ', '\t', '\n'] |> List.contains(c) - |> Str.from_utf8 + .replace_each(".0,", ",") + .replace_each(".0}", "}") + .to_utf8() + .drop_if(|c| { [' ', '\t', '\n'].contains(c) }) + ->Str.from_utf8() + } ) +} {% for supercase in cases %} ## @@ -23,7 +25,7 @@ standardize_result = |result| {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect +expect { database = { users: [ {%- for user in case["input"]["database"]["users"] %} @@ -44,14 +46,17 @@ expect {%- endfor %} ] } - result = database |> {{ case["property"] | to_snake }}({ + result = database -> {{ case["property"] | to_snake }}({ url: {{ case["input"]["url"] | to_roc }}, {%- if case["input"].get("payload", {}) != {} %} payload: {{ case["input"]["payload"] | tojson | to_roc }} {%- endif %} - }) |> standardize_result + }) -> standardize_result() expected = Ok({{ case["expected"] | tojson | replace(".0,", ",") | replace(".0}", "}") | replace(" ", "") | to_roc }}) result == expected +} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/rest-api/RestApi.roc b/exercises/practice/rest-api/RestApi.roc index 6480a401..ef6bfae4 100644 --- a/exercises/practice/rest-api/RestApi.roc +++ b/exercises/practice/rest-api/RestApi.roc @@ -1,6 +1,15 @@ -module [get, post] + import json.Json +RestApi :: {}.{ + get : Database, { url : Str, payload ?? Str } -> Result Str _ + get = |database, { url, payload ?? "" }| + crash("Please implement the 'get' function") + + post : Database, { url : Str, payload ?? Str } -> Result Str _ + post = |database, { url, payload ?? "" }| + crash("Please implement the 'post' function") +} User : { name : Str, @@ -10,11 +19,3 @@ User : { } Database : { users : List User } - -get : Database, { url : Str, payload ?? Str } -> Result Str _ -get = |database, { url, payload ?? "" }| - crash("Please implement the 'get' function") - -post : Database, { url : Str, payload ?? Str } -> Result Str _ -post = |database, { url, payload ?? "" }| - crash("Please implement the 'post' function") diff --git a/exercises/practice/rest-api/rest-api-test.roc b/exercises/practice/rest-api/rest-api-test.roc index 37149394..0ab09fea 100644 --- a/exercises/practice/rest-api/rest-api-test.roc +++ b/exercises/practice/rest-api/rest-api-test.roc @@ -1,332 +1,319 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/rest-api/canonical-data.json -# File last updated on 2025-09-15 +# File last updated on 2026-06-21 app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", - json: "https://github.com/lukewilliamboswell/roc-json/releases/download/0.13.0/RqendgZw5e1RsQa3kFhgtnMP8efWoqGRsAvubx4-zus.tar.br", + pf: platform "https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/download/0.9/8GdFEvQYS3TeAZxKvTzCLVdQiomweGtXcdZkXNDEeABq.tar.zst", + json: "https://github.com/lukewilliamboswell/roc-json/...", # TODO: update when a zig-compatible release is available } import pf.Stdout -main! = |_args| - Stdout.line!("") - import RestApi exposing [get, post] -standardize_result = |result| - result - |> Result.try( - |string| - string - |> Str.replace_each(".0,", ",") - |> Str.replace_each(".0}", "}") - |> Str.to_utf8 - |> List.drop_if |c| [' ', '\t', '\n'] |> List.contains(c) - |> Str.from_utf8, - ) +standardize_result = |result| { + result + ->Result.try( + |string| { + string + .replace_each(".0,", ",") + .replace_each(".0}", "}") + .to_utf8() + .drop_if( + |c| { + [' ', '\t', '\n'].contains(c) + }, + ) + ->Str.from_utf8() + }, + ) +} ## ## user management ## # no users -expect - database = { - users: [ - ], - } - result = - database - |> get( - { - url: "/users", - }, - ) - |> standardize_result - expected = Ok("{\"users\":[]}") - result == expected +expect { + database = { + users: [], + } + result = database->get( + { + url: "/users", + }, + )->standardize_result() + expected = Ok("{\"users\":[]}") + result == expected +} # add user -expect - database = { - users: [ - ], - } - result = - database - |> post( - { - url: "/add", - payload: "{\"user\": \"Adam\"}", - }, - ) - |> standardize_result - expected = Ok("{\"balance\":0,\"name\":\"Adam\",\"owed_by\":{},\"owes\":{}}") - result == expected +expect { + database = { + users: [], + } + result = database->post( + { + url: "/add", + payload: "{\"user\": \"Adam\"}", + }, + )->standardize_result() + expected = Ok("{\"balance\":0,\"name\":\"Adam\",\"owed_by\":{},\"owes\":{}}") + result == expected +} # get single user -expect - database = { - users: [ - { - name: "Adam", - owes: Dict.from_list([]), - owed_by: Dict.from_list([]), - balance: 0.0, - }, - { - name: "Bob", - owes: Dict.from_list([]), - owed_by: Dict.from_list([]), - balance: 0.0, - }, - ], - } - result = - database - |> get( - { - url: "/users", - payload: "{\"users\": [\"Bob\"]}", - }, - ) - |> standardize_result - expected = Ok("{\"users\":[{\"balance\":0,\"name\":\"Bob\",\"owed_by\":{},\"owes\":{}}]}") - result == expected +expect { + database = { + users: [ + { + name: "Adam", + owes: Dict.from_list([]), + owed_by: Dict.from_list([]), + balance: 0.0, + }, + { + name: "Bob", + owes: Dict.from_list([]), + owed_by: Dict.from_list([]), + balance: 0.0, + }, + ], + } + result = database->get( + { + url: "/users", + payload: "{\"users\": [\"Bob\"]}", + }, + )->standardize_result() + expected = Ok("{\"users\":[{\"balance\":0,\"name\":\"Bob\",\"owed_by\":{},\"owes\":{}}]}") + result == expected +} ## ## iou ## # both users have 0 balance -expect - database = { - users: [ - { - name: "Adam", - owes: Dict.from_list([]), - owed_by: Dict.from_list([]), - balance: 0.0, - }, - { - name: "Bob", - owes: Dict.from_list([]), - owed_by: Dict.from_list([]), - balance: 0.0, - }, - ], - } - result = - database - |> post( - { - url: "/iou", - payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}", - }, - ) - |> standardize_result - expected = Ok("{\"users\":[{\"balance\":3,\"name\":\"Adam\",\"owed_by\":{\"Bob\":3},\"owes\":{}},{\"balance\":-3,\"name\":\"Bob\",\"owed_by\":{},\"owes\":{\"Adam\":3}}]}") - result == expected +expect { + database = { + users: [ + { + name: "Adam", + owes: Dict.from_list([]), + owed_by: Dict.from_list([]), + balance: 0.0, + }, + { + name: "Bob", + owes: Dict.from_list([]), + owed_by: Dict.from_list([]), + balance: 0.0, + }, + ], + } + result = database->post( + { + url: "/iou", + payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}", + }, + )->standardize_result() + expected = Ok("{\"users\":[{\"balance\":3,\"name\":\"Adam\",\"owed_by\":{\"Bob\":3},\"owes\":{}},{\"balance\":-3,\"name\":\"Bob\",\"owed_by\":{},\"owes\":{\"Adam\":3}}]}") + result == expected +} # borrower has negative balance -expect - database = { - users: [ - { - name: "Adam", - owes: Dict.from_list([]), - owed_by: Dict.from_list([]), - balance: 0.0, - }, - { - name: "Bob", - owes: Dict.from_list( - [ - ("Chuck", 3.0), - ], - ), - owed_by: Dict.from_list([]), - balance: -3.0, - }, - { - name: "Chuck", - owes: Dict.from_list([]), - owed_by: Dict.from_list( - [ - ("Bob", 3.0), - ], - ), - balance: 3.0, - }, - ], - } - result = - database - |> post( - { - url: "/iou", - payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}", - }, - ) - |> standardize_result - expected = Ok("{\"users\":[{\"balance\":3,\"name\":\"Adam\",\"owed_by\":{\"Bob\":3},\"owes\":{}},{\"balance\":-6,\"name\":\"Bob\",\"owed_by\":{},\"owes\":{\"Adam\":3,\"Chuck\":3}}]}") - result == expected +expect { + database = { + users: [ + { + name: "Adam", + owes: Dict.from_list([]), + owed_by: Dict.from_list([]), + balance: 0.0, + }, + { + name: "Bob", + owes: Dict.from_list( + [ + ("Chuck", 3.0), + ], + ), + owed_by: Dict.from_list([]), + balance: -3.0, + }, + { + name: "Chuck", + owes: Dict.from_list([]), + owed_by: Dict.from_list( + [ + ("Bob", 3.0), + ], + ), + balance: 3.0, + }, + ], + } + result = database->post( + { + url: "/iou", + payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}", + }, + )->standardize_result() + expected = Ok("{\"users\":[{\"balance\":3,\"name\":\"Adam\",\"owed_by\":{\"Bob\":3},\"owes\":{}},{\"balance\":-6,\"name\":\"Bob\",\"owed_by\":{},\"owes\":{\"Adam\":3,\"Chuck\":3}}]}") + result == expected +} # lender has negative balance -expect - database = { - users: [ - { - name: "Adam", - owes: Dict.from_list([]), - owed_by: Dict.from_list([]), - balance: 0.0, - }, - { - name: "Bob", - owes: Dict.from_list( - [ - ("Chuck", 3.0), - ], - ), - owed_by: Dict.from_list([]), - balance: -3.0, - }, - { - name: "Chuck", - owes: Dict.from_list([]), - owed_by: Dict.from_list( - [ - ("Bob", 3.0), - ], - ), - balance: 3.0, - }, - ], - } - result = - database - |> post( - { - url: "/iou", - payload: "{\"amount\": 3.0, \"borrower\": \"Adam\", \"lender\": \"Bob\"}", - }, - ) - |> standardize_result - expected = Ok("{\"users\":[{\"balance\":-3,\"name\":\"Adam\",\"owed_by\":{},\"owes\":{\"Bob\":3}},{\"balance\":0,\"name\":\"Bob\",\"owed_by\":{\"Adam\":3},\"owes\":{\"Chuck\":3}}]}") - result == expected +expect { + database = { + users: [ + { + name: "Adam", + owes: Dict.from_list([]), + owed_by: Dict.from_list([]), + balance: 0.0, + }, + { + name: "Bob", + owes: Dict.from_list( + [ + ("Chuck", 3.0), + ], + ), + owed_by: Dict.from_list([]), + balance: -3.0, + }, + { + name: "Chuck", + owes: Dict.from_list([]), + owed_by: Dict.from_list( + [ + ("Bob", 3.0), + ], + ), + balance: 3.0, + }, + ], + } + result = database->post( + { + url: "/iou", + payload: "{\"amount\": 3.0, \"borrower\": \"Adam\", \"lender\": \"Bob\"}", + }, + )->standardize_result() + expected = Ok("{\"users\":[{\"balance\":-3,\"name\":\"Adam\",\"owed_by\":{},\"owes\":{\"Bob\":3}},{\"balance\":0,\"name\":\"Bob\",\"owed_by\":{\"Adam\":3},\"owes\":{\"Chuck\":3}}]}") + result == expected +} # lender owes borrower -expect - database = { - users: [ - { - name: "Adam", - owes: Dict.from_list( - [ - ("Bob", 3.0), - ], - ), - owed_by: Dict.from_list([]), - balance: -3.0, - }, - { - name: "Bob", - owes: Dict.from_list([]), - owed_by: Dict.from_list( - [ - ("Adam", 3.0), - ], - ), - balance: 3.0, - }, - ], - } - result = - database - |> post( - { - url: "/iou", - payload: "{\"amount\": 2.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}", - }, - ) - |> standardize_result - expected = Ok("{\"users\":[{\"balance\":-1,\"name\":\"Adam\",\"owed_by\":{},\"owes\":{\"Bob\":1}},{\"balance\":1,\"name\":\"Bob\",\"owed_by\":{\"Adam\":1},\"owes\":{}}]}") - result == expected +expect { + database = { + users: [ + { + name: "Adam", + owes: Dict.from_list( + [ + ("Bob", 3.0), + ], + ), + owed_by: Dict.from_list([]), + balance: -3.0, + }, + { + name: "Bob", + owes: Dict.from_list([]), + owed_by: Dict.from_list( + [ + ("Adam", 3.0), + ], + ), + balance: 3.0, + }, + ], + } + result = database->post( + { + url: "/iou", + payload: "{\"amount\": 2.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}", + }, + )->standardize_result() + expected = Ok("{\"users\":[{\"balance\":-1,\"name\":\"Adam\",\"owed_by\":{},\"owes\":{\"Bob\":1}},{\"balance\":1,\"name\":\"Bob\",\"owed_by\":{\"Adam\":1},\"owes\":{}}]}") + result == expected +} # lender owes borrower less than new loan -expect - database = { - users: [ - { - name: "Adam", - owes: Dict.from_list( - [ - ("Bob", 3.0), - ], - ), - owed_by: Dict.from_list([]), - balance: -3.0, - }, - { - name: "Bob", - owes: Dict.from_list([]), - owed_by: Dict.from_list( - [ - ("Adam", 3.0), - ], - ), - balance: 3.0, - }, - ], - } - result = - database - |> post( - { - url: "/iou", - payload: "{\"amount\": 4.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}", - }, - ) - |> standardize_result - expected = Ok("{\"users\":[{\"balance\":1,\"name\":\"Adam\",\"owed_by\":{\"Bob\":1},\"owes\":{}},{\"balance\":-1,\"name\":\"Bob\",\"owed_by\":{},\"owes\":{\"Adam\":1}}]}") - result == expected +expect { + database = { + users: [ + { + name: "Adam", + owes: Dict.from_list( + [ + ("Bob", 3.0), + ], + ), + owed_by: Dict.from_list([]), + balance: -3.0, + }, + { + name: "Bob", + owes: Dict.from_list([]), + owed_by: Dict.from_list( + [ + ("Adam", 3.0), + ], + ), + balance: 3.0, + }, + ], + } + result = database->post( + { + url: "/iou", + payload: "{\"amount\": 4.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}", + }, + )->standardize_result() + expected = Ok("{\"users\":[{\"balance\":1,\"name\":\"Adam\",\"owed_by\":{\"Bob\":1},\"owes\":{}},{\"balance\":-1,\"name\":\"Bob\",\"owed_by\":{},\"owes\":{\"Adam\":1}}]}") + result == expected +} # lender owes borrower same as new loan -expect - database = { - users: [ - { - name: "Adam", - owes: Dict.from_list( - [ - ("Bob", 3.0), - ], - ), - owed_by: Dict.from_list([]), - balance: -3.0, - }, - { - name: "Bob", - owes: Dict.from_list([]), - owed_by: Dict.from_list( - [ - ("Adam", 3.0), - ], - ), - balance: 3.0, - }, - ], - } - result = - database - |> post( - { - url: "/iou", - payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}", - }, - ) - |> standardize_result - expected = Ok("{\"users\":[{\"balance\":0,\"name\":\"Adam\",\"owed_by\":{},\"owes\":{}},{\"balance\":0,\"name\":\"Bob\",\"owed_by\":{},\"owes\":{}}]}") - result == expected +expect { + database = { + users: [ + { + name: "Adam", + owes: Dict.from_list( + [ + ("Bob", 3.0), + ], + ), + owed_by: Dict.from_list([]), + balance: -3.0, + }, + { + name: "Bob", + owes: Dict.from_list([]), + owed_by: Dict.from_list( + [ + ("Adam", 3.0), + ], + ), + balance: 3.0, + }, + ], + } + result = database->post( + { + url: "/iou", + payload: "{\"amount\": 3.0, \"borrower\": \"Bob\", \"lender\": \"Adam\"}", + }, + )->standardize_result() + expected = Ok("{\"users\":[{\"balance\":0,\"name\":\"Adam\",\"owed_by\":{},\"owes\":{}},{\"balance\":0,\"name\":\"Bob\",\"owed_by\":{},\"owes\":{}}]}") + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/reverse-string/.meta/Example.roc b/exercises/practice/reverse-string/.meta/Example.roc index 067dc413..b0304354 100644 --- a/exercises/practice/reverse-string/.meta/Example.roc +++ b/exercises/practice/reverse-string/.meta/Example.roc @@ -1,26 +1,27 @@ -module [reverse, reverse_ascii] -import unicode.Grapheme -## This function reverses the input string, e.g., "café" -> "éfac". -## This implementation uses the `unicode` package from github.com/roc-lang/unicode -## to split the input string into separate "Extended Grapheme Clusters". This is a -## fancy Unicode name for characters that human beings recognize as such: each EGC -## may actually be composed of several Unicode codepoints, e.g., the character -## é is recognized as a single character, but it may actually be represented as two -## Unicode codepoints (e + ´). -## To use the `unicode` package, its URL must be added to the app's header. -## Luckily, we've added it for you in reverse-string-test.roc. Take a look! -reverse : Str -> Str -reverse = |string| - when Grapheme.split(string) is - Ok(graphemes) -> graphemes |> List.reverse |> Str.join_with("") - Err(_) -> "Unexpected error: could not split the string into graphemes" +import unicode.Grapheme +ReverseString :: {}.{ + ## This function reverses the input string, e.g., "café" -> "éfac". + ## This implementation uses the `unicode` package from github.com/roc-lang/unicode + ## to split the input string into separate "Extended Grapheme Clusters". This is a + ## fancy Unicode name for characters that human beings recognize as such: each EGC + ## may actually be composed of several Unicode codepoints, e.g., the character + ## é is recognized as a single character, but it may actually be represented as two + ## Unicode codepoints (e + ´). + ## To use the `unicode` package, its URL must be added to the app's header. + ## Luckily, we've added it for you in reverse-string-test.roc. Take a look! + reverse : Str -> Str + reverse = |string| + when Grapheme.split(string) is + Ok(graphemes) -> graphemes |> List.reverse |> Str.join_with("") + Err(_) -> "Unexpected error: could not split the string into graphemes" -## This function reverses the input string, e.g., "hello" -> "olleh". It is -## faster and simpler than the implementation above, plus it does not require an -## external package, but it is only guaranteed to work on ASCII strings. -reverse_ascii = |string| - when string |> Str.to_utf8 |> List.reverse |> Str.from_utf8 is - Ok(reversed) -> reversed - Err(_) -> "This implementation online works on ASCII strings" + ## This function reverses the input string, e.g., "hello" -> "olleh". It is + ## faster and simpler than the implementation above, plus it does not require an + ## external package, but it is only guaranteed to work on ASCII strings. + reverse_ascii = |string| + when string |> Str.to_utf8 |> List.reverse |> Str.from_utf8 is + Ok(reversed) -> reversed + Err(_) -> "This implementation online works on ASCII strings" +} diff --git a/exercises/practice/reverse-string/.meta/template.j2 b/exercises/practice/reverse-string/.meta/template.j2 index 07e0a8df..8e877ad1 100644 --- a/exercises/practice/reverse-string/.meta/template.j2 +++ b/exercises/practice/reverse-string/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [reverse] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["value"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/reverse-string/ReverseString.roc b/exercises/practice/reverse-string/ReverseString.roc index 40d508e0..2fb6385b 100644 --- a/exercises/practice/reverse-string/ReverseString.roc +++ b/exercises/practice/reverse-string/ReverseString.roc @@ -1,9 +1,9 @@ -module [reverse] +ReverseString :: {}.{ + reverse : Str -> Str + reverse = |string| + crash("Please implement the 'reverse' function") -reverse : Str -> Str -reverse = |string| - crash("Please implement the `reverse` function") - -# HINT: we have added the `unicode` package to the app's header in -# reverse-string-test.roc, so you can use it here if you need to. -# For example, you could use unicode.Grapheme, just sayin'. + # HINT: we have added the `unicode` package to the app's header in + # reverse-string-test.roc, so you can use it here if you need to. + # For example, you could use unicode.Grapheme, just sayin'. +} diff --git a/exercises/practice/reverse-string/reverse-string-test.roc b/exercises/practice/reverse-string/reverse-string-test.roc index ca31f40f..0884b070 100644 --- a/exercises/practice/reverse-string/reverse-string-test.roc +++ b/exercises/practice/reverse-string/reverse-string-test.roc @@ -1,60 +1,70 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/reverse-string/canonical-data.json -# File last updated on 2025-09-15 +# File last updated on 2026-06-21 app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", - unicode: "https://github.com/roc-lang/unicode/releases/download/0.3.0/9KKFsA4CdOz0JIOL7iBSI_2jGIXQ6TsFBXgd086idpY.tar.br", + pf: platform "https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/download/0.9/8GdFEvQYS3TeAZxKvTzCLVdQiomweGtXcdZkXNDEeABq.tar.zst", + unicode: "https://github.com/roc-lang/unicode/...", # TODO: update when a zig-compatible release is available } import pf.Stdout -main! = |_args| - Stdout.line!("") - import ReverseString exposing [reverse] # an empty string -expect - result = reverse("") - result == "" +expect { + result = reverse("") + result == "" +} # a word -expect - result = reverse("robot") - result == "tobor" +expect { + result = reverse("robot") + result == "tobor" +} # a capitalized word -expect - result = reverse("Ramen") - result == "nemaR" +expect { + result = reverse("Ramen") + result == "nemaR" +} # a sentence with punctuation -expect - result = reverse("I'm hungry!") - result == "!yrgnuh m'I" +expect { + result = reverse("I'm hungry!") + result == "!yrgnuh m'I" +} # a palindrome -expect - result = reverse("racecar") - result == "racecar" +expect { + result = reverse("racecar") + result == "racecar" +} # an even-sized word -expect - result = reverse("drawer") - result == "reward" +expect { + result = reverse("drawer") + result == "reward" +} # wide characters -expect - result = reverse("子猫") - result == "猫子" +expect { + result = reverse("子猫") + result == "猫子" +} # grapheme cluster with pre-combined form -expect - result = reverse("Würstchenstand") - result == "dnatsnehctsrüW" +expect { + result = reverse("Würstchenstand") + result == "dnatsnehctsrüW" +} # grapheme clusters -expect - result = reverse("ผู้เขียนโปรแกรม") - result == "มรกแรปโนยขีเผู้" +expect { + result = reverse("ผู้เขียนโปรแกรม") + result == "มรกแรปโนยขีเผู้" +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/rna-transcription/.meta/Example.roc b/exercises/practice/rna-transcription/.meta/Example.roc index 9fb81478..7a445246 100644 --- a/exercises/practice/rna-transcription/.meta/Example.roc +++ b/exercises/practice/rna-transcription/.meta/Example.roc @@ -1,21 +1,29 @@ -module [to_rna] +RnaTranscription :: {}.{ + to_rna : Str -> Str + to_rna = |dna| { + maybe_rna = + dna + .to_utf8() + .map( + complement, + ) + ->Str.from_utf8() -complement = |nucleotide| - when nucleotide is - 'G' -> 'C' - 'C' -> 'G' - 'T' -> 'A' - 'A' -> 'U' - c -> c # invalid nucleotides are ignored + match maybe_rna { + Ok(rna) => rna + Err(_) => { + crash "Unreachable code: toUt8 -> fromUtf8 will always be Ok" + } + } + } +} -to_rna : Str -> Str -to_rna = |dna| - maybe_rna = - dna - |> Str.to_utf8 - |> List.map(complement) - |> Str.from_utf8 - - when maybe_rna is - Ok(rna) -> rna - Err(_) -> crash("Unreachable code: toUt8 -> fromUtf8 will always be Ok") +complement = |nucleotide| { + match nucleotide { + 'G' => 'C' + 'C' => 'G' + 'T' => 'A' + 'A' => 'U' + c => c + } +} diff --git a/exercises/practice/rna-transcription/.meta/template.j2 b/exercises/practice/rna-transcription/.meta/template.j2 index f88481ec..9b76bf72 100644 --- a/exercises/practice/rna-transcription/.meta/template.j2 +++ b/exercises/practice/rna-transcription/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [to_rna] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["dna"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/rna-transcription/RnaTranscription.roc b/exercises/practice/rna-transcription/RnaTranscription.roc index 1c7f505c..29e06bf3 100644 --- a/exercises/practice/rna-transcription/RnaTranscription.roc +++ b/exercises/practice/rna-transcription/RnaTranscription.roc @@ -1,5 +1,6 @@ -module [to_rna] - -to_rna : Str -> Str -to_rna = |dna| - crash("Please implement the 'to_rna' function") +RnaTranscription :: {}.{ + to_rna : Str -> Str + to_rna = |dna| { + crash "Please implement the 'to_rna' function" + } +} diff --git a/exercises/practice/rna-transcription/rna-transcription-test.roc b/exercises/practice/rna-transcription/rna-transcription-test.roc index 3af4af80..7b1b106e 100644 --- a/exercises/practice/rna-transcription/rna-transcription-test.roc +++ b/exercises/practice/rna-transcription/rna-transcription-test.roc @@ -1,44 +1,46 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/rna-transcription/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import RnaTranscription exposing [to_rna] # Empty RNA sequence -expect - result = to_rna("") - result == "" +expect { + result = to_rna("") + result == "" +} # RNA complement of cytosine is guanine -expect - result = to_rna("C") - result == "G" +expect { + result = to_rna("C") + result == "G" +} # RNA complement of guanine is cytosine -expect - result = to_rna("G") - result == "C" +expect { + result = to_rna("G") + result == "C" +} # RNA complement of thymine is adenine -expect - result = to_rna("T") - result == "A" +expect { + result = to_rna("T") + result == "A" +} # RNA complement of adenine is uracil -expect - result = to_rna("A") - result == "U" +expect { + result = to_rna("A") + result == "U" +} # RNA complement -expect - result = to_rna("ACGTGGTCTTAA") - result == "UGCACCAGAAUU" +expect { + result = to_rna("ACGTGGTCTTAA") + result == "UGCACCAGAAUU" +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/robot-simulator/.meta/Example.roc b/exercises/practice/robot-simulator/.meta/Example.roc index beec2f8f..570529b1 100644 --- a/exercises/practice/robot-simulator/.meta/Example.roc +++ b/exercises/practice/robot-simulator/.meta/Example.roc @@ -1,41 +1,45 @@ -module [create, move] +RobotSimulator :: {}.{ + Direction : [North, East, South, West] + Robot : { x : I64, y : I64, direction : Direction } -Direction : [North, East, South, West] -Robot : { x : I64, y : I64, direction : Direction } + move : Robot, Str -> Robot + move = |robot, instructions| { + instructions + .to_utf8() + .fold( + robot, + |{ x, y, direction }, command| { + match command { + 'R' => { + match direction { + North => { x, y, direction: East } + East => { x, y, direction: South } + South => { x, y, direction: West } + West => { x, y, direction: North } + } + } -create : { x ?? I64, y ?? I64, direction ?? Direction } -> Robot -create = |{ x ?? 0, y ?? 0, direction ?? North }| - { x, y, direction } + 'L' => { + match direction { + North => { x, y, direction: West } + East => { x, y, direction: North } + South => { x, y, direction: East } + West => { x, y, direction: South } + } + } -move : Robot, Str -> Robot -move = |robot, instructions| - instructions - |> Str.to_utf8 - |> List.walk( - robot, - |{ x, y, direction }, command| - when command is - 'R' -> - when direction is - North -> { x, y, direction: East } - East -> { x, y, direction: South } - South -> { x, y, direction: West } - West -> { x, y, direction: North } - - 'L' -> - when direction is - North -> { x, y, direction: West } - East -> { x, y, direction: North } - South -> { x, y, direction: East } - West -> { x, y, direction: South } - - 'A' -> - when direction is - North -> { x, y: y + 1, direction } - East -> { x: x + 1, y, direction } - South -> { x, y: y - 1, direction } - West -> { x: x - 1, y, direction } - - _ -> { x, y, direction }, - ) # invalid instructions are ignored + 'A' => { + match direction { + North => { x, y: y + 1, direction } + East => { x: x + 1, y, direction } + South => { x, y: y - 1, direction } + West => { x: x - 1, y, direction } + } + } + _ => { x, y, direction } + } + }, + ) + } +} diff --git a/exercises/practice/robot-simulator/.meta/plugins.py b/exercises/practice/robot-simulator/.meta/plugins.py index afb5b468..2615ad25 100644 --- a/exercises/practice/robot-simulator/.meta/plugins.py +++ b/exercises/practice/robot-simulator/.meta/plugins.py @@ -1,13 +1,5 @@ -def to_robot(robot, with_defaults): +def to_robot(robot): x = robot["position"]["x"] y = robot["position"]["y"] direction = robot["direction"] - fields = [] - if x != 0 or not with_defaults: - fields.append(f"x : {x}") - if y != 0 or not with_defaults: - fields.append(f"y : {y}") - if direction != "north" or not with_defaults: - fields.append(f"direction : {direction.capitalize()}") - content = ", ".join(fields) - return f"{{{content}}}" + return f"{{x : {x}, y : {y}, direction : {direction.capitalize()}}}" diff --git a/exercises/practice/robot-simulator/.meta/template.j2 b/exercises/practice/robot-simulator/.meta/template.j2 index 71c11f7b..2de4ca27 100644 --- a/exercises/practice/robot-simulator/.meta/template.j2 +++ b/exercises/practice/robot-simulator/.meta/template.j2 @@ -2,7 +2,7 @@ {{ macros.canonical_ref() }} {{ macros.header() }} -import {{ exercise | to_pascal }} exposing [create, move] +import {{ exercise | to_pascal }} exposing [move] {% for supercase in cases %} ## @@ -11,14 +11,13 @@ import {{ exercise | to_pascal }} exposing [create, move] {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect - {%- if case["input"]["instructions"] %} - robot = create({{ plugins.to_robot(case["input"], with_defaults=True) }}) - result = robot |> move({{ case["input"]["instructions"] | to_roc }}) - {%- else %} - result = create({{ plugins.to_robot(case["input"], with_defaults=True) }}) - {%- endif %} - result == {{ plugins.to_robot(case["expected"], with_defaults=False) }} +expect { + robot = {{ plugins.to_robot(case["input"]) }} + result = robot->move({{ case["input"]["instructions"] | to_roc }}) + result == {{ plugins.to_robot(case["expected"]) }} +} {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/robot-simulator/.meta/tests.toml b/exercises/practice/robot-simulator/.meta/tests.toml index 16da03d4..f2e52b19 100644 --- a/exercises/practice/robot-simulator/.meta/tests.toml +++ b/exercises/practice/robot-simulator/.meta/tests.toml @@ -11,9 +11,11 @@ [c557c16d-26c1-4e06-827c-f6602cd0785c] description = "Create robot -> at origin facing north" +include = false [bf0dffce-f11c-4cdb-8a5e-2c89d8a5a67d] description = "Create robot -> at negative position facing south" +include = false [8cbd0086-6392-4680-b9b9-73cf491e67e5] description = "Rotating clockwise -> changes north to east" diff --git a/exercises/practice/robot-simulator/RobotSimulator.roc b/exercises/practice/robot-simulator/RobotSimulator.roc index 24759203..0933626b 100644 --- a/exercises/practice/robot-simulator/RobotSimulator.roc +++ b/exercises/practice/robot-simulator/RobotSimulator.roc @@ -1,12 +1,9 @@ -module [create, move] +RobotSimulator :: {}.{ + Direction : [North, East, South, West] + Robot : { x : I64, y : I64, direction : Direction } -Direction : [North, East, South, West] -Robot : { x : I64, y : I64, direction : Direction } - -create : { x ?? I64, y ?? I64, direction ?? Direction } -> Robot -create = |{ x ?? 0, y ?? 0, direction ?? North }| - crash("Please implement the 'create' function") - -move : Robot, Str -> Robot -move = |robot, instructions| - crash("Please implement the 'move' function") + move : Robot, Str -> Robot + move = |robot, instructions| { + crash "Please implement the 'move' function" + } +} diff --git a/exercises/practice/robot-simulator/robot-simulator-test.roc b/exercises/practice/robot-simulator/robot-simulator-test.roc index bfc39ed5..1ac53bc8 100644 --- a/exercises/practice/robot-simulator/robot-simulator-test.roc +++ b/exercises/practice/robot-simulator/robot-simulator-test.roc @@ -1,140 +1,138 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/robot-simulator/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout +# File last updated on 2026-06-22 -main! = |_args| - Stdout.line!("") - -import RobotSimulator exposing [create, move] - -## -## Create robot -## - -# at origin facing north -expect - result = create({}) - result == { x: 0, y: 0, direction: North } - -# at negative position facing south -expect - result = create({ x: -1, y: -1, direction: South }) - result == { x: -1, y: -1, direction: South } +import RobotSimulator exposing [move] ## ## Rotating clockwise ## # changes north to east -expect - robot = create({}) - result = robot |> move("R") - result == { x: 0, y: 0, direction: East } +expect { + robot = { x: 0, y: 0, direction: North } + result = robot->move("R") + result == { x: 0, y: 0, direction: East } +} # changes east to south -expect - robot = create({ direction: East }) - result = robot |> move("R") - result == { x: 0, y: 0, direction: South } +expect { + robot = { x: 0, y: 0, direction: East } + result = robot->move("R") + result == { x: 0, y: 0, direction: South } +} # changes south to west -expect - robot = create({ direction: South }) - result = robot |> move("R") - result == { x: 0, y: 0, direction: West } +expect { + robot = { x: 0, y: 0, direction: South } + result = robot->move("R") + result == { x: 0, y: 0, direction: West } +} # changes west to north -expect - robot = create({ direction: West }) - result = robot |> move("R") - result == { x: 0, y: 0, direction: North } +expect { + robot = { x: 0, y: 0, direction: West } + result = robot->move("R") + result == { x: 0, y: 0, direction: North } +} ## ## Rotating counter-clockwise ## # changes north to west -expect - robot = create({}) - result = robot |> move("L") - result == { x: 0, y: 0, direction: West } +expect { + robot = { x: 0, y: 0, direction: North } + result = robot->move("L") + result == { x: 0, y: 0, direction: West } +} # changes west to south -expect - robot = create({ direction: West }) - result = robot |> move("L") - result == { x: 0, y: 0, direction: South } +expect { + robot = { x: 0, y: 0, direction: West } + result = robot->move("L") + result == { x: 0, y: 0, direction: South } +} # changes south to east -expect - robot = create({ direction: South }) - result = robot |> move("L") - result == { x: 0, y: 0, direction: East } +expect { + robot = { x: 0, y: 0, direction: South } + result = robot->move("L") + result == { x: 0, y: 0, direction: East } +} # changes east to north -expect - robot = create({ direction: East }) - result = robot |> move("L") - result == { x: 0, y: 0, direction: North } +expect { + robot = { x: 0, y: 0, direction: East } + result = robot->move("L") + result == { x: 0, y: 0, direction: North } +} ## ## Moving forward one ## # facing north increments Y -expect - robot = create({}) - result = robot |> move("A") - result == { x: 0, y: 1, direction: North } +expect { + robot = { x: 0, y: 0, direction: North } + result = robot->move("A") + result == { x: 0, y: 1, direction: North } +} # facing south decrements Y -expect - robot = create({ direction: South }) - result = robot |> move("A") - result == { x: 0, y: -1, direction: South } +expect { + robot = { x: 0, y: 0, direction: South } + result = robot->move("A") + result == { x: 0, y: -1, direction: South } +} # facing east increments X -expect - robot = create({ direction: East }) - result = robot |> move("A") - result == { x: 1, y: 0, direction: East } +expect { + robot = { x: 0, y: 0, direction: East } + result = robot->move("A") + result == { x: 1, y: 0, direction: East } +} # facing west decrements X -expect - robot = create({ direction: West }) - result = robot |> move("A") - result == { x: -1, y: 0, direction: West } +expect { + robot = { x: 0, y: 0, direction: West } + result = robot->move("A") + result == { x: -1, y: 0, direction: West } +} ## ## Follow series of instructions ## # moving east and north from README -expect - robot = create({ x: 7, y: 3 }) - result = robot |> move("RAALAL") - result == { x: 9, y: 4, direction: West } +expect { + robot = { x: 7, y: 3, direction: North } + result = robot->move("RAALAL") + result == { x: 9, y: 4, direction: West } +} # moving west and north -expect - robot = create({}) - result = robot |> move("LAAARALA") - result == { x: -4, y: 1, direction: West } +expect { + robot = { x: 0, y: 0, direction: North } + result = robot->move("LAAARALA") + result == { x: -4, y: 1, direction: West } +} # moving west and south -expect - robot = create({ x: 2, y: -7, direction: East }) - result = robot |> move("RRAAAAALA") - result == { x: -3, y: -8, direction: South } +expect { + robot = { x: 2, y: -7, direction: East } + result = robot->move("RRAAAAALA") + result == { x: -3, y: -8, direction: South } +} # moving east and north -expect - robot = create({ x: 8, y: 4, direction: South }) - result = robot |> move("LAAARRRALLLL") - result == { x: 11, y: 5, direction: North } +expect { + robot = { x: 8, y: 4, direction: South } + result = robot->move("LAAARRRALLLL") + result == { x: 11, y: 5, direction: North } +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/roman-numerals/.meta/Example.roc b/exercises/practice/roman-numerals/.meta/Example.roc index d7096008..d716ef78 100644 --- a/exercises/practice/roman-numerals/.meta/Example.roc +++ b/exercises/practice/roman-numerals/.meta/Example.roc @@ -1,17 +1,27 @@ -module [roman] - -roman : U64 -> Result Str [InvalidNumber U64] -roman = |number| - if number == 0 or number > 3999 then - Err(InvalidNumber(number)) - else - convert = |digit, x1, x5, x10| - when digit is - 4 -> "${x1}${x5}" - 9 -> "${x1}${x10}" - n -> if n <= 3 then Str.repeat(x1, n) else "${x5}${Str.repeat(x1, (n - 5))}" - thousands = convert((number // 1000), "M", "?", "?") - hundreds = convert((number % 1000 // 100), "C", "D", "M") - tens = convert((number % 100 // 10), "X", "L", "C") - units = convert((number % 10), "I", "V", "X") - Ok("${thousands}${hundreds}${tens}${units}") +RomanNumerals :: {}.{ + roman : U64 -> Try(Str, [InvalidNumber(U64)]) + roman = |number| { + if number == 0 or number > 3999 { + Err(InvalidNumber(number)) + } else { + convert = |digit, x1, x5, x10| { + match digit { + 4 => "${x1}${x5}" + 9 => "${x1}${x10}" + n => { + if n <= 3 { + x1.repeat(n) + } else { + "${x5}${x1.repeat(n - 5)}" + } + } + } + } + thousands = convert(number // 1000, "M", "?", "?") + hundreds = convert((number % 1000) // 100, "C", "D", "M") + tens = convert((number % 100) // 10, "X", "L", "C") + units = convert(number % 10, "I", "V", "X") + Ok("${thousands}${hundreds}${tens}${units}") + } + } +} diff --git a/exercises/practice/roman-numerals/.meta/template.j2 b/exercises/practice/roman-numerals/.meta/template.j2 index d2fb0f03..84470893 100644 --- a/exercises/practice/roman-numerals/.meta/template.j2 +++ b/exercises/practice/roman-numerals/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["number"] | to_roc }}) result == Ok({{ case["expected"] | to_roc }}) +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/roman-numerals/RomanNumerals.roc b/exercises/practice/roman-numerals/RomanNumerals.roc index 4aba0410..63a10483 100644 --- a/exercises/practice/roman-numerals/RomanNumerals.roc +++ b/exercises/practice/roman-numerals/RomanNumerals.roc @@ -1,5 +1,6 @@ -module [roman] - -roman : U64 -> Result Str _ -roman = |number| - crash("Please implement the 'roman' function") +RomanNumerals :: {}.{ + roman : U64 -> Try(Str, [InvalidNumber(U64)]) + roman = |number| { + crash "Please implement the 'roman' function" + } +} diff --git a/exercises/practice/roman-numerals/roman-numerals-test.roc b/exercises/practice/roman-numerals/roman-numerals-test.roc index 7df552a3..d68ccf4c 100644 --- a/exercises/practice/roman-numerals/roman-numerals-test.roc +++ b/exercises/practice/roman-numerals/roman-numerals-test.roc @@ -1,149 +1,172 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/roman-numerals/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import RomanNumerals exposing [roman] # 1 is I -expect - result = roman(1) - result == Ok("I") +expect { + result = roman(1) + result == Ok("I") +} # 2 is II -expect - result = roman(2) - result == Ok("II") +expect { + result = roman(2) + result == Ok("II") +} # 3 is III -expect - result = roman(3) - result == Ok("III") +expect { + result = roman(3) + result == Ok("III") +} # 4 is IV -expect - result = roman(4) - result == Ok("IV") +expect { + result = roman(4) + result == Ok("IV") +} # 5 is V -expect - result = roman(5) - result == Ok("V") +expect { + result = roman(5) + result == Ok("V") +} # 6 is VI -expect - result = roman(6) - result == Ok("VI") +expect { + result = roman(6) + result == Ok("VI") +} # 9 is IX -expect - result = roman(9) - result == Ok("IX") +expect { + result = roman(9) + result == Ok("IX") +} # 16 is XVI -expect - result = roman(16) - result == Ok("XVI") +expect { + result = roman(16) + result == Ok("XVI") +} # 27 is XXVII -expect - result = roman(27) - result == Ok("XXVII") +expect { + result = roman(27) + result == Ok("XXVII") +} # 48 is XLVIII -expect - result = roman(48) - result == Ok("XLVIII") +expect { + result = roman(48) + result == Ok("XLVIII") +} # 49 is XLIX -expect - result = roman(49) - result == Ok("XLIX") +expect { + result = roman(49) + result == Ok("XLIX") +} # 59 is LIX -expect - result = roman(59) - result == Ok("LIX") +expect { + result = roman(59) + result == Ok("LIX") +} # 66 is LXVI -expect - result = roman(66) - result == Ok("LXVI") +expect { + result = roman(66) + result == Ok("LXVI") +} # 93 is XCIII -expect - result = roman(93) - result == Ok("XCIII") +expect { + result = roman(93) + result == Ok("XCIII") +} # 141 is CXLI -expect - result = roman(141) - result == Ok("CXLI") +expect { + result = roman(141) + result == Ok("CXLI") +} # 163 is CLXIII -expect - result = roman(163) - result == Ok("CLXIII") +expect { + result = roman(163) + result == Ok("CLXIII") +} # 166 is CLXVI -expect - result = roman(166) - result == Ok("CLXVI") +expect { + result = roman(166) + result == Ok("CLXVI") +} # 402 is CDII -expect - result = roman(402) - result == Ok("CDII") +expect { + result = roman(402) + result == Ok("CDII") +} # 575 is DLXXV -expect - result = roman(575) - result == Ok("DLXXV") +expect { + result = roman(575) + result == Ok("DLXXV") +} # 666 is DCLXVI -expect - result = roman(666) - result == Ok("DCLXVI") +expect { + result = roman(666) + result == Ok("DCLXVI") +} # 911 is CMXI -expect - result = roman(911) - result == Ok("CMXI") +expect { + result = roman(911) + result == Ok("CMXI") +} # 1024 is MXXIV -expect - result = roman(1024) - result == Ok("MXXIV") +expect { + result = roman(1024) + result == Ok("MXXIV") +} # 1666 is MDCLXVI -expect - result = roman(1666) - result == Ok("MDCLXVI") +expect { + result = roman(1666) + result == Ok("MDCLXVI") +} # 3000 is MMM -expect - result = roman(3000) - result == Ok("MMM") +expect { + result = roman(3000) + result == Ok("MMM") +} # 3001 is MMMI -expect - result = roman(3001) - result == Ok("MMMI") +expect { + result = roman(3001) + result == Ok("MMMI") +} # 3888 is MMMDCCCLXXXVIII -expect - result = roman(3888) - result == Ok("MMMDCCCLXXXVIII") +expect { + result = roman(3888) + result == Ok("MMMDCCCLXXXVIII") +} # 3999 is MMMCMXCIX -expect - result = roman(3999) - result == Ok("MMMCMXCIX") +expect { + result = roman(3999) + result == Ok("MMMCMXCIX") +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/rotational-cipher/.meta/Example.roc b/exercises/practice/rotational-cipher/.meta/Example.roc index 509a9d72..9f564ebc 100644 --- a/exercises/practice/rotational-cipher/.meta/Example.roc +++ b/exercises/practice/rotational-cipher/.meta/Example.roc @@ -1,17 +1,22 @@ -module [rotate] +RotationalCipher :: {}.{ + rotate : Str, U8 -> Str + rotate = |text, shift_key| { + text + .to_utf8() + .map( + |c| shift_char(c, shift_key), + ) + ->Str.from_utf8() + ?? "Unreachable" + } +} -shift_char = |c, shift_key| - if c >= 'a' and c <= 'z' then - (c - 'a' + shift_key) % 26 + 'a' - else if c >= 'A' and c <= 'Z' then - (c - 'A' + shift_key) % 26 + 'A' - else - c - -rotate : Str, U8 -> Str -rotate = |text, shift_key| - text - |> Str.to_utf8 - |> List.map(|c| shift_char(c, shift_key)) - |> Str.from_utf8 - |> Result.with_default("Unreachable") +shift_char = |c, shift_key| { + if c >= 'a' and c <= 'z' { + (c - 'a' + shift_key) % 26 + 'a' + } else if c >= 'A' and c <= 'Z' { + (c - 'A' + shift_key) % 26 + 'A' + } else { + c + } +} diff --git a/exercises/practice/rotational-cipher/.meta/template.j2 b/exercises/practice/rotational-cipher/.meta/template.j2 index 6fb226c6..519c882d 100644 --- a/exercises/practice/rotational-cipher/.meta/template.j2 +++ b/exercises/practice/rotational-cipher/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [rotate] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["text"] | to_roc }}, {{ case["input"]["shiftKey"] }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/rotational-cipher/RotationalCipher.roc b/exercises/practice/rotational-cipher/RotationalCipher.roc index fec5b98f..27dc6264 100644 --- a/exercises/practice/rotational-cipher/RotationalCipher.roc +++ b/exercises/practice/rotational-cipher/RotationalCipher.roc @@ -1,5 +1,6 @@ -module [rotate] - -rotate : Str, U8 -> Str -rotate = |text, shift_key| - crash("Please implement the 'rotate' function") +RotationalCipher :: {}.{ + rotate : Str, U8 -> Str + rotate = |text, shift_key| { + crash "Please implement the 'rotate' function" + } +} diff --git a/exercises/practice/rotational-cipher/rotational-cipher-test.roc b/exercises/practice/rotational-cipher/rotational-cipher-test.roc index 73dce2b0..f2942e87 100644 --- a/exercises/practice/rotational-cipher/rotational-cipher-test.roc +++ b/exercises/practice/rotational-cipher/rotational-cipher-test.roc @@ -1,64 +1,70 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/rotational-cipher/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import RotationalCipher exposing [rotate] # rotate a by 0, same output as input -expect - result = rotate("a", 0) - result == "a" +expect { + result = rotate("a", 0) + result == "a" +} # rotate a by 1 -expect - result = rotate("a", 1) - result == "b" +expect { + result = rotate("a", 1) + result == "b" +} # rotate a by 26, same output as input -expect - result = rotate("a", 26) - result == "a" +expect { + result = rotate("a", 26) + result == "a" +} # rotate m by 13 -expect - result = rotate("m", 13) - result == "z" +expect { + result = rotate("m", 13) + result == "z" +} # rotate n by 13 with wrap around alphabet -expect - result = rotate("n", 13) - result == "a" +expect { + result = rotate("n", 13) + result == "a" +} # rotate capital letters -expect - result = rotate("OMG", 5) - result == "TRL" +expect { + result = rotate("OMG", 5) + result == "TRL" +} # rotate spaces -expect - result = rotate("O M G", 5) - result == "T R L" +expect { + result = rotate("O M G", 5) + result == "T R L" +} # rotate numbers -expect - result = rotate("Testing 1 2 3 testing", 4) - result == "Xiwxmrk 1 2 3 xiwxmrk" +expect { + result = rotate("Testing 1 2 3 testing", 4) + result == "Xiwxmrk 1 2 3 xiwxmrk" +} # rotate punctuation -expect - result = rotate("Let's eat, Grandma!", 21) - result == "Gzo'n zvo, Bmviyhv!" +expect { + result = rotate("Let's eat, Grandma!", 21) + result == "Gzo'n zvo, Bmviyhv!" +} # rotate all letters -expect - result = rotate("The quick brown fox jumps over the lazy dog.", 13) - result == "Gur dhvpx oebja sbk whzcf bire gur ynml qbt." +expect { + result = rotate("The quick brown fox jumps over the lazy dog.", 13) + result == "Gur dhvpx oebja sbk whzcf bire gur ynml qbt." +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/run-length-encoding/.meta/Example.roc b/exercises/practice/run-length-encoding/.meta/Example.roc index 736cb6ff..a97b1629 100644 --- a/exercises/practice/run-length-encoding/.meta/Example.roc +++ b/exercises/practice/run-length-encoding/.meta/Example.roc @@ -1,50 +1,80 @@ -module [encode, decode] +RunLengthEncoding :: {}.{ + encode : Str -> Try(Str, [BadUtf8(_), ..]) + encode = |string| { + append_count_and_letter = |state| { + match state.count { + 0 => [] + 1 => state.chars.append(state.last_char) + _ => { + digits = state.count.to_str().to_utf8() + state.chars.concat(digits).append(state.last_char) + } + } + } -encode : Str -> Result Str [BadUtf8 _] -encode = |string| - append_count_and_letter = |state| - if state.count == 0 then - [] - else if state.count == 1 then - state.chars |> List.append(state.last_char) - else - digits = state.count |> Num.to_str |> Str.to_utf8 - state.chars |> List.concat(digits) |> List.append(state.last_char) + string + .to_utf8() + .fold( + { chars: [], last_char: 0.U8, count: 0.U64 }, + |state, char| { + if state.count == 0 { + { chars: [], last_char: char, count: 1 } + } else if state.last_char == char { + { ..state, count: state.count + 1 } + } else { + chars = append_count_and_letter(state) + { chars, last_char: char, count: 1 } + } + }, + ) + ->append_count_and_letter() + ->Str.from_utf8() + } - string - |> Str.to_utf8 - |> List.walk( - { chars: [], last_char: 0, count: 0 }, - |state, char| - if state.count == 0 then - { chars: [], last_char: char, count: 1 } - else if state.last_char == char then - { state & count: state.count + 1 } - else - chars = append_count_and_letter(state) - { chars, last_char: char, count: 1 }, - ) - |> append_count_and_letter - |> Str.from_utf8 + decode : Str -> Try(Str, [BadUtf8(_), BadNumStr, ..]) + decode = |string| { + state_to_str = |state| state.chars->Str.from_utf8() + string + .to_utf8() + ->fold_try( + { chars: [], digits: [] }, + |state, char| { + if char >= '0' and char <= '9' { + digits = state.digits.append(char) + Ok({ ..state, digits }) + } else if state.digits == [] { + chars = state.chars.append(char) + Ok({ ..state, chars }) + } else { + count_str = state.digits->Str.from_utf8()? + count = count_str->U64.from_str()? + chars = state.chars.concat(char->List.repeat(count)) + Ok({ chars, digits: [] }) + } + }, + )? + ->state_to_str() -decode : Str -> Result Str [BadUtf8 _, InvalidNumStr] -decode = |string| - string - |> Str.to_utf8 - |> List.walk_try( - { chars: [], digits: [] }, - |state, char| - if char >= '0' and char <= '9' then - digits = state.digits |> List.append(char) - Ok({ state & digits }) - else if state.digits == [] then - chars = state.chars |> List.append(char) - Ok({ state & chars }) - else - count_str = Str.from_utf8(state.digits)? - count = Str.to_u64(count_str)? - chars = state.chars |> List.concat(List.repeat(char, count)) - Ok({ chars, digits: [] }), - )? - |> .chars - |> Str.from_utf8 + } +} + +# The following function should soon be available in Roc's builtins +fold_try : List(a), b, (b, a -> Try(b, err)) -> Try(b, err) +fold_try = |list, init, func| { + list.fold_until( + Ok(init), + |state, item| { + match state { + Ok(internal_state) => { + match func(internal_state, item) { + Ok(new_state) => Continue(Ok(new_state)) + Err(final_err) => Break(Err(final_err)) + } + } + Err(_) => { + crash "Unreachable" + } + } + }, + ) +} diff --git a/exercises/practice/run-length-encoding/.meta/template.j2 b/exercises/practice/run-length-encoding/.meta/template.j2 index 61c5c793..a6afb823 100644 --- a/exercises/practice/run-length-encoding/.meta/template.j2 +++ b/exercises/practice/run-length-encoding/.meta/template.j2 @@ -11,17 +11,20 @@ import {{ exercise | to_pascal }} exposing [encode, decode] {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect +expect { string = {{ case["input"]["string"] | to_roc }} {%- if case["property"] == "consistency" %} - result = string |> encode |> Result.try(decode) + encoded = string -> encode()? + result = encoded -> decode() result == Ok(string) -{%- else %} - result = string |> {{ case["property"] | to_snake }} + {%- else %} + result = string -> {{ case["property"] | to_snake }} expected = {{ case["expected"] | to_roc }} result == Ok(expected) -{%- endif %} + {%- endif %} +} {% endfor %} {% endfor %} +{{ macros.footer() }} diff --git a/exercises/practice/run-length-encoding/RunLengthEncoding.roc b/exercises/practice/run-length-encoding/RunLengthEncoding.roc index 3a239a66..8bf6372a 100644 --- a/exercises/practice/run-length-encoding/RunLengthEncoding.roc +++ b/exercises/practice/run-length-encoding/RunLengthEncoding.roc @@ -1,9 +1,11 @@ -module [encode, decode] +RunLengthEncoding :: {}.{ + encode : Str -> Try(Str, _) + encode = |string| { + crash "Please implement the 'encode' function" + } -encode : Str -> Result Str _ -encode = |string| - crash("Please implement the 'encode' function") - -decode : Str -> Result Str _ -decode = |string| - crash("Please implement the 'decode' function") + decode : Str -> Try(Str, _) + decode = |string| { + crash "Please implement the 'decode' function" + } +} 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 1b23ddbc..7ff16856 100644 --- a/exercises/practice/run-length-encoding/run-length-encoding-test.roc +++ b/exercises/practice/run-length-encoding/run-length-encoding-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/run-length-encoding/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import RunLengthEncoding exposing [encode, decode] @@ -17,100 +9,118 @@ import RunLengthEncoding exposing [encode, decode] ## # empty string -expect - string = "" - result = string |> encode - expected = "" - result == Ok(expected) +expect { + string = "" + result = string->encode() + expected = "" + result == Ok(expected) +} # single characters only are encoded without count -expect - string = "XYZ" - result = string |> encode - expected = "XYZ" - result == Ok(expected) +expect { + string = "XYZ" + result = string->encode() + expected = "XYZ" + result == Ok(expected) +} # string with no single characters -expect - string = "AABBBCCCC" - result = string |> encode - expected = "2A3B4C" - result == Ok(expected) +expect { + string = "AABBBCCCC" + result = string->encode() + expected = "2A3B4C" + result == Ok(expected) +} # single characters mixed with repeated characters -expect - string = "WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWB" - result = string |> encode - expected = "12WB12W3B24WB" - result == Ok(expected) +expect { + string = "WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWB" + result = string->encode() + expected = "12WB12W3B24WB" + result == Ok(expected) +} # multiple whitespace mixed in string -expect - string = " hsqq qww " - result = string |> encode - expected = "2 hs2q q2w2 " - result == Ok(expected) +expect { + string = " hsqq qww " + result = string->encode() + expected = "2 hs2q q2w2 " + result == Ok(expected) +} # lowercase characters -expect - string = "aabbbcccc" - result = string |> encode - expected = "2a3b4c" - result == Ok(expected) +expect { + string = "aabbbcccc" + result = string->encode() + expected = "2a3b4c" + result == Ok(expected) +} ## ## run-length decode a string ## # empty string -expect - string = "" - result = string |> decode - expected = "" - result == Ok(expected) +expect { + string = "" + result = string->decode() + expected = "" + result == Ok(expected) +} # single characters only -expect - string = "XYZ" - result = string |> decode - expected = "XYZ" - result == Ok(expected) +expect { + string = "XYZ" + result = string->decode() + expected = "XYZ" + result == Ok(expected) +} # string with no single characters -expect - string = "2A3B4C" - result = string |> decode - expected = "AABBBCCCC" - result == Ok(expected) +expect { + string = "2A3B4C" + result = string->decode() + expected = "AABBBCCCC" + result == Ok(expected) +} # single characters with repeated characters -expect - string = "12WB12W3B24WB" - result = string |> decode - expected = "WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWB" - result == Ok(expected) +expect { + string = "12WB12W3B24WB" + result = string->decode() + expected = "WWWWWWWWWWWWBWWWWWWWWWWWWBBBWWWWWWWWWWWWWWWWWWWWWWWWB" + result == Ok(expected) +} # multiple whitespace mixed in string -expect - string = "2 hs2q q2w2 " - result = string |> decode - expected = " hsqq qww " - result == Ok(expected) +expect { + string = "2 hs2q q2w2 " + result = string->decode() + expected = " hsqq qww " + result == Ok(expected) +} # lowercase string -expect - string = "2a3b4c" - result = string |> decode - expected = "aabbbcccc" - result == Ok(expected) +expect { + string = "2a3b4c" + result = string->decode() + expected = "aabbbcccc" + result == Ok(expected) +} ## ## encode and then decode ## # encode followed by decode gives original string -expect - string = "zzz ZZ zZ" - result = string |> encode |> Result.try(decode) - result == Ok(string) +expect { + string = "zzz ZZ zZ" + encoded = string->encode()? + result = encoded->decode() + result == Ok(string) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/saddle-points/.meta/Example.roc b/exercises/practice/saddle-points/.meta/Example.roc index 7beb8c81..99f88bd0 100644 --- a/exercises/practice/saddle-points/.meta/Example.roc +++ b/exercises/practice/saddle-points/.meta/Example.roc @@ -1,46 +1,82 @@ -module [saddle_points] +SaddlePoints :: {}.{ + Forest : List(List(U8)) + Position : { row : U64, column : U64 } -Forest : List (List U8) -Position : { row : U64, column : U64 } + saddle_points : Forest -> Set(Position) + saddle_points = |tree_heights| { + tallest_trees_east_west : Set(Position) + tallest_trees_east_west = { + tree_heights + .map_with_index( + |row, row_index| { + max_in_row : U8 + max_in_row = row.max() ?? 0.U8 + row.map_with_index( + |height, column_index| { + if height == max_in_row + [{ row: row_index + 1, column: column_index + 1 }] + else + [] + }, + ) + ->join_map(|id| id) # TODO: replace with .join() when available + }, + ) + ->join_map(|id2| id2) # TODO: replace with .join() when available + ->Set.from_list() + } -saddle_points : Forest -> Set Position -saddle_points = |tree_heights| - tallest_trees_east_west = - tree_heights - |> List.map_with_index( - |row, row_index| - max_in_row = row |> List.max |> Result.with_default(0) - row - |> List.map_with_index( - |height, column_index| - if height == max_in_row then [{ row: row_index + 1, column: column_index + 1 }] else [], - ) - |> List.join, - ) - |> List.join - |> Set.from_list + num_columns : U64 + num_columns = tree_heights.map(List.len).max() ?? 0 - num_columns = tree_heights |> List.map(List.len) |> List.max |> Result.with_default(0) - smallest_trees_north_south = - List.range({ start: At(0), end: Before(num_columns) }) - |> List.map( - |column_index| - column = - tree_heights - |> List.map_with_index( - |row, row_index| - row - |> List.get(column_index)? - |> |height| Ok({ height, row_index }), - ) - |> List.keep_oks(|id| id) + smallest_trees_north_south : Set(Position) + smallest_trees_north_south = + (0..join_map( + |column_index| { + column : List({ height : U8, row_index : U64 }) + column = + tree_heights + .map_with_index( + |row, row_index| { + height = row.get(column_index)? + Ok({ height, row_index }) + }, + ) + ->keep_oks(|id| id) - min_in_column = column |> List.map(.height) |> List.min |> Result.with_default(0) - column - |> List.keep_if(|{ height }| height == min_in_column) - |> List.map(|{ row_index }| { row: row_index + 1, column: column_index + 1 }), - ) - |> List.join_map(|id| id) - |> Set.from_list + min_in_column : U8 + min_in_column = column.map(|c| c.height).min() ?? 0.U8 + column + .keep_if(|{ height, row_index: _ }| height == min_in_column) + .map(|{ height: _, row_index }| { row: row_index + 1, column: column_index + 1 }) + }, + ) + ->Set.from_list() - tallest_trees_east_west |> Set.intersection(smallest_trees_north_south) + tallest_trees_east_west.intersection(smallest_trees_north_south) + } +} + +# The following functions should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} + +keep_oks = |iter, func| { + iter + ->join_map( + |item| { + match func(item) { + Ok(result) => [result] + Err(_) => [] + } + }, + ) +} diff --git a/exercises/practice/saddle-points/.meta/template.j2 b/exercises/practice/saddle-points/.meta/template.j2 index f8b9d3ad..f9cc4d5a 100644 --- a/exercises/practice/saddle-points/.meta/template.j2 +++ b/exercises/practice/saddle-points/.meta/template.j2 @@ -6,18 +6,21 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { tree_heights = [ {%- for row in case["input"]["matrix"] %} {{ row | to_roc }}, {%- endfor %} ] - result = tree_heights |> {{ case["property"] | to_snake }} + result = tree_heights -> {{ case["property"] | to_snake }} expected = Set.from_list([ {%- for tree in case["expected"] %} {{ tree | to_roc }}, {%- endfor %} ]) result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/saddle-points/SaddlePoints.roc b/exercises/practice/saddle-points/SaddlePoints.roc index d534dae6..db8a0d4b 100644 --- a/exercises/practice/saddle-points/SaddlePoints.roc +++ b/exercises/practice/saddle-points/SaddlePoints.roc @@ -1,8 +1,9 @@ -module [saddle_points] +SaddlePoints :: {}.{ + Forest : List(List(U8)) + Position : { row : U64, column : U64 } -Forest : List (List U8) -Position : { row : U64, column : U64 } - -saddle_points : Forest -> Set Position -saddle_points = |tree_heights| - crash("Please implement the 'saddle_points' function") + saddle_points : Forest -> Set(Position) + saddle_points = |tree_heights| { + crash "Please implement the 'saddle_points' function" + } +} diff --git a/exercises/practice/saddle-points/saddle-points-test.roc b/exercises/practice/saddle-points/saddle-points-test.roc index a3cee084..f3e7287f 100644 --- a/exercises/practice/saddle-points/saddle-points-test.roc +++ b/exercises/practice/saddle-points/saddle-points-test.roc @@ -1,150 +1,153 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/saddle-points/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import SaddlePoints exposing [saddle_points] # Can identify single saddle point -expect - tree_heights = [ - [9, 8, 7], - [5, 3, 2], - [6, 6, 7], - ] - result = tree_heights |> saddle_points - expected = Set.from_list( - [ - { row: 2, column: 1 }, - ], - ) - result == expected +expect { + tree_heights = [ + [9, 8, 7], + [5, 3, 2], + [6, 6, 7], + ] + result = tree_heights->saddle_points() + expected = Set.from_list( + [ + { row: 2, column: 1 }, + ], + ) + result == expected +} # Can identify that empty matrix has no saddle points -expect - tree_heights = [ - [], - ] - result = tree_heights |> saddle_points - expected = Set.from_list( - [ - ], - ) - result == expected +expect { + tree_heights = [ + [], + ] + result = tree_heights->saddle_points() + expected = Set.from_list( + [], + ) + result == expected +} # Can identify lack of saddle points when there are none -expect - tree_heights = [ - [1, 2, 3], - [3, 1, 2], - [2, 3, 1], - ] - result = tree_heights |> saddle_points - expected = Set.from_list( - [ - ], - ) - result == expected +expect { + tree_heights = [ + [1, 2, 3], + [3, 1, 2], + [2, 3, 1], + ] + result = tree_heights->saddle_points() + expected = Set.from_list( + [], + ) + result == expected +} # Can identify multiple saddle points in a column -expect - tree_heights = [ - [4, 5, 4], - [3, 5, 5], - [1, 5, 4], - ] - result = tree_heights |> saddle_points - expected = Set.from_list( - [ - { row: 1, column: 2 }, - { row: 2, column: 2 }, - { row: 3, column: 2 }, - ], - ) - result == expected +expect { + tree_heights = [ + [4, 5, 4], + [3, 5, 5], + [1, 5, 4], + ] + result = tree_heights->saddle_points() + expected = Set.from_list( + [ + { row: 1, column: 2 }, + { row: 2, column: 2 }, + { row: 3, column: 2 }, + ], + ) + result == expected +} # Can identify multiple saddle points in a row -expect - tree_heights = [ - [6, 7, 8], - [5, 5, 5], - [7, 5, 6], - ] - result = tree_heights |> saddle_points - expected = Set.from_list( - [ - { row: 2, column: 1 }, - { row: 2, column: 2 }, - { row: 2, column: 3 }, - ], - ) - result == expected +expect { + tree_heights = [ + [6, 7, 8], + [5, 5, 5], + [7, 5, 6], + ] + result = tree_heights->saddle_points() + expected = Set.from_list( + [ + { row: 2, column: 1 }, + { row: 2, column: 2 }, + { row: 2, column: 3 }, + ], + ) + result == expected +} # Can identify saddle point in bottom right corner -expect - tree_heights = [ - [8, 7, 9], - [6, 7, 6], - [3, 2, 5], - ] - result = tree_heights |> saddle_points - expected = Set.from_list( - [ - { row: 3, column: 3 }, - ], - ) - result == expected +expect { + tree_heights = [ + [8, 7, 9], + [6, 7, 6], + [3, 2, 5], + ] + result = tree_heights->saddle_points() + expected = Set.from_list( + [ + { row: 3, column: 3 }, + ], + ) + result == expected +} # Can identify saddle points in a non square matrix -expect - tree_heights = [ - [3, 1, 3], - [3, 2, 4], - ] - result = tree_heights |> saddle_points - expected = Set.from_list( - [ - { row: 1, column: 3 }, - { row: 1, column: 1 }, - ], - ) - result == expected +expect { + tree_heights = [ + [3, 1, 3], + [3, 2, 4], + ] + result = tree_heights->saddle_points() + expected = Set.from_list( + [ + { row: 1, column: 3 }, + { row: 1, column: 1 }, + ], + ) + result == expected +} # Can identify that saddle points in a single column matrix are those with the minimum value -expect - tree_heights = [ - [2], - [1], - [4], - [1], - ] - result = tree_heights |> saddle_points - expected = Set.from_list( - [ - { row: 2, column: 1 }, - { row: 4, column: 1 }, - ], - ) - result == expected +expect { + tree_heights = [ + [2], + [1], + [4], + [1], + ] + result = tree_heights->saddle_points() + expected = Set.from_list( + [ + { row: 2, column: 1 }, + { row: 4, column: 1 }, + ], + ) + result == expected +} # Can identify that saddle points in a single row matrix are those with the maximum value -expect - tree_heights = [ - [2, 5, 3, 5], - ] - result = tree_heights |> saddle_points - expected = Set.from_list( - [ - { row: 1, column: 2 }, - { row: 1, column: 4 }, - ], - ) - result == expected +expect { + tree_heights = [ + [2, 5, 3, 5], + ] + result = tree_heights->saddle_points() + expected = Set.from_list( + [ + { row: 1, column: 2 }, + { row: 1, column: 4 }, + ], + ) + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/say/.meta/Example.roc b/exercises/practice/say/.meta/Example.roc index 5e304842..cb5923c8 100644 --- a/exercises/practice/say/.meta/Example.roc +++ b/exercises/practice/say/.meta/Example.roc @@ -1,70 +1,108 @@ -module [say] +Say :: {}.{ + say : U64 -> Try(Str, [OutOfBounds]) + say = |number| { + if number < 20 { + Ok(zero_to_nineteen.get(number)?) + } else if number < 100 { + tens_word = tens_after_ten.get(number // 10 - 2)? + digit = number % 10 + if digit > 0 { + digit_word = say(digit)? + Ok("${tens_word}-${digit_word}") + } else { + Ok(tens_word) + } + } else if number < 1_000_000_000_000 { + words = + [ + (1_000_000_000_000, 1_000_000_000, "billion"), + (1_000_000_000, 1_000_000, "million"), + (1_000_000, 1000, "thousand"), + (1000, 100, "hundred"), + (100, 1, ""), + ] + ->keep_oks( + |triple| { + (modulo, divisor, name) = triple + how_many = (number % modulo) // divisor + if how_many == 0 { + Err(NothingToSay) + } else { + say_how_many = say(how_many).map_err(|_| NothingToSay)? + Ok("${say_how_many} ${name}") + } + }, + ) + ->Str.join_with(" ") + .trim_end() + Ok(words) + } else { + Err(OutOfBounds) + } + } +} zero_to_nineteen = [ - "zero", - "one", - "two", - "three", - "four", - "five", - "six", - "seven", - "eight", - "nine", - "ten", - "eleven", - "twelve", - "thirteen", - "fourteen", - "fifteen", - "sixteen", - "seventeen", - "eighteen", - "nineteen", + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", + "twelve", + "thirteen", + "fourteen", + "fifteen", + "sixteen", + "seventeen", + "eighteen", + "nineteen", ] tens_after_ten = [ - "twenty", - "thirty", - "forty", - "fifty", - "sixty", - "seventy", - "eighty", - "ninety", + "twenty", + "thirty", + "forty", + "fifty", + "sixty", + "seventy", + "eighty", + "ninety", ] -say : U64 -> Result Str [OutOfBounds] -say = |number| - if number < 20 then - zero_to_nineteen |> List.get(number)? |> Ok - else if number < 100 then - tens_word = tens_after_ten |> List.get(number // 10 - 2)? - digit = number % 10 - if digit > 0 then - digit_word = say(digit)? - Ok("${tens_word}-${digit_word}") - else - Ok(tens_word) - else if number < 1_000_000_000_000 then - [ - (1_000_000_000_000, 1_000_000_000, "billion"), - (1_000_000_000, 1_000_000, "million"), - (1_000_000, 1000, "thousand"), - (1000, 100, "hundred"), - (100, 1, ""), - ] - |> List.keep_oks( - |(modulo, divisor, name)| - how_many = (number % modulo) // divisor - if how_many == 0 then - Err(NothingToSay) - else - say_how_many = say(how_many)? - Ok("${say_how_many} ${name}"), - ) - |> Str.join_with(" ") - |> Str.trim_end - |> Ok - else - Err(OutOfBounds) +# The following functions should soon be available in Roc's builtins +keep_oks = |iter, func| { + iter + ->join_map( + |item| { + match func(item) { + Ok(result) => [result] + Err(_) => [] + } + }, + ) +} + +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} + +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/say/.meta/template.j2 b/exercises/practice/say/.meta/template.j2 index 2e8c177e..959b3aba 100644 --- a/exercises/practice/say/.meta/template.j2 +++ b/exercises/practice/say/.meta/template.j2 @@ -6,12 +6,15 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["number"] | to_roc }}) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} result == Ok({{ case["expected"] | to_roc }}) {% endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/say/Say.roc b/exercises/practice/say/Say.roc index 1995e45d..b4dfcd80 100644 --- a/exercises/practice/say/Say.roc +++ b/exercises/practice/say/Say.roc @@ -1,5 +1,6 @@ -module [say] - -say : U64 -> Result Str [OutOfBounds] -say = |number| - crash("Please implement the 'say' function") +Say :: {}.{ + say : U64 -> Try(Str, _) + say = |number| { + crash "Please implement the 'say' function" + } +} diff --git a/exercises/practice/say/say-test.roc b/exercises/practice/say/say-test.roc index 4bbaa23e..22f38c2b 100644 --- a/exercises/practice/say/say-test.roc +++ b/exercises/practice/say/say-test.roc @@ -1,104 +1,135 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/say/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Say exposing [say] # zero -expect - result = say(0) - result == Ok("zero") +expect { + result = say(0) + result == Ok("zero") + +} # one -expect - result = say(1) - result == Ok("one") +expect { + result = say(1) + result == Ok("one") + +} # fourteen -expect - result = say(14) - result == Ok("fourteen") +expect { + result = say(14) + result == Ok("fourteen") + +} # twenty -expect - result = say(20) - result == Ok("twenty") +expect { + result = say(20) + result == Ok("twenty") + +} # twenty-two -expect - result = say(22) - result == Ok("twenty-two") +expect { + result = say(22) + result == Ok("twenty-two") + +} # thirty -expect - result = say(30) - result == Ok("thirty") +expect { + result = say(30) + result == Ok("thirty") + +} # ninety-nine -expect - result = say(99) - result == Ok("ninety-nine") +expect { + result = say(99) + result == Ok("ninety-nine") + +} # one hundred -expect - result = say(100) - result == Ok("one hundred") +expect { + result = say(100) + result == Ok("one hundred") + +} # one hundred twenty-three -expect - result = say(123) - result == Ok("one hundred twenty-three") +expect { + result = say(123) + result == Ok("one hundred twenty-three") + +} # two hundred -expect - result = say(200) - result == Ok("two hundred") +expect { + result = say(200) + result == Ok("two hundred") + +} # nine hundred ninety-nine -expect - result = say(999) - result == Ok("nine hundred ninety-nine") +expect { + result = say(999) + result == Ok("nine hundred ninety-nine") + +} # one thousand -expect - result = say(1000) - result == Ok("one thousand") +expect { + result = say(1000) + result == Ok("one thousand") + +} # one thousand two hundred thirty-four -expect - result = say(1234) - result == Ok("one thousand two hundred thirty-four") +expect { + result = say(1234) + result == Ok("one thousand two hundred thirty-four") + +} # one million -expect - result = say(1000000) - result == Ok("one million") +expect { + result = say(1000000) + result == Ok("one million") + +} # one million two thousand three hundred forty-five -expect - result = say(1002345) - result == Ok("one million two thousand three hundred forty-five") +expect { + result = say(1002345) + result == Ok("one million two thousand three hundred forty-five") + +} # one billion -expect - result = say(1000000000) - result == Ok("one billion") +expect { + result = say(1000000000) + result == Ok("one billion") + +} # a big number -expect - result = say(987654321123) - result == Ok("nine hundred eighty-seven billion six hundred fifty-four million three hundred twenty-one thousand one hundred twenty-three") +expect { + result = say(987654321123) + result == Ok("nine hundred eighty-seven billion six hundred fifty-four million three hundred twenty-one thousand one hundred twenty-three") + +} # numbers above 999,999,999,999 are out of range -expect - result = say(1000000000000) - result |> Result.is_err +expect { + result = say(1000000000000) + 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/scrabble-score/.meta/Example.roc b/exercises/practice/scrabble-score/.meta/Example.roc index 61d87411..8eb93905 100644 --- a/exercises/practice/scrabble-score/.meta/Example.roc +++ b/exercises/practice/scrabble-score/.meta/Example.roc @@ -1,33 +1,40 @@ -module [score] - -score : Str -> U64 -score = |word| - word - |> Str.to_utf8 - |> List.map(letter_value) - |> List.sum +ScrabbleScore :: {}.{ + score : Str -> U64 + score = |word| { + word + .to_utf8() + .map(letter_value) + .sum() + } +} to_upper : U8 -> U8 -to_upper = |letter| - if letter >= 'a' and letter <= 'z' then - letter - 'a' + 'A' - else - letter +to_upper = |letter| { + if letter >= 'a' and letter <= 'z' { + letter - 'a' + 'A' + } else { + letter + } +} letter_value : U8 -> U64 -letter_value = |letter| - [ - ("AEIOULNRST", 1), - ("DG", 2), - ("BCMP", 3), - ("FHVWY", 4), - ("K", 5), - ("JX", 8), - ("QZ", 10), - ] - |> List.find_first( - |(letters, _)| - letters |> Str.to_utf8 |> List.contains(to_upper(letter)), - ) - |> Result.with_default(("", 0)) # ignore invalid characters - |> .1 +letter_value = |letter| { + (_, val) = + [ + ("AEIOULNRST", 1), + ("DG", 2), + ("BCMP", 3), + ("FHVWY", 4), + ("K", 5), + ("JX", 8), + ("QZ", 10), + ] + .find_first( + |(letters, _)| { + letters + ->Str.to_utf8() + .contains(to_upper(letter)) + }, + ) ?? ("", 0) + val +} diff --git a/exercises/practice/scrabble-score/.meta/template.j2 b/exercises/practice/scrabble-score/.meta/template.j2 index 26c059c0..6cb66b9a 100644 --- a/exercises/practice/scrabble-score/.meta/template.j2 +++ b/exercises/practice/scrabble-score/.meta/template.j2 @@ -6,9 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["word"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} +{{ macros.footer() }} diff --git a/exercises/practice/scrabble-score/ScrabbleScore.roc b/exercises/practice/scrabble-score/ScrabbleScore.roc index 1e26390c..20535c94 100644 --- a/exercises/practice/scrabble-score/ScrabbleScore.roc +++ b/exercises/practice/scrabble-score/ScrabbleScore.roc @@ -1,5 +1,6 @@ -module [score] - -score : Str -> U64 -score = |word| - crash("Please implement the 'score' function") +ScrabbleScore :: {}.{ + score : Str -> U64 + score = |word| { + crash "Please implement the 'score' function" + } +} diff --git a/exercises/practice/scrabble-score/scrabble-score-test.roc b/exercises/practice/scrabble-score/scrabble-score-test.roc index 5d9611ba..29a4016e 100644 --- a/exercises/practice/scrabble-score/scrabble-score-test.roc +++ b/exercises/practice/scrabble-score/scrabble-score-test.roc @@ -1,69 +1,76 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/scrabble-score/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import ScrabbleScore exposing [score] # lowercase letter -expect - result = score("a") - result == 1 +expect { + result = score("a") + result == 1 +} # uppercase letter -expect - result = score("A") - result == 1 +expect { + result = score("A") + result == 1 +} # valuable letter -expect - result = score("f") - result == 4 +expect { + result = score("f") + result == 4 +} # short word -expect - result = score("at") - result == 2 +expect { + result = score("at") + result == 2 +} # short, valuable word -expect - result = score("zoo") - result == 12 +expect { + result = score("zoo") + result == 12 +} # medium word -expect - result = score("street") - result == 6 +expect { + result = score("street") + result == 6 +} # medium, valuable word -expect - result = score("quirky") - result == 22 +expect { + result = score("quirky") + result == 22 +} # long, mixed-case word -expect - result = score("OxyphenButazone") - result == 41 +expect { + result = score("OxyphenButazone") + result == 41 +} # english-like word -expect - result = score("pinata") - result == 8 +expect { + result = score("pinata") + result == 8 +} # empty input -expect - result = score("") - result == 0 +expect { + result = score("") + result == 0 +} # entire alphabet available -expect - result = score("abcdefghijklmnopqrstuvwxyz") - result == 87 +expect { + result = score("abcdefghijklmnopqrstuvwxyz") + result == 87 +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/secret-handshake/.meta/Example.roc b/exercises/practice/secret-handshake/.meta/Example.roc index 49f26c13..a081b395 100644 --- a/exercises/practice/secret-handshake/.meta/Example.roc +++ b/exercises/practice/secret-handshake/.meta/Example.roc @@ -1,15 +1,33 @@ -module [commands] +SecretHandshake :: {}.{ + commands : U64 -> List(Str) + commands = |number| { + actions = + [(1, "wink"), (2, "double blink"), (4, "close your eyes"), (8, "jump")] + ->join_map( + |(mask, action)| { + if U64.bitwise_and(number, mask) == 0 { + [] + } else { + [action] + } + }, + ) -commands : U64 -> List Str -commands = |number| - actions = - [(1, "wink"), (2, "double blink"), (4, "close your eyes"), (8, "jump")] - |> List.join_map( - |(mask, action)| - if Num.bitwise_and(number, mask) == 0 then [] else [action], - ) + if U64.bitwise_and(number, 16) == 0 { + actions + } else { + actions.rev() + } + } +} - if Num.bitwise_and(number, 16) == 0 then - actions - else - List.reverse(actions) +# The following function should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} diff --git a/exercises/practice/secret-handshake/.meta/template.j2 b/exercises/practice/secret-handshake/.meta/template.j2 index a4eea5d5..dbae90c8 100644 --- a/exercises/practice/secret-handshake/.meta/template.j2 +++ b/exercises/practice/secret-handshake/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["number"] }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/secret-handshake/SecretHandshake.roc b/exercises/practice/secret-handshake/SecretHandshake.roc index ef974948..9b3cc9e9 100644 --- a/exercises/practice/secret-handshake/SecretHandshake.roc +++ b/exercises/practice/secret-handshake/SecretHandshake.roc @@ -1,5 +1,6 @@ -module [commands] - -commands : U64 -> List Str -commands = |number| - crash("Please implement the 'commands' function") +SecretHandshake :: {}.{ + commands : U64 -> List(Str) + commands = |number| { + crash "Please implement the 'commands' function" + } +} diff --git a/exercises/practice/secret-handshake/secret-handshake-test.roc b/exercises/practice/secret-handshake/secret-handshake-test.roc index 830756fe..9b5472a9 100644 --- a/exercises/practice/secret-handshake/secret-handshake-test.roc +++ b/exercises/practice/secret-handshake/secret-handshake-test.roc @@ -1,69 +1,76 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/secret-handshake/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import SecretHandshake exposing [commands] # wink for 1 -expect - result = commands(1) - result == ["wink"] +expect { + result = commands(1) + result == ["wink"] +} # double blink for 10 -expect - result = commands(2) - result == ["double blink"] +expect { + result = commands(2) + result == ["double blink"] +} # close your eyes for 100 -expect - result = commands(4) - result == ["close your eyes"] +expect { + result = commands(4) + result == ["close your eyes"] +} # jump for 1000 -expect - result = commands(8) - result == ["jump"] +expect { + result = commands(8) + result == ["jump"] +} # combine two actions -expect - result = commands(3) - result == ["wink", "double blink"] +expect { + result = commands(3) + result == ["wink", "double blink"] +} # reverse two actions -expect - result = commands(19) - result == ["double blink", "wink"] +expect { + result = commands(19) + result == ["double blink", "wink"] +} # reversing one action gives the same action -expect - result = commands(24) - result == ["jump"] +expect { + result = commands(24) + result == ["jump"] +} # reversing no actions still gives no actions -expect - result = commands(16) - result == [] +expect { + result = commands(16) + result == [] +} # all possible actions -expect - result = commands(15) - result == ["wink", "double blink", "close your eyes", "jump"] +expect { + result = commands(15) + result == ["wink", "double blink", "close your eyes", "jump"] +} # reverse all possible actions -expect - result = commands(31) - result == ["jump", "close your eyes", "double blink", "wink"] +expect { + result = commands(31) + result == ["jump", "close your eyes", "double blink", "wink"] +} # do nothing for zero -expect - result = commands(0) - result == [] +expect { + result = commands(0) + result == [] +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/series/.meta/Example.roc b/exercises/practice/series/.meta/Example.roc index 4ed73020..c1aa797d 100644 --- a/exercises/practice/series/.meta/Example.roc +++ b/exercises/practice/series/.meta/Example.roc @@ -1,20 +1,37 @@ -module [slices] +Series :: {}.{ + slices : Str, U64 -> List(Str) + slices = |string, slice_length| { + chars = string.to_utf8() + len = chars.len() + if len == 0 or slice_length == 0 or slice_length > len { + [] + } else { + maybe_list = + (0..=(len - slice_length)) + ->map_try( + |start_index| { + chars + .sublist( + { start: start_index, len: slice_length }, + ) + ->Str.from_utf8() + }, + ) + match maybe_list { + Ok(list) => list + Err(BadUtf8(_)) => { + crash "Only ASCII strings are supported" + } + } + } + } +} -slices : Str, U64 -> List Str -slices = |string, slice_length| - chars = string |> Str.to_utf8 - len = chars |> List.len - if len == 0 or slice_length == 0 or slice_length > len then - [] - else - maybe_list = - List.range({ start: At(0), end: At((len - slice_length)) }) - |> List.map_try( - |start_index| - chars - |> List.sublist({ start: start_index, len: slice_length }) - |> Str.from_utf8, - ) - when maybe_list is - Ok(list) -> list - Err(BadUtf8(_)) -> crash("Only ASCII strings are supported") +# The following function should soon be available in Roc's builtins +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/series/.meta/template.j2 b/exercises/practice/series/.meta/template.j2 index bc3bea6a..8260e386 100644 --- a/exercises/practice/series/.meta/template.j2 +++ b/exercises/practice/series/.meta/template.j2 @@ -7,12 +7,15 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} {%- if case["expected"]["error"] %} – just return an empty list{%- endif %} -expect - result = {{ case["input"]["series"] | to_roc }} |> {{ case["property"] | to_snake }}({{ case["input"]["sliceLength"] | to_roc }}) +expect { + result = {{ case["input"]["series"] | to_roc }} -> {{ case["property"] | to_snake }}({{ case["input"]["sliceLength"] | to_roc }}) {%- if case["expected"]["error"] %} result == [] {%- else %} result == {{ case["expected"] | to_roc }} {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/series/Series.roc b/exercises/practice/series/Series.roc index 4fb408be..4daac8ee 100644 --- a/exercises/practice/series/Series.roc +++ b/exercises/practice/series/Series.roc @@ -1,5 +1,6 @@ -module [slices] - -slices : Str, U64 -> List Str -slices = |string, slice_length| - crash("Please implement the 'slices' function") +Series :: {}.{ + slices : Str, U64 -> List(Str) + slices = |string, slice_length| { + crash "Please implement the 'slices' function" + } +} diff --git a/exercises/practice/series/series-test.roc b/exercises/practice/series/series-test.roc index 3e510bba..7bc99e7d 100644 --- a/exercises/practice/series/series-test.roc +++ b/exercises/practice/series/series-test.roc @@ -1,64 +1,70 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/series/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Series exposing [slices] # slices of one from one -expect - result = "1" |> slices(1) - result == ["1"] +expect { + result = "1"->slices(1) + result == ["1"] +} # slices of one from two -expect - result = "12" |> slices(1) - result == ["1", "2"] +expect { + result = "12"->slices(1) + result == ["1", "2"] +} # slices of two -expect - result = "35" |> slices(2) - result == ["35"] +expect { + result = "35"->slices(2) + result == ["35"] +} # slices of two overlap -expect - result = "9142" |> slices(2) - result == ["91", "14", "42"] +expect { + result = "9142"->slices(2) + result == ["91", "14", "42"] +} # slices can include duplicates -expect - result = "777777" |> slices(3) - result == ["777", "777", "777", "777"] +expect { + result = "777777"->slices(3) + result == ["777", "777", "777", "777"] +} # slices of a long series -expect - result = "918493904243" |> slices(5) - result == ["91849", "18493", "84939", "49390", "93904", "39042", "90424", "04243"] +expect { + result = "918493904243"->slices(5) + result == ["91849", "18493", "84939", "49390", "93904", "39042", "90424", "04243"] +} # slice length is too large – just return an empty list -expect - result = "12345" |> slices(6) - result == [] +expect { + result = "12345"->slices(6) + result == [] +} # slice length is way too large – just return an empty list -expect - result = "12345" |> slices(42) - result == [] +expect { + result = "12345"->slices(42) + result == [] +} # slice length cannot be zero – just return an empty list -expect - result = "12345" |> slices(0) - result == [] +expect { + result = "12345"->slices(0) + result == [] +} # empty series is invalid – just return an empty list -expect - result = "" |> slices(1) - result == [] +expect { + result = ""->slices(1) + result == [] +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/sgf-parsing/.meta/Example.roc b/exercises/practice/sgf-parsing/.meta/Example.roc index 08980461..c3c1eda1 100644 --- a/exercises/practice/sgf-parsing/.meta/Example.roc +++ b/exercises/practice/sgf-parsing/.meta/Example.roc @@ -1,7 +1,12 @@ -module [parse] + import parser.Parser as P import parser.String as S +SgfParsing :: {}.{ + parse : Str -> Result GameTree [ParsingFailure Str, ParsingIncomplete Str] + parse = |sgf| + S.parse_str(game_tree, sgf) +} # --- SGF GRAMMAR --- # Source: https://homepages.cwi.nl/~aeb/go/misc/sgfnotes.html @@ -110,7 +115,3 @@ value_type = uc_letter : P.Parser (List U8) U8 uc_letter = S.codeunit_satisfies(|b| b >= 'A' and b <= 'Z') - -parse : Str -> Result GameTree [ParsingFailure Str, ParsingIncomplete Str] -parse = |sgf| - S.parse_str(game_tree, sgf) diff --git a/exercises/practice/sgf-parsing/.meta/template.j2 b/exercises/practice/sgf-parsing/.meta/template.j2 index 3f4e160d..1807c550 100644 --- a/exercises/practice/sgf-parsing/.meta/template.j2 +++ b/exercises/practice/sgf-parsing/.meta/template.j2 @@ -20,15 +20,17 @@ GameNode({ {% for case in cases -%} # {{ case["description"] }} -expect +expect { sgf = {{ case["input"]["encoded"] | to_roc }} result = {{ case["property"] | to_snake }}(sgf) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} expected = {{ to_node(case["expected"]) | indent(4) }} result == Ok(expected) {%- endif %} - +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/sgf-parsing/SgfParsing.roc b/exercises/practice/sgf-parsing/SgfParsing.roc index 584f64cc..a24becbd 100644 --- a/exercises/practice/sgf-parsing/SgfParsing.roc +++ b/exercises/practice/sgf-parsing/SgfParsing.roc @@ -1,4 +1,9 @@ -module [parse] +SgfParsing :: {}.{ + parse : Str -> Result GameTree _ + parse = |sgf| + crash("Please implement the 'parse' function") +} + # HINT: we have added the `roc-parser` package to the app's header in # sgf-parsing-test.roc. You can use it if you want, particularly the @@ -13,7 +18,3 @@ NodeProperties : Dict Str (List Str) # the Roc compiler does not yet understand that an empty List can end the # recursion. GameTree : [Empty, GameNode { properties : NodeProperties, children : List GameTree }] - -parse : Str -> Result GameTree _ -parse = |sgf| - crash("Please implement the 'parse' function") diff --git a/exercises/practice/sgf-parsing/sgf-parsing-test.roc b/exercises/practice/sgf-parsing/sgf-parsing-test.roc index 8d141d8b..1647d6e5 100644 --- a/exercises/practice/sgf-parsing/sgf-parsing-test.roc +++ b/exercises/practice/sgf-parsing/sgf-parsing-test.roc @@ -1,405 +1,435 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/sgf-parsing/canonical-data.json -# File last updated on 2025-09-15 +# File last updated on 2026-06-21 app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", - parser: "https://github.com/lukewilliamboswell/roc-parser/releases/download/0.10.0/6eZYaXkrakq9fJ4oUc0VfdxU1Fap2iTuAN18q9OgQss.tar.br", + pf: platform "https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/download/0.9/8GdFEvQYS3TeAZxKvTzCLVdQiomweGtXcdZkXNDEeABq.tar.zst", + parser: "https://github.com/lukewilliamboswell/roc-parser/...", # TODO: update when a zig-compatible release is available } import pf.Stdout -main! = |_args| - Stdout.line!("") - import SgfParsing exposing [parse] # empty input -expect - sgf = "" - result = parse(sgf) - result |> Result.is_err +expect { + sgf = "" + result = parse(sgf) + result.is_err() +} # tree with no nodes -expect - sgf = "()" - result = parse(sgf) - result |> Result.is_err +expect { + sgf = "()" + result = parse(sgf) + result.is_err() +} # node without tree -expect - sgf = ";" - result = parse(sgf) - result |> Result.is_err +expect { + sgf = ";" + result = parse(sgf) + result.is_err() +} # node without properties -expect - sgf = "(;)" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list([]), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;)" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list([]), + children: [], + }, + ) + result == Ok(expected) +} # single node tree -expect - sgf = "(;A[B])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["B"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[B])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["B"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} # multiple properties -expect - sgf = "(;A[b]C[d])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["b"]), - ("C", ["d"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[b]C[d])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["b"]), + ("C", ["d"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} # properties without delimiter -expect - sgf = "(;A)" - result = parse(sgf) - result |> Result.is_err +expect { + sgf = "(;A)" + result = parse(sgf) + result.is_err() +} # all lowercase property -expect - sgf = "(;a[b])" - result = parse(sgf) - result |> Result.is_err +expect { + sgf = "(;a[b])" + result = parse(sgf) + result.is_err() +} # upper and lowercase property -expect - sgf = "(;Aa[b])" - result = parse(sgf) - result |> Result.is_err +expect { + sgf = "(;Aa[b])" + result = parse(sgf) + result.is_err() +} # two nodes -expect - sgf = "(;A[B];B[C])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["B"]), - ], - ), - children: [ - GameNode( - { - properties: Dict.from_list( - [ - ("B", ["C"]), - ], - ), - children: [], - }, - ), - ], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[B];B[C])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["B"]), + ], + ), + children: [ + + GameNode( + { + properties: Dict.from_list( + [ + ("B", ["C"]), + ], + ), + children: [], + }, + ), + ], + }, + ) + result == Ok(expected) +} # two child trees -expect - sgf = "(;A[B](;B[C])(;C[D]))" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["B"]), - ], - ), - children: [ - GameNode( - { - properties: Dict.from_list( - [ - ("B", ["C"]), - ], - ), - children: [], - }, - ), - GameNode( - { - properties: Dict.from_list( - [ - ("C", ["D"]), - ], - ), - children: [], - }, - ), - ], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[B](;B[C])(;C[D]))" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["B"]), + ], + ), + children: [ + + GameNode( + { + properties: Dict.from_list( + [ + ("B", ["C"]), + ], + ), + children: [], + }, + ), + + GameNode( + { + properties: Dict.from_list( + [ + ("C", ["D"]), + ], + ), + children: [], + }, + ), + ], + }, + ) + result == Ok(expected) +} # multiple property values -expect - sgf = "(;A[b][c][d])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["b", "c", "d"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[b][c][d])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["b", "c", "d"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} # within property values, whitespace characters such as tab are converted to spaces -expect - sgf = "(;A[hello\t\tworld])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["hello world"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[hello\t\tworld])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["hello world"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} # within property values, newlines remain as newlines -expect - sgf = "(;A[hello\n\nworld])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["hello\n\nworld"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[hello\n\nworld])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["hello\n\nworld"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} # escaped closing bracket within property value becomes just a closing bracket -expect - sgf = "(;A[\\]])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["]"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[\\]])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["]"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} # escaped backslash in property value becomes just a backslash -expect - sgf = "(;A[\\\\])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["\\"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[\\\\])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["\\"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} # opening bracket within property value doesn't need to be escaped -expect - sgf = "(;A[x[y\\]z][foo]B[bar];C[baz])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["x[y]z", "foo"]), - ("B", ["bar"]), - ], - ), - children: [ - GameNode( - { - properties: Dict.from_list( - [ - ("C", ["baz"]), - ], - ), - children: [], - }, - ), - ], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[x[y\\]z][foo]B[bar];C[baz])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["x[y]z", "foo"]), + ("B", ["bar"]), + ], + ), + children: [ + + GameNode( + { + properties: Dict.from_list( + [ + ("C", ["baz"]), + ], + ), + children: [], + }, + ), + ], + }, + ) + result == Ok(expected) +} # semicolon in property value doesn't need to be escaped -expect - sgf = "(;A[a;b][foo]B[bar];C[baz])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["a;b", "foo"]), - ("B", ["bar"]), - ], - ), - children: [ - GameNode( - { - properties: Dict.from_list( - [ - ("C", ["baz"]), - ], - ), - children: [], - }, - ), - ], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[a;b][foo]B[bar];C[baz])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["a;b", "foo"]), + ("B", ["bar"]), + ], + ), + children: [ + + GameNode( + { + properties: Dict.from_list( + [ + ("C", ["baz"]), + ], + ), + children: [], + }, + ), + ], + }, + ) + result == Ok(expected) +} # parentheses in property value don't need to be escaped -expect - sgf = "(;A[x(y)z][foo]B[bar];C[baz])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["x(y)z", "foo"]), - ("B", ["bar"]), - ], - ), - children: [ - GameNode( - { - properties: Dict.from_list( - [ - ("C", ["baz"]), - ], - ), - children: [], - }, - ), - ], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[x(y)z][foo]B[bar];C[baz])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["x(y)z", "foo"]), + ("B", ["bar"]), + ], + ), + children: [ + + GameNode( + { + properties: Dict.from_list( + [ + ("C", ["baz"]), + ], + ), + children: [], + }, + ), + ], + }, + ) + result == Ok(expected) +} # escaped tab in property value is converted to space -expect - sgf = "(;A[hello\\\tworld])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["hello world"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[hello\\\tworld])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["hello world"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} # escaped newline in property value is converted to nothing at all -expect - sgf = "(;A[hello\\\nworld])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["helloworld"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[hello\\\nworld])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["helloworld"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} # escaped t and n in property value are just letters, not whitespace -expect - sgf = "(;A[\\t = t and \\n = n])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["t = t and n = n"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[\\t = t and \\n = n])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["t = t and n = n"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} # mixing various kinds of whitespace and escaped characters in property value -expect - sgf = "(;A[\\]b\nc\\\nd\t\te\\\\ \\\n\\]])" - result = parse(sgf) - expected = - GameNode( - { - properties: Dict.from_list( - [ - ("A", ["]b\ncd e\\ ]"]), - ], - ), - children: [], - }, - ) - result == Ok(expected) +expect { + sgf = "(;A[\\]b\nc\\\nd\t\te\\\\ \\\n\\]])" + result = parse(sgf) + expected = + GameNode( + { + properties: Dict.from_list( + [ + ("A", ["]b\ncd e\\ ]"]), + ], + ), + children: [], + }, + ) + result == Ok(expected) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/sieve/.meta/Example.roc b/exercises/practice/sieve/.meta/Example.roc index 34253bb2..3e82516c 100644 --- a/exercises/practice/sieve/.meta/Example.roc +++ b/exercises/practice/sieve/.meta/Example.roc @@ -1,20 +1,37 @@ -module [primes] +Sieve :: {}.{ + primes : U64 -> List(U64) + primes = |limit| { + if limit < 2 { + [] + } else { + help_sieve = |candidates, found_primes, limit2| { + match candidates { + [] => found_primes + [0, .. as rest] => { + help_sieve(rest, found_primes, limit2) + } + [prime, .. as rest] => { + ((prime * 2)..=limit2).step_by(prime) + .fold( + rest, + |filtered_candidates, multiple_of_prime| { + match filtered_candidates.replace(multiple_of_prime - prime - 1, 0) { + Ok(result) => result.list + Err(_) => { + crash "Unreachable" + } + } + }, + ) + ->help_sieve(found_primes.append(prime), limit2) + } + } + } -primes : U64 -> List U64 -primes = |limit| - if limit < 2 then - [] - else - help_sieve = |candidates, found_primes| - when candidates is - [] -> found_primes - [0, .. as rest] -> rest |> help_sieve(found_primes) - [prime, .. as rest] -> - List.range({ start: After(prime), end: At(limit), step: prime }) - |> List.walk( - rest, - |filtered_candidates, multiple_of_prime| - filtered_candidates |> List.replace((multiple_of_prime - prime - 1), 0) |> .list, - ) - |> help_sieve((found_primes |> List.append(prime))) - help_sieve(List.range({ start: At(2), end: At(limit) }), []) + initial_candidates = List.from_iter(2..=limit) + help_sieve(initial_candidates, [], limit) + } + } +} + +# TODO: update once https://github.com/roc-lang/roc/issues/9690 is fixed diff --git a/exercises/practice/sieve/.meta/template.j2 b/exercises/practice/sieve/.meta/template.j2 index 353a7c2a..8189c1a0 100644 --- a/exercises/practice/sieve/.meta/template.j2 +++ b/exercises/practice/sieve/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["limit"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/sieve/Sieve.roc b/exercises/practice/sieve/Sieve.roc index 1485e34b..649d96b5 100644 --- a/exercises/practice/sieve/Sieve.roc +++ b/exercises/practice/sieve/Sieve.roc @@ -1,5 +1,6 @@ -module [primes] - -primes : U64 -> List U64 -primes = |limit| - crash("Please implement the 'primes' function") +Sieve :: {}.{ + primes : U64 -> List(U64) + primes = |limit| { + crash "Please implement the 'primes' function" + } +} diff --git a/exercises/practice/sieve/sieve-test.roc b/exercises/practice/sieve/sieve-test.roc index c1746d79..7f8e2e0f 100644 --- a/exercises/practice/sieve/sieve-test.roc +++ b/exercises/practice/sieve/sieve-test.roc @@ -1,39 +1,40 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/sieve/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Sieve exposing [primes] # no primes under two -expect - result = primes(1) - result == [] +expect { + result = primes(1) + result == [] +} # find first prime -expect - result = primes(2) - result == [2] +expect { + result = primes(2) + result == [2] +} # find primes up to 10 -expect - result = primes(10) - result == [2, 3, 5, 7] +expect { + result = primes(10) + result == [2, 3, 5, 7] +} # limit is prime -expect - result = primes(13) - result == [2, 3, 5, 7, 11, 13] +expect { + result = primes(13) + result == [2, 3, 5, 7, 11, 13] +} # find primes up to 1000 -expect - result = primes(1000) - result == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997] +expect { + result = primes(1000) + result == [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997] +} +# 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/.meta/Example.roc b/exercises/practice/simple-linked-list/.meta/Example.roc index 4551fd71..c6f90af9 100644 --- a/exercises/practice/simple-linked-list/.meta/Example.roc +++ b/exercises/practice/simple-linked-list/.meta/Example.roc @@ -1,37 +1,50 @@ -module [from_list, to_list, push, pop, reverse, len] +SimpleLinkedList :: [Nil, Cons(U64, SimpleLinkedList)].{ + from_list : List(U64) -> SimpleLinkedList + from_list = |list| list.fold(Nil, push) -SimpleLinkedList : [Nil, Cons U64 SimpleLinkedList] + to_list : SimpleLinkedList -> List(U64) + to_list = |linked_list| { + match linked_list { + Nil => [] + Cons(head, tail) => tail.to_list().append(head) + } + } -from_list : List U64 -> SimpleLinkedList -from_list = |list| - list |> List.walk(Nil, push) + push : SimpleLinkedList, U64 -> SimpleLinkedList + push = |linked_list, item| Cons(item, linked_list) -to_list : SimpleLinkedList -> List U64 -to_list = |linked_list| - when linked_list is - Nil -> [] - Cons(head, tail) -> tail |> to_list |> List.append(head) + pop : SimpleLinkedList -> Try({ value : U64, updated_list : SimpleLinkedList }, [LinkedListWasEmpty]) + pop = |linked_list| { + match linked_list { + Nil => Err(LinkedListWasEmpty) + Cons(head, tail) => Ok({ value: head, updated_list: tail }) + } + } -push : SimpleLinkedList, U64 -> SimpleLinkedList -push = |linked_list, item| - Cons(item, linked_list) + peek : SimpleLinkedList -> Try(U64, [LinkedListWasEmpty]) + peek = |linked_list| { + match linked_list { + Nil => Err(LinkedListWasEmpty) + Cons(head, _) => Ok(head) + } + } -pop : SimpleLinkedList -> Result { value : U64, linked_list : SimpleLinkedList } [LinkedListWasEmpty] -pop = |linked_list| - when linked_list is - Nil -> Err(LinkedListWasEmpty) - Cons(head, tail) -> Ok({ value: head, linked_list: tail }) + reverse : SimpleLinkedList -> SimpleLinkedList + reverse = |linked_list| { + help = |result, rest| { + match rest { + Nil => result + Cons(head, tail) => help((result->push(head)), tail) + } + } + help(Nil, linked_list) + } -reverse : SimpleLinkedList -> SimpleLinkedList -reverse = |linked_list| - help = |result, rest| - when rest is - Nil -> result - Cons(head, tail) -> help((result |> push(head)), tail) - help(Nil, linked_list) - -len : SimpleLinkedList -> U64 -len = |linked_list| - when linked_list is - Nil -> 0 - Cons(_, tail) -> 1 + len(tail) + len : SimpleLinkedList -> U64 + len = |linked_list| { + match linked_list { + Nil => 0 + Cons(_, tail) => 1 + tail.len() + } + } +} diff --git a/exercises/practice/simple-linked-list/.meta/template.j2 b/exercises/practice/simple-linked-list/.meta/template.j2 new file mode 100644 index 00000000..aeb54851 --- /dev/null +++ b/exercises/practice/simple-linked-list/.meta/template.j2 @@ -0,0 +1,81 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +import {{ exercise | to_pascal }} + +{% for supercase in cases if supercase["description"] != "toList LIFO" %} +## +## {{ supercase["description"] }} +## + +{% for case in supercase["cases"] -%} +# {{ case["description"] }} +expect { + result = + SimpleLinkedList.from_list({{ case["input"]["initialValues"] | to_roc }}) + {%- for op in case["input"]["operations"] %} + {%- if op["operation"] == "push" %} + .push({{ op["value"] }}) + {%- elif op["operation"] == "pop" %} + .pop() + {%- if not op["expected"] is mapping %}? + {%- if loop.last %} + .value + {%- else %} + ->expect_value({{ op["expected"] | to_roc }})? + {%- endif %} + {%- endif %} + {%- elif op["operation"] == "peek" %} + {%- if loop.last %} + .peek(){%- if not op["expected"] is mapping %}?{%- endif %} + {%- else %} + ->expect_peek({{ op["expected"] | to_roc }})? + {%- endif %} + {%- elif op["operation"] == "count" %} + {%- if loop.last %} + .len() + {%- else %} + ->expect_len({{ op["expected"] | to_roc }})? + {%- endif %} + {%- elif op["operation"] == "toList" %} + .to_list() + {%- else %} + .{{ op["operation"] }}({{ op["value"] }}) + {%- endif %} + + {%- if loop.last %} + {%- if op["expected"] is mapping %} + + result.is_err() + {%- else %} + + result == {{ op["expected"] | to_roc }} + {%- endif %} + {%- endif %} + {%- endfor %} +} + +{% endfor %} +{% endfor %} + +expect_value : { updated_list: SimpleLinkedList, value: U64 }, U64 -> Try(SimpleLinkedList, [UnexpectedValue(U64)]) +expect_value = |{ updated_list, value }, expected_value| { + if value == expected_value Ok(updated_list) else Err(UnexpectedValue(value)) +} + +expect_len : SimpleLinkedList, U64 -> Try(SimpleLinkedList, [UnexpectedLen(U64)]) +expect_len = |list, expected_len| { + actual_len = list.len() + if actual_len == expected_len Ok(list) else Err(UnexpectedLen(actual_len)) +} + +expect_peek : SimpleLinkedList, U64 -> Try(SimpleLinkedList, [UnexpectedPeek(U64), LinkedListWasEmpty]) +expect_peek = |list, expected_peek| { + match list.peek() { + Ok(actual_peek) => if actual_peek == expected_peek Ok(list) else Err(UnexpectedPeek(actual_peek)) + Err(LinkedListWasEmpty) => Err(LinkedListWasEmpty) + } +} + +{{ macros.footer() }} diff --git a/exercises/practice/simple-linked-list/.meta/tests.toml b/exercises/practice/simple-linked-list/.meta/tests.toml new file mode 100644 index 00000000..736247f2 --- /dev/null +++ b/exercises/practice/simple-linked-list/.meta/tests.toml @@ -0,0 +1,103 @@ +# 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. + +[962d998c-c203-41e2-8fbd-85a7b98b79b9] +description = "count -> Empty list has length of zero" + +[9760262e-d7e4-4639-9840-87e2e2fbb115] +description = "count -> Singleton list has length of one" + +[d9955c90-637c-441b-b41d-8cfb48e924a8] +description = "count -> Non-empty list has correct length" + +[0c3966db-58f9-4632-b94c-8ea13e54c2c8] +description = "pop -> Pop from empty list is an error" + +[a4f9d2e1-7425-49ef-9ee8-6c0cb3407cf0] +description = "pop -> Can pop from singleton list" + +[6dcbb2c9-d98a-47bc-a010-9c19703d3ea2] +description = "pop -> Can pop from non-empty list" + +[e83aade9-f030-4096-aaf0-f9dc6491e6cf] +description = "pop -> Can pop multiple items" + +[5c46bcf2-c0a9-4654-ae17-f3192436fcf1] +description = "pop -> Pop updates the count" + +[70d747a1-2e84-4ebc-bc3f-dcbee6a05f6b] +description = "push -> Can push to an empty list" +include = false + +[f3197f0a-1fea-45a5-939f-4a5ea60387ec] +description = "push -> Can push to an empty list" +reimplements = "70d747a1-2e84-4ebc-bc3f-dcbee6a05f6b" +include = false + +[391e332e-1f91-4033-b1e0-0e0c17812fa7] +description = "push -> Can push to a non-empty list" +include = false + +[ed4b0e01-3bbd-4895-af25-152b5914b3da] +description = "push -> Push updates count" + +[41666790-b932-4e5a-b323-e848a83d12d5] +description = "push -> Push and pop" + +[930a4a5c-76f6-47ec-9be3-4e70993173a1] +description = "peek -> Peek on empty list is an error" + +[43255a50-d919-4e81-afce-e4a271eaedbd] +description = "peek -> Can peek on singleton list" + +[48353020-e25d-4621-a854-e35fb1e15fa7] +description = "peek -> Can peek on non-empty list" + +[96fcead9-a713-46c2-8005-3f246c873851] +description = "peek -> Peek does not change the count" + +[7576ed05-7ff7-4b84-8efb-d34d62c110f5] +description = "peek -> Can peek after a pop and push" + +[b97d00b6-2fab-435d-ae74-3233dcc13698] +description = "toList LIFO -> Empty linked list to list is empty" + +[eedeb95f-b5cf-431d-8ad6-5854ba6b251c] +description = "toList LIFO -> To list with multiple values" + +[838678de-eaf3-4c14-b34e-7e35b6d851e8] +description = "toList LIFO -> To list after a pop" + +[03fc83a5-48a8-470b-a2d2-a286c5e8365f] +description = "toList FIFO -> Empty linked list to list is empty" + +[1282484e-a58c-426a-972e-90746bda61fc] +description = "toList FIFO -> To list with multiple values" + +[05ca3109-1249-4c0c-a567-a3b2f8352a7c] +description = "toList FIFO -> To list after a pop" + +[5e6c1a3d-e34b-46d3-be59-3f132a820ed5] +description = "reverse -> Reversed empty list has same values" + +[93c87ed3-862a-474f-820b-ba3fd6b6daf6] +description = "reverse -> Reversed singleton list is same list" + +[92851ebe-9f52-4406-b92e-0718c441a2ab] +description = "reverse -> Reversed non-empty list is reversed" +include = false + +[1210eeda-b23f-4790-930c-7ac6d0c8e723] +description = "reverse -> Reversed non-empty list is reversed" +reimplements = "92851ebe-9f52-4406-b92e-0718c441a2ab" + +[9b53af96-7494-4cfa-9b77-b7366fed5c4c] +description = "reverse -> Double reverse" diff --git a/exercises/practice/simple-linked-list/SimpleLinkedList.roc b/exercises/practice/simple-linked-list/SimpleLinkedList.roc index 8e7201ce..bd440b02 100644 --- a/exercises/practice/simple-linked-list/SimpleLinkedList.roc +++ b/exercises/practice/simple-linked-list/SimpleLinkedList.roc @@ -1,33 +1,36 @@ -module [from_list, to_list, push, pop, reverse, len] +SimpleLinkedList :: [Nil, Cons(U64, SimpleLinkedList)].{ + from_list : List(U64) -> SimpleLinkedList + from_list = |list| { + crash "Please implement the 'from_list' function" + } -SimpleLinkedList : { - # TODO: change this type however you need - todo : U64, - todo2 : U64, - todo3 : U64, - # etc. -} - -from_list : List U64 -> SimpleLinkedList -from_list = |list| - crash("Please implement the 'from_list' function") + to_list : SimpleLinkedList -> List(U64) + to_list = |linked_list| { + crash "Please implement the 'to_list' function" + } -to_list : SimpleLinkedList -> List U64 -to_list = |linked_list| - crash("Please implement the 'to_list' function") + push : SimpleLinkedList, U64 -> SimpleLinkedList + push = |linked_list, item| { + crash "Please implement the 'push' function" + } -push : SimpleLinkedList, U64 -> SimpleLinkedList -push = |linked_list, item| - crash("Please implement the 'push' function") + pop : SimpleLinkedList -> Try({ value : U64, updated_list : SimpleLinkedList }, [LinkedListWasEmpty]) + pop = |linked_list| { + crash "Please implement the 'pop' function" + } -pop : SimpleLinkedList -> Result { value : U64, linked_list : SimpleLinkedList } _ -pop = |linked_list| - crash("Please implement the 'pop' function") + peek : SimpleLinkedList -> Try(U64, [LinkedListWasEmpty]) + peek = |linked_list| { + crash "Please implement the 'peek' function" + } -reverse : SimpleLinkedList -> SimpleLinkedList -reverse = |linked_list| - crash("Please implement the 'reverse' function") + reverse : SimpleLinkedList -> SimpleLinkedList + reverse = |linked_list| { + crash "Please implement the 'reverse' function" + } -len : SimpleLinkedList -> U64 -len = |linked_list| - crash("Please implement the 'len' function") + len : SimpleLinkedList -> U64 + len = |linked_list| { + crash "Please implement the 'len' function" + } +} 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 4eac0970..15e1001e 100644 --- a/exercises/practice/simple-linked-list/simple-linked-list-test.roc +++ b/exercises/practice/simple-linked-list/simple-linked-list-test.roc @@ -1,78 +1,305 @@ -# File last updated on 2025-1-4 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") - -import SimpleLinkedList exposing [from_list, to_list, push, pop, reverse, len] - -# can create an empty linked list -expect - result = [] |> from_list |> to_list - expected = [] - result == expected - -# can create a linked list with a single element -expect - result = [123] |> from_list |> to_list - expected = [123] - result == expected - -# can create a linked list with multiple elements -expect - result = [123, 456, 789] |> from_list |> to_list - expected = [123, 456, 789] - result == expected - -# can push items to a linked list -expect - result = [123] |> from_list |> push(456) |> push(789) |> to_list - expected = [123, 456, 789] - result == expected - -# can pop an item from a linked list -expect - pop_result = [123, 456, 789] |> from_list |> pop - result = pop_result |> Result.try(|popped| Ok(popped.value)) - expected = Ok(789) - result == expected - -# the last element should be gone after pop -expect - pop_result = [123, 456, 789] |> from_list |> pop - result = pop_result |> Result.try(|popped| Ok((popped.linked_list |> to_list))) - expected = Ok([123, 456]) - result == expected - -# cannot pop an empty linked list -expect - result = [] |> from_list |> pop - result |> Result.is_err - -# can reverse a linked list -expect - result = [123, 456, 789] |> from_list |> reverse |> to_list - expected = [789, 456, 123] - result == expected - -# can reverse an empty linked list and it's still empty -expect - result = [] |> from_list |> reverse |> to_list - expected = [] - result == expected - -# can get the length of a linked list -expect - result = [123, 456, 789] |> from_list |> len - expected = 3 - result == expected - -# can get the length of an empty linked list -expect - result = [] |> from_list |> len - expected = 0 - result == expected +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/simple-linked-list/canonical-data.json +# File last updated on 2026-06-21 + +import SimpleLinkedList + +## +## count +## + +# Empty list has length of zero +expect { + result = + SimpleLinkedList.from_list([]) + .len() + + result == 0 +} + +# Singleton list has length of one +expect { + result = + SimpleLinkedList.from_list([1]) + .len() + + result == 1 +} + +# Non-empty list has correct length +expect { + result = + SimpleLinkedList.from_list([1, 2, 3]) + .len() + + result == 3 +} + +## +## pop +## + +# Pop from empty list is an error +expect { + result = + SimpleLinkedList.from_list([]) + .pop() + + result.is_err() +} + +# Can pop from singleton list +expect { + result = + SimpleLinkedList.from_list([1]) + .pop()? + .value + + result == 1 +} + +# Can pop from non-empty list +expect { + result = + SimpleLinkedList.from_list([1, 2]) + .pop()? + .value + + result == 2 +} + +# Can pop multiple items +expect { + result = + SimpleLinkedList.from_list([1, 2]) + .pop()? + ->expect_value(2)? + .pop()? + .value + + result == 1 +} + +# Pop updates the count +expect { + result1 = + SimpleLinkedList.from_list([1, 2]) + ->expect_len(2)? + .pop()? + ->expect_value(2)? + result2 = # TODO: remove this workaround when issue #9743 is fixed + result1 + ->expect_len(1)? + .pop()? + ->expect_value(1)? + .len() + + result2 == 0 +} + +## +## push +## + +# Push updates count +expect { + result = + SimpleLinkedList.from_list([1, 2]) + .push(3) + .len() + + result == 3 +} + +# Push and pop +expect { + result = + SimpleLinkedList.from_list([]) + .push(1) + .push(2) + .pop()? + ->expect_value(2)? + .push(3) + ->expect_len(2)? + .pop()? + ->expect_value(3)? + .pop()? + ->expect_value(1)? + .len() + + result == 0 +} + +## +## peek +## + +# Peek on empty list is an error +expect { + result = + SimpleLinkedList.from_list([]) + .peek() + + result.is_err() +} + +# Can peek on singleton list +expect { + result = + SimpleLinkedList.from_list([1]) + .peek()? + + result == 1 +} + +# Can peek on non-empty list +expect { + result = + SimpleLinkedList.from_list([1, 2]) + .peek()? + + result == 2 +} + +# Peek does not change the count +expect { + result = + SimpleLinkedList.from_list([1, 2]) + ->expect_peek(2)? + .len() + + result == 2 +} + +# Can peek after a pop and push +expect { + result = + SimpleLinkedList.from_list([]) + .push(1) + .push(2) + ->expect_peek(2)? + .pop()? + ->expect_value(2)? + ->expect_peek(1)? + .push(3) + .peek()? + + result == 3 +} + +## +## toList FIFO +## + +# Empty linked list to list is empty +expect { + result = + SimpleLinkedList.from_list([]) + .to_list() + + result == [] +} + +# To list with multiple values +expect { + result = + SimpleLinkedList.from_list([1, 2, 3]) + .to_list() + + result == [1, 2, 3] +} + +# To list after a pop +expect { + result = + SimpleLinkedList.from_list([]) + .push(1) + .push(2) + .push(3) + .pop()? + ->expect_value(3)? + .push(4) + .to_list() + + result == [1, 2, 4] +} + +## +## reverse +## + +# Reversed empty list has same values +expect { + result = + SimpleLinkedList.from_list([]) + .reverse() + .to_list() + + result == [] +} + +# Reversed singleton list is same list +expect { + result = + SimpleLinkedList.from_list([1]) + .reverse() + .to_list() + + result == [1] +} + +# Reversed non-empty list is reversed +expect { + result = + SimpleLinkedList.from_list([1, 2, 3]) + .reverse() + ->expect_len(3)? + .pop()? + ->expect_value(1)? + .pop()? + ->expect_value(2)? + .pop()? + .value + + result == 3 +} + +# Double reverse +expect { + result = + SimpleLinkedList.from_list([1, 2, 3]) + .reverse() + .reverse() + .pop()? + ->expect_value(3)? + .pop()? + ->expect_value(2)? + .pop()? + .value + + result == 1 +} + +expect_value : { updated_list : SimpleLinkedList, value : U64 }, U64 -> Try(SimpleLinkedList, [UnexpectedValue(U64)]) +expect_value = |{ updated_list, value }, expected_value| { + if value == expected_value Ok(updated_list) else Err(UnexpectedValue(value)) +} + +expect_len : SimpleLinkedList, U64 -> Try(SimpleLinkedList, [UnexpectedLen(U64)]) +expect_len = |list, expected_len| { + actual_len = list.len() + if actual_len == expected_len Ok(list) else Err(UnexpectedLen(actual_len)) +} + +expect_peek : SimpleLinkedList, U64 -> Try(SimpleLinkedList, [UnexpectedPeek(U64), LinkedListWasEmpty]) +expect_peek = |list, expected_peek| { + match list.peek() { + Ok(actual_peek) => if actual_peek == expected_peek Ok(list) else Err(UnexpectedPeek(actual_peek)) + Err(LinkedListWasEmpty) => Err(LinkedListWasEmpty) + } +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/space-age/.meta/Example.roc b/exercises/practice/space-age/.meta/Example.roc index 71ede8fc..d18846bc 100644 --- a/exercises/practice/space-age/.meta/Example.roc +++ b/exercises/practice/space-age/.meta/Example.roc @@ -1,35 +1,44 @@ -module [age] +SpaceAge :: {}.{ + Planet := [ + Mercury, + Venus, + Earth, + Mars, + Jupiter, + Saturn, + Uranus, + Neptune, + ] -Planet : [ - Mercury, - Venus, - Earth, - Mars, - Jupiter, - Saturn, - Uranus, - Neptune, -] + age : Planet, Dec -> Dec + age = |planet, seconds| { + period_in_earth_years = orbital_period_in_earth_years(planet) + period_in_seconds = period_in_earth_years * 365.25 * 24 * 60 * 60 + planet_years = seconds / period_in_seconds + round(planet_years) + } +} -age : Planet, Dec -> Dec -age = |planet, seconds| - period_in_earth_years = orbital_period_in_earth_years(planet) - period_in_seconds = period_in_earth_years * 365.25 * 24 * 60 * 60 - planet_years = seconds / period_in_seconds - round(planet_years) +orbital_period_in_earth_years = |planet| { + match planet { + Mercury => 0.2408467 + Venus => 0.61519726 + Earth => 1.0 + Mars => 1.8808158 + Jupiter => 11.862615 + Saturn => 29.447498 + Uranus => 84.016846 + Neptune => 164.79132 + } +} +# The following function will soon be available in Roc's builtins round : Dec -> Dec -round = |value| - pow = 10.0 |> Num.pow(2) - value * pow |> Num.round |> Num.to_frac |> Num.div(pow) - -orbital_period_in_earth_years = |planet| - when planet is - Mercury -> 0.2408467 - Venus -> 0.61519726 - Earth -> 1.0 - Mars -> 1.8808158 - Jupiter -> 11.862615 - Saturn -> 29.447498 - Uranus -> 84.016846 - Neptune -> 164.79132 +round = |value| { + pow = 100.0 + ( + (value * pow + 0.5).to_u64_try() ?? { + crash "Unreachable" + }, + ).to_dec() / pow +} diff --git a/exercises/practice/space-age/.meta/template.j2 b/exercises/practice/space-age/.meta/template.j2 index d589ff9d..bc601a8b 100644 --- a/exercises/practice/space-age/.meta/template.j2 +++ b/exercises/practice/space-age/.meta/template.j2 @@ -6,8 +6,25 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["planet"] | to_pascal }}, {{ case["input"]["seconds"] }}) - Num.is_approx_eq(result, {{ case["expected"] }}, { atol: 0.01 }) + is_approx_eq(result, {{ case["expected"] }}, { atol: 0.01 }) +} {% endfor %} + +# The following function should soon be available in Roc's builtins +is_approx_eq : Dec, Dec, { atol: Dec } -> Bool +is_approx_eq = |x, y, {atol}| { + to_int : Dec -> Try(I64, [OutOfRange]) + to_int = |f| { + (f / atol + 0.5).to_i64_try() + } + + match (to_int(x), to_int(y)) { + (Ok(xi), Ok(yi)) => (xi == yi) + _ => Bool.False + } +} + +{{ macros.footer() }} diff --git a/exercises/practice/space-age/SpaceAge.roc b/exercises/practice/space-age/SpaceAge.roc index 616135d8..e3197f66 100644 --- a/exercises/practice/space-age/SpaceAge.roc +++ b/exercises/practice/space-age/SpaceAge.roc @@ -1,16 +1,17 @@ -module [age] +SpaceAge :: {}.{ + Planet := [ + Mercury, + Venus, + Earth, + Mars, + Jupiter, + Saturn, + Uranus, + Neptune, + ] -Planet : [ - Mercury, - Venus, - Earth, - Mars, - Jupiter, - Saturn, - Uranus, - Neptune, -] - -age : Planet, Dec -> Dec -age = |planet, seconds| - crash("Please implement the 'age' function") + age : Planet, Dec -> Dec + age = |planet, seconds| { + crash "Please implement the 'age' function" + } +} diff --git a/exercises/practice/space-age/space-age-test.roc b/exercises/practice/space-age/space-age-test.roc index 09b01663..39cb5d08 100644 --- a/exercises/practice/space-age/space-age-test.roc +++ b/exercises/practice/space-age/space-age-test.roc @@ -1,54 +1,72 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/space-age/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import SpaceAge exposing [age] # age on Earth -expect - result = age(Earth, 1000000000) - Num.is_approx_eq(result, 31.69, { atol: 0.01 }) +expect { + result = age(Earth, 1000000000) + is_approx_eq(result, 31.69, { atol: 0.01 }) +} # age on Mercury -expect - result = age(Mercury, 2134835688) - Num.is_approx_eq(result, 280.88, { atol: 0.01 }) +expect { + result = age(Mercury, 2134835688) + is_approx_eq(result, 280.88, { atol: 0.01 }) +} # age on Venus -expect - result = age(Venus, 189839836) - Num.is_approx_eq(result, 9.78, { atol: 0.01 }) +expect { + result = age(Venus, 189839836) + is_approx_eq(result, 9.78, { atol: 0.01 }) +} # age on Mars -expect - result = age(Mars, 2129871239) - Num.is_approx_eq(result, 35.88, { atol: 0.01 }) +expect { + result = age(Mars, 2129871239) + is_approx_eq(result, 35.88, { atol: 0.01 }) +} # age on Jupiter -expect - result = age(Jupiter, 901876382) - Num.is_approx_eq(result, 2.41, { atol: 0.01 }) +expect { + result = age(Jupiter, 901876382) + is_approx_eq(result, 2.41, { atol: 0.01 }) +} # age on Saturn -expect - result = age(Saturn, 2000000000) - Num.is_approx_eq(result, 2.15, { atol: 0.01 }) +expect { + result = age(Saturn, 2000000000) + is_approx_eq(result, 2.15, { atol: 0.01 }) +} # age on Uranus -expect - result = age(Uranus, 1210123456) - Num.is_approx_eq(result, 0.46, { atol: 0.01 }) +expect { + result = age(Uranus, 1210123456) + is_approx_eq(result, 0.46, { atol: 0.01 }) +} # age on Neptune -expect - result = age(Neptune, 1821023456) - Num.is_approx_eq(result, 0.35, { atol: 0.01 }) +expect { + result = age(Neptune, 1821023456) + is_approx_eq(result, 0.35, { atol: 0.01 }) +} + +# The following function should soon be available in Roc's builtins +is_approx_eq : Dec, Dec, { atol : Dec } -> Bool +is_approx_eq = |x, y, { atol }| { + to_int : Dec -> Try(I64, [OutOfRange]) + to_int = |f| { + (f / atol + 0.5).to_i64_try() + } + match (to_int(x), to_int(y)) { + (Ok(xi), Ok(yi)) => (xi == yi) + _ => Bool.False + } +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/spiral-matrix/.meta/Example.roc b/exercises/practice/spiral-matrix/.meta/Example.roc index 94923e23..57088cfa 100644 --- a/exercises/practice/spiral-matrix/.meta/Example.roc +++ b/exercises/practice/spiral-matrix/.meta/Example.roc @@ -1,31 +1,49 @@ -module [spiral_matrix] +SpiralMatrix :: {}.{ + spiral_matrix : U64 -> List(List(U64)) + spiral_matrix = |size| { + zero_matrix = List.repeat(List.repeat(0, size), size) + (1..=(size * size)) + .fold( + { x: -1, y: 0, dx: 1, dy: 0, matrix: zero_matrix }, + |state, index| { + get_value : I64, I64 -> Try(U64, _) + get_value = |x_i64, y_i64| { + row = state.matrix.get(y_i64.to_u64_try()?)? + row.get(x_i64.to_u64_try()?) + } -spiral_matrix : U64 -> List (List U64) -spiral_matrix = |size| - zero_matrix = List.repeat(List.repeat(0, size), size) - List.range({ start: At(1), end: At((size * size)) }) - |> List.walk( - { x: -1, y: 0, dx: 1, dy: 0, matrix: zero_matrix }, - |state, index| - get_value = |x_i64, y_i64| - row = state.matrix |> List.get(Num.to_u64(y_i64)) |> Result.with_default([]) - row |> List.get(Num.to_u64(x_i64)) + (dx, dy) = { + (nx, ny) = (state.x + state.dx, state.y + state.dy) + if nx < 0 or ny < 0 or get_value(nx, ny) != Ok(0) { + (-state.dy, state.dx) + } else { + (state.dx, state.dy) + } + } - (dx, dy) = - (nx, ny) = (state.x + state.dx, state.y + state.dy) - if nx < 0 or ny < 0 or get_value(nx, ny) != Ok(0) then - (-state.dy, state.dx) # outside or non-zero value => turn right - else - (state.dx, state.dy) # or else continue in the same direction - - (x, y) = (state.x + dx, state.y + dy) - matrix = - state.matrix - |> List.update( - (y |> Num.to_u64), - |row| - row |> List.update((x |> Num.to_u64), |_| index), - ) - { x, y, dx, dy, matrix }, - ) - |> .matrix + (x, y) = (state.x + dx, state.y + dy) + matrix = + state.matrix + .update( + y.to_u64_try() ?? { + crash "Unreachable" + }, + |row| { + row.update( + x.to_u64_try() ?? { + crash "Unreachable" + }, + |_| index, + ) ?? { + crash "Unreachable" + } + }, + ) ?? { + crash "Unreachable" + } + { x, y, dx, dy, matrix } + }, + ) + .matrix + } +} diff --git a/exercises/practice/spiral-matrix/.meta/template.j2 b/exercises/practice/spiral-matrix/.meta/template.j2 index 9f973930..0a917df8 100644 --- a/exercises/practice/spiral-matrix/.meta/template.j2 +++ b/exercises/practice/spiral-matrix/.meta/template.j2 @@ -6,7 +6,7 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["size"] }}) {%- if case["expected"] == [] %} result == [] @@ -18,5 +18,8 @@ expect ] result == expected {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/spiral-matrix/SpiralMatrix.roc b/exercises/practice/spiral-matrix/SpiralMatrix.roc index 8e313a2f..bf2ba92d 100644 --- a/exercises/practice/spiral-matrix/SpiralMatrix.roc +++ b/exercises/practice/spiral-matrix/SpiralMatrix.roc @@ -1,5 +1,6 @@ -module [spiral_matrix] - -spiral_matrix : U64 -> List (List U64) -spiral_matrix = |size| - crash("Please implement the 'spiral_matrix' function") +SpiralMatrix :: {}.{ + spiral_matrix : U64 -> List(List(U64)) + spiral_matrix = |size| { + crash "Please implement the 'spiral_matrix' function" + } +} diff --git a/exercises/practice/spiral-matrix/spiral-matrix-test.roc b/exercises/practice/spiral-matrix/spiral-matrix-test.roc index 319e124d..fed93d09 100644 --- a/exercises/practice/spiral-matrix/spiral-matrix-test.roc +++ b/exercises/practice/spiral-matrix/spiral-matrix-test.roc @@ -1,69 +1,71 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/spiral-matrix/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import SpiralMatrix exposing [spiral_matrix] # empty spiral -expect - result = spiral_matrix(0) - result == [] +expect { + result = spiral_matrix(0) + result == [] +} # trivial spiral -expect - result = spiral_matrix(1) - expected = [ - [1], - ] - result == expected +expect { + result = spiral_matrix(1) + expected = [ + [1], + ] + result == expected +} # spiral of size 2 -expect - result = spiral_matrix(2) - expected = [ - [1, 2], - [4, 3], - ] - result == expected +expect { + result = spiral_matrix(2) + expected = [ + [1, 2], + [4, 3], + ] + result == expected +} # spiral of size 3 -expect - result = spiral_matrix(3) - expected = [ - [1, 2, 3], - [8, 9, 4], - [7, 6, 5], - ] - result == expected +expect { + result = spiral_matrix(3) + expected = [ + [1, 2, 3], + [8, 9, 4], + [7, 6, 5], + ] + result == expected +} # spiral of size 4 -expect - result = spiral_matrix(4) - expected = [ - [1, 2, 3, 4], - [12, 13, 14, 5], - [11, 16, 15, 6], - [10, 9, 8, 7], - ] - result == expected +expect { + result = spiral_matrix(4) + expected = [ + [1, 2, 3, 4], + [12, 13, 14, 5], + [11, 16, 15, 6], + [10, 9, 8, 7], + ] + result == expected +} # spiral of size 5 -expect - result = spiral_matrix(5) - expected = [ - [1, 2, 3, 4, 5], - [16, 17, 18, 19, 6], - [15, 24, 25, 20, 7], - [14, 23, 22, 21, 8], - [13, 12, 11, 10, 9], - ] - result == expected +expect { + result = spiral_matrix(5) + expected = [ + [1, 2, 3, 4, 5], + [16, 17, 18, 19, 6], + [15, 24, 25, 20, 7], + [14, 23, 22, 21, 8], + [13, 12, 11, 10, 9], + ] + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/square-root/.docs/instructions.append.md b/exercises/practice/square-root/.docs/instructions.append.md index b91263a2..7008cfcd 100644 --- a/exercises/practice/square-root/.docs/instructions.append.md +++ b/exercises/practice/square-root/.docs/instructions.append.md @@ -2,11 +2,11 @@ ## Implementation -This problem can be solved easily using Roc's built-in `Num` module, including the [`Num.sqrt`][sqrt] function. +This problem can be solved easily using [Roc's built-in `sqrt` function](sqrt). However, we'd like you to consider the challenge of solving this exercise without using built-ins or modules. While there is a mathematical formula that will find the square root of _any_ number, we have gone the route of having only [natural numbers][natural-number] (positive integers) as solutions. -[sqrt]: https://www.roc-lang.org/builtins/Num#sqrt +[sqrt]: https://www.roc-lang.org/builtins/main/#Builtin.Num.F64.sqrt [natural-number]: https://en.wikipedia.org/wiki/Natural_number diff --git a/exercises/practice/square-root/.meta/Example.roc b/exercises/practice/square-root/.meta/Example.roc index 1f559797..2377d2e4 100644 --- a/exercises/practice/square-root/.meta/Example.roc +++ b/exercises/practice/square-root/.meta/Example.roc @@ -1,19 +1,20 @@ -module [square_root, square_rootTheSimpleWay] +SquareRoot :: {}.{ + square_root : U64 -> U64 + square_root = |radicand| { + binary_search = |min, max, target_square| { + val = (min + max) // 2 + square = val * val + if square == target_square or min >= max { + val + } else if square > target_square { + binary_search(min, (val - 1), target_square) + } else { + binary_search((val + 1), max, target_square) + } + } -square_root : U64 -> U64 -square_root = |radicand| - binary_search = |min, max| - val = (min + max) // 2 - square = val * val - if square == radicand or min >= max then - val - else if square > radicand then - binary_search(min, (val - 1)) - else - binary_search((val + 1), max) + binary_search(0, radicand, radicand) + } +} - binary_search(0, radicand) - -square_rootTheSimpleWay = |radicand| - # This works too... but it's cheating, right? - radicand |> Num.to_f64 |> Num.sqrt |> Num.round +# TODO: update once https://github.com/roc-lang/roc/issues/9690 is fixed diff --git a/exercises/practice/square-root/.meta/template.j2 b/exercises/practice/square-root/.meta/template.j2 index 9f905aa8..fa85fcf3 100644 --- a/exercises/practice/square-root/.meta/template.j2 +++ b/exercises/practice/square-root/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["radicand"] | to_roc }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/square-root/SquareRoot.roc b/exercises/practice/square-root/SquareRoot.roc index b8070acf..3e95499f 100644 --- a/exercises/practice/square-root/SquareRoot.roc +++ b/exercises/practice/square-root/SquareRoot.roc @@ -1,5 +1,6 @@ -module [square_root] - -square_root : U64 -> U64 -square_root = |radicand| - crash("Please implement the 'square_root' function") +SquareRoot :: {}.{ + square_root : U64 -> U64 + square_root = |radicand| { + crash "Please implement the 'square_root' function" + } +} diff --git a/exercises/practice/square-root/square-root-test.roc b/exercises/practice/square-root/square-root-test.roc index 8f57f30e..09776269 100644 --- a/exercises/practice/square-root/square-root-test.roc +++ b/exercises/practice/square-root/square-root-test.roc @@ -1,44 +1,46 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/square-root/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import SquareRoot exposing [square_root] # root of 1 -expect - result = square_root(1) - result == 1 +expect { + result = square_root(1) + result == 1 +} # root of 4 -expect - result = square_root(4) - result == 2 +expect { + result = square_root(4) + result == 2 +} # root of 25 -expect - result = square_root(25) - result == 5 +expect { + result = square_root(25) + result == 5 +} # root of 81 -expect - result = square_root(81) - result == 9 +expect { + result = square_root(81) + result == 9 +} # root of 196 -expect - result = square_root(196) - result == 14 +expect { + result = square_root(196) + result == 14 +} # root of 65025 -expect - result = square_root(65025) - result == 255 +expect { + result = square_root(65025) + result == 255 +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/strain/.meta/Example.roc b/exercises/practice/strain/.meta/Example.roc index 825bb268..1a933a18 100644 --- a/exercises/practice/strain/.meta/Example.roc +++ b/exercises/practice/strain/.meta/Example.roc @@ -1,27 +1,30 @@ -module [keep, discard] +Strain :: {}.{ + keep : List(a), (a -> Bool) -> List(a) + keep = |list, predicate| { + var $result = [] + for item in list { + if predicate(item) { + $result = $result.append(item) + } + } + $result + } -keep : List a, (a -> Bool) -> List a -keep = |list, predicate| - loop = |sub_list, kept_items| - when sub_list is - [] -> kept_items - [first, .. as rest] -> - if predicate(first) then - rest |> loop(List.append(kept_items, first)) - else - rest |> loop(kept_items) + discard : List(a), (a -> Bool) -> List(a) + discard = |list, predicate| { + loop = |sub_list, non_discarded_items| { + match sub_list { + [] => non_discarded_items + [first, .. as rest] => { + if predicate(first) { + rest->loop(non_discarded_items) + } else { + rest->loop(non_discarded_items.append(first)) + } + } + } + } - loop(list, []) - -discard : List a, (a -> Bool) -> List a -discard = |list, predicate| - loop = |sub_list, non_discarded_items| - when sub_list is - [] -> non_discarded_items - [first, .. as rest] -> - if predicate(first) then - rest |> loop(non_discarded_items) - else - rest |> loop(List.append(non_discarded_items, first)) - - loop(list, []) + loop(list, []) + } +} diff --git a/exercises/practice/strain/.meta/template.j2 b/exercises/practice/strain/.meta/template.j2 index 4635e345..515b1223 100644 --- a/exercises/practice/strain/.meta/template.j2 +++ b/exercises/practice/strain/.meta/template.j2 @@ -5,21 +5,24 @@ import {{ exercise | to_pascal }} exposing [keep, discard] {% set function_map = { - "fn(x) -> contains(x, 5)": "|x| x |> List.contains(5)", - "fn(x) -> false": "\\_ -> Bool.false", - "fn(x) -> starts_with(x, 'z')": "|x| x |> Str.starts_with(\"z\")", - "fn(x) -> true": "\\_ -> Bool.true", - "fn(x) -> x % 2 == 0": "\\x -> x % 2 == 0", - "fn(x) -> x % 2 == 1": "\\x -> x % 2 == 1", + "fn(x) -> contains(x, 5)": "|x| x.contains(5)", + "fn(x) -> false": "|_| Bool.False", + "fn(x) -> starts_with(x, 'z')": "|x| x.starts_with(\"z\")", + "fn(x) -> true": "|_| Bool.True", + "fn(x) -> x % 2 == 0": "|x| x % 2 == 0", + "fn(x) -> x % 2 == 1": "|x| x % 2 == 1", } %} {% for case in cases -%} # {{ case["description"] }} -expect +expect { list = {{ case["input"]["list"] | to_roc }} - result = list |> {{ case["property"] | to_snake }}({{ function_map[case["input"]["predicate"]] }}) + result = list -> {{ case["property"] | to_snake }}({{ function_map[case["input"]["predicate"]] }}) expected = {{ case["expected"] | to_roc }} result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/strain/Strain.roc b/exercises/practice/strain/Strain.roc index d55941da..197fb440 100644 --- a/exercises/practice/strain/Strain.roc +++ b/exercises/practice/strain/Strain.roc @@ -1,9 +1,11 @@ -module [keep, discard] +Strain :: {}.{ + keep : List(a), (a -> Bool) -> List(a) + keep = |list, predicate| { + crash "Please implement the 'keep' function" + } -keep : List a, (a -> Bool) -> List a -keep = |list, predicate| - crash("Please implement the 'keep' function") - -discard : List a, (a -> Bool) -> List a -discard = |list, predicate| - crash("Please implement the 'discard' function") + discard : List(a), (a -> Bool) -> List(a) + discard = |list, predicate| { + crash "Please implement the 'discard' function" + } +} diff --git a/exercises/practice/strain/strain-test.roc b/exercises/practice/strain/strain-test.roc index ed49e927..d8683514 100644 --- a/exercises/practice/strain/strain-test.roc +++ b/exercises/practice/strain/strain-test.roc @@ -1,112 +1,122 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/strain/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Strain exposing [keep, discard] # keep on empty list returns empty list -expect - list = [] - result = list |> keep(|_| Bool.true) - expected = [] - result == expected +expect { + list = [] + result = list->keep(|_| Bool.True) + expected = [] + result == expected +} # keeps everything -expect - list = [1, 3, 5] - result = list |> keep(|_| Bool.true) - expected = [1, 3, 5] - result == expected +expect { + list = [1, 3, 5] + result = list->keep(|_| Bool.True) + expected = [1, 3, 5] + result == expected +} # keeps nothing -expect - list = [1, 3, 5] - result = list |> keep(|_| Bool.false) - expected = [] - result == expected +expect { + list = [1, 3, 5] + result = list->keep(|_| Bool.False) + expected = [] + result == expected +} # keeps first and last -expect - list = [1, 2, 3] - result = list |> keep(|x| x % 2 == 1) - expected = [1, 3] - result == expected +expect { + list = [1, 2, 3] + result = list->keep(|x| x % 2 == 1) + expected = [1, 3] + result == expected +} # keeps neither first nor last -expect - list = [1, 2, 3] - result = list |> keep(|x| x % 2 == 0) - expected = [2] - result == expected +expect { + list = [1, 2, 3] + result = list->keep(|x| x % 2 == 0) + expected = [2] + result == expected +} # keeps strings -expect - list = ["apple", "zebra", "banana", "zombies", "cherimoya", "zealot"] - result = list |> keep(|x| x |> Str.starts_with("z")) - expected = ["zebra", "zombies", "zealot"] - result == expected +expect { + list = ["apple", "zebra", "banana", "zombies", "cherimoya", "zealot"] + result = list->keep(|x| x.starts_with("z")) + expected = ["zebra", "zombies", "zealot"] + result == expected +} # keeps lists -expect - list = [[1, 2, 3], [5, 5, 5], [5, 1, 2], [2, 1, 2], [1, 5, 2], [2, 2, 1], [1, 2, 5]] - result = list |> keep(|x| x |> List.contains(5)) - expected = [[5, 5, 5], [5, 1, 2], [1, 5, 2], [1, 2, 5]] - result == expected +expect { + list = [[1, 2, 3], [5, 5, 5], [5, 1, 2], [2, 1, 2], [1, 5, 2], [2, 2, 1], [1, 2, 5]] + result = list->keep(|x| x.contains(5)) + expected = [[5, 5, 5], [5, 1, 2], [1, 5, 2], [1, 2, 5]] + result == expected +} # discard on empty list returns empty list -expect - list = [] - result = list |> discard(|_| Bool.true) - expected = [] - result == expected +expect { + list = [] + result = list->discard(|_| Bool.True) + expected = [] + result == expected +} # discards everything -expect - list = [1, 3, 5] - result = list |> discard(|_| Bool.true) - expected = [] - result == expected +expect { + list = [1, 3, 5] + result = list->discard(|_| Bool.True) + expected = [] + result == expected +} # discards nothing -expect - list = [1, 3, 5] - result = list |> discard(|_| Bool.false) - expected = [1, 3, 5] - result == expected +expect { + list = [1, 3, 5] + result = list->discard(|_| Bool.False) + expected = [1, 3, 5] + result == expected +} # discards first and last -expect - list = [1, 2, 3] - result = list |> discard(|x| x % 2 == 1) - expected = [2] - result == expected +expect { + list = [1, 2, 3] + result = list->discard(|x| x % 2 == 1) + expected = [2] + result == expected +} # discards neither first nor last -expect - list = [1, 2, 3] - result = list |> discard(|x| x % 2 == 0) - expected = [1, 3] - result == expected +expect { + list = [1, 2, 3] + result = list->discard(|x| x % 2 == 0) + expected = [1, 3] + result == expected +} # discards strings -expect - list = ["apple", "zebra", "banana", "zombies", "cherimoya", "zealot"] - result = list |> discard(|x| x |> Str.starts_with("z")) - expected = ["apple", "banana", "cherimoya"] - result == expected +expect { + list = ["apple", "zebra", "banana", "zombies", "cherimoya", "zealot"] + result = list->discard(|x| x.starts_with("z")) + expected = ["apple", "banana", "cherimoya"] + result == expected +} # discards lists -expect - list = [[1, 2, 3], [5, 5, 5], [5, 1, 2], [2, 1, 2], [1, 5, 2], [2, 2, 1], [1, 2, 5]] - result = list |> discard(|x| x |> List.contains(5)) - expected = [[1, 2, 3], [2, 1, 2], [2, 2, 1]] - result == expected +expect { + list = [[1, 2, 3], [5, 5, 5], [5, 1, 2], [2, 1, 2], [1, 5, 2], [2, 2, 1], [1, 2, 5]] + result = list->discard(|x| x.contains(5)) + expected = [[1, 2, 3], [2, 1, 2], [2, 2, 1]] + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/sublist/.meta/Example.roc b/exercises/practice/sublist/.meta/Example.roc index 3ba0ea99..e4db77d4 100644 --- a/exercises/practice/sublist/.meta/Example.roc +++ b/exercises/practice/sublist/.meta/Example.roc @@ -1,29 +1,57 @@ -module [sublist] +Sublist :: {}.{ + sublist : List(U8), List(U8) -> [Equal, Sublist, Superlist, Unequal] + sublist = |list1, list2| { + match list1.len()->compare(list2.len()) { + GT => { + match sublist(list2, list1) { + Sublist => Superlist + Unequal => Unequal + Superlist => { + crash "Unreachable: list 2 is shorter than list 1" + } + Equal => { + crash "Unreachable: list 1 and 2 don't have the same length" + } + } + } -sublist : List U8, List U8 -> [Equal, Sublist, Superlist, Unequal] -sublist = |list1, list2| - when List.len(list1) |> Num.compare(List.len(list2)) is - GT -> - when list2 |> sublist(list1) is - Sublist -> Superlist - Unequal -> Unequal - Superlist -> crash("Unreachable: list 2 is shorter than list 1") - Equal -> crash("Unreachable: list 1 and 2 don't have the same length") + EQ => { + if list1 == list2 { + Equal + } else { + Unequal + } + } - EQ -> - if list1 == list2 then Equal else Unequal + LT => { + length_diff = list2.len() - list1.len() + maybe_equal_index = + (0..=length_diff) + .fold([], |acc, x| acc.append(x)) + .find_first( + |start| { + list2 + .sublist({ start, len: list1.len() }) + == list1 + }, + ) - LT -> - length_diff = List.len(list2) - List.len(list1) - maybe_equal_index = - List.range({ start: At(0), end: At(length_diff) }) - |> List.find_first( - |start| - list2 - |> List.sublist({ start, len: List.len(list1) }) - |> Bool.is_eq(list1), - ) + match maybe_equal_index { + Ok(_) => Sublist + Err(NotFound) => Unequal + } + } + } + } +} - when maybe_equal_index is - Ok(_) -> Sublist - Err(NotFound) -> Unequal +# The following function should soon be available in Roc's builtins +compare = |a, b| { + if a < b { + LT + } else if a > b { + GT + } else { + EQ + } +} diff --git a/exercises/practice/sublist/.meta/template.j2 b/exercises/practice/sublist/.meta/template.j2 index 9b1776c0..59a2327a 100644 --- a/exercises/practice/sublist/.meta/template.j2 +++ b/exercises/practice/sublist/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect - result = {{ case["input"]["listOne"] | to_roc }} |> {{ case["property"] | to_snake }}({{ case["input"]["listTwo"] | to_roc }}) +expect { + result = {{ case["input"]["listOne"] | to_roc }} -> {{ case["property"] | to_snake }}({{ case["input"]["listTwo"] | to_roc }}) result == {{ case["expected"] | to_pascal }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/sublist/Sublist.roc b/exercises/practice/sublist/Sublist.roc index 98a1af62..658084f1 100644 --- a/exercises/practice/sublist/Sublist.roc +++ b/exercises/practice/sublist/Sublist.roc @@ -1,5 +1,6 @@ -module [sublist] - -sublist : List U8, List U8 -> [Equal, Sublist, Superlist, Unequal] -sublist = |list1, list2| - crash("Please implement the 'sublist' function") +Sublist :: {}.{ + sublist : List(U8), List(U8) -> [Equal, Sublist, Superlist, Unequal] + sublist = |list1, list2| { + crash "Please implement the 'sublist' function" + } +} diff --git a/exercises/practice/sublist/sublist-test.roc b/exercises/practice/sublist/sublist-test.roc index 36ba4198..15ef6c1f 100644 --- a/exercises/practice/sublist/sublist-test.roc +++ b/exercises/practice/sublist/sublist-test.roc @@ -1,104 +1,118 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/sublist/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Sublist exposing [sublist] # empty lists -expect - result = [] |> sublist([]) - result == Equal +expect { + result = []->sublist([]) + result == Equal +} # empty list within non empty list -expect - result = [] |> sublist([1, 2, 3]) - result == Sublist +expect { + result = []->sublist([1, 2, 3]) + result == Sublist +} # non empty list contains empty list -expect - result = [1, 2, 3] |> sublist([]) - result == Superlist +expect { + result = [1, 2, 3]->sublist([]) + result == Superlist +} # list equals itself -expect - result = [1, 2, 3] |> sublist([1, 2, 3]) - result == Equal +expect { + result = [1, 2, 3]->sublist([1, 2, 3]) + result == Equal +} # different lists -expect - result = [1, 2, 3] |> sublist([2, 3, 4]) - result == Unequal +expect { + result = [1, 2, 3]->sublist([2, 3, 4]) + result == Unequal +} # false start -expect - result = [1, 2, 5] |> sublist([0, 1, 2, 3, 1, 2, 5, 6]) - result == Sublist +expect { + result = [1, 2, 5]->sublist([0, 1, 2, 3, 1, 2, 5, 6]) + result == Sublist +} # consecutive -expect - result = [1, 1, 2] |> sublist([0, 1, 1, 1, 2, 1, 2]) - result == Sublist +expect { + result = [1, 1, 2]->sublist([0, 1, 1, 1, 2, 1, 2]) + result == Sublist +} # sublist at start -expect - result = [0, 1, 2] |> sublist([0, 1, 2, 3, 4, 5]) - result == Sublist +expect { + result = [0, 1, 2]->sublist([0, 1, 2, 3, 4, 5]) + result == Sublist +} # sublist in middle -expect - result = [2, 3, 4] |> sublist([0, 1, 2, 3, 4, 5]) - result == Sublist +expect { + result = [2, 3, 4]->sublist([0, 1, 2, 3, 4, 5]) + result == Sublist +} # sublist at end -expect - result = [3, 4, 5] |> sublist([0, 1, 2, 3, 4, 5]) - result == Sublist +expect { + result = [3, 4, 5]->sublist([0, 1, 2, 3, 4, 5]) + result == Sublist +} # at start of superlist -expect - result = [0, 1, 2, 3, 4, 5] |> sublist([0, 1, 2]) - result == Superlist +expect { + result = [0, 1, 2, 3, 4, 5]->sublist([0, 1, 2]) + result == Superlist +} # in middle of superlist -expect - result = [0, 1, 2, 3, 4, 5] |> sublist([2, 3]) - result == Superlist +expect { + result = [0, 1, 2, 3, 4, 5]->sublist([2, 3]) + result == Superlist +} # at end of superlist -expect - result = [0, 1, 2, 3, 4, 5] |> sublist([3, 4, 5]) - result == Superlist +expect { + result = [0, 1, 2, 3, 4, 5]->sublist([3, 4, 5]) + result == Superlist +} # first list missing element from second list -expect - result = [1, 3] |> sublist([1, 2, 3]) - result == Unequal +expect { + result = [1, 3]->sublist([1, 2, 3]) + result == Unequal +} # second list missing element from first list -expect - result = [1, 2, 3] |> sublist([1, 3]) - result == Unequal +expect { + result = [1, 2, 3]->sublist([1, 3]) + result == Unequal +} # first list missing additional digits from second list -expect - result = [1, 2] |> sublist([1, 22]) - result == Unequal +expect { + result = [1, 2]->sublist([1, 22]) + result == Unequal +} # order matters to a list -expect - result = [1, 2, 3] |> sublist([3, 2, 1]) - result == Unequal +expect { + result = [1, 2, 3]->sublist([3, 2, 1]) + result == Unequal +} # same digits but different numbers -expect - result = [1, 0, 1] |> sublist([10, 1]) - result == Unequal +expect { + result = [1, 0, 1]->sublist([10, 1]) + result == Unequal +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/sum-of-multiples/.meta/Example.roc b/exercises/practice/sum-of-multiples/.meta/Example.roc index d3fd8200..286982a8 100644 --- a/exercises/practice/sum-of-multiples/.meta/Example.roc +++ b/exercises/practice/sum-of-multiples/.meta/Example.roc @@ -1,13 +1,22 @@ -module [sum_of_multiples] +SumOfMultiples :: {}.{ + sum_of_multiples : List(U64), U64 -> U64 + sum_of_multiples = |factors, limit| { + factors + .keep_if(|factor| factor > 0) + ->join_map(|factor| (factor..List.from_iter()) + ->Set.from_list() + .to_list() + .sum() + } +} -sum_of_multiples : List U64, U64 -> U64 -sum_of_multiples = |factors, limit| - factors - |> List.keep_if(|factor| factor > 0) - |> List.join_map( - |factor| - List.range({ start: At(factor), end: Before(limit), step: factor }), - ) - |> Set.from_list - |> Set.to_list - |> List.sum +# The following function should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} diff --git a/exercises/practice/sum-of-multiples/.meta/template.j2 b/exercises/practice/sum-of-multiples/.meta/template.j2 index 52e64a81..ddf38306 100644 --- a/exercises/practice/sum-of-multiples/.meta/template.j2 +++ b/exercises/practice/sum-of-multiples/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ exercise | to_snake }}] {% for case in cases -%} # {{ case["description"] }} -expect - result = {{ case["input"]["factors"] | to_roc }} |> {{ exercise | to_snake }}({{ case["input"]["limit"] }}) +expect { + result = {{ case["input"]["factors"] | to_roc }} -> {{ exercise | to_snake }}({{ case["input"]["limit"] }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/sum-of-multiples/SumOfMultiples.roc b/exercises/practice/sum-of-multiples/SumOfMultiples.roc index fca4b8c1..57741519 100644 --- a/exercises/practice/sum-of-multiples/SumOfMultiples.roc +++ b/exercises/practice/sum-of-multiples/SumOfMultiples.roc @@ -1,5 +1,6 @@ -module [sum_of_multiples] - -sum_of_multiples : List U64, U64 -> U64 -sum_of_multiples = |factors, limit| - crash("Please implement the 'sum_of_multiples' function") +SumOfMultiples :: {}.{ + sum_of_multiples : List(U64), U64 -> U64 + sum_of_multiples = |factors, limit| { + crash "Please implement the 'sum_of_multiples' function" + } +} diff --git a/exercises/practice/sum-of-multiples/sum-of-multiples-test.roc b/exercises/practice/sum-of-multiples/sum-of-multiples-test.roc index 30bb1bba..3f607c52 100644 --- a/exercises/practice/sum-of-multiples/sum-of-multiples-test.roc +++ b/exercises/practice/sum-of-multiples/sum-of-multiples-test.roc @@ -1,94 +1,106 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/sum-of-multiples/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import SumOfMultiples exposing [sum_of_multiples] # no multiples within limit -expect - result = [3, 5] |> sum_of_multiples(1) - result == 0 +expect { + result = [3, 5]->sum_of_multiples(1) + result == 0 +} # one factor has multiples within limit -expect - result = [3, 5] |> sum_of_multiples(4) - result == 3 +expect { + result = [3, 5]->sum_of_multiples(4) + result == 3 +} # more than one multiple within limit -expect - result = [3] |> sum_of_multiples(7) - result == 9 +expect { + result = [3]->sum_of_multiples(7) + result == 9 +} # more than one factor with multiples within limit -expect - result = [3, 5] |> sum_of_multiples(10) - result == 23 +expect { + result = [3, 5]->sum_of_multiples(10) + result == 23 +} # each multiple is only counted once -expect - result = [3, 5] |> sum_of_multiples(100) - result == 2318 +expect { + result = [3, 5]->sum_of_multiples(100) + result == 2318 +} # a much larger limit -expect - result = [3, 5] |> sum_of_multiples(1000) - result == 233168 +expect { + result = [3, 5]->sum_of_multiples(1000) + result == 233168 +} # three factors -expect - result = [7, 13, 17] |> sum_of_multiples(20) - result == 51 +expect { + result = [7, 13, 17]->sum_of_multiples(20) + result == 51 +} # factors not relatively prime -expect - result = [4, 6] |> sum_of_multiples(15) - result == 30 +expect { + result = [4, 6]->sum_of_multiples(15) + result == 30 +} # some pairs of factors relatively prime and some not -expect - result = [5, 6, 8] |> sum_of_multiples(150) - result == 4419 +expect { + result = [5, 6, 8]->sum_of_multiples(150) + result == 4419 +} # one factor is a multiple of another -expect - result = [5, 25] |> sum_of_multiples(51) - result == 275 +expect { + result = [5, 25]->sum_of_multiples(51) + result == 275 +} # much larger factors -expect - result = [43, 47] |> sum_of_multiples(10000) - result == 2203160 +expect { + result = [43, 47]->sum_of_multiples(10000) + result == 2203160 +} # all numbers are multiples of 1 -expect - result = [1] |> sum_of_multiples(100) - result == 4950 +expect { + result = [1]->sum_of_multiples(100) + result == 4950 +} # no factors means an empty sum -expect - result = [] |> sum_of_multiples(10000) - result == 0 +expect { + result = []->sum_of_multiples(10000) + result == 0 +} # the only multiple of 0 is 0 -expect - result = [0] |> sum_of_multiples(1) - result == 0 +expect { + result = [0]->sum_of_multiples(1) + result == 0 +} # the factor 0 does not affect the sum of multiples of other factors -expect - result = [3, 0] |> sum_of_multiples(4) - result == 3 +expect { + result = [3, 0]->sum_of_multiples(4) + result == 3 +} # solutions using include-exclude must extend to cardinality greater than 3 -expect - result = [2, 3, 5, 7, 11] |> sum_of_multiples(10000) - result == 39614537 +expect { + result = [2, 3, 5, 7, 11]->sum_of_multiples(10000) + result == 39614537 +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/tournament/.meta/Example.roc b/exercises/practice/tournament/.meta/Example.roc index 618df903..a6fa76c4 100644 --- a/exercises/practice/tournament/.meta/Example.roc +++ b/exercises/practice/tournament/.meta/Example.roc @@ -1,117 +1,188 @@ -module [tally] +Tournament :: {}.{ + tally : Str -> Try(Str, [InvalidRow(Str), InvalidResult(Str)]) + tally = |table| { + if table == "" { + Ok(header) + } else { + rows = table.split_on("\n") + parsed_rows = + rows->map_try( + |row| { + match row.split_on(";") { + [team1, team2, result_str] => { + result = parse_result(result_str)? + Ok((team1, team2, result)) + } + _ => Err(InvalidRow(row)) + } + }, + )? + tally_dict = { + parsed_rows.fold( + Dict.empty(), + |tally_dict_acc, triple| { + (team1, team2, result) = triple + tally_dict_acc + ->update_tally_dict(team1, result) + ->update_tally_dict(team2, opposite_result(result)) + }, + ) + } + Ok(tally_dict_to_table(tally_dict)) + } + } +} MatchResult : [Win, Loss, Draw] -TeamTally : { mp : U64, w : U64, d : U64, l : U64, p : U64 } - -tally : Str -> Result Str [InvalidRow Str, InvalidResult Str] -tally = |table| - if table == "" then - Ok(header) - else - table - |> Str.split_on("\n") - |> List.map_try( - |row| - when row |> Str.split_on(";") is - [team1, team2, result_str] -> - result = result_str |> parse_result? - Ok((team1, team2, result)) - _ -> Err(InvalidRow(row)), - )? - |> List.walk( - Dict.empty({}), - |tally_dict, (team1, team2, result)| - tally_dict - |> update_tally_dict(team1, result) - |> update_tally_dict(team2, opposite_result(result)), - ) - |> tally_dict_to_table - |> Ok +TeamTally : { mp : U64, w : U64, d : U64, l : U64, p : U64 } -parse_result : Str -> Result MatchResult [InvalidResult Str] -parse_result = |result_str| - when result_str is - "win" -> Ok(Win) - "loss" -> Ok(Loss) - "draw" -> Ok(Draw) - _ -> Err(InvalidResult(result_str)) +parse_result : Str -> Try(MatchResult, [InvalidResult(Str), ..]) +parse_result = |result_str| { + match result_str { + "win" => Ok(Win) + "loss" => Ok(Loss) + "draw" => Ok(Draw) + _ => Err(InvalidResult(result_str)) + } +} opposite_result : MatchResult -> MatchResult -opposite_result = |result| - when result is - Win -> Loss - Loss -> Win - Draw -> Draw +opposite_result = |result| { + match result { + Win => Loss + Loss => Win + Draw => Draw + } +} -update_tally_dict : Dict Str TeamTally, Str, MatchResult -> Dict Str TeamTally -update_tally_dict = |tally_dict, team, result| - tally_dict - |> Dict.update( - team, - |maybe_team_tally| - when maybe_team_tally is - Ok(team_tally) -> Ok((team_tally |> update_team_tally(result))) - Err(Missing) -> Ok(({ mp: 0, w: 0, d: 0, l: 0, p: 0 } |> update_team_tally(result))), - ) +update_tally_dict : Dict(Str, TeamTally), Str, MatchResult -> Dict(Str, TeamTally) +update_tally_dict = |tally_dict, team, result| { + tally_dict + .update( + team, + |maybe_team_tally| { + match maybe_team_tally { + Ok(team_tally) => Ok(update_team_tally(team_tally, result)) + Err(Missing) => Ok(update_team_tally({ mp: 0, w: 0, d: 0, l: 0, p: 0 }, result)) + } + }, + ) +} update_team_tally : TeamTally, MatchResult -> TeamTally -update_team_tally = |team_tally, result| - when result is - Win -> { team_tally & mp: team_tally.mp + 1, w: team_tally.w + 1, p: team_tally.p + 3 } - Draw -> { team_tally & mp: team_tally.mp + 1, d: team_tally.d + 1, p: team_tally.p + 1 } - Loss -> { team_tally & mp: team_tally.mp + 1, l: team_tally.l + 1 } +update_team_tally = |team_tally, result| { + match result { + Win => { ..team_tally, mp: team_tally.mp + 1, w: team_tally.w + 1, p: team_tally.p + 3 } + Draw => { ..team_tally, mp: team_tally.mp + 1, d: team_tally.d + 1, p: team_tally.p + 1 } + Loss => { ..team_tally, mp: team_tally.mp + 1, l: team_tally.l + 1 } + } +} -tally_dict_to_table : Dict Str TeamTally -> Str -tally_dict_to_table = |tally_dict| - table_content = - tally_dict - |> Dict.to_list - |> List.sort_with( - |(team1, team_tally1), (team2, team_tally2)| - when Num.compare(team_tally1.p, team_tally2.p) is - GT -> LT - LT -> GT - EQ -> compare_strings(team1, team2), - ) - |> List.map( - |(team, team_tally)| - tally_columns = - [.mp, .w, .d, .l, .p] - |> List.map(|field| team_tally |> field |> align_right) - |> Str.join_with(" | ") - "${team |> pad_right(30)} | ${tally_columns}", - ) - |> Str.join_with("\n") - "${header}\n${table_content}" +tally_dict_to_table : Dict(Str, TeamTally) -> Str +tally_dict_to_table = |tally_dict| { + table_content = + tally_dict + .to_list() + .sort_with( + |pair1, pair2| { + (team1, team_tally1) = pair1 + (team2, team_tally2) = pair2 + p1 = team_tally1.p + p2 = team_tally2.p + if p1 < p2 { + GT + } else if p1 > p2 { + LT + } else { + compare_strings(team1, team2) + } + }, + ) + .map( + |pair| { + (team, team_tally) = pair + tally_columns = + [team_tally.mp, team_tally.w, team_tally.d, team_tally.l, team_tally.p] + .map(align_right) + ->Str.join_with(" | ") + "${pad_right(team, 30)} | ${tally_columns}" + }, + ) + ->Str.join_with("\n") + "${header}\n${table_content}" +} header : Str header = "Team | MP | W | D | L | P" compare_strings : Str, Str -> [LT, EQ, GT] -compare_strings = |string1, string2| - b1 = string1 |> Str.to_utf8 - b2 = string2 |> Str.to_utf8 - result = - List.map2(b1, b2, |c1, c2| Num.compare(c1, c2)) - |> List.walk_try( - Ok(EQ), - |_state, cmp| - when cmp is - EQ -> Ok(EQ) - res -> Err(res), - ) - when result is - Ok(_cmp) -> Num.compare(List.len(b1), List.len(b2)) - Err(res) -> res +compare_strings = |string1, string2| { + b1 = string1.to_utf8() + b2 = string2.to_utf8() + cmp_result = + List.map2( + b1, + b2, + |c1, c2| if c1 < c2 { + LT + } else if c1 > c2 { + GT + } else { + EQ + }, + ) + .fold_until( + EQ, + |_, cmp| { + match cmp { + EQ => Continue(EQ) + res => Break(res) + } + }, + ) + match cmp_result { + EQ => { + len1 = List.len(b1) + len2 = List.len(b2) + if len1 < len2 { + LT + } else if len1 > len2 { + GT + } else { + EQ + } + } + res => res + } +} pad_right : Str, U64 -> Str -pad_right = |string, width| - chars = string |> Str.to_utf8 - length = chars |> List.len - spaces = if length < width then List.repeat(" ", (width - length)) |> Str.join_with("") else "" - "${string}${spaces}" +pad_right = |string, width| { + chars = string.to_utf8() + length = chars.len() + spaces = if length < width { + List.repeat(" ", (width - length))->Str.join_with("") + } else { + "" + } + "${string}${spaces}" +} align_right : U64 -> Str -align_right = |number| - if number < 10 then " ${number |> Num.to_str}" else "${number |> Num.to_str}" +align_right = |number| { + if number < 10 { + " ${number.to_str()}" + } else { + "${number.to_str()}" + } +} + +# The following functions should soon be available in Roc's builtins +map_try = |iter, func| { + var $state = [] + for item in iter { + $state = $state.append(func(item)?) + } + Ok($state) +} diff --git a/exercises/practice/tournament/.meta/template.j2 b/exercises/practice/tournament/.meta/template.j2 index 0a8f88be..1df6f05f 100644 --- a/exercises/practice/tournament/.meta/template.j2 +++ b/exercises/practice/tournament/.meta/template.j2 @@ -6,10 +6,13 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { table = {{ case["input"]["rows"] | to_roc_multiline_string | indent(8) }} result = {{ case["property"] | to_snake }}(table) expected = Ok({{ case["expected"] | to_roc_multiline_string | indent(8) }}) result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/tournament/Tournament.roc b/exercises/practice/tournament/Tournament.roc index 2dffe164..1642ac43 100644 --- a/exercises/practice/tournament/Tournament.roc +++ b/exercises/practice/tournament/Tournament.roc @@ -1,5 +1,6 @@ -module [tally] - -tally : Str -> Result Str _ -tally = |table| - crash("Please implement the 'tally' function") +Tournament :: {}.{ + tally : Str -> Try(Str, _) + tally = |table| { + crash "Please implement the 'tally' function" + } +} diff --git a/exercises/practice/tournament/tournament-test.roc b/exercises/practice/tournament/tournament-test.roc index 29af8863..35c91299 100644 --- a/exercises/practice/tournament/tournament-test.roc +++ b/exercises/practice/tournament/tournament-test.roc @@ -1,213 +1,203 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/tournament/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-13 import Tournament exposing [tally] # just the header if no input -expect - table = "" - result = tally(table) - expected = Ok("Team | MP | W | D | L | P") - result == expected +expect { + table = "" + result = tally(table) + expected = Ok("Team | MP | W | D | L | P") + result == expected +} # a win is three points, a loss is zero points -expect - table = "Allegoric Alaskans;Blithering Badgers;win" - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Allegoric Alaskans | 1 | 1 | 0 | 0 | 3 - Blithering Badgers | 1 | 0 | 0 | 1 | 0 - """, - ) - result == expected +expect { + table = "Allegoric Alaskans;Blithering Badgers;win" + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Allegoric Alaskans | 1 | 1 | 0 | 0 | 3 + \\Blithering Badgers | 1 | 0 | 0 | 1 | 0 + , + ) + result == expected +} # a win can also be expressed as a loss -expect - table = "Blithering Badgers;Allegoric Alaskans;loss" - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Allegoric Alaskans | 1 | 1 | 0 | 0 | 3 - Blithering Badgers | 1 | 0 | 0 | 1 | 0 - """, - ) - result == expected +expect { + table = "Blithering Badgers;Allegoric Alaskans;loss" + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Allegoric Alaskans | 1 | 1 | 0 | 0 | 3 + \\Blithering Badgers | 1 | 0 | 0 | 1 | 0 + , + ) + result == expected +} # a different team can win -expect - table = "Blithering Badgers;Allegoric Alaskans;win" - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Blithering Badgers | 1 | 1 | 0 | 0 | 3 - Allegoric Alaskans | 1 | 0 | 0 | 1 | 0 - """, - ) - result == expected +expect { + table = "Blithering Badgers;Allegoric Alaskans;win" + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Blithering Badgers | 1 | 1 | 0 | 0 | 3 + \\Allegoric Alaskans | 1 | 0 | 0 | 1 | 0 + , + ) + result == expected +} # a draw is one point each -expect - table = "Allegoric Alaskans;Blithering Badgers;draw" - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Allegoric Alaskans | 1 | 0 | 1 | 0 | 1 - Blithering Badgers | 1 | 0 | 1 | 0 | 1 - """, - ) - result == expected +expect { + table = "Allegoric Alaskans;Blithering Badgers;draw" + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Allegoric Alaskans | 1 | 0 | 1 | 0 | 1 + \\Blithering Badgers | 1 | 0 | 1 | 0 | 1 + , + ) + result == expected +} # There can be more than one match -expect - table = - """ - Allegoric Alaskans;Blithering Badgers;win - Allegoric Alaskans;Blithering Badgers;win - """ - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Allegoric Alaskans | 2 | 2 | 0 | 0 | 6 - Blithering Badgers | 2 | 0 | 0 | 2 | 0 - """, - ) - result == expected +expect { + table = + \\Allegoric Alaskans;Blithering Badgers;win + \\Allegoric Alaskans;Blithering Badgers;win + + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Allegoric Alaskans | 2 | 2 | 0 | 0 | 6 + \\Blithering Badgers | 2 | 0 | 0 | 2 | 0 + , + ) + result == expected +} # There can be more than one winner -expect - table = - """ - Allegoric Alaskans;Blithering Badgers;loss - Allegoric Alaskans;Blithering Badgers;win - """ - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Allegoric Alaskans | 2 | 1 | 0 | 1 | 3 - Blithering Badgers | 2 | 1 | 0 | 1 | 3 - """, - ) - result == expected +expect { + table = + \\Allegoric Alaskans;Blithering Badgers;loss + \\Allegoric Alaskans;Blithering Badgers;win + + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Allegoric Alaskans | 2 | 1 | 0 | 1 | 3 + \\Blithering Badgers | 2 | 1 | 0 | 1 | 3 + , + ) + result == expected +} # There can be more than two teams -expect - table = - """ - Allegoric Alaskans;Blithering Badgers;win - Blithering Badgers;Courageous Californians;win - Courageous Californians;Allegoric Alaskans;loss - """ - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Allegoric Alaskans | 2 | 2 | 0 | 0 | 6 - Blithering Badgers | 2 | 1 | 0 | 1 | 3 - Courageous Californians | 2 | 0 | 0 | 2 | 0 - """, - ) - result == expected +expect { + table = + \\Allegoric Alaskans;Blithering Badgers;win + \\Blithering Badgers;Courageous Californians;win + \\Courageous Californians;Allegoric Alaskans;loss + + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Allegoric Alaskans | 2 | 2 | 0 | 0 | 6 + \\Blithering Badgers | 2 | 1 | 0 | 1 | 3 + \\Courageous Californians | 2 | 0 | 0 | 2 | 0 + , + ) + result == expected +} # typical input -expect - table = - """ - Allegoric Alaskans;Blithering Badgers;win - Devastating Donkeys;Courageous Californians;draw - Devastating Donkeys;Allegoric Alaskans;win - Courageous Californians;Blithering Badgers;loss - Blithering Badgers;Devastating Donkeys;loss - Allegoric Alaskans;Courageous Californians;win - """ - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Devastating Donkeys | 3 | 2 | 1 | 0 | 7 - Allegoric Alaskans | 3 | 2 | 0 | 1 | 6 - Blithering Badgers | 3 | 1 | 0 | 2 | 3 - Courageous Californians | 3 | 0 | 1 | 2 | 1 - """, - ) - result == expected +expect { + table = + \\Allegoric Alaskans;Blithering Badgers;win + \\Devastating Donkeys;Courageous Californians;draw + \\Devastating Donkeys;Allegoric Alaskans;win + \\Courageous Californians;Blithering Badgers;loss + \\Blithering Badgers;Devastating Donkeys;loss + \\Allegoric Alaskans;Courageous Californians;win + + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Devastating Donkeys | 3 | 2 | 1 | 0 | 7 + \\Allegoric Alaskans | 3 | 2 | 0 | 1 | 6 + \\Blithering Badgers | 3 | 1 | 0 | 2 | 3 + \\Courageous Californians | 3 | 0 | 1 | 2 | 1 + , + ) + result == expected +} # incomplete competition (not all pairs have played) -expect - table = - """ - Allegoric Alaskans;Blithering Badgers;loss - Devastating Donkeys;Allegoric Alaskans;loss - Courageous Californians;Blithering Badgers;draw - Allegoric Alaskans;Courageous Californians;win - """ - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Allegoric Alaskans | 3 | 2 | 0 | 1 | 6 - Blithering Badgers | 2 | 1 | 1 | 0 | 4 - Courageous Californians | 2 | 0 | 1 | 1 | 1 - Devastating Donkeys | 1 | 0 | 0 | 1 | 0 - """, - ) - result == expected +expect { + table = + \\Allegoric Alaskans;Blithering Badgers;loss + \\Devastating Donkeys;Allegoric Alaskans;loss + \\Courageous Californians;Blithering Badgers;draw + \\Allegoric Alaskans;Courageous Californians;win + + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Allegoric Alaskans | 3 | 2 | 0 | 1 | 6 + \\Blithering Badgers | 2 | 1 | 1 | 0 | 4 + \\Courageous Californians | 2 | 0 | 1 | 1 | 1 + \\Devastating Donkeys | 1 | 0 | 0 | 1 | 0 + , + ) + result == expected +} # ties broken alphabetically -expect - table = - """ - Courageous Californians;Devastating Donkeys;win - Allegoric Alaskans;Blithering Badgers;win - Devastating Donkeys;Allegoric Alaskans;loss - Courageous Californians;Blithering Badgers;win - Blithering Badgers;Devastating Donkeys;draw - Allegoric Alaskans;Courageous Californians;draw - """ - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Allegoric Alaskans | 3 | 2 | 1 | 0 | 7 - Courageous Californians | 3 | 2 | 1 | 0 | 7 - Blithering Badgers | 3 | 0 | 1 | 2 | 1 - Devastating Donkeys | 3 | 0 | 1 | 2 | 1 - """, - ) - result == expected +expect { + table = + \\Courageous Californians;Devastating Donkeys;win + \\Allegoric Alaskans;Blithering Badgers;win + \\Devastating Donkeys;Allegoric Alaskans;loss + \\Courageous Californians;Blithering Badgers;win + \\Blithering Badgers;Devastating Donkeys;draw + \\Allegoric Alaskans;Courageous Californians;draw + + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Allegoric Alaskans | 3 | 2 | 1 | 0 | 7 + \\Courageous Californians | 3 | 2 | 1 | 0 | 7 + \\Blithering Badgers | 3 | 0 | 1 | 2 | 1 + \\Devastating Donkeys | 3 | 0 | 1 | 2 | 1 + , + ) + result == expected +} # ensure points sorted numerically -expect - table = - """ - Devastating Donkeys;Blithering Badgers;win - Devastating Donkeys;Blithering Badgers;win - Devastating Donkeys;Blithering Badgers;win - Devastating Donkeys;Blithering Badgers;win - Blithering Badgers;Devastating Donkeys;win - """ - result = tally(table) - expected = Ok( - """ - Team | MP | W | D | L | P - Devastating Donkeys | 5 | 4 | 0 | 1 | 12 - Blithering Badgers | 5 | 1 | 0 | 4 | 3 - """, - ) - result == expected +expect { + table = + \\Devastating Donkeys;Blithering Badgers;win + \\Devastating Donkeys;Blithering Badgers;win + \\Devastating Donkeys;Blithering Badgers;win + \\Devastating Donkeys;Blithering Badgers;win + \\Blithering Badgers;Devastating Donkeys;win + + result = tally(table) + expected = Ok( + \\Team | MP | W | D | L | P + \\Devastating Donkeys | 5 | 4 | 0 | 1 | 12 + \\Blithering Badgers | 5 | 1 | 0 | 4 | 3 + , + ) + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/transpose/.meta/Example.roc b/exercises/practice/transpose/.meta/Example.roc index b3077876..5c231e6a 100644 --- a/exercises/practice/transpose/.meta/Example.roc +++ b/exercises/practice/transpose/.meta/Example.roc @@ -1,22 +1,32 @@ -module [transpose] +Transpose :: {}.{ + # # Transpose the input string. Input string must be ASCII. + transpose : Str -> Str + transpose = |string| { + chars = string.to_utf8().split_on('\n') + get_char = |row, col| { + chars.get(row)?.get(col) + } + max_width = chars.map(List.len).max() ?? 0 + (0.. col, + ) + ?? 0 -## Transpose the input string. Input string must be ASCII. -transpose : Str -> Str -transpose = |string| - chars = string |> Str.to_utf8 |> List.split_on('\n') - get_char = |row, col| - chars |> List.get(row)? |> List.get(col) - max_width = chars |> List.map(List.len) |> List.max |> Result.with_default(0) - List.range({ start: At(0), end: Before(max_width) }) - |> List.map( - |col| - max_row = - chars - |> List.find_last_index(|row_chars| List.len(row_chars) > col) - |> Result.with_default(0) - List.range({ start: At(0), end: At(max_row) }) - |> List.map(|row| get_char(row, col) |> Result.with_default(' ')) - |> Str.from_utf8 - |> Result.with_default(""), - ) # unreachable because string is ASCII - |> Str.join_with("\n") + (0..=max_row) + .map( + |row| get_char(row, col) ?? ' ', + ) + ->List.from_iter() + ->Str.from_utf8() + ?? "" + }, + ) + ->List.from_iter() + ->Str.join_with("\n") + } +} diff --git a/exercises/practice/transpose/.meta/template.j2 b/exercises/practice/transpose/.meta/template.j2 index 4ea165d5..d3b5cf75 100644 --- a/exercises/practice/transpose/.meta/template.j2 +++ b/exercises/practice/transpose/.meta/template.j2 @@ -6,10 +6,13 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect - input = {{ case["input"]["lines"] | to_roc_multiline_string | replace(" ", "□") | indent(8) }} |> Str.replace_each("□", " ") - result = {{ case["property"] | to_snake }}(input) |> Str.replace_each(" ", "□") - expected = {{ case["expected"] | to_roc_multiline_string | replace(" ", "□") | indent(8) }} +expect { + input = {{ case["input"]["lines"] | to_roc_multiline_string | indent(8) }} + result = {{ case["property"] | to_snake }}(input) + expected = {{ case["expected"] | to_roc_multiline_string | indent(8) }} result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/transpose/Transpose.roc b/exercises/practice/transpose/Transpose.roc index f2a7822f..34b20cf1 100644 --- a/exercises/practice/transpose/Transpose.roc +++ b/exercises/practice/transpose/Transpose.roc @@ -1,5 +1,6 @@ -module [transpose] - -transpose : Str -> Str -transpose = |string| - crash("Please implement the 'transpose' function") +Transpose :: {}.{ + transpose : Str -> Str + transpose = |string| { + crash "Please implement the 'transpose' function" + } +} diff --git a/exercises/practice/transpose/transpose-test.roc b/exercises/practice/transpose/transpose-test.roc index 5d7b52dc..a98cc6da 100644 --- a/exercises/practice/transpose/transpose-test.roc +++ b/exercises/practice/transpose/transpose-test.roc @@ -1,269 +1,249 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/transpose/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Transpose exposing [transpose] # empty string -expect - input = "" |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = "" - result == expected +expect { + input = "" + result = transpose(input) + expected = "" + result == expected +} # two characters in a row -expect - input = "A1" |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = - """ - A - 1 - """ - result == expected +expect { + input = "A1" + result = transpose(input) + expected = + \\A + \\1 + + result == expected +} # two characters in a column -expect - input = - """ - A - 1 - """ - |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = "A1" - result == expected +expect { + input = + \\A + \\1 + + result = transpose(input) + expected = "A1" + result == expected +} # simple -expect - input = - """ - ABC - 123 - """ - |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = - """ - A1 - B2 - C3 - """ - result == expected +expect { + input = + \\ABC + \\123 + + result = transpose(input) + expected = + \\A1 + \\B2 + \\C3 + + result == expected +} # single line -expect - input = "Single□line." |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = - """ - S - i - n - g - l - e - □ - l - i - n - e - . - """ - result == expected +expect { + input = "Single line." + result = transpose(input) + expected = + \\S + \\i + \\n + \\g + \\l + \\e + \\ + \\l + \\i + \\n + \\e + \\. + + result == expected +} # first line longer than second line -expect - input = - """ - The□fourth□line. - The□fifth□line. - """ - |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = - """ - TT - hh - ee - □□ - ff - oi - uf - rt - th - h□ - □l - li - in - ne - e. - . - """ - result == expected +expect { + input = + \\The fourth line. + \\The fifth line. + + result = transpose(input) + expected = + \\TT + \\hh + \\ee + \\ + \\ff + \\oi + \\uf + \\rt + \\th + \\h + \\ l + \\li + \\in + \\ne + \\e. + \\. + + result == expected +} # second line longer than first line -expect - input = - """ - The□first□line. - The□second□line. - """ - |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = - """ - TT - hh - ee - □□ - fs - ie - rc - so - tn - □d - l□ - il - ni - en - .e - □. - """ - result == expected +expect { + input = + \\The first line. + \\The second line. + + result = transpose(input) + expected = + \\TT + \\hh + \\ee + \\ + \\fs + \\ie + \\rc + \\so + \\tn + \\ d + \\l + \\il + \\ni + \\en + \\.e + \\ . + + result == expected +} # mixed line length -expect - input = - """ - The□longest□line. - A□long□line. - A□longer□line. - A□line. - """ - |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = - """ - TAAA - h□□□ - elll - □ooi - lnnn - ogge - n□e. - glr - ei□ - snl - tei - □.n - l□e - i□. - n - e - . - """ - result == expected +expect { + input = + \\The longest line. + \\A long line. + \\A longer line. + \\A line. + + result = transpose(input) + expected = + \\TAAA + \\h + \\elll + \\ ooi + \\lnnn + \\ogge + \\n e. + \\glr + \\ei + \\snl + \\tei + \\ .n + \\l e + \\i . + \\n + \\e + \\. + + result == expected +} # square -expect - input = - """ - HEART - EMBER - ABUSE - RESIN - TREND - """ - |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = - """ - HEART - EMBER - ABUSE - RESIN - TREND - """ - result == expected +expect { + input = + \\HEART + \\EMBER + \\ABUSE + \\RESIN + \\TREND + + result = transpose(input) + expected = + \\HEART + \\EMBER + \\ABUSE + \\RESIN + \\TREND + + result == expected +} # rectangle -expect - input = - """ - FRACTURE - OUTLINED - BLOOMING - SEPTETTE - """ - |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = - """ - FOBS - RULE - ATOP - CLOT - TIME - UNIT - RENT - EDGE - """ - result == expected +expect { + input = + \\FRACTURE + \\OUTLINED + \\BLOOMING + \\SEPTETTE + + result = transpose(input) + expected = + \\FOBS + \\RULE + \\ATOP + \\CLOT + \\TIME + \\UNIT + \\RENT + \\EDGE + + result == expected +} # triangle -expect - input = - """ - T - EE - AAA - SSSS - EEEEE - RRRRRR - """ - |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = - """ - TEASER - □EASER - □□ASER - □□□SER - □□□□ER - □□□□□R - """ - result == expected +expect { + input = + \\T + \\EE + \\AAA + \\SSSS + \\EEEEE + \\RRRRRR + + result = transpose(input) + expected = + \\TEASER + \\ EASER + \\ ASER + \\ SER + \\ ER + \\ R + + result == expected +} # jagged triangle -expect - input = - """ - 11 - 2 - 3333 - 444 - 555555 - 66666 - """ - |> Str.replace_each("□", " ") - result = transpose(input) |> Str.replace_each(" ", "□") - expected = - """ - 123456 - 1□3456 - □□3456 - □□3□56 - □□□□56 - □□□□5 - """ - result == expected +expect { + input = + \\11 + \\2 + \\3333 + \\444 + \\555555 + \\66666 + + result = transpose(input) + expected = + \\123456 + \\1 3456 + \\ 3456 + \\ 3 56 + \\ 56 + \\ 5 + + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/triangle/.meta/Example.roc b/exercises/practice/triangle/.meta/Example.roc index 4c063425..808ffc00 100644 --- a/exercises/practice/triangle/.meta/Example.roc +++ b/exercises/practice/triangle/.meta/Example.roc @@ -1,19 +1,34 @@ -module [is_equilateral, is_isosceles, is_scalene] +Triangle :: {}.{ + is_equilateral : (F64, F64, F64) -> Bool + is_equilateral = |(a, b, c)| { + is_valid_triangle((a, b, c)) and approx_eq(a, b) and approx_eq(b, c) + } -approx_eq = |x, y| - Num.is_approx_eq(x, y, {}) + is_isosceles : (F64, F64, F64) -> Bool + is_isosceles = |(a, b, c)| { + is_valid_triangle((a, b, c)) and (approx_eq(a, b) or approx_eq(b, c) or approx_eq(a, c)) + } -is_valid_triangle = |(a, b, c)| - a > 0 and b > 0 and c > 0 and a + b >= c and a + c >= b and b + c >= a + is_scalene : (F64, F64, F64) -> Bool + is_scalene = |(a, b, c)| { + is_valid_triangle((a, b, c)) and !(is_isosceles((a, b, c))) + } +} -is_equilateral : (F64, F64, F64) -> Bool -is_equilateral = |(a, b, c)| - is_valid_triangle((a, b, c)) and approx_eq(a, b) and approx_eq(b, c) +is_valid_triangle = |(a, b, c)| { + a > 0 and b > 0 and c > 0 and a + b >= c and a + c >= b and b + c >= a +} -is_isosceles : (F64, F64, F64) -> Bool -is_isosceles = |(a, b, c)| - is_valid_triangle((a, b, c)) and (approx_eq(a, b) or approx_eq(b, c) or approx_eq(a, c)) +# The following function should soon be available in Roc's builtins +approx_eq : F64, F64 -> Bool +approx_eq = |x, y| { + to_int : F64 -> Try(I64, [OutOfRange]) + to_int = |f| { + (f * 1e6).to_i64_try() + } -is_scalene : (F64, F64, F64) -> Bool -is_scalene = |(a, b, c)| - is_valid_triangle((a, b, c)) and !(is_isosceles((a, b, c))) + match (to_int(x), to_int(y)) { + (Ok(xi), Ok(yi)) => (xi == yi) + _ => Bool.False + } +} diff --git a/exercises/practice/triangle/.meta/template.j2 b/exercises/practice/triangle/.meta/template.j2 index d32731f6..c46324ca 100644 --- a/exercises/practice/triangle/.meta/template.j2 +++ b/exercises/practice/triangle/.meta/template.j2 @@ -11,9 +11,12 @@ import {{ exercise | to_pascal }} exposing [is_equilateral, is_isosceles, is_sca {% for case in supercase["cases"] -%} # {{ case["description"] }} -expect +expect { result = is_{{ case["property"] }}({{ case["input"]["sides"] | to_roc_tuple }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} -{% endfor %} \ No newline at end of file +{% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/triangle/Triangle.roc b/exercises/practice/triangle/Triangle.roc index 41ebf7b2..23e7f92a 100644 --- a/exercises/practice/triangle/Triangle.roc +++ b/exercises/practice/triangle/Triangle.roc @@ -1,13 +1,16 @@ -module [is_equilateral, is_isosceles, is_scalene] +Triangle :: {}.{ + is_equilateral : (F64, F64, F64) -> Bool + is_equilateral = |(a, b, c)| { + crash "Please implement the 'is_equilateral' function" + } -is_equilateral : (F64, F64, F64) -> Bool -is_equilateral = |(a, b, c)| - crash("Please implement the 'is_equilateral' function") + is_isosceles : (F64, F64, F64) -> Bool + is_isosceles = |(a, b, c)| { + crash "Please implement the 'is_isosceles' function" + } -is_isosceles : (F64, F64, F64) -> Bool -is_isosceles = |(a, b, c)| - crash("Please implement the 'is_isosceles' function") - -is_scalene : (F64, F64, F64) -> Bool -is_scalene = |(a, b, c)| - crash("Please implement the 'is_scalene' function") + is_scalene : (F64, F64, F64) -> Bool + is_scalene = |(a, b, c)| { + crash "Please implement the 'is_scalene' function" + } +} diff --git a/exercises/practice/triangle/triangle-test.roc b/exercises/practice/triangle/triangle-test.roc index 90faf9c3..480cb34d 100644 --- a/exercises/practice/triangle/triangle-test.roc +++ b/exercises/practice/triangle/triangle-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/triangle/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Triangle exposing [is_equilateral, is_isosceles, is_scalene] @@ -17,90 +9,110 @@ import Triangle exposing [is_equilateral, is_isosceles, is_scalene] ## # all sides are equal -expect - result = is_equilateral((2, 2, 2)) - result == Bool.true +expect { + result = is_equilateral((2, 2, 2)) + result == Bool.True +} # any side is unequal -expect - result = is_equilateral((2, 3, 2)) - result == Bool.false +expect { + result = is_equilateral((2, 3, 2)) + result == Bool.False +} # no sides are equal -expect - result = is_equilateral((5, 4, 6)) - result == Bool.false +expect { + result = is_equilateral((5, 4, 6)) + result == Bool.False +} # sides may be floats -expect - result = is_equilateral((0.5f64, 0.5f64, 0.5f64)) - result == Bool.true +expect { + result = is_equilateral((0.5.F64, 0.5.F64, 0.5.F64)) + result == Bool.True +} ## ## isosceles triangle ## # last two sides are equal -expect - result = is_isosceles((3, 4, 4)) - result == Bool.true +expect { + result = is_isosceles((3, 4, 4)) + result == Bool.True +} # first two sides are equal -expect - result = is_isosceles((4, 4, 3)) - result == Bool.true +expect { + result = is_isosceles((4, 4, 3)) + result == Bool.True +} # first and last sides are equal -expect - result = is_isosceles((4, 3, 4)) - result == Bool.true +expect { + result = is_isosceles((4, 3, 4)) + result == Bool.True +} # equilateral triangles are also isosceles -expect - result = is_isosceles((4, 4, 4)) - result == Bool.true +expect { + result = is_isosceles((4, 4, 4)) + result == Bool.True +} # no sides are equal -expect - result = is_isosceles((2, 3, 4)) - result == Bool.false +expect { + result = is_isosceles((2, 3, 4)) + result == Bool.False +} # sides may be floats -expect - result = is_isosceles((0.5f64, 0.4f64, 0.5f64)) - result == Bool.true +expect { + result = is_isosceles((0.5.F64, 0.4.F64, 0.5.F64)) + result == Bool.True +} ## ## scalene triangle ## # no sides are equal -expect - result = is_scalene((5, 4, 6)) - result == Bool.true +expect { + result = is_scalene((5, 4, 6)) + result == Bool.True +} # all sides are equal -expect - result = is_scalene((4, 4, 4)) - result == Bool.false +expect { + result = is_scalene((4, 4, 4)) + result == Bool.False +} # first and second sides are equal -expect - result = is_scalene((4, 4, 3)) - result == Bool.false +expect { + result = is_scalene((4, 4, 3)) + result == Bool.False +} # first and third sides are equal -expect - result = is_scalene((3, 4, 3)) - result == Bool.false +expect { + result = is_scalene((3, 4, 3)) + result == Bool.False +} # second and third sides are equal -expect - result = is_scalene((4, 3, 3)) - result == Bool.false +expect { + result = is_scalene((4, 3, 3)) + result == Bool.False +} # sides may be floats -expect - result = is_scalene((0.5f64, 0.4f64, 0.6f64)) - result == Bool.true +expect { + result = is_scalene((0.5.F64, 0.4.F64, 0.6.F64)) + result == Bool.True +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/two-bucket/.meta/Example.roc b/exercises/practice/two-bucket/.meta/Example.roc index fec53c7f..779e7ff5 100644 --- a/exercises/practice/two-bucket/.meta/Example.roc +++ b/exercises/practice/two-bucket/.meta/Example.roc @@ -1,88 +1,122 @@ -module [measure] +TwoBucket :: {}.{ + measure : + { bucket_one : U64, bucket_two : U64, goal : U64, start_bucket : [One, Two] } -> Try({ moves : U64, goal_bucket : [One, Two], other_bucket : U64 }, [NoSolutionExists]) + measure = |{ bucket_one, bucket_two, goal, start_bucket }| { + if goal == 0 { + Ok({ moves: 0, goal_bucket: One, other_bucket: 0 }) + } else { + start = + match start_bucket { + One => { volume_one: bucket_one, volume_two: 0 } + Two => { volume_one: 0, volume_two: bucket_two } + } -## Breadth-First Search finds the shortest path from the `start` node to any successful -## node (i.e., such that `success node == Bool.true`). The `neighbors` function must -## return the list of direct neighbors of a given node. -bfs = |{ start, neighbors, success }| - help = |to_visit, visited, from| - when to_visit is - [] -> Err(NoPathExists) - [node, .. as rest_to_visit] -> - if visited |> Set.contains(node) then - help(rest_to_visit, visited, from) - else if success(node) then - path_back_to_start = |path, step| - updated_path = path |> List.append(step) - when from |> Dict.get(step) is - Ok(previous) -> path_back_to_start(updated_path, previous) - Err(KeyNotFound) -> updated_path - path_back_to_start([], node) |> List.reverse |> Ok - else - neighbor_nodes = neighbors(node) - new_from = - neighbor_nodes - |> List.drop_if(|neighbor| visited |> Set.contains(neighbor)) - |> List.map(|neighbor| (neighbor, node)) - |> Dict.from_list - updated_from = from |> Dict.insert_all(new_from) - updated_visited = visited |> Set.insert(node) - updated_to_visit = rest_to_visit |> List.concat(neighbor_nodes) - help(updated_to_visit, updated_visited, updated_from) - help([start], Set.empty({}), Dict.empty({})) + neighbors = |{ volume_one, volume_two }| { + volume_one_to_two = if volume_one < (bucket_two - volume_two) { + volume_one + } else { + bucket_two - volume_two + } + volume_two_to_one = if volume_two < (bucket_one - volume_one) { + volume_two + } else { + bucket_one - volume_one + } + [ + { volume_one: 0, volume_two }, # empty bucket one + { volume_one, volume_two: 0 }, # empty bucket two + { volume_one: bucket_one, volume_two }, # fill bucket one + { volume_one, volume_two: bucket_two }, # fill bucket two + { + # pour bucket one into bucket two + volume_one: volume_one - volume_one_to_two, + volume_two: volume_two + volume_one_to_two, + }, + { + # pour bucket two into bucket one + volume_one: volume_one + volume_two_to_one, + volume_two: volume_two - volume_two_to_one, + }, + ] + .drop_if( + |{ volume_one: v1, volume_two: v2 }| { + (v1 == volume_one and v2 == volume_two) # no change + or + # forbidden move: cannot end up with the starting bucket empty and + # the other bucket full + match start_bucket { + One => v1 == 0 and v2 == bucket_two + Two => v1 == bucket_one and v2 == 0 + } + }, + ) + } -measure : - { bucket_one : U64, bucket_two : U64, goal : U64, start_bucket : [One, Two] } - -> - Result { moves : U64, goal_bucket : [One, Two], other_bucket : U64 } [NoSolutionExists] -measure = |{ bucket_one, bucket_two, goal, start_bucket }| - if goal == 0 then - Ok({ moves: 0, goal_bucket: One, other_bucket: 0 }) - else - start = - when start_bucket is - One -> { volume_one: bucket_one, volume_two: 0 } - Two -> { volume_one: 0, volume_two: bucket_two } + success = |{ volume_one, volume_two }| volume_one == goal or volume_two == goal - neighbors = |{ volume_one, volume_two }| - volume_one_to_two = Num.min(volume_one, (bucket_two - volume_two)) - volume_two_to_one = Num.min(volume_two, (bucket_one - volume_one)) - [ - { volume_one: 0, volume_two }, # empty bucket one - { volume_one, volume_two: 0 }, # empty bucket two - { volume_one: bucket_one, volume_two }, # fill bucket one - { volume_one, volume_two: bucket_two }, # fill bucket two - { - # pour bucket one into bucket two - volume_one: volume_one - volume_one_to_two, - volume_two: volume_two + volume_one_to_two, - }, - { - # pour bucket two into bucket one - volume_one: volume_one + volume_two_to_one, - volume_two: volume_two - volume_two_to_one, - }, - ] - |> List.drop_if( - |{ volume_one: v1, volume_two: v2 }| - (v1 == volume_one and v2 == volume_two) # no change - or - # forbidden move: cannot end up with the starting bucket empty and - # the other bucket full - when start_bucket is - One -> v1 == 0 and v2 == bucket_two - Two -> v1 == bucket_one and v2 == 0, - ) + match bfs({ start, neighbors, success }) { + Ok(path) => { + match path { + [.. as rest, last] => { + Ok( + { + moves: rest.len() + 1, + goal_bucket: if last.volume_one == goal { + One + } else { + Two + }, + other_bucket: if last.volume_one == goal { + last.volume_two + } else { + last.volume_one + }, + }, + ) + } + _ => Err(NoSolutionExists) + } + } - success = |{ volume_one, volume_two }| volume_one == goal or volume_two == goal + _ => Err(NoSolutionExists) + } + } + } +} - when bfs({ start, neighbors, success }) is - Ok([.. as rest, last]) -> - Ok( - { - moves: List.len(rest) + 1, - goal_bucket: if last.volume_one == goal then One else Two, - other_bucket: if last.volume_one == goal then last.volume_two else last.volume_one, - }, - ) +bfs = |{ start, neighbors, success }| { + help = |to_visit, visited, from| { + match to_visit { + [] => Err(NoPathExists) + [node, .. as rest_to_visit] => { + if visited.contains(node) { + help(rest_to_visit, visited, from) + } else if success(node) { + path_back_to_start = |path, step| { + updated_path = path.append(step) + match from.get(to_str(step)) { + Ok(previous) => path_back_to_start(updated_path, previous) + Err(KeyNotFound) => updated_path + } + } + Ok(path_back_to_start([], node).rev()) + } else { + neighbor_nodes = neighbors(node) + updated_from = + neighbor_nodes + .drop_if(|neighbor| visited.contains(neighbor)) + .fold(from, |acc_from, neighbor| acc_from.insert(to_str(neighbor), node)) + updated_visited = visited.insert(node) + updated_to_visit = rest_to_visit.concat(neighbor_nodes) + help(updated_to_visit, updated_visited, updated_from) + } + } + } + } + help([start], Set.empty(), Dict.empty()) +} - _ -> Err(NoSolutionExists) +# TODO: remove to_str once records have a to_hash method +to_str = |{ volume_one, volume_two }| { + volume_one.to_str().concat(volume_two.to_str()) +} diff --git a/exercises/practice/two-bucket/.meta/template.j2 b/exercises/practice/two-bucket/.meta/template.j2 index 0847d62c..9db17cc4 100644 --- a/exercises/practice/two-bucket/.meta/template.j2 +++ b/exercises/practice/two-bucket/.meta/template.j2 @@ -6,7 +6,7 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({ bucket_one: {{ case["input"]["bucketOne"] }}, bucket_two: {{ case["input"]["bucketTwo"] }}, @@ -14,7 +14,7 @@ expect start_bucket: {{ case["input"]["startBucket"] | to_pascal }}, }) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} expected = Ok({ moves: {{ case["expected"]["moves"] }}, @@ -23,5 +23,8 @@ expect }) result == expected {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/two-bucket/TwoBucket.roc b/exercises/practice/two-bucket/TwoBucket.roc index 5e467b1d..e62d222a 100644 --- a/exercises/practice/two-bucket/TwoBucket.roc +++ b/exercises/practice/two-bucket/TwoBucket.roc @@ -1,8 +1,7 @@ -module [measure] - -measure : - { bucket_one : U64, bucket_two : U64, goal : U64, start_bucket : [One, Two] } - -> - Result { moves : U64, goal_bucket : [One, Two], other_bucket : U64 } _ -measure = |{ bucket_one, bucket_two, goal, start_bucket }| - crash("Please implement the 'measure' function") +TwoBucket :: {}.{ + measure : + { bucket_one : U64, bucket_two : U64, goal : U64, start_bucket : [One, Two] } -> Try({ moves : U64, goal_bucket : [One, Two], other_bucket : U64 }, _) + measure = |{ bucket_one, bucket_two, goal, start_bucket }| { + crash "Please implement the 'measure' function" + } +} diff --git a/exercises/practice/two-bucket/two-bucket-test.roc b/exercises/practice/two-bucket/two-bucket-test.roc index 89a6d978..02bd5acd 100644 --- a/exercises/practice/two-bucket/two-bucket-test.roc +++ b/exercises/practice/two-bucket/two-bucket-test.roc @@ -1,209 +1,216 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/two-bucket/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import TwoBucket exposing [measure] # Measure using bucket one of size 3 and bucket two of size 5 - start with bucket one -expect - result = measure( - { - bucket_one: 3, - bucket_two: 5, - goal: 1, - start_bucket: One, - }, - ) - expected = Ok( - { - moves: 4, - goal_bucket: One, - other_bucket: 5, - }, - ) - result == expected +expect { + result = measure( + { + bucket_one: 3, + bucket_two: 5, + goal: 1, + start_bucket: One, + }, + ) + expected = Ok( + { + moves: 4, + goal_bucket: One, + other_bucket: 5, + }, + ) + result == expected +} # Measure using bucket one of size 3 and bucket two of size 5 - start with bucket two -expect - result = measure( - { - bucket_one: 3, - bucket_two: 5, - goal: 1, - start_bucket: Two, - }, - ) - expected = Ok( - { - moves: 8, - goal_bucket: Two, - other_bucket: 3, - }, - ) - result == expected +expect { + result = measure( + { + bucket_one: 3, + bucket_two: 5, + goal: 1, + start_bucket: Two, + }, + ) + expected = Ok( + { + moves: 8, + goal_bucket: Two, + other_bucket: 3, + }, + ) + result == expected +} # Measure using bucket one of size 7 and bucket two of size 11 - start with bucket one -expect - result = measure( - { - bucket_one: 7, - bucket_two: 11, - goal: 2, - start_bucket: One, - }, - ) - expected = Ok( - { - moves: 14, - goal_bucket: One, - other_bucket: 11, - }, - ) - result == expected +expect { + result = measure( + { + bucket_one: 7, + bucket_two: 11, + goal: 2, + start_bucket: One, + }, + ) + expected = Ok( + { + moves: 14, + goal_bucket: One, + other_bucket: 11, + }, + ) + result == expected +} # Measure using bucket one of size 7 and bucket two of size 11 - start with bucket two -expect - result = measure( - { - bucket_one: 7, - bucket_two: 11, - goal: 2, - start_bucket: Two, - }, - ) - expected = Ok( - { - moves: 18, - goal_bucket: Two, - other_bucket: 7, - }, - ) - result == expected +expect { + result = measure( + { + bucket_one: 7, + bucket_two: 11, + goal: 2, + start_bucket: Two, + }, + ) + expected = Ok( + { + moves: 18, + goal_bucket: Two, + other_bucket: 7, + }, + ) + result == expected +} # Measure one step using bucket one of size 1 and bucket two of size 3 - start with bucket two -expect - result = measure( - { - bucket_one: 1, - bucket_two: 3, - goal: 3, - start_bucket: Two, - }, - ) - expected = Ok( - { - moves: 1, - goal_bucket: Two, - other_bucket: 0, - }, - ) - result == expected +expect { + result = measure( + { + bucket_one: 1, + bucket_two: 3, + goal: 3, + start_bucket: Two, + }, + ) + expected = Ok( + { + moves: 1, + goal_bucket: Two, + other_bucket: 0, + }, + ) + result == expected +} # Measure using bucket one of size 2 and bucket two of size 3 - start with bucket one and end with bucket two -expect - result = measure( - { - bucket_one: 2, - bucket_two: 3, - goal: 3, - start_bucket: One, - }, - ) - expected = Ok( - { - moves: 2, - goal_bucket: Two, - other_bucket: 2, - }, - ) - result == expected +expect { + result = measure( + { + bucket_one: 2, + bucket_two: 3, + goal: 3, + start_bucket: One, + }, + ) + expected = Ok( + { + moves: 2, + goal_bucket: Two, + other_bucket: 2, + }, + ) + result == expected +} # Measure using bucket one much bigger than bucket two -expect - result = measure( - { - bucket_one: 5, - bucket_two: 1, - goal: 2, - start_bucket: One, - }, - ) - expected = Ok( - { - moves: 6, - goal_bucket: One, - other_bucket: 1, - }, - ) - result == expected +expect { + result = measure( + { + bucket_one: 5, + bucket_two: 1, + goal: 2, + start_bucket: One, + }, + ) + expected = Ok( + { + moves: 6, + goal_bucket: One, + other_bucket: 1, + }, + ) + result == expected +} # Measure using bucket one much smaller than bucket two -expect - result = measure( - { - bucket_one: 3, - bucket_two: 15, - goal: 9, - start_bucket: One, - }, - ) - expected = Ok( - { - moves: 6, - goal_bucket: Two, - other_bucket: 0, - }, - ) - result == expected +expect { + result = measure( + { + bucket_one: 3, + bucket_two: 15, + goal: 9, + start_bucket: One, + }, + ) + expected = Ok( + { + moves: 6, + goal_bucket: Two, + other_bucket: 0, + }, + ) + result == expected +} # Not possible to reach the goal -expect - result = measure( - { - bucket_one: 6, - bucket_two: 15, - goal: 5, - start_bucket: One, - }, - ) - result |> Result.is_err +expect { + result = measure( + { + bucket_one: 6, + bucket_two: 15, + goal: 5, + start_bucket: One, + }, + ) + result.is_err() +} # With the same buckets but a different goal, then it is possible -expect - result = measure( - { - bucket_one: 6, - bucket_two: 15, - goal: 9, - start_bucket: One, - }, - ) - expected = Ok( - { - moves: 10, - goal_bucket: Two, - other_bucket: 0, - }, - ) - result == expected +expect { + result = measure( + { + bucket_one: 6, + bucket_two: 15, + goal: 9, + start_bucket: One, + }, + ) + expected = Ok( + { + moves: 10, + goal_bucket: Two, + other_bucket: 0, + }, + ) + result == expected +} # Goal larger than both buckets is impossible -expect - result = measure( - { - bucket_one: 5, - bucket_two: 7, - goal: 8, - start_bucket: One, - }, - ) - result |> Result.is_err +expect { + result = measure( + { + bucket_one: 5, + bucket_two: 7, + goal: 8, + start_bucket: One, + }, + ) + 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/two-fer/.meta/Example.roc b/exercises/practice/two-fer/.meta/Example.roc index a5559fe6..65c8ada7 100644 --- a/exercises/practice/two-fer/.meta/Example.roc +++ b/exercises/practice/two-fer/.meta/Example.roc @@ -1,7 +1,9 @@ -module [two_fer] - -two_fer : [Name Str, Anonymous] -> Str -two_fer = |name| - when name is - Anonymous -> "One for you, one for me." - Name(n) -> "One for ${n}, one for me." +TwoFer :: {}.{ + two_fer : [Name(Str), Anonymous] -> Str + two_fer = |name| { + match name { + Anonymous => "One for you, one for me." + Name(n) => "One for ${n}, one for me." + } + } +} diff --git a/exercises/practice/two-fer/.meta/template.j2 b/exercises/practice/two-fer/.meta/template.j2 index e1673c82..102b569f 100644 --- a/exercises/practice/two-fer/.meta/template.j2 +++ b/exercises/practice/two-fer/.meta/template.j2 @@ -6,12 +6,15 @@ import {{ exercise | to_pascal }} exposing [{{ exercise | to_snake }}] {% for case in cases -%} # {{ case["description"] }} -expect +expect { {%- if case["input"]["name"] == None %} result = {{ case["property"] | to_snake }}(Anonymous) {%- else %} result = {{ case["property"] | to_snake }}((Name({{ case["input"]["name"] | to_roc }}))) {%- endif %} result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/two-fer/TwoFer.roc b/exercises/practice/two-fer/TwoFer.roc index 2ba99aec..7af7d755 100644 --- a/exercises/practice/two-fer/TwoFer.roc +++ b/exercises/practice/two-fer/TwoFer.roc @@ -1,5 +1,6 @@ -module [two_fer] - -two_fer : [Name Str, Anonymous] -> Str -two_fer = |name| - crash("Please implement the 'two_fer' function") +TwoFer :: {}.{ + two_fer : [Name(Str), Anonymous] -> Str + two_fer = |name| { + crash "Please implement the 'two_fer' function" + } +} diff --git a/exercises/practice/two-fer/two-fer-test.roc b/exercises/practice/two-fer/two-fer-test.roc index 7ab95b07..682bb039 100644 --- a/exercises/practice/two-fer/two-fer-test.roc +++ b/exercises/practice/two-fer/two-fer-test.roc @@ -1,29 +1,28 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/two-fer/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import TwoFer exposing [two_fer] # no name given -expect - result = two_fer(Anonymous) - result == "One for you, one for me." +expect { + result = two_fer(Anonymous) + result == "One for you, one for me." +} # a name given -expect - result = two_fer(Name("Alice")) - result == "One for Alice, one for me." +expect { + result = two_fer((Name("Alice"))) + result == "One for Alice, one for me." +} # another name given -expect - result = two_fer(Name("Bob")) - result == "One for Bob, one for me." +expect { + result = two_fer((Name("Bob"))) + result == "One for Bob, one for me." +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/variable-length-quantity/.meta/Example.roc b/exercises/practice/variable-length-quantity/.meta/Example.roc index 7f88477a..78f60c0e 100644 --- a/exercises/practice/variable-length-quantity/.meta/Example.roc +++ b/exercises/practice/variable-length-quantity/.meta/Example.roc @@ -1,42 +1,65 @@ -module [encode, decode] +VariableLengthQuantity :: {}.{ + encode : List(U32) -> List(U8) + encode = |integers| { + integers->join_map(encode_integer) + } -encode : List U32 -> List U8 -encode = |integers| - integers |> List.join_map(encode_integer) + decode : List(U8) -> Try(List(U32), [IncompleteSequence]) + decode = |bytes| { + match bytes { + [] => Err(IncompleteSequence) + [.., last] if last >= 128 => Err(IncompleteSequence) + _ => { + res = bytes.fold( + { integers: [], integer: 0 }, + |state, byte| { + last_7_bits = byte % 128 + integer = state.integer * 128 + last_7_bits.to_u32() + if byte >= 128 { + { ..state, integer } + } else { + { integers: state.integers.append(integer), integer: 0 } + } + }, + ) + Ok(res.integers) + } + } + } +} -encode_integer : U32 -> List U8 -encode_integer = |integer| - help = |bytes, n| - if n == 0 then - bytes - else - next_n = n // 128 # same as n |> Num.shift_right_zf_by 7 - last_7_bits = n % 128 |> Num.to_u8 # same as n |> Num.bitwise_and 0b1111111 |> Num.to_u8 - byte = - if bytes == [] then - last_7_bits - else - last_7_bits + 128 # same as last7Bits |> Num.bitwise_or 0b10000000 - help((bytes |> List.append(byte)), next_n) +encode_integer : U32 -> List(U8) +encode_integer = |integer| { + help : List(U8), U32 -> List(U8) + help = |bytes, n| { + if n == 0 { + bytes + } else { + next_n = n // 128 + last_7_bits = (n % 128).to_u8_try() ?? { + crash "Unreachable" + } + byte = match bytes { + [] => last_7_bits + _ => last_7_bits + 128 + } + help(bytes.append(byte), next_n) + } + } + if integer == 0 { + [0] + } else { + help([], integer).rev() + } +} - if integer == 0 then [0] else help([], integer) |> List.reverse - -decode : List U8 -> Result (List U32) [IncompleteSequence] -decode = |bytes| - when bytes is - [] -> Err(IncompleteSequence) - [.., last] if last >= 128 -> Err(IncompleteSequence) - _ -> - bytes - |> List.walk( - { integers: [], integer: 0 }, - |state, byte| - last_7_bits = byte % 128 - integer = state.integer * 128 + Num.to_u32(last_7_bits) - if byte >= 128 then - { state & integer } - else - { integers: state.integers |> List.append(integer), integer: 0 }, - ) - |> .integers - |> Ok +# The following functions should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} diff --git a/exercises/practice/variable-length-quantity/.meta/template.j2 b/exercises/practice/variable-length-quantity/.meta/template.j2 index 98023e7d..610258f9 100644 --- a/exercises/practice/variable-length-quantity/.meta/template.j2 +++ b/exercises/practice/variable-length-quantity/.meta/template.j2 @@ -12,22 +12,27 @@ import {{ exercise | to_pascal }} exposing [encode, decode] {% for case in supercase["cases"] -%} # {{ case["description"] }} {%- if case["property"] == "encode" %} -expect +expect { integers = {{ case["input"]["integers"] | to_roc }} result = encode(integers) expected = {{ case["expected"] | to_roc }} result == expected +} {%- else %} -expect +expect { bytes = {{ case["input"]["integers"] | to_roc }} - result = decode bytes + result = decode(bytes) {%- if case["expected"]["error"] %} - result |> Result.is_err + result.is_err() {%- else %} expected = Ok({{ case["expected"] | to_roc }}) result == expected {%- endif %} +} {%- endif %} + {% endfor %} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/variable-length-quantity/VariableLengthQuantity.roc b/exercises/practice/variable-length-quantity/VariableLengthQuantity.roc index 7b49852b..b9aa9c8f 100644 --- a/exercises/practice/variable-length-quantity/VariableLengthQuantity.roc +++ b/exercises/practice/variable-length-quantity/VariableLengthQuantity.roc @@ -1,9 +1,11 @@ -module [encode, decode] +VariableLengthQuantity :: {}.{ + encode : List(U32) -> List(U8) + encode = |integers| { + crash "Please implement the 'encode' function" + } -encode : List U32 -> List U8 -encode = |integers| - crash("Please implement the 'encode' function") - -decode : List U8 -> Result (List U32) _ -decode = |bytes| - crash("Please implement the 'decode' function") + decode : List(U8) -> Try(List(U32), _) + decode = |bytes| { + crash "Please implement the 'decode' function" + } +} diff --git a/exercises/practice/variable-length-quantity/variable-length-quantity-test.roc b/exercises/practice/variable-length-quantity/variable-length-quantity-test.roc index e2316abd..8c82bc05 100644 --- a/exercises/practice/variable-length-quantity/variable-length-quantity-test.roc +++ b/exercises/practice/variable-length-quantity/variable-length-quantity-test.roc @@ -1,14 +1,6 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/variable-length-quantity/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import VariableLengthQuantity exposing [encode, decode] @@ -17,221 +9,256 @@ import VariableLengthQuantity exposing [encode, decode] ## # zero -expect - integers = [0] - result = encode(integers) - expected = [0] - result == expected +expect { + integers = [0] + result = encode(integers) + expected = [0] + result == expected +} # arbitrary single byte -expect - integers = [64] - result = encode(integers) - expected = [64] - result == expected +expect { + integers = [64] + result = encode(integers) + expected = [64] + result == expected +} # asymmetric single byte -expect - integers = [83] - result = encode(integers) - expected = [83] - result == expected +expect { + integers = [83] + result = encode(integers) + expected = [83] + result == expected +} # largest single byte -expect - integers = [127] - result = encode(integers) - expected = [127] - result == expected +expect { + integers = [127] + result = encode(integers) + expected = [127] + result == expected +} # smallest double byte -expect - integers = [128] - result = encode(integers) - expected = [129, 0] - result == expected +expect { + integers = [128] + result = encode(integers) + expected = [129, 0] + result == expected +} # arbitrary double byte -expect - integers = [8192] - result = encode(integers) - expected = [192, 0] - result == expected +expect { + integers = [8192] + result = encode(integers) + expected = [192, 0] + result == expected +} # asymmetric double byte -expect - integers = [173] - result = encode(integers) - expected = [129, 45] - result == expected +expect { + integers = [173] + result = encode(integers) + expected = [129, 45] + result == expected +} # largest double byte -expect - integers = [16383] - result = encode(integers) - expected = [255, 127] - result == expected +expect { + integers = [16383] + result = encode(integers) + expected = [255, 127] + result == expected +} # smallest triple byte -expect - integers = [16384] - result = encode(integers) - expected = [129, 128, 0] - result == expected +expect { + integers = [16384] + result = encode(integers) + expected = [129, 128, 0] + result == expected +} # arbitrary triple byte -expect - integers = [1048576] - result = encode(integers) - expected = [192, 128, 0] - result == expected +expect { + integers = [1048576] + result = encode(integers) + expected = [192, 128, 0] + result == expected +} # asymmetric triple byte -expect - integers = [120220] - result = encode(integers) - expected = [135, 171, 28] - result == expected +expect { + integers = [120220] + result = encode(integers) + expected = [135, 171, 28] + result == expected +} # largest triple byte -expect - integers = [2097151] - result = encode(integers) - expected = [255, 255, 127] - result == expected +expect { + integers = [2097151] + result = encode(integers) + expected = [255, 255, 127] + result == expected +} # smallest quadruple byte -expect - integers = [2097152] - result = encode(integers) - expected = [129, 128, 128, 0] - result == expected +expect { + integers = [2097152] + result = encode(integers) + expected = [129, 128, 128, 0] + result == expected +} # arbitrary quadruple byte -expect - integers = [134217728] - result = encode(integers) - expected = [192, 128, 128, 0] - result == expected +expect { + integers = [134217728] + result = encode(integers) + expected = [192, 128, 128, 0] + result == expected +} # asymmetric quadruple byte -expect - integers = [3503876] - result = encode(integers) - expected = [129, 213, 238, 4] - result == expected +expect { + integers = [3503876] + result = encode(integers) + expected = [129, 213, 238, 4] + result == expected +} # largest quadruple byte -expect - integers = [268435455] - result = encode(integers) - expected = [255, 255, 255, 127] - result == expected +expect { + integers = [268435455] + result = encode(integers) + expected = [255, 255, 255, 127] + result == expected +} # smallest quintuple byte -expect - integers = [268435456] - result = encode(integers) - expected = [129, 128, 128, 128, 0] - result == expected +expect { + integers = [268435456] + result = encode(integers) + expected = [129, 128, 128, 128, 0] + result == expected +} # arbitrary quintuple byte -expect - integers = [4278190080] - result = encode(integers) - expected = [143, 248, 128, 128, 0] - result == expected +expect { + integers = [4278190080] + result = encode(integers) + expected = [143, 248, 128, 128, 0] + result == expected +} # asymmetric quintuple byte -expect - integers = [2254790917] - result = encode(integers) - expected = [136, 179, 149, 194, 5] - result == expected +expect { + integers = [2254790917] + result = encode(integers) + expected = [136, 179, 149, 194, 5] + result == expected +} # maximum 32-bit integer input -expect - integers = [4294967295] - result = encode(integers) - expected = [143, 255, 255, 255, 127] - result == expected +expect { + integers = [4294967295] + result = encode(integers) + expected = [143, 255, 255, 255, 127] + result == expected +} # two single-byte values -expect - integers = [64, 127] - result = encode(integers) - expected = [64, 127] - result == expected +expect { + integers = [64, 127] + result = encode(integers) + expected = [64, 127] + result == expected +} # two multi-byte values -expect - integers = [16384, 1193046] - result = encode(integers) - expected = [129, 128, 0, 200, 232, 86] - result == expected +expect { + integers = [16384, 1193046] + result = encode(integers) + expected = [129, 128, 0, 200, 232, 86] + result == expected +} # many multi-byte values -expect - integers = [8192, 1193046, 268435455, 0, 16383, 16384] - result = encode(integers) - expected = [192, 0, 200, 232, 86, 255, 255, 255, 127, 0, 255, 127, 129, 128, 0] - result == expected +expect { + integers = [8192, 1193046, 268435455, 0, 16383, 16384] + result = encode(integers) + expected = [192, 0, 200, 232, 86, 255, 255, 255, 127, 0, 255, 127, 129, 128, 0] + result == expected +} ## ## Decode a series of bytes, producing a series of integers. ## # one byte -expect - bytes = [127] - result = decode bytes - expected = Ok([127]) - result == expected +expect { + bytes = [127] + result = decode(bytes) + expected = Ok([127]) + result == expected +} # two bytes -expect - bytes = [192, 0] - result = decode bytes - expected = Ok([8192]) - result == expected +expect { + bytes = [192, 0] + result = decode(bytes) + expected = Ok([8192]) + result == expected +} # three bytes -expect - bytes = [255, 255, 127] - result = decode bytes - expected = Ok([2097151]) - result == expected +expect { + bytes = [255, 255, 127] + result = decode(bytes) + expected = Ok([2097151]) + result == expected +} # four bytes -expect - bytes = [129, 128, 128, 0] - result = decode bytes - expected = Ok([2097152]) - result == expected +expect { + bytes = [129, 128, 128, 0] + result = decode(bytes) + expected = Ok([2097152]) + result == expected +} # maximum 32-bit integer -expect - bytes = [143, 255, 255, 255, 127] - result = decode bytes - expected = Ok([4294967295]) - result == expected +expect { + bytes = [143, 255, 255, 255, 127] + result = decode(bytes) + expected = Ok([4294967295]) + result == expected +} # incomplete sequence causes error -expect - bytes = [255] - result = decode bytes - result |> Result.is_err +expect { + bytes = [255] + result = decode(bytes) + result.is_err() +} # incomplete sequence causes error, even if value is zero -expect - bytes = [128] - result = decode bytes - result |> Result.is_err +expect { + bytes = [128] + result = decode(bytes) + result.is_err() +} # multiple values -expect - bytes = [192, 0, 200, 232, 86, 255, 255, 255, 127, 0, 255, 127, 129, 128, 0] - result = decode bytes - expected = Ok([8192, 1193046, 268435455, 0, 16383, 16384]) - result == expected +expect { + bytes = [192, 0, 200, 232, 86, 255, 255, 255, 127, 0, 255, 127, 129, 128, 0] + result = decode(bytes) + expected = Ok([8192, 1193046, 268435455, 0, 16383, 16384]) + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/word-count/.meta/Example.roc b/exercises/practice/word-count/.meta/Example.roc index b66f1581..7ced94c5 100644 --- a/exercises/practice/word-count/.meta/Example.roc +++ b/exercises/practice/word-count/.meta/Example.roc @@ -1,47 +1,56 @@ -module [count_words] +WordCount :: {}.{ + count_words : Str -> Dict(Str, U64) + count_words = |sentence| { + all_words = + sentence + .to_utf8() + .append(' ') + .fold( + { words: [], word: [], contraction_started: Bool.False }, + |state, char| { + { words, word, contraction_started } = state + if char >= 'A' and char <= 'Z' { + { words, word: word.append(char - 'A' + 'a'), contraction_started: Bool.False } + } else if (char >= 'a' and char <= 'z') or (char >= '0' and char <= '9') { + { words, word: word.append(char), contraction_started: Bool.False } + } else { + if word.is_empty() { + state + } else if char != '\'' or contraction_started { + if contraction_started { + { words: words.append(word.drop_last(1)), word: [], contraction_started: Bool.False } + } else { + { words: words.append(word), word: [], contraction_started: Bool.False } + } + } else { + { words, word: word.append(char), contraction_started: Bool.True } + } + } + }, + ) + .words + .drop_if(List.is_empty) -count_words : Str -> Dict Str U64 -count_words = |sentence| - sentence - |> Str.to_utf8 - |> List.append(' ') # to ensure the last word is added - |> List.walk( - { words: [], word: [], contraction_started: Bool.false }, - |state, char| - { words, word, contraction_started } = state - when char is - c if c >= 'A' and c <= 'Z' -> - { words, word: word |> List.append((c - 'A' + 'a')), contraction_started: Bool.false } - - c if c >= 'a' and c <= 'z' or c >= '0' and c <= '9' -> - { words, word: word |> List.append(c), contraction_started: Bool.false } - - c -> - if List.is_empty(word) then - state - else if c != '\'' or contraction_started then - if contraction_started then - { words: words |> List.append((word |> List.drop_last(1))), word: [], contraction_started: Bool.false } - else - { words: words |> List.append(word), word: [], contraction_started: Bool.false } - else - { words, word: word |> List.append(c), contraction_started: Bool.true }, - ) - |> .words - |> List.drop_if(List.is_empty) - |> List.walk( - Dict.empty({}), - |result, chars| - word = - when chars |> Str.from_utf8 is - Ok(parsed_word) -> parsed_word - Err(BadUtf8(_)) -> crash("Unreachable: we only use ASCII characters") - result - |> Dict.update( - word, - |maybe_count| - when maybe_count is - Ok(count) -> Ok((count + 1)) - Err(Missing) -> Ok(1), - ), - ) + all_words.fold( + Dict.empty(), + |result, chars| { + word = + match Str.from_utf8(chars) { + Ok(parsed_word) => parsed_word + Err(BadUtf8(_)) => { + crash "Unreachable: we only use ASCII characters" + } + } + result.update( + word, + |maybe_count| { + match maybe_count { + Ok(count) => Ok(count + 1) + Err(Missing) => Ok(1) + } + }, + ) + }, + ) + } +} diff --git a/exercises/practice/word-count/.meta/template.j2 b/exercises/practice/word-count/.meta/template.j2 index 1a6dccc4..e128f38d 100644 --- a/exercises/practice/word-count/.meta/template.j2 +++ b/exercises/practice/word-count/.meta/template.j2 @@ -6,7 +6,7 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["sentence"] | to_roc }}) expected = Dict.from_list([ {%- for word, count in case["expected"].items() %} @@ -14,5 +14,8 @@ expect {%- endfor %} ]) result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/word-count/WordCount.roc b/exercises/practice/word-count/WordCount.roc index b8aa1ea4..299d206d 100644 --- a/exercises/practice/word-count/WordCount.roc +++ b/exercises/practice/word-count/WordCount.roc @@ -1,5 +1,6 @@ -module [count_words] - -count_words : Str -> Dict Str U64 -count_words = |sentence| - crash("Please implement the 'count_words' function") +WordCount :: {}.{ + count_words : Str -> Dict(Str, U64) + count_words = |sentence| { + crash "Please implement the 'count_words' function" + } +} diff --git a/exercises/practice/word-count/word-count-test.roc b/exercises/practice/word-count/word-count-test.roc index 8eab5385..625beb8b 100644 --- a/exercises/practice/word-count/word-count-test.roc +++ b/exercises/practice/word-count/word-count-test.roc @@ -1,194 +1,204 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/word-count/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import WordCount exposing [count_words] # count one word -expect - result = count_words("word") - expected = Dict.from_list( - [ - ("word", 1), - ], - ) - result == expected +expect { + result = count_words("word") + expected = Dict.from_list( + [ + ("word", 1), + ], + ) + result == expected +} # count one of each word -expect - result = count_words("one of each") - expected = Dict.from_list( - [ - ("one", 1), - ("of", 1), - ("each", 1), - ], - ) - result == expected +expect { + result = count_words("one of each") + expected = Dict.from_list( + [ + ("one", 1), + ("of", 1), + ("each", 1), + ], + ) + result == expected +} # multiple occurrences of a word -expect - result = count_words("one fish two fish red fish blue fish") - expected = Dict.from_list( - [ - ("one", 1), - ("fish", 4), - ("two", 1), - ("red", 1), - ("blue", 1), - ], - ) - result == expected +expect { + result = count_words("one fish two fish red fish blue fish") + expected = Dict.from_list( + [ + ("one", 1), + ("fish", 4), + ("two", 1), + ("red", 1), + ("blue", 1), + ], + ) + result == expected +} # handles cramped lists -expect - result = count_words("one,two,three") - expected = Dict.from_list( - [ - ("one", 1), - ("two", 1), - ("three", 1), - ], - ) - result == expected +expect { + result = count_words("one,two,three") + expected = Dict.from_list( + [ + ("one", 1), + ("two", 1), + ("three", 1), + ], + ) + result == expected +} # handles expanded lists -expect - result = count_words("one,\ntwo,\nthree") - expected = Dict.from_list( - [ - ("one", 1), - ("two", 1), - ("three", 1), - ], - ) - result == expected +expect { + result = count_words("one,\ntwo,\nthree") + expected = Dict.from_list( + [ + ("one", 1), + ("two", 1), + ("three", 1), + ], + ) + result == expected +} # ignore punctuation -expect - result = count_words("car: carpet as java: javascript!!&@$%^&") - expected = Dict.from_list( - [ - ("car", 1), - ("carpet", 1), - ("as", 1), - ("java", 1), - ("javascript", 1), - ], - ) - result == expected +expect { + result = count_words("car: carpet as java: javascript!!&@$%^&") + expected = Dict.from_list( + [ + ("car", 1), + ("carpet", 1), + ("as", 1), + ("java", 1), + ("javascript", 1), + ], + ) + result == expected +} # include numbers -expect - result = count_words("testing, 1, 2 testing") - expected = Dict.from_list( - [ - ("testing", 2), - ("1", 1), - ("2", 1), - ], - ) - result == expected +expect { + result = count_words("testing, 1, 2 testing") + expected = Dict.from_list( + [ + ("testing", 2), + ("1", 1), + ("2", 1), + ], + ) + result == expected +} # normalize case -expect - result = count_words("go Go GO Stop stop") - expected = Dict.from_list( - [ - ("go", 3), - ("stop", 2), - ], - ) - result == expected +expect { + result = count_words("go Go GO Stop stop") + expected = Dict.from_list( + [ + ("go", 3), + ("stop", 2), + ], + ) + result == expected +} # with apostrophes -expect - result = count_words("'First: don't laugh. Then: don't cry. You're getting it.'") - expected = Dict.from_list( - [ - ("first", 1), - ("don't", 2), - ("laugh", 1), - ("then", 1), - ("cry", 1), - ("you're", 1), - ("getting", 1), - ("it", 1), - ], - ) - result == expected +expect { + result = count_words("'First: don't laugh. Then: don't cry. You're getting it.'") + expected = Dict.from_list( + [ + ("first", 1), + ("don't", 2), + ("laugh", 1), + ("then", 1), + ("cry", 1), + ("you're", 1), + ("getting", 1), + ("it", 1), + ], + ) + result == expected +} # with quotations -expect - result = count_words("Joe can't tell between 'large' and large.") - expected = Dict.from_list( - [ - ("joe", 1), - ("can't", 1), - ("tell", 1), - ("between", 1), - ("large", 2), - ("and", 1), - ], - ) - result == expected +expect { + result = count_words("Joe can't tell between 'large' and large.") + expected = Dict.from_list( + [ + ("joe", 1), + ("can't", 1), + ("tell", 1), + ("between", 1), + ("large", 2), + ("and", 1), + ], + ) + result == expected +} # substrings from the beginning -expect - result = count_words("Joe can't tell between app, apple and a.") - expected = Dict.from_list( - [ - ("joe", 1), - ("can't", 1), - ("tell", 1), - ("between", 1), - ("app", 1), - ("apple", 1), - ("and", 1), - ("a", 1), - ], - ) - result == expected +expect { + result = count_words("Joe can't tell between app, apple and a.") + expected = Dict.from_list( + [ + ("joe", 1), + ("can't", 1), + ("tell", 1), + ("between", 1), + ("app", 1), + ("apple", 1), + ("and", 1), + ("a", 1), + ], + ) + result == expected +} # multiple spaces not detected as a word -expect - result = count_words(" multiple whitespaces") - expected = Dict.from_list( - [ - ("multiple", 1), - ("whitespaces", 1), - ], - ) - result == expected +expect { + result = count_words(" multiple whitespaces") + expected = Dict.from_list( + [ + ("multiple", 1), + ("whitespaces", 1), + ], + ) + result == expected +} # alternating word separators not detected as a word -expect - result = count_words(",\n,one,\n ,two \n 'three'") - expected = Dict.from_list( - [ - ("one", 1), - ("two", 1), - ("three", 1), - ], - ) - result == expected +expect { + result = count_words(",\n,one,\n ,two \n 'three'") + expected = Dict.from_list( + [ + ("one", 1), + ("two", 1), + ("three", 1), + ], + ) + result == expected +} # quotation for word with apostrophe -expect - result = count_words("can, can't, 'can't'") - expected = Dict.from_list( - [ - ("can", 1), - ("can't", 2), - ], - ) - result == expected +expect { + result = count_words("can, can't, 'can't'") + expected = Dict.from_list( + [ + ("can", 1), + ("can't", 2), + ], + ) + result == expected +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/word-search/.meta/Example.roc b/exercises/practice/word-search/.meta/Example.roc index 54014a4b..cd6778d1 100644 --- a/exercises/practice/word-search/.meta/Example.roc +++ b/exercises/practice/word-search/.meta/Example.roc @@ -1,71 +1,100 @@ -module [search] +WordSearch :: {}.{ + Position : { column : U64, row : U64 } + WordLocation : { start : Position, end : Position } -Position : { column : U64, row : U64 } -WordLocation : { start : Position, end : Position } + search : Str, List(Str) -> Dict(Str, WordLocation) + search = |grid, words_to_search_for| { + { rows, width } = pad_right(grid.to_utf8().split_on('\n')) + height = rows.len() + height_i64 = height.to_i64_try() ?? 0 + width_i64 = width.to_i64_try() ?? 0 + max_length = ( + if width > height { + width + } else { + height + }, + ).to_i64_try() ?? 0 + + # for all possible starting positions: + (0..join_map( + |column_index| { + (0..join_map( + |row_index| { + # for all possible directions: + [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] + ->join_map( + |dir| { + (dir_column, dir_row) = dir + start = { column: column_index + 1, row: row_index + 1 } + # for all possible lengths: + (0..List.from_iter() + .fold_until( + { found_words: [], chars: [] }, + |state, index| { + end_column_index = (column_index.to_i64_try() ?? 0) + dir_column * index + end_row_index = (row_index.to_i64_try() ?? 0) + dir_row * index + if end_column_index < 0 or end_column_index >= width_i64 or end_row_index < 0 or end_row_index >= height_i64 { + Break(state) + } else { + end_column_index_u64 = end_column_index.to_u64_try() ?? 0 + end_row_index_u64 = end_row_index.to_u64_try() ?? 0 + char = get_char(rows, end_column_index_u64, end_row_index_u64) + new_chars = state.chars.append(char) + end = { column: end_column_index_u64 + 1, row: end_row_index_u64 + 1 } + maybe_word = words_to_search_for.find_first(|word| word.to_utf8() == new_chars) + new_found_words = + match maybe_word { + Ok(word) => state.found_words.append((word, { start, end })) + Err(NotFound) => state.found_words + } + Continue({ found_words: new_found_words, chars: new_chars }) + } + }, + ) + .found_words + }, + ) + }, + ) + }, + ) + ->Dict.from_list() + } +} ## Pad each row with spaces to ensure all rows have the same width -pad_right : List (List U8) -> { rows : List (List U8), width : U64 } -pad_right = |grid| - width = grid |> List.map(List.len) |> List.max |> Result.with_default(0) - pad_spaces = |chars| List.repeat(' ', (width - List.len(chars))) - { - rows: grid |> List.map(|chars| chars |> List.concat(pad_spaces(chars))), - width, - } +pad_right : List(List(U8)) -> { rows : List(List(U8)), width : U64 } +pad_right = |grid| { + width = grid.map(List.len).fold( + 0, + |max_val, len| if len > max_val { + len + } else { + max_val + }, + ) + pad_spaces = |chars| List.repeat(' ', (width - chars.len())) + { + rows: grid.map(|chars| chars.concat(pad_spaces(chars))), + width, + } +} -get_char : List (List U8), U64, U64 -> U8 -get_char = |grid, column_index, row_index| - grid - |> List.get(row_index) - |> Result.with_default([]) - |> List.get(column_index) - |> Result.with_default(' ') +get_char : List(List(U8)), U64, U64 -> U8 +get_char = |grid, column_index, row_index| { + row = grid.get(row_index) ?? [] + row.get(column_index) ?? ' ' +} -search : Str, List Str -> Dict Str WordLocation -search = |grid, words_to_search_for| - { rows, width } = grid |> Str.to_utf8 |> List.split_on('\n') |> pad_right - height = List.len(rows) - height_i64 = height |> Num.to_i64 - width_i64 = width |> Num.to_i64 - max_length = Num.max(width, height) |> Num.to_i64 - # for all possible starting positions: - List.range({ start: At(0), end: Before(width) }) - |> List.join_map( - |column_index| - List.range({ start: At(0), end: Before(height) }) - |> List.join_map( - |row_index| - # for all possible directions: - [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)] - |> List.join_map( - |(dir_column, dir_row)| - start = { column: column_index + 1, row: row_index + 1 } - # for all possible lengths: - List.range({ start: At(0), end: Before(max_length) }) # for all possible words starting at the given position and - # going in the given direction, take note of the start and end - # positions - |> List.walk_until( - { found_words: [], chars: [] }, - |state, index| - end_column_index = Num.to_i64(column_index) + dir_column * index - end_row_index = Num.to_i64(row_index) + dir_row * index - if end_column_index < 0 or end_column_index >= width_i64 or end_row_index < 0 or end_row_index >= height_i64 then - Break(state) - else - end_column_index_u64 = end_column_index |> Num.to_u64 - end_row_index_u64 = end_row_index |> Num.to_u64 - char = rows |> get_char(end_column_index_u64, end_row_index_u64) - new_chars = state.chars |> List.append(char) - end = { column: end_column_index_u64 + 1, row: end_row_index_u64 + 1 } - maybe_word = words_to_search_for |> List.find_first(|word| word |> Str.to_utf8 == new_chars) - new_found_words = - when maybe_word is - Ok(word) -> state.found_words |> List.append((word, { start, end })) - Err(NotFound) -> state.found_words - Continue({ found_words: new_found_words, chars: new_chars }), - ) - |> .found_words, - ), - ), - ) - |> Dict.from_list +# The following function should soon be available in Roc's builtins +join_map = |iter, func| { + var $state = [] + for item in iter { + for subitem in func(item) { + $state = $state.append(subitem) + } + } + $state +} diff --git a/exercises/practice/word-search/.meta/template.j2 b/exercises/practice/word-search/.meta/template.j2 index 40fcd147..79301224 100644 --- a/exercises/practice/word-search/.meta/template.j2 +++ b/exercises/practice/word-search/.meta/template.j2 @@ -6,10 +6,10 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect +expect { grid = {{ case["input"]["grid"] | to_roc_multiline_string | indent(8) }} words_to_search_for = {{ case["input"]["wordsToSearchFor"] | to_roc }} - result = grid |> {{ case["property"] | to_snake }}(words_to_search_for) + result = grid -> {{ case["property"] | to_snake }}(words_to_search_for) expected = Dict.from_list([ {%- for word, result in case["expected"].items() %} {%- if result is none %} @@ -20,5 +20,8 @@ expect {%- endfor %} ]) result == expected +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/word-search/WordSearch.roc b/exercises/practice/word-search/WordSearch.roc index 0d1f45af..70e70143 100644 --- a/exercises/practice/word-search/WordSearch.roc +++ b/exercises/practice/word-search/WordSearch.roc @@ -1,8 +1,9 @@ -module [search] +WordSearch :: {}.{ + Position : { column : U64, row : U64 } + WordLocation : { start : Position, end : Position } -Position : { column : U64, row : U64 } -WordLocation : { start : Position, end : Position } - -search : Str, List Str -> Dict Str WordLocation -search = |grid, words_to_search_for| - crash("Please implement the 'search' function") + search : Str, List(Str) -> Dict(Str, WordLocation) + search = |grid, words_to_search_for| { + crash "Please implement the 'search' function" + } +} diff --git a/exercises/practice/word-search/word-search-test.roc b/exercises/practice/word-search/word-search-test.roc index 614cb6f8..2994f1af 100644 --- a/exercises/practice/word-search/word-search-test.roc +++ b/exercises/practice/word-search/word-search-test.roc @@ -1,512 +1,502 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/word-search/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import WordSearch exposing [search] # Should accept an initial game grid and a target search word -expect - grid = "jefblpepre" - words_to_search_for = ["clojure"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - # "clojure" is not in the grid - ], - ) - result == expected +expect { + grid = "jefblpepre" + words_to_search_for = ["clojure"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [], + ) + result == expected +} # Should locate one word written left to right -expect - grid = "clojurermt" - words_to_search_for = ["clojure"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 1 }, end: { column: 7, row: 1 } }), - ], - ) - result == expected +expect { + grid = "clojurermt" + words_to_search_for = ["clojure"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 1 }, end: { column: 7, row: 1 } }), + ], + ) + result == expected +} # Should locate the same word written left to right in a different position -expect - grid = "mtclojurer" - words_to_search_for = ["clojure"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 3, row: 1 }, end: { column: 9, row: 1 } }), - ], - ) - result == expected +expect { + grid = "mtclojurer" + words_to_search_for = ["clojure"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 3, row: 1 }, end: { column: 9, row: 1 } }), + ], + ) + result == expected +} # Should locate a different left to right word -expect - grid = "coffeelplx" - words_to_search_for = ["coffee"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("coffee", { start: { column: 1, row: 1 }, end: { column: 6, row: 1 } }), - ], - ) - result == expected +expect { + grid = "coffeelplx" + words_to_search_for = ["coffee"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("coffee", { start: { column: 1, row: 1 }, end: { column: 6, row: 1 } }), + ], + ) + result == expected +} # Should locate that different left to right word in a different position -expect - grid = "xcoffeezlp" - words_to_search_for = ["coffee"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("coffee", { start: { column: 2, row: 1 }, end: { column: 7, row: 1 } }), - ], - ) - result == expected +expect { + grid = "xcoffeezlp" + words_to_search_for = ["coffee"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("coffee", { start: { column: 2, row: 1 }, end: { column: 7, row: 1 } }), + ], + ) + result == expected +} # Should locate a left to right word in two line grid -expect - grid = - """ - jefblpepre - tclojurerm - """ - words_to_search_for = ["clojure"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 2, row: 2 }, end: { column: 8, row: 2 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\tclojurerm + + words_to_search_for = ["clojure"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 2, row: 2 }, end: { column: 8, row: 2 } }), + ], + ) + result == expected +} # Should locate a left to right word in three line grid -expect - grid = - """ - camdcimgtc - jefblpepre - clojurermt - """ - words_to_search_for = ["clojure"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 3 }, end: { column: 7, row: 3 } }), - ], - ) - result == expected +expect { + grid = + \\camdcimgtc + \\jefblpepre + \\clojurermt + + words_to_search_for = ["clojure"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 3 }, end: { column: 7, row: 3 } }), + ], + ) + result == expected +} # Should locate a left to right word in ten line grid -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - screeaumgr - alxhpburyi - jalaycalmp - clojurermt - """ - words_to_search_for = ["clojure"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\screeaumgr + \\alxhpburyi + \\jalaycalmp + \\clojurermt + + words_to_search_for = ["clojure"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), + ], + ) + result == expected +} # Should locate that left to right word in a different position in a ten line grid -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - screeaumgr - alxhpburyi - clojurermt - jalaycalmp - """ - words_to_search_for = ["clojure"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 9 }, end: { column: 7, row: 9 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\screeaumgr + \\alxhpburyi + \\clojurermt + \\jalaycalmp + + words_to_search_for = ["clojure"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 9 }, end: { column: 7, row: 9 } }), + ], + ) + result == expected +} # Should locate a different left to right word in a ten line grid -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - fortranftw - alxhpburyi - clojurermt - jalaycalmp - """ - words_to_search_for = ["fortran"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("fortran", { start: { column: 1, row: 7 }, end: { column: 7, row: 7 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\fortranftw + \\alxhpburyi + \\clojurermt + \\jalaycalmp + + words_to_search_for = ["fortran"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("fortran", { start: { column: 1, row: 7 }, end: { column: 7, row: 7 } }), + ], + ) + result == expected +} # Should locate multiple words -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - fortranftw - alxhpburyi - jalaycalmp - clojurermt - """ - words_to_search_for = ["fortran", "clojure"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), - ("fortran", { start: { column: 1, row: 7 }, end: { column: 7, row: 7 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\fortranftw + \\alxhpburyi + \\jalaycalmp + \\clojurermt + + words_to_search_for = ["fortran", "clojure"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), + ("fortran", { start: { column: 1, row: 7 }, end: { column: 7, row: 7 } }), + ], + ) + result == expected +} # Should locate a single word written right to left -expect - grid = "rixilelhrs" - words_to_search_for = ["elixir"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("elixir", { start: { column: 6, row: 1 }, end: { column: 1, row: 1 } }), - ], - ) - result == expected +expect { + grid = "rixilelhrs" + words_to_search_for = ["elixir"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("elixir", { start: { column: 6, row: 1 }, end: { column: 1, row: 1 } }), + ], + ) + result == expected +} # Should locate multiple words written in different horizontal directions -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - screeaumgr - alxhpburyi - jalaycalmp - clojurermt - """ - words_to_search_for = ["elixir", "clojure"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), - ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\screeaumgr + \\alxhpburyi + \\jalaycalmp + \\clojurermt + + words_to_search_for = ["elixir", "clojure"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), + ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), + ], + ) + result == expected +} # Should locate words written top to bottom -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - screeaumgr - alxhpburyi - jalaycalmp - clojurermt - """ - words_to_search_for = ["clojure", "elixir", "ecmascript"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), - ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), - ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\screeaumgr + \\alxhpburyi + \\jalaycalmp + \\clojurermt + + words_to_search_for = ["clojure", "elixir", "ecmascript"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), + ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), + ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), + ], + ) + result == expected +} # Should locate words written bottom to top -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - screeaumgr - alxhpburyi - jalaycalmp - clojurermt - """ - words_to_search_for = ["clojure", "elixir", "ecmascript", "rust"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), - ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), - ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), - ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\screeaumgr + \\alxhpburyi + \\jalaycalmp + \\clojurermt + + words_to_search_for = ["clojure", "elixir", "ecmascript", "rust"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), + ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), + ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), + ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), + ], + ) + result == expected +} # Should locate words written top left to bottom right -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - screeaumgr - alxhpburyi - jalaycalmp - clojurermt - """ - words_to_search_for = ["clojure", "elixir", "ecmascript", "rust", "java"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), - ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), - ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), - ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), - ("java", { start: { column: 1, row: 1 }, end: { column: 4, row: 4 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\screeaumgr + \\alxhpburyi + \\jalaycalmp + \\clojurermt + + words_to_search_for = ["clojure", "elixir", "ecmascript", "rust", "java"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), + ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), + ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), + ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), + ("java", { start: { column: 1, row: 1 }, end: { column: 4, row: 4 } }), + ], + ) + result == expected +} # Should locate words written bottom right to top left -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - screeaumgr - alxhpburyi - jalaycalmp - clojurermt - """ - words_to_search_for = ["clojure", "elixir", "ecmascript", "rust", "java", "lua"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), - ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), - ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), - ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), - ("java", { start: { column: 1, row: 1 }, end: { column: 4, row: 4 } }), - ("lua", { start: { column: 8, row: 9 }, end: { column: 6, row: 7 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\screeaumgr + \\alxhpburyi + \\jalaycalmp + \\clojurermt + + words_to_search_for = ["clojure", "elixir", "ecmascript", "rust", "java", "lua"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), + ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), + ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), + ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), + ("java", { start: { column: 1, row: 1 }, end: { column: 4, row: 4 } }), + ("lua", { start: { column: 8, row: 9 }, end: { column: 6, row: 7 } }), + ], + ) + result == expected +} # Should locate words written bottom left to top right -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - screeaumgr - alxhpburyi - jalaycalmp - clojurermt - """ - words_to_search_for = ["clojure", "elixir", "ecmascript", "rust", "java", "lua", "lisp"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), - ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), - ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), - ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), - ("java", { start: { column: 1, row: 1 }, end: { column: 4, row: 4 } }), - ("lua", { start: { column: 8, row: 9 }, end: { column: 6, row: 7 } }), - ("lisp", { start: { column: 3, row: 6 }, end: { column: 6, row: 3 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\screeaumgr + \\alxhpburyi + \\jalaycalmp + \\clojurermt + + words_to_search_for = ["clojure", "elixir", "ecmascript", "rust", "java", "lua", "lisp"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), + ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), + ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), + ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), + ("java", { start: { column: 1, row: 1 }, end: { column: 4, row: 4 } }), + ("lua", { start: { column: 8, row: 9 }, end: { column: 6, row: 7 } }), + ("lisp", { start: { column: 3, row: 6 }, end: { column: 6, row: 3 } }), + ], + ) + result == expected +} # Should locate words written top right to bottom left -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - screeaumgr - alxhpburyi - jalaycalmp - clojurermt - """ - words_to_search_for = ["clojure", "elixir", "ecmascript", "rust", "java", "lua", "lisp", "ruby"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), - ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), - ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), - ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), - ("java", { start: { column: 1, row: 1 }, end: { column: 4, row: 4 } }), - ("lua", { start: { column: 8, row: 9 }, end: { column: 6, row: 7 } }), - ("lisp", { start: { column: 3, row: 6 }, end: { column: 6, row: 3 } }), - ("ruby", { start: { column: 8, row: 6 }, end: { column: 5, row: 9 } }), - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\screeaumgr + \\alxhpburyi + \\jalaycalmp + \\clojurermt + + words_to_search_for = ["clojure", "elixir", "ecmascript", "rust", "java", "lua", "lisp", "ruby"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), + ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), + ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), + ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), + ("java", { start: { column: 1, row: 1 }, end: { column: 4, row: 4 } }), + ("lua", { start: { column: 8, row: 9 }, end: { column: 6, row: 7 } }), + ("lisp", { start: { column: 3, row: 6 }, end: { column: 6, row: 3 } }), + ("ruby", { start: { column: 8, row: 6 }, end: { column: 5, row: 9 } }), + ], + ) + result == expected +} # Should fail to locate a word that is not in the puzzle -expect - grid = - """ - jefblpepre - camdcimgtc - oivokprjsm - pbwasqroua - rixilelhrs - wolcqlirpc - screeaumgr - alxhpburyi - jalaycalmp - clojurermt - """ - words_to_search_for = ["clojure", "elixir", "ecmascript", "rust", "java", "lua", "lisp", "ruby", "haskell"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), - ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), - ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), - ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), - ("java", { start: { column: 1, row: 1 }, end: { column: 4, row: 4 } }), - ("lua", { start: { column: 8, row: 9 }, end: { column: 6, row: 7 } }), - ("lisp", { start: { column: 3, row: 6 }, end: { column: 6, row: 3 } }), - ("ruby", { start: { column: 8, row: 6 }, end: { column: 5, row: 9 } }), - # "haskell" is not in the grid - ], - ) - result == expected +expect { + grid = + \\jefblpepre + \\camdcimgtc + \\oivokprjsm + \\pbwasqroua + \\rixilelhrs + \\wolcqlirpc + \\screeaumgr + \\alxhpburyi + \\jalaycalmp + \\clojurermt + + words_to_search_for = ["clojure", "elixir", "ecmascript", "rust", "java", "lua", "lisp", "ruby", "haskell"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [ + ("clojure", { start: { column: 1, row: 10 }, end: { column: 7, row: 10 } }), + ("elixir", { start: { column: 6, row: 5 }, end: { column: 1, row: 5 } }), + ("ecmascript", { start: { column: 10, row: 1 }, end: { column: 10, row: 10 } }), + ("rust", { start: { column: 9, row: 5 }, end: { column: 9, row: 2 } }), + ("java", { start: { column: 1, row: 1 }, end: { column: 4, row: 4 } }), + ("lua", { start: { column: 8, row: 9 }, end: { column: 6, row: 7 } }), + ("lisp", { start: { column: 3, row: 6 }, end: { column: 6, row: 3 } }), + ("ruby", { start: { column: 8, row: 6 }, end: { column: 5, row: 9 } }), + # "haskell" is not in the grid + ], + ) + result == expected +} # Should fail to locate words that are not on horizontal, vertical, or diagonal lines -expect - grid = - """ - abc - def - """ - words_to_search_for = ["aef", "ced", "abf", "cbd"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - # "aef" is not in the grid - # "ced" is not in the grid - # "abf" is not in the grid - # "cbd" is not in the grid - ], - ) - result == expected +expect { + grid = + \\abc + \\def + + words_to_search_for = ["aef", "ced", "abf", "cbd"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [], + ) + result == expected +} # Should not concatenate different lines to find a horizontal word -expect - grid = - """ - abceli - xirdfg - """ - words_to_search_for = ["elixir"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - # "elixir" is not in the grid - ], - ) - result == expected +expect { + grid = + \\abceli + \\xirdfg + + words_to_search_for = ["elixir"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [], + ) + result == expected +} # Should not wrap around horizontally to find a word -expect - grid = "silabcdefp" - words_to_search_for = ["lisp"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - # "lisp" is not in the grid - ], - ) - result == expected +expect { + grid = "silabcdefp" + words_to_search_for = ["lisp"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [], + ) + result == expected +} # Should not wrap around vertically to find a word -expect - grid = - """ - s - u - r - a - b - c - t - """ - words_to_search_for = ["rust"] - result = grid |> search(words_to_search_for) - expected = Dict.from_list( - [ - # "rust" is not in the grid - ], - ) - result == expected +expect { + grid = + \\s + \\u + \\r + \\a + \\b + \\c + \\t + words_to_search_for = ["rust"] + result = grid->search(words_to_search_for) + expected = Dict.from_list( + [], + ) + result == expected +} + +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/wordy/.meta/Example.roc b/exercises/practice/wordy/.meta/Example.roc index b7c06d0f..27193928 100644 --- a/exercises/practice/wordy/.meta/Example.roc +++ b/exercises/practice/wordy/.meta/Example.roc @@ -1,39 +1,45 @@ -module [answer] - -evaluate_expression = |accumulator, operations| - when operations is - [] -> Ok(accumulator) - ["plus", number_string, .. as rest] -> - evaluate_expression((accumulator + Str.to_i64(number_string)?), rest) - - ["minus", number_string, .. as rest] -> - evaluate_expression(accumulator - Str.to_i64(number_string)?, rest) - - ["multiplied", "by", number_string, .. as rest] -> - evaluate_expression(accumulator * Str.to_i64(number_string)?, rest) - - ["divided", "by", number_string, .. as rest] -> - evaluate_expression(accumulator // Str.to_i64(number_string)?, rest) - - ["cubed"] -> Err(OperationsArgHadAnInvalidOperation(operations)) - _ -> Err(OperationsArgHadASyntaxError(operations)) - -answer : Str -> Result I64 [QuestionArgHadAnUnknownOperation Str, QuestionArgHadASyntaxError Str] -answer = |question| - words = question |> Str.replace_each("?", " ?") |> Str.split_on(" ") - when words is - ["What", "is", number_string, .. as operations, "?"] -> - maybe_start_number = Str.to_i64(number_string) - when maybe_start_number is - Ok(start_number) -> - when evaluate_expression(start_number, operations) is - Err(OperationsArgHadAnInvalidOperation(_)) -> Err(QuestionArgHadAnUnknownOperation(question)) - Err(OperationsArgHadASyntaxError(_)) -> Err(QuestionArgHadASyntaxError(question)) - Err(InvalidNumStr) -> Err(QuestionArgHadASyntaxError(question)) - Ok(result) -> Ok(result) - - Err(InvalidNumStr) -> Err(QuestionArgHadASyntaxError(question)) - - [_, "is", _, .., "?"] -> Err(QuestionArgHadAnUnknownOperation(question)) - [_, "are", .., "?"] -> Err(QuestionArgHadAnUnknownOperation(question)) - _ -> Err(QuestionArgHadASyntaxError(question)) +Wordy :: {}.{ + answer : Str -> Try(I64, [QuestionArgHadAnUnknownOperation(Str), QuestionArgHadASyntaxError(Str)]) + answer = |question| { + words = question.drop_suffix("?").split_on(" ") + match words { + ["What", "is", number_string, .. as operations] => { + maybe_start_number = I64.from_str(number_string) + match maybe_start_number { + Ok(start_number) => { + match evaluate_expression(start_number, operations) { + Err(OperationsArgHadAnInvalidOperation(_)) => Err(QuestionArgHadAnUnknownOperation(question)) + Err(OperationsArgHadASyntaxError(_)) => Err(QuestionArgHadASyntaxError(question)) + Err(BadNumStr) => Err(QuestionArgHadASyntaxError(question)) + Ok(result) => Ok(result) + } + } + Err(BadNumStr) => Err(QuestionArgHadASyntaxError(question)) + } + } + [_, "is", _, ..] => Err(QuestionArgHadAnUnknownOperation(question)) + [_, "are", ..] => Err(QuestionArgHadAnUnknownOperation(question)) + _ => Err(QuestionArgHadASyntaxError(question)) + } + } +} + +evaluate_expression = |accumulator, operations| { + match operations { + [] => Ok(accumulator) + ["plus", number_string, .. as rest] => { + evaluate_expression((accumulator + I64.from_str(number_string)?), rest) + } + ["minus", number_string, .. as rest] => { + evaluate_expression((accumulator - I64.from_str(number_string)?), rest) + } + ["multiplied", "by", number_string, .. as rest] => { + evaluate_expression((accumulator * I64.from_str(number_string)?), rest) + } + ["divided", "by", number_string, .. as rest] => { + evaluate_expression((accumulator // I64.from_str(number_string)?), rest) + } + ["cubed"] => Err(OperationsArgHadAnInvalidOperation(operations)) + _ => Err(OperationsArgHadASyntaxError(operations)) + } +} diff --git a/exercises/practice/wordy/.meta/template.j2 b/exercises/practice/wordy/.meta/template.j2 index d814b72c..90876432 100644 --- a/exercises/practice/wordy/.meta/template.j2 +++ b/exercises/practice/wordy/.meta/template.j2 @@ -6,12 +6,15 @@ import {{ exercise | to_pascal }} exposing [answer] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }}({{ case["input"]["question"] | to_roc }}) {%- if case["expected"]["error"] %} - Result.is_err(result) + result.is_err() {%- else %} result == Ok({{ case["expected"] }}) {%- endif %} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/wordy/Wordy.roc b/exercises/practice/wordy/Wordy.roc index 471c091a..64593e4d 100644 --- a/exercises/practice/wordy/Wordy.roc +++ b/exercises/practice/wordy/Wordy.roc @@ -1,5 +1,6 @@ -module [answer] - -answer : Str -> Result I64 _ -answer = |question| - crash("Please implement the 'answer' function") +Wordy :: {}.{ + answer : Str -> Try(I64, _) + answer = |question| { + crash "Please implement the 'answer' function" + } +} diff --git a/exercises/practice/wordy/wordy-test.roc b/exercises/practice/wordy/wordy-test.roc index a3f523a3..7100ebc3 100644 --- a/exercises/practice/wordy/wordy-test.roc +++ b/exercises/practice/wordy/wordy-test.roc @@ -1,149 +1,172 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/wordy/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Wordy exposing [answer] # just a number -expect - result = answer("What is 5?") - result == Ok(5) +expect { + result = answer("What is 5?") + result == Ok(5) +} # just a zero -expect - result = answer("What is 0?") - result == Ok(0) +expect { + result = answer("What is 0?") + result == Ok(0) +} # just a negative number -expect - result = answer("What is -123?") - result == Ok(-123) +expect { + result = answer("What is -123?") + result == Ok(-123) +} # addition -expect - result = answer("What is 1 plus 1?") - result == Ok(2) +expect { + result = answer("What is 1 plus 1?") + result == Ok(2) +} # addition with a left hand zero -expect - result = answer("What is 0 plus 2?") - result == Ok(2) +expect { + result = answer("What is 0 plus 2?") + result == Ok(2) +} # addition with a right hand zero -expect - result = answer("What is 3 plus 0?") - result == Ok(3) +expect { + result = answer("What is 3 plus 0?") + result == Ok(3) +} # more addition -expect - result = answer("What is 53 plus 2?") - result == Ok(55) +expect { + result = answer("What is 53 plus 2?") + result == Ok(55) +} # addition with negative numbers -expect - result = answer("What is -1 plus -10?") - result == Ok(-11) +expect { + result = answer("What is -1 plus -10?") + result == Ok(-11) +} # large addition -expect - result = answer("What is 123 plus 45678?") - result == Ok(45801) +expect { + result = answer("What is 123 plus 45678?") + result == Ok(45801) +} # subtraction -expect - result = answer("What is 4 minus -12?") - result == Ok(16) +expect { + result = answer("What is 4 minus -12?") + result == Ok(16) +} # multiplication -expect - result = answer("What is -3 multiplied by 25?") - result == Ok(-75) +expect { + result = answer("What is -3 multiplied by 25?") + result == Ok(-75) +} # division -expect - result = answer("What is 33 divided by -3?") - result == Ok(-11) +expect { + result = answer("What is 33 divided by -3?") + result == Ok(-11) +} # multiple additions -expect - result = answer("What is 1 plus 1 plus 1?") - result == Ok(3) +expect { + result = answer("What is 1 plus 1 plus 1?") + result == Ok(3) +} # addition and subtraction -expect - result = answer("What is 1 plus 5 minus -2?") - result == Ok(8) +expect { + result = answer("What is 1 plus 5 minus -2?") + result == Ok(8) +} # multiple subtraction -expect - result = answer("What is 20 minus 4 minus 13?") - result == Ok(3) +expect { + result = answer("What is 20 minus 4 minus 13?") + result == Ok(3) +} # subtraction then addition -expect - result = answer("What is 17 minus 6 plus 3?") - result == Ok(14) +expect { + result = answer("What is 17 minus 6 plus 3?") + result == Ok(14) +} # multiple multiplication -expect - result = answer("What is 2 multiplied by -2 multiplied by 3?") - result == Ok(-12) +expect { + result = answer("What is 2 multiplied by -2 multiplied by 3?") + result == Ok(-12) +} # addition and multiplication -expect - result = answer("What is -3 plus 7 multiplied by -2?") - result == Ok(-8) +expect { + result = answer("What is -3 plus 7 multiplied by -2?") + result == Ok(-8) +} # multiple division -expect - result = answer("What is -12 divided by 2 divided by -3?") - result == Ok(2) +expect { + result = answer("What is -12 divided by 2 divided by -3?") + result == Ok(2) +} # unknown operation -expect - result = answer("What is 52 cubed?") - Result.is_err(result) +expect { + result = answer("What is 52 cubed?") + result.is_err() +} # Non math question -expect - result = answer("Who is the President of the United States?") - Result.is_err(result) +expect { + result = answer("Who is the President of the United States?") + result.is_err() +} # reject problem missing an operand -expect - result = answer("What is 1 plus?") - Result.is_err(result) +expect { + result = answer("What is 1 plus?") + result.is_err() +} # reject problem with no operands or operators -expect - result = answer("What is?") - Result.is_err(result) +expect { + result = answer("What is?") + result.is_err() +} # reject two operations in a row -expect - result = answer("What is 1 plus plus 2?") - Result.is_err(result) +expect { + result = answer("What is 1 plus plus 2?") + result.is_err() +} # reject two numbers in a row -expect - result = answer("What is 1 plus 2 1?") - Result.is_err(result) +expect { + result = answer("What is 1 plus 2 1?") + result.is_err() +} # reject postfix notation -expect - result = answer("What is 1 2 plus?") - Result.is_err(result) +expect { + result = answer("What is 1 2 plus?") + result.is_err() +} # reject prefix notation -expect - result = answer("What is plus 1 2?") - Result.is_err(result) +expect { + result = answer("What is plus 1 2?") + 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/yacht/.meta/Example.roc b/exercises/practice/yacht/.meta/Example.roc index 45da41b1..f4775eea 100644 --- a/exercises/practice/yacht/.meta/Example.roc +++ b/exercises/practice/yacht/.meta/Example.roc @@ -1,56 +1,93 @@ -module [score] +Yacht :: {}.{ + Category := [Ones, Twos, Threes, Fours, Fives, Sixes, FullHouse, FourOfAKind, LittleStraight, BigStraight, Choice, Yacht] -Category : [Ones, Twos, Threes, Fours, Fives, Sixes, FullHouse, FourOfAKind, LittleStraight, BigStraight, Choice, Yacht] + score : List(U8), Category -> U8 + score = |dice, category| { + if dice.len() != 5 or dice.any(|die| die < 1 or die > 6) { + 0 + } else { + match category { + Ones => score_ones_to_sixes(dice, 1) + Twos => score_ones_to_sixes(dice, 2) + Threes => score_ones_to_sixes(dice, 3) + Fours => score_ones_to_sixes(dice, 4) + Fives => score_ones_to_sixes(dice, 5) + Sixes => score_ones_to_sixes(dice, 6) + FullHouse => score_full_house(dice) + FourOfAKind => score_four_of_a_kind(dice) + LittleStraight => score_straight(dice, [1, 2, 3, 4, 5]) + BigStraight => score_straight(dice, [2, 3, 4, 5, 6]) + Choice => dice.sum() + Yacht => { + if Set.from_list(dice).len() == 1 { + 50 + } else { + 0 + } + } + } + } + } +} -score_ones_to_sixes : List U8, U8 -> U8 -score_ones_to_sixes = |dice, value| - dice |> List.keep_if(|die| die == value) |> List.sum +score_ones_to_sixes : List(U8), U8 -> U8 +score_ones_to_sixes = |dice, value| { + dice.keep_if(|die| die == value).sum() +} -value_counts : List U8 -> List U8 -value_counts = |dice| - dice - |> List.walk( - [0, 0, 0, 0, 0, 0], - |counts, die| - counts |> List.update((die - 1 |> Num.to_u64), |x| x + 1), - ) +value_counts : List(U8) -> List(U8) +value_counts = |dice| { + dice.fold( + [0, 0, 0, 0, 0, 0], + |counts, die| { + counts.update(U8.to_u64(die - 1), |x| x + 1) ?? counts + }, + ) +} -score_full_house : List U8 -> U8 -score_full_house = |dice| - if dice |> value_counts |> Set.from_list == Set.from_list([0, 2, 3]) then - dice |> List.sum - else - 0 +score_full_house : List(U8) -> U8 +score_full_house = |dice| { + if Set.from_list(value_counts(dice)) == Set.from_list([0, 2, 3]) { + dice.sum() + } else { + 0 + } +} -score_four_of_a_kind : List U8 -> U8 -score_four_of_a_kind = |dice| - dice - |> value_counts - |> List.walk_with_index_until( - 0, - |_, count, index| - if count < 4 then Continue(0) else (index + 1 |> Num.to_u8) * 4 |> Break, - ) +score_four_of_a_kind : List(U8) -> U8 +score_four_of_a_kind = |dice| { + value_counts(dice) + .fold_with_index_until( + 0, + |_, count, index| { + if count < 4 { + Continue(0) + } else { + Break(((index + 1).to_u8_try() ?? 0) * 4) + } + }, + ) +} -score_straight : List U8, List U8 -> U8 -score_straight = |dice, target| - if dice |> List.sort_asc == target then 30 else 0 +score_straight : List(U8), List(U8) -> U8 +score_straight = |dice, target| { + if sort_asc(dice) == target { + 30 + } else { + 0 + } +} -score : List U8, Category -> U8 -score = |dice, category| - if dice |> List.len != 5 or dice |> List.any(|die| die < 1 or die > 6) then - 0 - else - when category is - Ones -> dice |> score_ones_to_sixes(1) - Twos -> dice |> score_ones_to_sixes(2) - Threes -> dice |> score_ones_to_sixes(3) - Fours -> dice |> score_ones_to_sixes(4) - Fives -> dice |> score_ones_to_sixes(5) - Sixes -> dice |> score_ones_to_sixes(6) - FullHouse -> dice |> score_full_house - FourOfAKind -> dice |> score_four_of_a_kind - LittleStraight -> dice |> score_straight([1, 2, 3, 4, 5]) - BigStraight -> dice |> score_straight([2, 3, 4, 5, 6]) - Choice -> dice |> List.sum - Yacht -> if dice |> Set.from_list |> Set.len == 1 then 50 else 0 +sort_asc = |list| { + list.sort_with( + |a, b| { + if a < b { + LT + } else if a > b { + GT + } else { + EQ + } + }, + ) +} diff --git a/exercises/practice/yacht/.meta/template.j2 b/exercises/practice/yacht/.meta/template.j2 index a429ba1b..af483896 100644 --- a/exercises/practice/yacht/.meta/template.j2 +++ b/exercises/practice/yacht/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [{{ cases[0]["property"] | to_snake } {% for case in cases -%} # {{ case["description"] }} -expect - result = {{ case["input"]["dice"] | to_roc }} |> {{ case["property"] | to_snake }}({{ case["input"]["category"] | to_pascal }}) +expect { + result = {{ case["input"]["dice"] | to_roc }} -> {{ case["property"] | to_snake }}({{ case["input"]["category"] | to_pascal }}) result == {{ case["expected"] | to_roc }} +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/yacht/Yacht.roc b/exercises/practice/yacht/Yacht.roc index d38ff702..e142bccb 100644 --- a/exercises/practice/yacht/Yacht.roc +++ b/exercises/practice/yacht/Yacht.roc @@ -1,7 +1,8 @@ -module [score] +Yacht :: {}.{ + Category := [Ones, Twos, Threes, Fours, Fives, Sixes, FullHouse, FourOfAKind, LittleStraight, BigStraight, Choice, Yacht] -Category : [Ones, Twos, Threes, Fours, Fives, Sixes, FullHouse, FourOfAKind, LittleStraight, BigStraight, Choice, Yacht] - -score : List U8, Category -> U8 -score = |dice, category| - crash("Please implement the 'score' function") + score : List(U8), Category -> U8 + score = |dice, category| { + crash "Please implement the 'score' function" + } +} diff --git a/exercises/practice/yacht/yacht-test.roc b/exercises/practice/yacht/yacht-test.roc index 12764068..71cfbe6b 100644 --- a/exercises/practice/yacht/yacht-test.roc +++ b/exercises/practice/yacht/yacht-test.roc @@ -1,159 +1,184 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/yacht/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-22 import Yacht exposing [score] # Yacht -expect - result = [5, 5, 5, 5, 5] |> score(Yacht) - result == 50 +expect { + result = [5, 5, 5, 5, 5]->score(Yacht) + result == 50 +} # Not Yacht -expect - result = [1, 3, 3, 2, 5] |> score(Yacht) - result == 0 +expect { + result = [1, 3, 3, 2, 5]->score(Yacht) + result == 0 +} # Ones -expect - result = [1, 1, 1, 3, 5] |> score(Ones) - result == 3 +expect { + result = [1, 1, 1, 3, 5]->score(Ones) + result == 3 +} # Ones, out of order -expect - result = [3, 1, 1, 5, 1] |> score(Ones) - result == 3 +expect { + result = [3, 1, 1, 5, 1]->score(Ones) + result == 3 +} # No ones -expect - result = [4, 3, 6, 5, 5] |> score(Ones) - result == 0 +expect { + result = [4, 3, 6, 5, 5]->score(Ones) + result == 0 +} # Twos -expect - result = [2, 3, 4, 5, 6] |> score(Twos) - result == 2 +expect { + result = [2, 3, 4, 5, 6]->score(Twos) + result == 2 +} # Fours -expect - result = [1, 4, 1, 4, 1] |> score(Fours) - result == 8 +expect { + result = [1, 4, 1, 4, 1]->score(Fours) + result == 8 +} # Yacht counted as threes -expect - result = [3, 3, 3, 3, 3] |> score(Threes) - result == 15 +expect { + result = [3, 3, 3, 3, 3]->score(Threes) + result == 15 +} # Yacht of 3s counted as fives -expect - result = [3, 3, 3, 3, 3] |> score(Fives) - result == 0 +expect { + result = [3, 3, 3, 3, 3]->score(Fives) + result == 0 +} # Fives -expect - result = [1, 5, 3, 5, 3] |> score(Fives) - result == 10 +expect { + result = [1, 5, 3, 5, 3]->score(Fives) + result == 10 +} # Sixes -expect - result = [2, 3, 4, 5, 6] |> score(Sixes) - result == 6 +expect { + result = [2, 3, 4, 5, 6]->score(Sixes) + result == 6 +} # Full house two small, three big -expect - result = [2, 2, 4, 4, 4] |> score(FullHouse) - result == 16 +expect { + result = [2, 2, 4, 4, 4]->score(FullHouse) + result == 16 +} # Full house three small, two big -expect - result = [5, 3, 3, 5, 3] |> score(FullHouse) - result == 19 +expect { + result = [5, 3, 3, 5, 3]->score(FullHouse) + result == 19 +} # Two pair is not a full house -expect - result = [2, 2, 4, 4, 5] |> score(FullHouse) - result == 0 +expect { + result = [2, 2, 4, 4, 5]->score(FullHouse) + result == 0 +} # Four of a kind is not a full house -expect - result = [1, 4, 4, 4, 4] |> score(FullHouse) - result == 0 +expect { + result = [1, 4, 4, 4, 4]->score(FullHouse) + result == 0 +} # Yacht is not a full house -expect - result = [2, 2, 2, 2, 2] |> score(FullHouse) - result == 0 +expect { + result = [2, 2, 2, 2, 2]->score(FullHouse) + result == 0 +} # Four of a Kind -expect - result = [6, 6, 4, 6, 6] |> score(FourOfAKind) - result == 24 +expect { + result = [6, 6, 4, 6, 6]->score(FourOfAKind) + result == 24 +} # Yacht can be scored as Four of a Kind -expect - result = [3, 3, 3, 3, 3] |> score(FourOfAKind) - result == 12 +expect { + result = [3, 3, 3, 3, 3]->score(FourOfAKind) + result == 12 +} # Full house is not Four of a Kind -expect - result = [3, 3, 3, 5, 5] |> score(FourOfAKind) - result == 0 +expect { + result = [3, 3, 3, 5, 5]->score(FourOfAKind) + result == 0 +} # Little Straight -expect - result = [3, 5, 4, 1, 2] |> score(LittleStraight) - result == 30 +expect { + result = [3, 5, 4, 1, 2]->score(LittleStraight) + result == 30 +} # Little Straight as Big Straight -expect - result = [1, 2, 3, 4, 5] |> score(BigStraight) - result == 0 +expect { + result = [1, 2, 3, 4, 5]->score(BigStraight) + result == 0 +} # Four in order but not a little straight -expect - result = [1, 1, 2, 3, 4] |> score(LittleStraight) - result == 0 +expect { + result = [1, 1, 2, 3, 4]->score(LittleStraight) + result == 0 +} # No pairs but not a little straight -expect - result = [1, 2, 3, 4, 6] |> score(LittleStraight) - result == 0 +expect { + result = [1, 2, 3, 4, 6]->score(LittleStraight) + result == 0 +} # Minimum is 1, maximum is 5, but not a little straight -expect - result = [1, 1, 3, 4, 5] |> score(LittleStraight) - result == 0 +expect { + result = [1, 1, 3, 4, 5]->score(LittleStraight) + result == 0 +} # Big Straight -expect - result = [4, 6, 2, 5, 3] |> score(BigStraight) - result == 30 +expect { + result = [4, 6, 2, 5, 3]->score(BigStraight) + result == 30 +} # Big Straight as little straight -expect - result = [6, 5, 4, 3, 2] |> score(LittleStraight) - result == 0 +expect { + result = [6, 5, 4, 3, 2]->score(LittleStraight) + result == 0 +} # No pairs but not a big straight -expect - result = [6, 5, 4, 3, 1] |> score(BigStraight) - result == 0 +expect { + result = [6, 5, 4, 3, 1]->score(BigStraight) + result == 0 +} # Choice -expect - result = [3, 3, 5, 6, 6] |> score(Choice) - result == 23 +expect { + result = [3, 3, 5, 6, 6]->score(Choice) + result == 23 +} # Yacht as choice -expect - result = [2, 2, 2, 2, 2] |> score(Choice) - result == 10 +expect { + result = [2, 2, 2, 2, 2]->score(Choice) + result == 10 +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +} diff --git a/exercises/practice/zebra-puzzle/.meta/Example.roc b/exercises/practice/zebra-puzzle/.meta/Example.roc index 0e78ee67..731f5dee 100644 --- a/exercises/practice/zebra-puzzle/.meta/Example.roc +++ b/exercises/practice/zebra-puzzle/.meta/Example.roc @@ -1,211 +1,246 @@ -module [owns_zebra, drinks_water] +ZebraPuzzle :: {}.{ + Person : [Englishman, Spaniard, Ukrainian, Norwegian, Japanese] -Person : [Englishman, Spaniard, Ukrainian, Norwegian, Japanese] + owns_zebra : Try(Person, [NotFound]) + owns_zebra = owner_of(|house| house.animal == 5) + + drinks_water : Try(Person, [NotFound]) + drinks_water = owner_of(|house| house.drink == 5) +} House : { - activity : U8, # 0 = Undefined, 1 = Dancing, 2 = Painting, 3 = Reading, 4 = Football, 5 = Chess - animal : U8, # 0 = Undefined, 1 = Dog, 2 = Snail, 3 = Fox, 4 = Horse, 5 = Zebra - color : U8, # 0 = Undefined, 1 = Red, 2 = Green, 3 = Ivory, 4 = Yellow, 5 = Blue - drink : U8, # 0 = Undefined, 1 = Coffee, 2 = Tea, 3 = Milk, 4 = OrangeJuice, 5 = Water - person : U8, # 0 = Undefined, 1 = Englishman, 2 = Spaniard, 3 = Ukrainian, 4 = Norwegian, 5 = Japanese -} - -owns_zebra : Result Person [NotFound] -owns_zebra = - owner_of(|house| house.animal == 5) - -drinks_water : Result Person [NotFound] -drinks_water = - owner_of(|house| house.drink == 5) - -owner_of : (House -> Bool) -> Result Person [NotFound] -owner_of = |condition| - when solution is - Ok(houses) -> - when houses |> List.find_first(condition)? |> .person is - 1 -> Ok(Englishman) - 2 -> Ok(Spaniard) - 3 -> Ok(Ukrainian) - 4 -> Ok(Norwegian) - 5 -> Ok(Japanese) - _ -> Err(NotFound) - - Err(NotFound) -> Err(NotFound) - -solution : Result (List House) [NotFound] -solution = - num_houses = 5 # rule 1 - find_first_solution = |houses, house, field_index| - [1, 2, 3, 4, 5] - |> List.walk_until( - Err(NotFound), - |state, value| - updated_house = house |> set_field(field_index, value) - updated_houses = houses |> List.append(updated_house) - if updated_houses |> is_valid then - if List.len(updated_houses) == num_houses and field_index == 4 then - Break(Ok(updated_houses)) - else - (next_houses, next_house, next_field_index) = - if field_index == 4 then - (updated_houses, init_house, 0) - else - (houses, updated_house, field_index + 1) - - when find_first_solution(next_houses, next_house, next_field_index) is - Ok(found) -> Break(Ok(found)) - Err(NotFound) -> Continue(state) - else - Continue(state), - ) - init_house = { activity: 0, animal: 0, color: 0, drink: 0, person: 0 } - find_first_solution([], init_house, 0) + activity : U8, # 0 = Undefined, 1 = Dancing, 2 = Painting, 3 = Reading, 4 = Football, 5 = Chess + animal : U8, # 0 = Undefined, 1 = Dog, 2 = Snail, 3 = Fox, 4 = Horse, 5 = Zebra + color : U8, # 0 = Undefined, 1 = Red, 2 = Green, 3 = Ivory, 4 = Yellow, 5 = Blue + drink : U8, # 0 = Undefined, 1 = Coffee, 2 = Tea, 3 = Milk, 4 = OrangeJuice, 5 = Water + person : U8, # 0 = Undefined, 1 = Englishman, 2 = Spaniard, 3 = Ukrainian, 4 = Norwegian, 5 = Japanese +} + +owner_of : (House -> Bool) -> Try(Person, [NotFound]) +owner_of = |condition| { + houses = solution? + house = houses.find_first(condition)? + match house.person { + 1 => Ok(Englishman) + 2 => Ok(Spaniard) + 3 => Ok(Ukrainian) + 4 => Ok(Norwegian) + 5 => Ok(Japanese) + _ => Err(NotFound) + } +} + +solution : Try(List(House), [NotFound]) +solution = { + num_houses = 5 # rule 1 + init_house = { activity: 0, animal: 0, color: 0, drink: 0, person: 0 } + find_first_solution = |houses, house, field_index| { + [1, 2, 3, 4, 5] + .fold_until( + Err(NotFound), + |state, value| { + updated_house = house->set_field(field_index, value) + updated_houses = houses.append(updated_house) + if updated_houses->is_valid() { + if updated_houses.len() == num_houses and field_index == 4 { + Break(Ok(updated_houses)) + } else { + (next_houses, next_house, next_field_index) = + if field_index == 4 { + (updated_houses, init_house, 0) + } else { + (houses, updated_house, field_index + 1) + } + + match find_first_solution(next_houses, next_house, next_field_index) { + Ok(found) => Break(Ok(found)) + Err(NotFound) => Continue(state) + } + } + } else { + Continue(state) + } + }, + ) + } + find_first_solution([], init_house, 0) +} set_field : House, U8, U8 -> House -set_field = |house, field_index, value| - when field_index is - 0 -> { house & activity: value } - 1 -> { house & animal: value } - 2 -> { house & color: value } - 3 -> { house & drink: value } - 4 -> { house & person: value } - _ -> crash("field_index should always be between 0 and 4") - -is_valid : List House -> Bool -is_valid = |houses| - [ - rule2, - rule3, - rule4, - rule5, - rule6, - rule7, - rule8, - rule9, - rule10, - rule11, - rule12, - rule13, - rule14, - rule15, - rule16, - ] - |> List.all(|rule| rule(houses)) - -same_house : List House, (House -> U8, U8), (House -> U8, U8) -> Bool -same_house = |houses, (field1, value1), (field2, value2)| - houses - |> List.all( - |house| - f1 = field1(house) - f2 = field2(house) - (f1 == 0 or f2 == 0) or (f1 == value1 and f2 == value2) or (f1 != value1 and f2 != value2), - ) +set_field = |house, field_index, value| { + match field_index { + 0 => { ..house, activity: value } + 1 => { ..house, animal: value } + 2 => { ..house, color: value } + 3 => { ..house, drink: value } + 4 => { ..house, person: value } + _ => { + crash "field_index should always be between 0 and 4" + } + } +} + +is_valid : List(House) -> Bool +is_valid = |houses| { + [ + rule2, + rule3, + rule4, + rule5, + rule6, + rule7, + rule8, + rule9, + rule10, + rule11, + rule12, + rule13, + rule14, + rule15, + rule16, + ] + .all(|rule| rule(houses)) +} + +same_house : List(House), ((House -> U8), U8), ((House -> U8), U8) -> Bool +same_house = |houses, (field1, value1), (field2, value2)| { + houses + .all( + |house| { + f1 = field1(house) + f2 = field2(house) + (f1 == 0 or f2 == 0) or (f1 == value1 and f2 == value2) or (f1 != value1 and f2 != value2) + }, + ) +} # The Englishman lives in the red house. -rule2 : List House -> Bool -rule2 = |houses| - houses |> same_house((.person, 1), (.color, 1)) +rule2 : List(House) -> Bool +rule2 = |houses| { + houses->same_house((|h| h.person, 1), (|h| h.color, 1)) +} # The Spaniard owns the dog. -rule3 : List House -> Bool -rule3 = |houses| - houses |> same_house((.person, 2), (.animal, 1)) +rule3 : List(House) -> Bool +rule3 = |houses| { + same_house(houses, (|h| h.person, 2), (|h| h.animal, 1)) +} # The person in the green house drinks coffee. -rule4 : List House -> Bool -rule4 = |houses| - houses |> same_house((.color, 2), (.drink, 1)) +rule4 : List(House) -> Bool +rule4 = |houses| { + same_house(houses, (|h| h.color, 2), (|h| h.drink, 1)) +} # The Ukrainian drinks tea. -rule5 : List House -> Bool -rule5 = |houses| - houses |> same_house((.person, 3), (.drink, 2)) +rule5 : List(House) -> Bool +rule5 = |houses| { + same_house(houses, (|h| h.person, 3), (|h| h.drink, 2)) +} # The green house is immediately to the right of the ivory house. -rule6 : List House -> Bool -rule6 = |houses| - green_house = houses |> List.find_first_index(|house| house.color == 2) - ivory_house = houses |> List.find_first_index(|house| house.color == 3) - when (green_house, ivory_house) is - (Ok(green_index), Ok(ivory_index)) -> green_index == ivory_index + 1 - _ -> Bool.true +rule6 : List(House) -> Bool +rule6 = |houses| { + green_house = houses.find_first_index(|house| house.color == 2) + ivory_house = houses.find_first_index(|house| house.color == 3) + match (green_house, ivory_house) { + (Ok(green_index), Ok(ivory_index)) => green_index == ivory_index + 1 + _ => Bool.True + } +} # The snail owner likes to go dancing. -rule7 : List House -> Bool -rule7 = |houses| - houses |> same_house((.animal, 2), (.activity, 1)) +rule7 : List(House) -> Bool +rule7 = |houses| { + same_house(houses, (|h| h.animal, 2), (|h| h.activity, 1)) +} # The person in the yellow house is a painter. -rule8 : List House -> Bool -rule8 = |houses| - houses |> same_house((.color, 4), (.activity, 2)) +rule8 : List(House) -> Bool +rule8 = |houses| { + same_house(houses, (|h| h.color, 4), (|h| h.activity, 2)) +} # The person in the middle house drinks milk. -rule9 : List House -> Bool -rule9 = |houses| - houses - |> List.map_with_index(|house, index| (house, index)) - |> List.all( - |(house, index)| - (house.drink == 0) or (index == 2 and house.drink == 3) or (index != 2 and house.drink != 3), - ) +rule9 : List(House) -> Bool +rule9 = |houses| { + houses + .map_with_index(|house, index| (house, index)) + .all( + |(house, index)| { + (house.drink == 0) or (index == 2 and house.drink == 3) or (index != 2 and house.drink != 3) + }, + ) +} # The Norwegian lives in the first house. -rule10 : List House -> Bool -rule10 = |houses| - houses - |> List.map_with_index(|house, index| (house, index)) - |> List.all( - |(house, index)| - (house.person == 0) or (index == 0 and house.person == 4) or (index != 0 and house.person != 4), - ) +rule10 : List(House) -> Bool +rule10 = |houses| { + houses + .map_with_index(|house, index| (house, index)) + .all( + |(house, index)| { + (house.person == 0) or (index == 0 and house.person == 4) or (index != 0 and house.person != 4) + }, + ) +} # The person who enjoys reading lives in the house next to the person with the fox. -rule11 : List House -> Bool -rule11 = |houses| - reader_house = houses |> List.find_first_index(|house| house.activity == 3) - fox_house = houses |> List.find_first_index(|house| house.animal == 3) - when (reader_house, fox_house) is - (Ok(reader_index), Ok(fox_index)) -> (reader_index == fox_index + 1) or (fox_index == reader_index + 1) - _ -> Bool.true +rule11 : List(House) -> Bool +rule11 = |houses| { + reader_house = houses.find_first_index(|house| house.activity == 3) + fox_house = houses.find_first_index(|house| house.animal == 3) + match (reader_house, fox_house) { + (Ok(reader_index), Ok(fox_index)) => (reader_index == fox_index + 1) or (fox_index == reader_index + 1) + _ => Bool.True + } +} # The painter's house is next to the house with the horse. -rule12 : List House -> Bool -rule12 = |houses| - painter_house = houses |> List.find_first_index(|house| house.activity == 2) - horse_house = houses |> List.find_first_index(|house| house.animal == 4) - when (painter_house, horse_house) is - (Ok(painter_index), Ok(horse_index)) -> (painter_index == horse_index + 1) or (horse_index == painter_index + 1) - _ -> Bool.true - -# The List person -> Bool -rule13 : List House -> Bool -rule13 = |houses| - houses |> same_house((.activity, 4), (.drink, 4)) - -# The List Japanese -> Bool -rule14 : List House -> Bool -rule14 = |houses| - houses |> same_house((.person, 5), (.activity, 5)) +rule12 : List(House) -> Bool +rule12 = |houses| { + painter_house = houses.find_first_index(|house| house.activity == 2) + horse_house = houses.find_first_index(|house| house.animal == 4) + match (painter_house, horse_house) { + (Ok(painter_index), Ok(horse_index)) => (painter_index == horse_index + 1) or (horse_index == painter_index + 1) + _ => Bool.True + } +} + +# The football person drinks orange juice. +rule13 : List(House) -> Bool +rule13 = |houses| { + same_house(houses, (|h| h.activity, 4), (|h| h.drink, 4)) +} + +# The Japanese plays chess. +rule14 : List(House) -> Bool +rule14 = |houses| { + same_house(houses, (|h| h.person, 5), (|h| h.activity, 5)) +} # The Norwegian lives next to the blue house. -rule15 : List House -> Bool -rule15 = |houses| - norwegian_house = houses |> List.find_first_index(|house| house.person == 4) - blue_house = houses |> List.find_first_index(|house| house.color == 5) - when (norwegian_house, blue_house) is - (Ok(norwegian_index), Ok(blue_index)) -> (norwegian_index == blue_index + 1) or (blue_index == norwegian_index + 1) - _ -> Bool.true - -rule16 : List House -> Bool -rule16 = |houses| - [.activity, .animal, .color, .drink, .person] - |> List.all( - |field| - values = - houses - |> List.map(field) - |> List.keep_if(|value| value != 0) - List.len(values) == Set.len(Set.from_list(values)), - ) +rule15 : List(House) -> Bool +rule15 = |houses| { + norwegian_house = houses.find_first_index(|house| house.person == 4) + blue_house = houses.find_first_index(|house| house.color == 5) + match (norwegian_house, blue_house) { + (Ok(norwegian_index), Ok(blue_index)) => (norwegian_index == blue_index + 1) or (blue_index == norwegian_index + 1) + _ => Bool.True + } +} + +# There must be no duplicates (e.g., two green houses) +rule16 : List(House) -> Bool +rule16 = |houses| { + [|h| h.activity, |h| h.animal, |h| h.color, |h| h.drink, |h| h.person] + .all( + |field| { + values = + houses + .map(field) + .keep_if(|value| value != 0) + values.len() == Set.from_list(values).len() + }, + ) +} diff --git a/exercises/practice/zebra-puzzle/.meta/template.j2 b/exercises/practice/zebra-puzzle/.meta/template.j2 index 70da5c15..096ab687 100644 --- a/exercises/practice/zebra-puzzle/.meta/template.j2 +++ b/exercises/practice/zebra-puzzle/.meta/template.j2 @@ -6,8 +6,11 @@ import {{ exercise | to_pascal }} exposing [owns_zebra, drinks_water] {% for case in cases -%} # {{ case["description"] }} -expect +expect { result = {{ case["property"] | to_snake }} result == Ok({{ case["expected"] }}) +} {% endfor %} + +{{ macros.footer() }} diff --git a/exercises/practice/zebra-puzzle/ZebraPuzzle.roc b/exercises/practice/zebra-puzzle/ZebraPuzzle.roc index 2d0677ee..f1b4645e 100644 --- a/exercises/practice/zebra-puzzle/ZebraPuzzle.roc +++ b/exercises/practice/zebra-puzzle/ZebraPuzzle.roc @@ -1,11 +1,13 @@ -module [owns_zebra, drinks_water] +ZebraPuzzle :: {}.{ + Person : [Englishman, Spaniard, Ukrainian, Norwegian, Japanese] -Person : [Englishman, Spaniard, Ukrainian, Norwegian, Japanese] + owns_zebra : Try(Person, _) + owns_zebra = { + crash "Please implement the 'owns_zebra' function" + } -owns_zebra : Result Person _ -owns_zebra = - crash("Please implement 'owns_zebra'") - -drinks_water : Result Person _ -drinks_water = - crash("Please implement 'drinks_water'") + drinks_water : Try(Person, _) + drinks_water = { + crash "Please implement the 'drinks_water' function" + } +} diff --git a/exercises/practice/zebra-puzzle/zebra-puzzle-test.roc b/exercises/practice/zebra-puzzle/zebra-puzzle-test.roc index a8dd3c80..bfcf9b27 100644 --- a/exercises/practice/zebra-puzzle/zebra-puzzle-test.roc +++ b/exercises/practice/zebra-puzzle/zebra-puzzle-test.roc @@ -1,24 +1,22 @@ # These tests are auto-generated with test data from: # https://github.com/exercism/problem-specifications/tree/main/exercises/zebra-puzzle/canonical-data.json -# File last updated on 2025-09-15 -app [main!] { - pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br", -} - -import pf.Stdout - -main! = |_args| - Stdout.line!("") +# File last updated on 2026-06-13 import ZebraPuzzle exposing [owns_zebra, drinks_water] # resident who drinks water -expect - result = drinks_water - result == Ok(Norwegian) +expect { + result = drinks_water + result == Ok(Norwegian) +} # resident who owns zebra -expect - result = owns_zebra - result == Ok(Japanese) +expect { + result = owns_zebra + result == Ok(Japanese) +} +# This program is only used to run tests with `roc test`, so main! does nothing. +main! = |_args| { + Ok({}) +}