diff --git a/config.json b/config.json index ea518d2..8ad055d 100644 --- a/config.json +++ b/config.json @@ -606,6 +606,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "food-chain", + "name": "Food Chain", + "uuid": "18810489-b71e-4d1c-9f2d-e9ac28580fd4", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "knapsack", "name": "Knapsack", diff --git a/exercises/practice/food-chain/.docs/instructions.md b/exercises/practice/food-chain/.docs/instructions.md new file mode 100644 index 0000000..125820e --- /dev/null +++ b/exercises/practice/food-chain/.docs/instructions.md @@ -0,0 +1,64 @@ +# Instructions + +Generate the lyrics of the song 'I Know an Old Lady Who Swallowed a Fly'. + +While you could copy/paste the lyrics, or read them from a file, this problem is much more interesting if you approach it algorithmically. + +This is a [cumulative song][cumulative-song] of unknown origin. + +This is one of many common variants. + +```text +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! +``` + +[cumulative-song]: https://en.wikipedia.org/wiki/Cumulative_song diff --git a/exercises/practice/food-chain/.meta/config.json b/exercises/practice/food-chain/.meta/config.json new file mode 100644 index 0000000..e5dc0ff --- /dev/null +++ b/exercises/practice/food-chain/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "food-chain.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "Generate the lyrics of the song 'I Know an Old Lady Who Swallowed a Fly'.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/There_Was_an_Old_Lady_Who_Swallowed_a_Fly" +} diff --git a/exercises/practice/food-chain/.meta/example.sml b/exercises/practice/food-chain/.meta/example.sml new file mode 100644 index 0000000..8d225f3 --- /dev/null +++ b/exercises/practice/food-chain/.meta/example.sml @@ -0,0 +1,37 @@ +fun recite (startVerse: int, endVerse: int): string = + let + val animals = [ "", "fly", "spider", "bird", "cat", "dog", "goat", "cow", "horse" ] + + val animal = List.nth (animals, startVerse) + + val exclamations = [ + "", + "I don't know why she swallowed the fly. Perhaps she'll die.", + "It wriggled and jiggled and tickled inside her.\n", + "How absurd to swallow a bird!\n", + "Imagine that, to swallow a cat!\n", + "What a hog, to swallow a dog!\n", + "Just opened her throat and swallowed a goat!\n", + "I don't know how she swallowed a cow!\n", + "She's dead, of course!" + ] + + val exclamation = List.nth (exclamations, startVerse) + + fun loop (index: int): string = + let + val current = List.nth (animals, index) + + val previous = List.nth (animals, index - 1) ^ (if index = 3 then " that wriggled and jiggled and tickled inside her" else "") + + val suffix = if index = 2 then List.nth (exclamations, 1) else loop (index - 1) + in + "She swallowed the " ^ current ^ " to catch the " ^ previous ^ ".\n" ^ suffix + end + + val reasons = (if startVerse = 1 orelse startVerse = 8 then "" else loop(startVerse)) + + val suffix = if startVerse = endVerse then "" else "\n\n" ^ recite (startVerse + 1, endVerse) + in + "I know an old lady who swallowed a " ^ animal ^ ".\n" ^ exclamation ^ reasons ^ suffix + end diff --git a/exercises/practice/food-chain/.meta/tests.toml b/exercises/practice/food-chain/.meta/tests.toml new file mode 100644 index 0000000..30c5b98 --- /dev/null +++ b/exercises/practice/food-chain/.meta/tests.toml @@ -0,0 +1,40 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[751dce68-9412-496e-b6e8-855998c56166] +description = "fly" + +[6c56f861-0c5e-4907-9a9d-b2efae389379] +description = "spider" + +[3edf5f33-bef1-4e39-ae67-ca5eb79203fa] +description = "bird" + +[e866a758-e1ff-400e-9f35-f27f28cc288f] +description = "cat" + +[3f02c30e-496b-4b2a-8491-bc7e2953cafb] +description = "dog" + +[4b3fd221-01ea-46e0-825b-5734634fbc59] +description = "goat" + +[1b707da9-7001-4fac-941f-22ad9c7a65d4] +description = "cow" + +[3cb10d46-ae4e-4d2c-9296-83c9ffc04cdc] +description = "horse" + +[22b863d5-17e4-4d1e-93e4-617329a5c050] +description = "multiple verses" + +[e626b32b-745c-4101-bcbd-3b13456893db] +description = "full song" diff --git a/exercises/practice/food-chain/food-chain.sml b/exercises/practice/food-chain/food-chain.sml new file mode 100644 index 0000000..7dfed99 --- /dev/null +++ b/exercises/practice/food-chain/food-chain.sml @@ -0,0 +1,2 @@ +fun recite (startVerse: int, endVerse: int): string = + raise Fail "'recite' is not implemented" diff --git a/exercises/practice/food-chain/test.sml b/exercises/practice/food-chain/test.sml new file mode 100644 index 0000000..f19ede4 --- /dev/null +++ b/exercises/practice/food-chain/test.sml @@ -0,0 +1,188 @@ +(* version 1.0.0 *) + +use "testlib.sml"; +use "food-chain.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "food-chain" [ + test "fly" + (fn _ => let + val expected = + "I know an old lady who swallowed a fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die." + in + recite (1, 1) |> Expect.equalTo expected + end), + + test "spider" + (fn _ => let + val expected = + "I know an old lady who swallowed a spider.\n\ + \It wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die." + in + recite (2, 2) |> Expect.equalTo expected + end), + + test "bird" + (fn _ => let + val expected = + "I know an old lady who swallowed a bird.\n\ + \How absurd to swallow a bird!\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die." + in + recite (3, 3) |> Expect.equalTo expected + end), + + test "cat" + (fn _ => let + val expected = + "I know an old lady who swallowed a cat.\n\ + \Imagine that, to swallow a cat!\n\ + \She swallowed the cat to catch the bird.\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die." + in + recite (4, 4) |> Expect.equalTo expected + end), + + test "dog" + (fn _ => let + val expected = + "I know an old lady who swallowed a dog.\n\ + \What a hog, to swallow a dog!\n\ + \She swallowed the dog to catch the cat.\n\ + \She swallowed the cat to catch the bird.\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die." + in + recite (5, 5) |> Expect.equalTo expected + end), + + test "goat" + (fn _ => let + val expected = + "I know an old lady who swallowed a goat.\n\ + \Just opened her throat and swallowed a goat!\n\ + \She swallowed the goat to catch the dog.\n\ + \She swallowed the dog to catch the cat.\n\ + \She swallowed the cat to catch the bird.\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die." + in + recite (6, 6) |> Expect.equalTo expected + end), + + test "cow" + (fn _ => let + val expected = + "I know an old lady who swallowed a cow.\n\ + \I don't know how she swallowed a cow!\n\ + \She swallowed the cow to catch the goat.\n\ + \She swallowed the goat to catch the dog.\n\ + \She swallowed the dog to catch the cat.\n\ + \She swallowed the cat to catch the bird.\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die." + in + recite (7, 7) |> Expect.equalTo expected + end), + + test "horse" + (fn _ => let + val expected = + "I know an old lady who swallowed a horse.\n\ + \She's dead, of course!" + in + recite (8, 8) |> Expect.equalTo expected + end), + + test "multiple verses" + (fn _ => let + val expected = + "I know an old lady who swallowed a fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die.\n\ + \\n\ + \I know an old lady who swallowed a spider.\n\ + \It wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die.\n\ + \\n\ + \I know an old lady who swallowed a bird.\n\ + \How absurd to swallow a bird!\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die." + in + recite (1, 3) |> Expect.equalTo expected + end), + + test "full song" + (fn _ => let + val expected = + "I know an old lady who swallowed a fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die.\n\ + \\n\ + \I know an old lady who swallowed a spider.\n\ + \It wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die.\n\ + \\n\ + \I know an old lady who swallowed a bird.\n\ + \How absurd to swallow a bird!\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die.\n\ + \\n\ + \I know an old lady who swallowed a cat.\n\ + \Imagine that, to swallow a cat!\n\ + \She swallowed the cat to catch the bird.\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die.\n\ + \\n\ + \I know an old lady who swallowed a dog.\n\ + \What a hog, to swallow a dog!\n\ + \She swallowed the dog to catch the cat.\n\ + \She swallowed the cat to catch the bird.\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die.\n\ + \\n\ + \I know an old lady who swallowed a goat.\n\ + \Just opened her throat and swallowed a goat!\n\ + \She swallowed the goat to catch the dog.\n\ + \She swallowed the dog to catch the cat.\n\ + \She swallowed the cat to catch the bird.\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die.\n\ + \\n\ + \I know an old lady who swallowed a cow.\n\ + \I don't know how she swallowed a cow!\n\ + \She swallowed the cow to catch the goat.\n\ + \She swallowed the goat to catch the dog.\n\ + \She swallowed the dog to catch the cat.\n\ + \She swallowed the cat to catch the bird.\n\ + \She swallowed the bird to catch the spider that wriggled and jiggled and tickled inside her.\n\ + \She swallowed the spider to catch the fly.\n\ + \I don't know why she swallowed the fly. Perhaps she'll die.\n\ + \\n\ + \I know an old lady who swallowed a horse.\n\ + \She's dead, of course!" + in + recite (1, 8) |> Expect.equalTo expected + end) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/food-chain/testlib.sml b/exercises/practice/food-chain/testlib.sml new file mode 100644 index 0000000..0c8370c --- /dev/null +++ b/exercises/practice/food-chain/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)