diff --git a/config.json b/config.json index 1e65d29..e56e6f6 100644 --- a/config.json +++ b/config.json @@ -74,6 +74,17 @@ "floats" ] }, + { + "slug": "strain", + "name": "Strain", + "uuid": "e929f6cf-04f8-41dd-b333-69be7b06d857", + "practices": [], + "prerequisites": [], + "difficulty": 3, + "topics": [ + "lists" + ] + }, { "slug": "pangram", "name": "Pangram", @@ -231,8 +242,10 @@ "uuid": "c8b94fa9-d758-4dca-b7fc-7a130061e064", "practices": [], "prerequisites": [], - "difficulty": 1, - "topics": [] + "difficulty": 4, + "topics": [ + "lists" + ] }, { "slug": "nth-prime", diff --git a/exercises/practice/strain/.docs/instructions.md b/exercises/practice/strain/.docs/instructions.md new file mode 100644 index 0000000..3469ae6 --- /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 0000000..ff594bf --- /dev/null +++ b/exercises/practice/strain/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "strain.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "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.", + "source": "Conversation with James Edward Gray II", + "source_url": "http://graysoftinc.com/" +} diff --git a/exercises/practice/strain/.meta/example.sml b/exercises/practice/strain/.meta/example.sml new file mode 100644 index 0000000..1f96965 --- /dev/null +++ b/exercises/practice/strain/.meta/example.sml @@ -0,0 +1,17 @@ +fun keep f nil = nil + | keep f (first :: rest) = + let + val suffix = keep f rest + in + if f first then first :: suffix + else suffix + end + +fun discard f nil = nil + | discard f (first :: rest) = + let + val suffix = discard f rest + in + if f first then suffix + else first :: suffix + end diff --git a/exercises/practice/strain/.meta/tests.toml b/exercises/practice/strain/.meta/tests.toml new file mode 100644 index 0000000..3a617b4 --- /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.sml b/exercises/practice/strain/strain.sml new file mode 100644 index 0000000..238a0e4 --- /dev/null +++ b/exercises/practice/strain/strain.sml @@ -0,0 +1,5 @@ +fun keep f l = + raise Fail "'keep' is not implemented" + +fun discard f l = + raise Fail "'discard' is not implemented" diff --git a/exercises/practice/strain/test.sml b/exercises/practice/strain/test.sml new file mode 100644 index 0000000..9008be4 --- /dev/null +++ b/exercises/practice/strain/test.sml @@ -0,0 +1,110 @@ +(* version 1.0.0 *) + +use "testlib.sml"; +use "strain.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "strain" [ + test "keep on empty list returns empty list" + (fn _ => let + val l = [] + in + keep (fn x => true) l |> Expect.equalTo [] + end), + + test "keeps everything" + (fn _ => let + val l = [1, 3, 5] + in + keep (fn x => true) l |> Expect.equalTo [1, 3, 5] + end), + + test "keeps nothing" + (fn _ => let + val l = [1, 3, 5] + in + keep (fn x => false) l |> Expect.equalTo [] + end), + + test "keeps first and last" + (fn _ => let + val l = [1, 2, 3] + in + keep (fn x => x mod 2 = 1) l |> Expect.equalTo [1, 3] + end), + + test "keeps neither first nor last" + (fn _ => let + val l = [1, 2, 3] + in + keep (fn x => x mod 2 = 0) l |> Expect.equalTo [2] + end), + + test "keeps strings" + (fn _ => let + val l = ["apple", "zebra", "banana", "zombies", "cherimoya", "zealot"] + in + keep (String.isPrefix "z") l |> Expect.equalTo ["zebra", "zombies", "zealot"] + end), + + test "keeps lists" + (fn _ => let + val l = [[1, 2, 3], [5, 5, 5], [5, 1, 2], [2, 1, 2], [1, 5, 2], [2, 2, 1], [1, 2, 5]] + in + keep (List.exists (fn x => x = 5)) l |> Expect.equalTo [[5, 5, 5], [5, 1, 2], [1, 5, 2], [1, 2, 5]] + end), + + test "discard on empty list returns empty list" + (fn _ => let + val l = [] + in + discard (fn x => true) l |> Expect.equalTo [] + end), + + test "discards everything" + (fn _ => let + val l = [1, 3, 5] + in + discard (fn x => true) l |> Expect.equalTo [] + end), + + test "discards nothing" + (fn _ => let + val l = [1, 3, 5] + in + discard (fn x => false) l |> Expect.equalTo [1, 3, 5] + end), + + test "discards first and last" + (fn _ => let + val l = [1, 2, 3] + in + discard (fn x => x mod 2 = 1) l |> Expect.equalTo [2] + end), + + test "discards neither first nor last" + (fn _ => let + val l = [1, 2, 3] + in + discard (fn x => x mod 2 = 0) l |> Expect.equalTo [1, 3] + end), + + test "discards strings" + (fn _ => let + val l = ["apple", "zebra", "banana", "zombies", "cherimoya", "zealot"] + in + discard (String.isPrefix "z") l |> Expect.equalTo ["apple", "banana", "cherimoya"] + end), + + test "discards lists" + (fn _ => let + val l = [[1, 2, 3], [5, 5, 5], [5, 1, 2], [2, 1, 2], [1, 5, 2], [2, 2, 1], [1, 2, 5]] + in + discard (List.exists (fn x => x = 5)) l |> Expect.equalTo [[1, 2, 3], [2, 1, 2], [2, 2, 1]] + end) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/strain/testlib.sml b/exercises/practice/strain/testlib.sml new file mode 100644 index 0000000..0c8370c --- /dev/null +++ b/exercises/practice/strain/testlib.sml @@ -0,0 +1,160 @@ +structure Expect = +struct + datatype expectation = Pass | Fail of string * string + + local + fun failEq b a = + Fail ("Expected: " ^ b, "Got: " ^ a) + + fun failExn b a = + Fail ("Expected: " ^ b, "Raised: " ^ a) + + fun exnName (e: exn): string = General.exnName e + in + fun truthy a = + if a + then Pass + else failEq "true" "false" + + fun falsy a = + if a + then failEq "false" "true" + else Pass + + fun equalTo b a = + if a = b + then Pass + else failEq (PolyML.makestring b) (PolyML.makestring a) + + fun nearTo delta b a = + if Real.abs (a - b) <= delta * Real.abs a orelse + Real.abs (a - b) <= delta * Real.abs b + then Pass + else failEq (Real.toString b ^ " +/- " ^ Real.toString delta) (Real.toString a) + + fun anyError f = + ( + f (); + failExn "an exception" "Nothing" + ) handle _ => Pass + + fun error e f = + ( + f (); + failExn (exnName e) "Nothing" + ) handle e' => if exnMessage e' = exnMessage e + then Pass + else failExn (exnMessage e) (exnMessage e') + end +end + +structure TermColor = +struct + datatype color = Red | Green | Yellow | Normal + + fun f Red = "\027[31m" + | f Green = "\027[32m" + | f Yellow = "\027[33m" + | f Normal = "\027[0m" + + fun colorize color s = (f color) ^ s ^ (f Normal) + + val redit = colorize Red + + val greenit = colorize Green + + val yellowit = colorize Yellow +end + +structure Test = +struct + datatype testnode = TestGroup of string * testnode list + | Test of string * (unit -> Expect.expectation) + + local + datatype evaluation = Success of string + | Failure of string * string * string + | Error of string * string + + fun indent n s = (implode (List.tabulate (n, fn _ => #" "))) ^ s + + fun fmt indentlvl ev = + let + val check = TermColor.greenit "\226\156\148 " (* ✔ *) + val cross = TermColor.redit "\226\156\150 " (* ✖ *) + val indentlvl = indentlvl * 2 + in + case ev of + Success descr => indent indentlvl (check ^ descr) + | Failure (descr, exp, got) => + String.concatWith "\n" [indent indentlvl (cross ^ descr), + indent (indentlvl + 2) exp, + indent (indentlvl + 2) got] + | Error (descr, reason) => + String.concatWith "\n" [indent indentlvl (cross ^ descr), + indent (indentlvl + 2) (TermColor.redit reason)] + end + + fun eval (TestGroup _) = raise Fail "Only a 'Test' can be evaluated" + | eval (Test (descr, thunk)) = + ( + case thunk () of + Expect.Pass => ((1, 0, 0), Success descr) + | Expect.Fail (s, s') => ((0, 1, 0), Failure (descr, s, s')) + ) + handle e => ((0, 0, 1), Error (descr, "Unexpected error: " ^ exnMessage e)) + + fun flatten depth testnode = + let + fun sum (x, y, z) (a, b, c) = (x + a, y + b, z + c) + + fun aux (t, (counter, acc)) = + let + val (counter', texts) = flatten (depth + 1) t + in + (sum counter' counter, texts :: acc) + end + in + case testnode of + TestGroup (descr, ts) => + let + val (counter, texts) = foldr aux ((0, 0, 0), []) ts + in + (counter, (indent (depth * 2) descr) :: List.concat texts) + end + | Test _ => + let + val (counter, evaluation) = eval testnode + in + (counter, [fmt depth evaluation]) + end + end + + fun println s = print (s ^ "\n") + in + fun run suite = + let + val ((succeeded, failed, errored), texts) = flatten 0 suite + + val summary = String.concatWith ", " [ + TermColor.greenit ((Int.toString succeeded) ^ " passed"), + TermColor.redit ((Int.toString failed) ^ " failed"), + TermColor.redit ((Int.toString errored) ^ " errored"), + (Int.toString (succeeded + failed + errored)) ^ " total" + ] + + val status = if failed = 0 andalso errored = 0 + then OS.Process.success + else OS.Process.failure + + in + List.app println texts; + println ""; + println ("Tests: " ^ summary); + OS.Process.exit status + end + end +end + +fun describe description tests = Test.TestGroup (description, tests) +fun test description thunk = Test.Test (description, thunk)