From e418fb5b71b70b34f782eba0ab5294badc54b855 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Sat, 20 Jun 2026 11:34:58 +1000 Subject: [PATCH] 31 more exercises [no important files changed] --- .../dnd-character/test_dnd_character.zig | 28 ++++---- .../matching-brackets/.meta/supplements.json | 12 ++++ .../practice/pangram/.meta/supplements.json | 12 ++++ .../queen-attack/.meta/supplements.json | 10 +++ .../practice/space-age/test_space_age.zig | 2 + .../sum-of-multiples/.meta/supplements.json | 14 ++++ .../test_sum_of_multiples.zig | 2 +- generators/exercises/acronym.py | 16 +++++ generators/exercises/allergies.py | 40 +++++++++++ generators/exercises/anagram.py | 43 ++++++++++++ generators/exercises/atbash_cipher.py | 25 +++++++ generators/exercises/bob.py | 15 ++++ generators/exercises/collatz_conjecture.py | 20 ++++++ generators/exercises/darts.py | 27 ++++++++ generators/exercises/difference_of_squares.py | 29 ++++++++ generators/exercises/dnd_character.py | 55 +++++++++++++++ generators/exercises/grains.py | 39 +++++++++++ generators/exercises/hamming.py | 40 +++++++++++ generators/exercises/hello_world.py | 13 ++++ generators/exercises/isogram.py | 8 +++ generators/exercises/leap.py | 4 ++ generators/exercises/linked_list.py | 69 +++++++++++++++++++ generators/exercises/luhn.py | 17 +++++ generators/exercises/matching_brackets.py | 24 +++++++ generators/exercises/nucleotide_count.py | 20 ++++++ generators/exercises/pangram.py | 8 +++ generators/exercises/proverb.py | 42 +++++++++++ generators/exercises/queen_attack.py | 51 ++++++++++++++ generators/exercises/raindrops.py | 18 +++++ generators/exercises/resistor_color.py | 35 ++++++++++ generators/exercises/resistor_color_duo.py | 20 ++++++ generators/exercises/rna_transcription.py | 21 ++++++ generators/exercises/scrabble_score.py | 14 ++++ generators/exercises/secret_handshake.py | 19 +++++ generators/exercises/sieve.py | 25 +++++++ generators/exercises/space_age.py | 25 +++++++ generators/exercises/sum_of_multiples.py | 26 +++++++ generators/exercises/two_fer.py | 15 ++++ 38 files changed, 888 insertions(+), 15 deletions(-) create mode 100644 exercises/practice/matching-brackets/.meta/supplements.json create mode 100644 exercises/practice/pangram/.meta/supplements.json create mode 100644 exercises/practice/queen-attack/.meta/supplements.json create mode 100644 exercises/practice/sum-of-multiples/.meta/supplements.json create mode 100644 generators/exercises/acronym.py create mode 100644 generators/exercises/allergies.py create mode 100644 generators/exercises/anagram.py create mode 100644 generators/exercises/atbash_cipher.py create mode 100644 generators/exercises/bob.py create mode 100644 generators/exercises/collatz_conjecture.py create mode 100644 generators/exercises/darts.py create mode 100644 generators/exercises/difference_of_squares.py create mode 100644 generators/exercises/dnd_character.py create mode 100644 generators/exercises/grains.py create mode 100644 generators/exercises/hamming.py create mode 100644 generators/exercises/hello_world.py create mode 100644 generators/exercises/isogram.py create mode 100644 generators/exercises/leap.py create mode 100644 generators/exercises/linked_list.py create mode 100644 generators/exercises/luhn.py create mode 100644 generators/exercises/matching_brackets.py create mode 100644 generators/exercises/nucleotide_count.py create mode 100644 generators/exercises/pangram.py create mode 100644 generators/exercises/proverb.py create mode 100644 generators/exercises/queen_attack.py create mode 100644 generators/exercises/raindrops.py create mode 100644 generators/exercises/resistor_color.py create mode 100644 generators/exercises/resistor_color_duo.py create mode 100644 generators/exercises/rna_transcription.py create mode 100644 generators/exercises/scrabble_score.py create mode 100644 generators/exercises/secret_handshake.py create mode 100644 generators/exercises/sieve.py create mode 100644 generators/exercises/space_age.py create mode 100644 generators/exercises/sum_of_multiples.py create mode 100644 generators/exercises/two_fer.py diff --git a/exercises/practice/dnd-character/test_dnd_character.zig b/exercises/practice/dnd-character/test_dnd_character.zig index 402f590d..b81bfbb1 100644 --- a/exercises/practice/dnd-character/test_dnd_character.zig +++ b/exercises/practice/dnd-character/test_dnd_character.zig @@ -4,6 +4,20 @@ const testing = std.testing; const dnd_character = @import("dnd_character.zig"); const Character = dnd_character.Character; +fn isValidAbilityScore(n: isize) bool { + return n >= 3 and n <= 18; +} + +fn isValid(c: Character) bool { + return isValidAbilityScore(c.strength) and + isValidAbilityScore(c.dexterity) and + isValidAbilityScore(c.constitution) and + isValidAbilityScore(c.intelligence) and + isValidAbilityScore(c.wisdom) and + isValidAbilityScore(c.charisma) and + (c.hitpoints == 10 + dnd_character.modifier(c.constitution)); +} + test "ability modifier for score 3 is -4" { const expected: i8 = -4; const actual = dnd_character.modifier(3); @@ -100,10 +114,6 @@ test "ability modifier for score 18 is +4" { try testing.expectEqual(expected, actual); } -fn isValidAbilityScore(n: isize) bool { - return n >= 3 and n <= 18; -} - test "random ability is within range" { for (0..20) |_| { const actual = dnd_character.ability(); @@ -111,16 +121,6 @@ test "random ability is within range" { } } -fn isValid(c: Character) bool { - return isValidAbilityScore(c.strength) and - isValidAbilityScore(c.dexterity) and - isValidAbilityScore(c.constitution) and - isValidAbilityScore(c.intelligence) and - isValidAbilityScore(c.wisdom) and - isValidAbilityScore(c.charisma) and - (c.hitpoints == 10 + dnd_character.modifier(c.constitution)); -} - test "random character is valid" { for (0..20) |_| { const character = Character.init(); diff --git a/exercises/practice/matching-brackets/.meta/supplements.json b/exercises/practice/matching-brackets/.meta/supplements.json new file mode 100644 index 00000000..afaa26ed --- /dev/null +++ b/exercises/practice/matching-brackets/.meta/supplements.json @@ -0,0 +1,12 @@ +{ + "cases": [ + { + "description": "maximum required level of nesting", + "property": "isPaired", + "input": { + "value": "(((_[[[_{{{_()_}}}_]]]_)))" + }, + "expected": true + } + ] +} diff --git a/exercises/practice/pangram/.meta/supplements.json b/exercises/practice/pangram/.meta/supplements.json new file mode 100644 index 00000000..6678da07 --- /dev/null +++ b/exercises/practice/pangram/.meta/supplements.json @@ -0,0 +1,12 @@ +{ + "cases": [ + { + "description": "non-alphanumeric printable ASCII", + "property": "isPangram", + "input": { + "sentence": " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~" + }, + "expected": false + } + ] +} diff --git a/exercises/practice/queen-attack/.meta/supplements.json b/exercises/practice/queen-attack/.meta/supplements.json new file mode 100644 index 00000000..4ff50e7c --- /dev/null +++ b/exercises/practice/queen-attack/.meta/supplements.json @@ -0,0 +1,10 @@ +{ + "cases": [ + { + "description": "queen has exactly two fields", + "property": "fields", + "input": {}, + "expected": 2 + } + ] +} diff --git a/exercises/practice/space-age/test_space_age.zig b/exercises/practice/space-age/test_space_age.zig index 918879ee..87169c75 100644 --- a/exercises/practice/space-age/test_space_age.zig +++ b/exercises/practice/space-age/test_space_age.zig @@ -1,4 +1,6 @@ const std = @import("std"); +const testing = std.testing; + const expectApproxEqAbs = std.testing.expectApproxEqAbs; const space_age = @import("space_age.zig"); diff --git a/exercises/practice/sum-of-multiples/.meta/supplements.json b/exercises/practice/sum-of-multiples/.meta/supplements.json new file mode 100644 index 00000000..ddf3cf82 --- /dev/null +++ b/exercises/practice/sum-of-multiples/.meta/supplements.json @@ -0,0 +1,14 @@ +{ + "cases": [ + { + "description": "sum is greater than maximum value of u32", + "property": "sum", + "comment": "Note that for a u32 `limit`, the maximum sum of multiples fits in a u64.", + "input": { + "factors": [100000000], + "limit": 1000000000 + }, + "expected": 4500000000 + } + ] +} diff --git a/exercises/practice/sum-of-multiples/test_sum_of_multiples.zig b/exercises/practice/sum-of-multiples/test_sum_of_multiples.zig index 0c9c5a40..3967aac3 100644 --- a/exercises/practice/sum-of-multiples/test_sum_of_multiples.zig +++ b/exercises/practice/sum-of-multiples/test_sum_of_multiples.zig @@ -92,7 +92,7 @@ test "much larger factors" { } test "all numbers are multiples of 1" { - const expected: u64 = 4_950; + const expected: u64 = 4950; const factors = [_]u32{1}; const limit = 100; const actual = try sum(testing.allocator, &factors, limit); diff --git a/generators/exercises/acronym.py b/generators/exercises/acronym.py new file mode 100644 index 00000000..496e6fd3 --- /dev/null +++ b/generators/exercises/acronym.py @@ -0,0 +1,16 @@ +from lib import zstr + +IMPORT_SELF = False + +HEADER = 'const abbreviate = @import("acronym.zig").abbreviate;' + + +def gen_case(case): + phrase = case["input"]["phrase"] + expected = case["expected"] + return ( + f" const expected = {zstr(expected)};\n" + f" const actual = try abbreviate(testing.allocator, {zstr(phrase)});\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/allergies.py b/generators/exercises/allergies.py new file mode 100644 index 00000000..53e31040 --- /dev/null +++ b/generators/exercises/allergies.py @@ -0,0 +1,40 @@ +from lib import zint + +HEADER = ( + "const isAllergicTo = allergies.isAllergicTo;\n" + "const initAllergenSet = allergies.initAllergenSet;\n" +) + + +def describe(case, parent): + # Leaf cases carry their own description; group nodes pass it down via `parent`. + if "cases" in case: + return case["description"] + prop = case.get("property") + desc = case["description"] + if prop == "allergicTo": + return f"{case['input']['item']}: {desc}" + if prop == "list": + return f"initAllergenSet: {desc}" + return desc + + +def gen_case(case): + prop = case["property"] + if prop == "allergicTo": + item = case["input"]["item"] + score = case["input"]["score"] + expected = case["expected"] + bang = "" if expected else "!" + return f" try testing.expect({bang}isAllergicTo({zint(score)}, .{item}));\n" + # property == "list": exercise the EnumSet returned by initAllergenSet. + score = case["input"]["score"] + allergens = case["expected"] + s = ( + f" const expected_count: usize = {len(allergens)};\n" + f" const actual = initAllergenSet({zint(score)});\n" + f" try testing.expectEqual(expected_count, actual.count());\n" + ) + for a in allergens: + s += f" try testing.expect(actual.contains(.{a}));\n" + return s diff --git a/generators/exercises/anagram.py b/generators/exercises/anagram.py new file mode 100644 index 00000000..c65334e8 --- /dev/null +++ b/generators/exercises/anagram.py @@ -0,0 +1,43 @@ +from lib import zstr + +IMPORT_SELF = False + +HEADER = """const detectAnagrams = @import("anagram.zig").detectAnagrams; + +fn testAnagrams( + allocator: std.mem.Allocator, + expected: []const []const u8, + word: []const u8, + candidates: []const []const u8, +) !void { + var actual = try detectAnagrams(allocator, word, candidates); + defer actual.deinit(); + try testing.expectEqual(expected.len, actual.count()); + for (expected) |e| { + try testing.expect(actual.contains(e)); + } +} +""" + + +def _strlist(values): + if not values: + return "[_][]const u8{}" + inner = ", ".join(zstr(v) for v in values) + return f"[_][]const u8{{ {inner} }}" + + +def gen_case(case): + word = case["input"]["subject"] + candidates = case["input"]["candidates"] + expected = case["expected"] + return ( + f" const expected = {_strlist(expected)};\n" + f" const word = {zstr(word)};\n" + f" const candidates = {_strlist(candidates)};\n" + f" try std.testing.checkAllAllocationFailures(\n" + f" std.testing.allocator,\n" + f" testAnagrams,\n" + f" .{{ &expected, word, &candidates }},\n" + f" );\n" + ) diff --git a/generators/exercises/atbash_cipher.py b/generators/exercises/atbash_cipher.py new file mode 100644 index 00000000..dca8def1 --- /dev/null +++ b/generators/exercises/atbash_cipher.py @@ -0,0 +1,25 @@ +from lib import zstr + +HEADER = """const encode = atbash_cipher.encode; +const decode = atbash_cipher.decode;""" + + +def describe(case, parent): + # The target uses only the leaf description (no parent group prefix) and + # lowercases it, so "encode OMG" becomes "encode omg". + if "cases" in case: + return None + return case["description"].lower() + + +def gen_case(case): + fn = case["property"] # "encode" or "decode" + phrase = case["input"]["phrase"] + expected = case["expected"] + return ( + f" const expected = {zstr(expected)};\n" + f" const s = {zstr(phrase)};\n" + f" const actual = try {fn}(testing.allocator, s);\n" + f" defer testing.allocator.free(actual);\n" + f" try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/bob.py b/generators/exercises/bob.py new file mode 100644 index 00000000..ba248320 --- /dev/null +++ b/generators/exercises/bob.py @@ -0,0 +1,15 @@ +from lib import zstr + +IMPORT_SELF = False + +HEADER = 'const response = @import("bob.zig").response;' + + +def gen_case(case): + s = case["input"]["heyBob"] + expected = case["expected"] + return ( + f" const expected = {zstr(expected)};\n" + f" const actual = response({zstr(s)});\n" + f" try testing.expectEqualStrings(expected, actual);\n" + ) diff --git a/generators/exercises/collatz_conjecture.py b/generators/exercises/collatz_conjecture.py new file mode 100644 index 00000000..e8be8c71 --- /dev/null +++ b/generators/exercises/collatz_conjecture.py @@ -0,0 +1,20 @@ +from lib import zint, is_error + +IMPORT_SELF = True +HEADER = "const ComputationError = collatz_conjecture.ComputationError;" + + +def gen_case(case): + number = zint(case["input"]["number"]) + expected = case["expected"] + if is_error(expected): + return ( + " const expected = ComputationError.IllegalArgument;\n" + f" const actual = collatz_conjecture.steps({number});\n" + " try testing.expectError(expected, actual);\n" + ) + return ( + f" const expected: usize = {zint(expected)};\n" + f" const actual = try collatz_conjecture.steps({number});\n" + " try testing.expectEqual(expected, actual);\n" + ) diff --git a/generators/exercises/darts.py b/generators/exercises/darts.py new file mode 100644 index 00000000..ea6513ec --- /dev/null +++ b/generators/exercises/darts.py @@ -0,0 +1,27 @@ +from lib import zfloat + +IMPORT_SELF = True + + +def describe(case, parent): + return case["description"][0].lower() + case["description"][1:] + + +# The committed target flips the signs on this symmetric case (score depends only +# on x^2 + y^2, so it is equivalent to the canonical x=-3.5, y=3.5). +_INPUT_OVERRIDE = { + "just within the middle circle": {"x": 3.5, "y": -3.5}, +} + + +def gen_case(case): + inp = _INPUT_OVERRIDE.get(case["description"], case["input"]) + x = zfloat(inp["x"]) + y = zfloat(inp["y"]) + e = case["expected"] + return ( + f" const expected: usize = {e};\n" + f" const coordinate = darts.Coordinate.init({x}, {y});\n" + f" const actual = coordinate.score();\n" + f" try testing.expectEqual(expected, actual);\n" + ) diff --git a/generators/exercises/difference_of_squares.py b/generators/exercises/difference_of_squares.py new file mode 100644 index 00000000..f53b8020 --- /dev/null +++ b/generators/exercises/difference_of_squares.py @@ -0,0 +1,29 @@ +from lib import zint + +IMPORT_SELF = True + +_LABEL = { + "squareOfSum": "square of sum up to", + "sumOfSquares": "sum of squares up to", + "differenceOfSquares": "difference of squares up to", +} + + +def describe(case, parent): + # Leaf cases carry a property; group nodes do not. Group descriptions are + # dropped so leaves get a clean "