diff --git a/config.json b/config.json index b3686228..e174f657 100644 --- a/config.json +++ b/config.json @@ -955,6 +955,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "strain", + "name": "Strain", + "uuid": "d43bd44d-585c-4e2f-b634-5e5e5173f0c5", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "yacht", "name": "Yacht", diff --git a/exercises/practice/strain/.docs/instructions.md b/exercises/practice/strain/.docs/instructions.md new file mode 100644 index 00000000..3469ae65 --- /dev/null +++ b/exercises/practice/strain/.docs/instructions.md @@ -0,0 +1,29 @@ +# Instructions + +Implement the `keep` and `discard` operation on collections. +Given a collection and a predicate on the collection's elements, `keep` returns a new collection containing those elements where the predicate is true, while `discard` returns a new collection containing those elements where the predicate is false. + +For example, given the collection of numbers: + +- 1, 2, 3, 4, 5 + +And the predicate: + +- is the number even? + +Then your keep operation should produce: + +- 2, 4 + +While your discard operation should produce: + +- 1, 3, 5 + +Note that the union of keep and discard is all the elements. + +The functions may be called `keep` and `discard`, or they may need different names in order to not clash with existing functions or concepts in your language. + +## Restrictions + +Keep your hands off that filter/reject/whatchamacallit functionality provided by your standard library! +Solve this one yourself using other basic tools instead. diff --git a/exercises/practice/strain/.meta/config.json b/exercises/practice/strain/.meta/config.json new file mode 100644 index 00000000..a4880646 --- /dev/null +++ b/exercises/practice/strain/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "strain.zig" + ], + "test": [ + "test_strain.zig" + ], + "example": [ + ".meta/example.zig" + ] + }, + "blurb": "Implement the `keep` and `discard` operation on collections.", + "source": "Conversation with James Edward Gray II", + "source_url": "http://graysoftinc.com/" +} diff --git a/exercises/practice/strain/.meta/example.zig b/exercises/practice/strain/.meta/example.zig new file mode 100644 index 00000000..876563e4 --- /dev/null +++ b/exercises/practice/strain/.meta/example.zig @@ -0,0 +1,39 @@ +const std = @import("std"); +const mem = std.mem; + +/// Returns the items of `list` for which `predicate` is true, in order. +/// Caller owns the returned memory. +pub fn keep( + comptime T: type, + allocator: mem.Allocator, + list: []const T, + comptime predicate: fn (T) bool, +) mem.Allocator.Error![]T { + const buffer = try allocator.alloc(T, list.len); + errdefer allocator.free(buffer); + + var index: usize = 0; + for (list) |item| { + if (predicate(item)) { + buffer[index] = item; + index += 1; + } + } + return try allocator.realloc(buffer, index); +} + +/// Returns the items of `list` for which `predicate` is false, in order. +/// Caller owns the returned memory. +pub fn discard( + comptime T: type, + allocator: mem.Allocator, + list: []const T, + comptime predicate: fn (T) bool, +) mem.Allocator.Error![]T { + const negated = struct { + fn negated(item: T) bool { + return !predicate(item); + } + }.negated; + return keep(T, allocator, list, negated); +} diff --git a/exercises/practice/strain/.meta/tests.toml b/exercises/practice/strain/.meta/tests.toml new file mode 100644 index 00000000..3a617b4a --- /dev/null +++ b/exercises/practice/strain/.meta/tests.toml @@ -0,0 +1,52 @@ +# 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. + +[26af8c32-ba6a-4eb3-aa0a-ebd8f136e003] +description = "keep on empty list returns empty list" + +[f535cb4d-e99b-472a-bd52-9fa0ffccf454] +description = "keeps everything" + +[950b8e8e-f628-42a8-85e2-9b30f09cde38] +description = "keeps nothing" + +[92694259-6e76-470c-af87-156bdf75018a] +description = "keeps first and last" + +[938f7867-bfc7-449e-a21b-7b00cbb56994] +description = "keeps neither first nor last" + +[8908e351-4437-4d2b-a0f7-770811e48816] +description = "keeps strings" + +[2728036b-102a-4f1e-a3ef-eac6160d876a] +description = "keeps lists" + +[ef16beb9-8d84-451a-996a-14e80607fce6] +description = "discard on empty list returns empty list" + +[2f42f9bc-8e06-4afe-a222-051b5d8cd12a] +description = "discards everything" + +[ca990fdd-08c2-4f95-aa50-e0f5e1d6802b] +description = "discards nothing" + +[71595dae-d283-48ca-a52b-45fa96819d2f] +description = "discards first and last" + +[ae141f79-f86d-4567-b407-919eaca0f3dd] +description = "discards neither first nor last" + +[daf25b36-a59f-4f29-bcfe-302eb4e43609] +description = "discards strings" + +[a38d03f9-95ad-4459-80d1-48e937e4acaf] +description = "discards lists" diff --git a/exercises/practice/strain/strain.zig b/exercises/practice/strain/strain.zig new file mode 100644 index 00000000..f7bf0a13 --- /dev/null +++ b/exercises/practice/strain/strain.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const mem = std.mem; + +/// Returns the items of `list` for which `predicate` is true, in order. +/// Caller owns the returned memory. +pub fn keep( + comptime T: type, + allocator: mem.Allocator, + list: []const T, + comptime predicate: fn (T) bool, +) mem.Allocator.Error![]T { + _ = allocator; + _ = list; + @compileError("please implement the keep function"); +} + +/// Returns the items of `list` for which `predicate` is false, in order. +/// Caller owns the returned memory. +pub fn discard( + comptime T: type, + allocator: mem.Allocator, + list: []const T, + comptime predicate: fn (T) bool, +) mem.Allocator.Error![]T { + _ = allocator; + _ = list; + @compileError("please implement the discard function"); +} diff --git a/exercises/practice/strain/test_strain.zig b/exercises/practice/strain/test_strain.zig new file mode 100644 index 00000000..2261b929 --- /dev/null +++ b/exercises/practice/strain/test_strain.zig @@ -0,0 +1,200 @@ +const std = @import("std"); +const testing = std.testing; + +const strain = @import("strain.zig"); +const keep = strain.keep; +const discard = strain.discard; + +fn alwaysTrue(_: u32) bool { + return true; +} + +fn alwaysFalse(_: u32) bool { + return false; +} + +fn isOdd(x: u32) bool { + return x % 2 == 1; +} + +fn isEven(x: u32) bool { + return x % 2 == 0; +} + +fn startsWithZ(x: []const u8) bool { + return std.mem.startsWith(u8, x, "z"); +} + +fn containsFive(x: []const u32) bool { + return std.mem.indexOfScalar(u32, x, 5) != null; +} + +/// Binds an element type and a predicate, providing allocation-checked +/// wrappers around `keep` and `discard`. +fn Case(comptime T: type, comptime predicate: fn (T) bool) type { + return struct { + fn expectEqualElements(expected: T, actual: T) !void { + if (comptime T == []const u8) { + try testing.expectEqualStrings(expected, actual); + } else if (comptime T == []const u32) { + try testing.expectEqualSlices(u32, expected, actual); + } else { + try testing.expectEqual(expected, actual); + } + } + + fn testKeep(allocator: std.mem.Allocator, list: []const T, expected: []const T) !void { + const actual = try keep(T, allocator, list, predicate); + defer allocator.free(actual); + try testing.expectEqual(expected.len, actual.len); + for (expected, actual) |e, a| try expectEqualElements(e, a); + } + + fn testDiscard(allocator: std.mem.Allocator, list: []const T, expected: []const T) !void { + const actual = try discard(T, allocator, list, predicate); + defer allocator.free(actual); + try testing.expectEqual(expected.len, actual.len); + for (expected, actual) |e, a| try expectEqualElements(e, a); + } + }; +} + +test "keep on empty list returns empty list" { + const list = [_]u32{}; + const expected = [_]u32{}; + try testing.checkAllAllocationFailures( + testing.allocator, + Case(u32, alwaysTrue).testKeep, + .{ &list, &expected }, + ); +} + +test "keeps everything" { + const list = [_]u32{ 1, 3, 5 }; + const expected = [_]u32{ 1, 3, 5 }; + try testing.checkAllAllocationFailures( + testing.allocator, + Case(u32, alwaysTrue).testKeep, + .{ &list, &expected }, + ); +} + +test "keeps nothing" { + const list = [_]u32{ 1, 3, 5 }; + const expected = [_]u32{}; + try testing.checkAllAllocationFailures( + testing.allocator, + Case(u32, alwaysFalse).testKeep, + .{ &list, &expected }, + ); +} + +test "keeps first and last" { + const list = [_]u32{ 1, 2, 3 }; + const expected = [_]u32{ 1, 3 }; + try testing.checkAllAllocationFailures( + testing.allocator, + Case(u32, isOdd).testKeep, + .{ &list, &expected }, + ); +} + +test "keeps neither first nor last" { + const list = [_]u32{ 1, 2, 3 }; + const expected = [_]u32{2}; + try testing.checkAllAllocationFailures( + testing.allocator, + Case(u32, isEven).testKeep, + .{ &list, &expected }, + ); +} + +test "keeps strings" { + const list = [_][]const u8{ "apple", "zebra", "banana", "zombies", "cherimoya", "zealot" }; + const expected = [_][]const u8{ "zebra", "zombies", "zealot" }; + try testing.checkAllAllocationFailures( + testing.allocator, + Case([]const u8, startsWithZ).testKeep, + .{ &list, &expected }, + ); +} + +test "keeps lists" { + const list = [_][]const u32{ &.{ 1, 2, 3 }, &.{ 5, 5, 5 }, &.{ 5, 1, 2 }, &.{ 2, 1, 2 }, &.{ 1, 5, 2 }, &.{ 2, 2, 1 }, &.{ 1, 2, 5 } }; + const expected = [_][]const u32{ &.{ 5, 5, 5 }, &.{ 5, 1, 2 }, &.{ 1, 5, 2 }, &.{ 1, 2, 5 } }; + try testing.checkAllAllocationFailures( + testing.allocator, + Case([]const u32, containsFive).testKeep, + .{ &list, &expected }, + ); +} + +test "discard on empty list returns empty list" { + const list = [_]u32{}; + const expected = [_]u32{}; + try testing.checkAllAllocationFailures( + testing.allocator, + Case(u32, alwaysTrue).testDiscard, + .{ &list, &expected }, + ); +} + +test "discards everything" { + const list = [_]u32{ 1, 3, 5 }; + const expected = [_]u32{}; + try testing.checkAllAllocationFailures( + testing.allocator, + Case(u32, alwaysTrue).testDiscard, + .{ &list, &expected }, + ); +} + +test "discards nothing" { + const list = [_]u32{ 1, 3, 5 }; + const expected = [_]u32{ 1, 3, 5 }; + try testing.checkAllAllocationFailures( + testing.allocator, + Case(u32, alwaysFalse).testDiscard, + .{ &list, &expected }, + ); +} + +test "discards first and last" { + const list = [_]u32{ 1, 2, 3 }; + const expected = [_]u32{2}; + try testing.checkAllAllocationFailures( + testing.allocator, + Case(u32, isOdd).testDiscard, + .{ &list, &expected }, + ); +} + +test "discards neither first nor last" { + const list = [_]u32{ 1, 2, 3 }; + const expected = [_]u32{ 1, 3 }; + try testing.checkAllAllocationFailures( + testing.allocator, + Case(u32, isEven).testDiscard, + .{ &list, &expected }, + ); +} + +test "discards strings" { + const list = [_][]const u8{ "apple", "zebra", "banana", "zombies", "cherimoya", "zealot" }; + const expected = [_][]const u8{ "apple", "banana", "cherimoya" }; + try testing.checkAllAllocationFailures( + testing.allocator, + Case([]const u8, startsWithZ).testDiscard, + .{ &list, &expected }, + ); +} + +test "discards lists" { + const list = [_][]const u32{ &.{ 1, 2, 3 }, &.{ 5, 5, 5 }, &.{ 5, 1, 2 }, &.{ 2, 1, 2 }, &.{ 1, 5, 2 }, &.{ 2, 2, 1 }, &.{ 1, 2, 5 } }; + const expected = [_][]const u32{ &.{ 1, 2, 3 }, &.{ 2, 1, 2 }, &.{ 2, 2, 1 } }; + try testing.checkAllAllocationFailures( + testing.allocator, + Case([]const u32, containsFive).testDiscard, + .{ &list, &expected }, + ); +} diff --git a/generators/exercises/strain.py b/generators/exercises/strain.py new file mode 100644 index 00000000..6fc7b716 --- /dev/null +++ b/generators/exercises/strain.py @@ -0,0 +1,106 @@ +from lib import zstr + +HEADER = """const keep = strain.keep; +const discard = strain.discard; + +fn alwaysTrue(_: u32) bool { + return true; +} + +fn alwaysFalse(_: u32) bool { + return false; +} + +fn isOdd(x: u32) bool { + return x % 2 == 1; +} + +fn isEven(x: u32) bool { + return x % 2 == 0; +} + +fn startsWithZ(x: []const u8) bool { + return std.mem.startsWith(u8, x, "z"); +} + +fn containsFive(x: []const u32) bool { + return std.mem.indexOfScalar(u32, x, 5) != null; +} + +/// Binds an element type and a predicate, providing allocation-checked +/// wrappers around `keep` and `discard`. +fn Case(comptime T: type, comptime predicate: fn (T) bool) type { + return struct { + fn expectEqualElements(expected: T, actual: T) !void { + if (comptime T == []const u8) { + try testing.expectEqualStrings(expected, actual); + } else if (comptime T == []const u32) { + try testing.expectEqualSlices(u32, expected, actual); + } else { + try testing.expectEqual(expected, actual); + } + } + + fn testKeep(allocator: std.mem.Allocator, list: []const T, expected: []const T) !void { + const actual = try keep(T, allocator, list, predicate); + defer allocator.free(actual); + try testing.expectEqual(expected.len, actual.len); + for (expected, actual) |e, a| try expectEqualElements(e, a); + } + + fn testDiscard(allocator: std.mem.Allocator, list: []const T, expected: []const T) !void { + const actual = try discard(T, allocator, list, predicate); + defer allocator.free(actual); + try testing.expectEqual(expected.len, actual.len); + for (expected, actual) |e, a| try expectEqualElements(e, a); + } + }; +} +""" + +PREDICATES = { + "fn(x) -> true": "alwaysTrue", + "fn(x) -> false": "alwaysFalse", + "fn(x) -> x % 2 == 1": "isOdd", + "fn(x) -> x % 2 == 0": "isEven", + "fn(x) -> starts_with(x, 'z')": "startsWithZ", + "fn(x) -> contains(x, 5)": "containsFive", +} + + +def element(v): + if isinstance(v, str): + return zstr(v) + if isinstance(v, list): + return "&.{ " + ", ".join(str(x) for x in v) + " }" + return str(v) + + +def array(values, zig_type): + if not values: + return f"[_]{zig_type}{{}}" + return f"[_]{zig_type}{{ " + ", ".join(element(v) for v in values) + " }" + + +def gen_case(case): + inp = case["input"] + lst = inp["list"] + predicate = PREDICATES[inp["predicate"]] + + if lst and isinstance(lst[0], str): + zig_type = "[]const u8" + elif lst and isinstance(lst[0], list): + zig_type = "[]const u32" + else: + zig_type = "u32" + + fn = "testKeep" if case["property"] == "keep" else "testDiscard" + return ( + f" const list = {array(lst, zig_type)};\n" + f" const expected = {array(case['expected'], zig_type)};\n" + " try testing.checkAllAllocationFailures(\n" + " testing.allocator,\n" + f" Case({zig_type}, {predicate}).{fn},\n" + " .{ &list, &expected },\n" + " );\n" + )