From 3087b0bc840be8b77c8ebaf27b1e9258fc5007ee Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Sun, 8 Dec 2024 14:40:18 -0500 Subject: [PATCH] Add larget-series-product exercise (#322) --- config.json | 8 + .../.docs/instructions.md | 26 +++ .../.docs/introduction.md | 5 + .../largest-series-product/.meta/config.json | 19 +++ .../largest-series-product/.meta/example.sml | 22 +++ .../largest-series-product/.meta/tests.toml | 60 +++++++ .../largest-series-product.sml | 2 + .../practice/largest-series-product/test.sml | 55 ++++++ .../largest-series-product/testlib.sml | 160 ++++++++++++++++++ 9 files changed, 357 insertions(+) create mode 100644 exercises/practice/largest-series-product/.docs/instructions.md create mode 100644 exercises/practice/largest-series-product/.docs/introduction.md create mode 100644 exercises/practice/largest-series-product/.meta/config.json create mode 100644 exercises/practice/largest-series-product/.meta/example.sml create mode 100644 exercises/practice/largest-series-product/.meta/tests.toml create mode 100644 exercises/practice/largest-series-product/largest-series-product.sml create mode 100644 exercises/practice/largest-series-product/test.sml create mode 100644 exercises/practice/largest-series-product/testlib.sml diff --git a/config.json b/config.json index bcdba926..f2fab558 100644 --- a/config.json +++ b/config.json @@ -535,6 +535,14 @@ "lists" ] }, + { + "slug": "largest-series-product", + "name": "Largest Series Product", + "uuid": "ac954648-3670-4268-9cf2-907d8588efe1", + "practices": [], + "prerequisites": [], + "difficulty": 3 + }, { "slug": "queen-attack", "name": "Queen Attack", diff --git a/exercises/practice/largest-series-product/.docs/instructions.md b/exercises/practice/largest-series-product/.docs/instructions.md new file mode 100644 index 00000000..f297b57f --- /dev/null +++ b/exercises/practice/largest-series-product/.docs/instructions.md @@ -0,0 +1,26 @@ +# Instructions + +Your task is to look for patterns in the long sequence of digits in the encrypted signal. + +The technique you're going to use here is called the largest series product. + +Let's define a few terms, first. + +- **input**: the sequence of digits that you need to analyze +- **series**: a sequence of adjacent digits (those that are next to each other) that is contained within the input +- **span**: how many digits long each series is +- **product**: what you get when you multiply numbers together + +Let's work through an example, with the input `"63915"`. + +- To form a series, take adjacent digits in the original input. +- If you are working with a span of `3`, there will be three possible series: + - `"639"` + - `"391"` + - `"915"` +- Then we need to calculate the product of each series: + - The product of the series `"639"` is 162 (`6 × 3 × 9 = 162`) + - The product of the series `"391"` is 27 (`3 × 9 × 1 = 27`) + - The product of the series `"915"` is 45 (`9 × 1 × 5 = 45`) +- 162 is bigger than both 27 and 45, so the largest series product of `"63915"` is from the series `"639"`. + So the answer is **162**. diff --git a/exercises/practice/largest-series-product/.docs/introduction.md b/exercises/practice/largest-series-product/.docs/introduction.md new file mode 100644 index 00000000..597bb5fa --- /dev/null +++ b/exercises/practice/largest-series-product/.docs/introduction.md @@ -0,0 +1,5 @@ +# Introduction + +You work for a government agency that has intercepted a series of encrypted communication signals from a group of bank robbers. +The signals contain a long sequence of digits. +Your team needs to use various digital signal processing techniques to analyze the signals and identify any patterns that may indicate the planning of a heist. diff --git a/exercises/practice/largest-series-product/.meta/config.json b/exercises/practice/largest-series-product/.meta/config.json new file mode 100644 index 00000000..d98999e1 --- /dev/null +++ b/exercises/practice/largest-series-product/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "largest-series-product.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "Given a string of digits, calculate the largest product for a contiguous substring of digits of length n.", + "source": "A variation on Problem 8 at Project Euler", + "source_url": "https://projecteuler.net/problem=8" +} diff --git a/exercises/practice/largest-series-product/.meta/example.sml b/exercises/practice/largest-series-product/.meta/example.sml new file mode 100644 index 00000000..607bd95f --- /dev/null +++ b/exercises/practice/largest-series-product/.meta/example.sml @@ -0,0 +1,22 @@ +local + infix |> (* left-associative *) + fun (x |> f) = f x + + val product = List.foldl op* 1 + val max = List.foldl Int.max ~1 + fun spans _ [] = [] + | spans span ns = + if span > length ns then [] + else (List.take (ns, span)) :: spans span (tl ns) +in + fun largestProduct (digits: string, span: int): int = + if span > size digits then raise Fail "span must be smaller than string length" + else if span < 0 then raise Fail "span must not be negative" + else if span = 0 then 1 + else explode digits + |> List.map (valOf o Int.fromString o str) + |> spans span + |> List.map product + |> max + handle Option => raise Fail "digits input must only contain digits" +end diff --git a/exercises/practice/largest-series-product/.meta/tests.toml b/exercises/practice/largest-series-product/.meta/tests.toml new file mode 100644 index 00000000..6c111adf --- /dev/null +++ b/exercises/practice/largest-series-product/.meta/tests.toml @@ -0,0 +1,60 @@ +# 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. + +[7c82f8b7-e347-48ee-8a22-f672323324d4] +description = "finds the largest product if span equals length" + +[88523f65-21ba-4458-a76a-b4aaf6e4cb5e] +description = "can find the largest product of 2 with numbers in order" + +[f1376b48-1157-419d-92c2-1d7e36a70b8a] +description = "can find the largest product of 2" + +[46356a67-7e02-489e-8fea-321c2fa7b4a4] +description = "can find the largest product of 3 with numbers in order" + +[a2dcb54b-2b8f-4993-92dd-5ce56dece64a] +description = "can find the largest product of 3" + +[673210a3-33cd-4708-940b-c482d7a88f9d] +description = "can find the largest product of 5 with numbers in order" + +[02acd5a6-3bbf-46df-8282-8b313a80a7c9] +description = "can get the largest product of a big number" + +[76dcc407-21e9-424c-a98e-609f269622b5] +description = "reports zero if the only digits are zero" + +[6ef0df9f-52d4-4a5d-b210-f6fae5f20e19] +description = "reports zero if all spans include zero" + +[5d81aaf7-4f67-4125-bf33-11493cc7eab7] +description = "rejects span longer than string length" + +[06bc8b90-0c51-4c54-ac22-3ec3893a079e] +description = "reports 1 for empty string and empty product (0 span)" + +[3ec0d92e-f2e2-4090-a380-70afee02f4c0] +description = "reports 1 for nonempty string and empty product (0 span)" + +[6d96c691-4374-4404-80ee-2ea8f3613dd4] +description = "rejects empty string and nonzero span" + +[7a38f2d6-3c35-45f6-8d6f-12e6e32d4d74] +description = "rejects invalid character in digits" + +[5fe3c0e5-a945-49f2-b584-f0814b4dd1ef] +description = "rejects negative span" +include = false + +[c859f34a-9bfe-4897-9c2f-6d7f8598e7f0] +description = "rejects negative span" +reimplements = "5fe3c0e5-a945-49f2-b584-f0814b4dd1ef" diff --git a/exercises/practice/largest-series-product/largest-series-product.sml b/exercises/practice/largest-series-product/largest-series-product.sml new file mode 100644 index 00000000..98014f65 --- /dev/null +++ b/exercises/practice/largest-series-product/largest-series-product.sml @@ -0,0 +1,2 @@ +fun largestProduct (digits: string, span: int): int = + raise Fail "'largestProduct' is not implemented" \ No newline at end of file diff --git a/exercises/practice/largest-series-product/test.sml b/exercises/practice/largest-series-product/test.sml new file mode 100644 index 00000000..00e2be1a --- /dev/null +++ b/exercises/practice/largest-series-product/test.sml @@ -0,0 +1,55 @@ +use "testlib.sml"; +use "largest-series-product.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "largest-series-product" [ + test "finds the largest product if span equals length" + (fn _ => largestProduct ("29", 2) |> Expect.equalTo 18), + + test "can find the largest product of 2 with numbers in order" + (fn _ => largestProduct ("0123456789", 2) |> Expect.equalTo 72), + + test "can find the largest product of 2" + (fn _ => largestProduct ("576802143", 2) |> Expect.equalTo 48), + + test "can find the largest product of 3 with numbers in order" + (fn _ => largestProduct ("0123456789", 3) |> Expect.equalTo 504), + + test "can find the largest product of 3" + (fn _ => largestProduct ("1027839564", 3) |> Expect.equalTo 270), + + test "can find the largest product of 5 with numbers in order" + (fn _ => largestProduct ("0123456789", 5) |> Expect.equalTo 15120), + + test "can get the largest product of a big number" + (fn _ => largestProduct ("73167176531330624919225119674426574742355349194934", 6) |> Expect.equalTo 23520), + + test "reports zero if the only digits are zero" + (fn _ => largestProduct ("0000", 2) |> Expect.equalTo 0), + + test "reports zero if all spans include zero" + (fn _ => largestProduct ("99099", 3) |> Expect.equalTo 0), + + test "rejects span longer than string length" + (fn _ => (fn _ => largestProduct ("123", 4)) |> Expect.error (Fail "span must be smaller than string length")), + + test "reports 1 for empty string and empty product (0 span)" + (fn _ => largestProduct ("", 0) |> Expect.equalTo 1), + + test "reports 1 for nonempty string and empty product (0 span)" + (fn _ => largestProduct ("123", 0) |> Expect.equalTo 1), + + test "rejects empty string and nonzero span" + (fn _ => (fn _ => largestProduct ("", 1)) |> Expect.error (Fail "span must be smaller than string length")), + + test "rejects invalid character in digits" + (fn _ => (fn _ => largestProduct ("1234a5", 2)) |> Expect.error (Fail "digits input must only contain digits")), + + test "rejects negative span" + (fn _ => (fn _ => largestProduct ("12345", ~1)) |> Expect.error (Fail "span must not be negative")) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/largest-series-product/testlib.sml b/exercises/practice/largest-series-product/testlib.sml new file mode 100644 index 00000000..0c8370c0 --- /dev/null +++ b/exercises/practice/largest-series-product/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)