From 0e543cf14760b47ecb46b3f95c6acb1829a26942 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Sun, 8 Dec 2024 03:55:43 -0500 Subject: [PATCH] Add sieve exercise (#319) --- config.json | 8 + .../practice/sieve/.docs/instructions.md | 42 +++++ .../practice/sieve/.docs/introduction.md | 7 + exercises/practice/sieve/.meta/config.json | 19 +++ exercises/practice/sieve/.meta/example.sml | 20 +++ exercises/practice/sieve/.meta/tests.toml | 25 +++ exercises/practice/sieve/sieve.sml | 2 + exercises/practice/sieve/test.sml | 25 +++ exercises/practice/sieve/testlib.sml | 160 ++++++++++++++++++ 9 files changed, 308 insertions(+) create mode 100644 exercises/practice/sieve/.docs/instructions.md create mode 100644 exercises/practice/sieve/.docs/introduction.md create mode 100644 exercises/practice/sieve/.meta/config.json create mode 100644 exercises/practice/sieve/.meta/example.sml create mode 100644 exercises/practice/sieve/.meta/tests.toml create mode 100644 exercises/practice/sieve/sieve.sml create mode 100644 exercises/practice/sieve/test.sml create mode 100644 exercises/practice/sieve/testlib.sml diff --git a/config.json b/config.json index d5a144db..bf4b3ec0 100644 --- a/config.json +++ b/config.json @@ -600,6 +600,14 @@ "practices": [], "prerequisites": [], "difficulty": 5 + }, + { + "slug": "sieve", + "name": "Sieve", + "uuid": "f0274c1a-f0dc-4aa5-8517-3afd27c65fe8", + "practices": [], + "prerequisites": [], + "difficulty": 4 } ], "foregone": [ diff --git a/exercises/practice/sieve/.docs/instructions.md b/exercises/practice/sieve/.docs/instructions.md new file mode 100644 index 00000000..085c0a57 --- /dev/null +++ b/exercises/practice/sieve/.docs/instructions.md @@ -0,0 +1,42 @@ +# Instructions + +Your task is to create a program that implements the Sieve of Eratosthenes algorithm to find all prime numbers less than or equal to a given number. + +A prime number is a number larger than 1 that is only divisible by 1 and itself. +For example, 2, 3, 5, 7, 11, and 13 are prime numbers. +By contrast, 6 is _not_ a prime number as it not only divisible by 1 and itself, but also by 2 and 3. + +To use the Sieve of Eratosthenes, you first create a list of all the numbers between 2 and your given number. +Then you repeat the following steps: + +1. Find the next unmarked number in your list (skipping over marked numbers). + This is a prime number. +2. Mark all the multiples of that prime number as **not** prime. + +You keep repeating these steps until you've gone through every number in your list. +At the end, all the unmarked numbers are prime. + +~~~~exercism/note +The tests don't check that you've implemented the algorithm, only that you've come up with the correct list of primes. +To check you are implementing the Sieve correctly, a good first test is to check that you do not use division or remainder operations. +~~~~ + +## Example + +Let's say you're finding the primes less than or equal to 10. + +- List out 2, 3, 4, 5, 6, 7, 8, 9, 10, leaving them all unmarked. +- 2 is unmarked and is therefore a prime. + Mark 4, 6, 8 and 10 as "not prime". +- 3 is unmarked and is therefore a prime. + Mark 6 and 9 as not prime _(marking 6 is optional - as it's already been marked)_. +- 4 is marked as "not prime", so we skip over it. +- 5 is unmarked and is therefore a prime. + Mark 10 as not prime _(optional - as it's already been marked)_. +- 6 is marked as "not prime", so we skip over it. +- 7 is unmarked and is therefore a prime. +- 8 is marked as "not prime", so we skip over it. +- 9 is marked as "not prime", so we skip over it. +- 10 is marked as "not prime", so we stop as there are no more numbers to check. + +You've examined all numbers and found 2, 3, 5, and 7 are still unmarked, which means they're the primes less than or equal to 10. diff --git a/exercises/practice/sieve/.docs/introduction.md b/exercises/practice/sieve/.docs/introduction.md new file mode 100644 index 00000000..f6c1cf79 --- /dev/null +++ b/exercises/practice/sieve/.docs/introduction.md @@ -0,0 +1,7 @@ +# Introduction + +You bought a big box of random computer parts at a garage sale. +You've started putting the parts together to build custom computers. + +You want to test the performance of different combinations of parts, and decide to create your own benchmarking program to see how your computers compare. +You choose the famous "Sieve of Eratosthenes" algorithm, an ancient algorithm, but one that should push your computers to the limits. diff --git a/exercises/practice/sieve/.meta/config.json b/exercises/practice/sieve/.meta/config.json new file mode 100644 index 00000000..24283d74 --- /dev/null +++ b/exercises/practice/sieve/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "sieve.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "Use the Sieve of Eratosthenes to find all the primes from 2 up to a given number.", + "source": "Sieve of Eratosthenes at Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes" +} diff --git a/exercises/practice/sieve/.meta/example.sml b/exercises/practice/sieve/.meta/example.sml new file mode 100644 index 00000000..6b2aedb0 --- /dev/null +++ b/exercises/practice/sieve/.meta/example.sml @@ -0,0 +1,20 @@ +fun primes (limit: int): int list = + let val marks = Array.array (limit + 1, true) + + fun markMultiples prev = + case Array.findi (fn (i, isPrime) => i > prev andalso isPrime) marks + of NONE => () + | SOME (prime, _) => (doMark prime (2 * prime); markMultiples prime) + + and doMark p m = + if m > limit then () + else (Array.update (marks, m, false); doMark p (m + p)) + + in Array.update (marks, 0, false); + Array.update (marks, 1, false); + markMultiples 1; + Array.foldri + (fn (i, isPrime, ps) => if isPrime then i :: ps else ps) + [] + marks + end diff --git a/exercises/practice/sieve/.meta/tests.toml b/exercises/practice/sieve/.meta/tests.toml new file mode 100644 index 00000000..fec5e1a1 --- /dev/null +++ b/exercises/practice/sieve/.meta/tests.toml @@ -0,0 +1,25 @@ +# 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. + +[88529125-c4ce-43cc-bb36-1eb4ddd7b44f] +description = "no primes under two" + +[4afe9474-c705-4477-9923-840e1024cc2b] +description = "find first prime" + +[974945d8-8cd9-4f00-9463-7d813c7f17b7] +description = "find primes up to 10" + +[2e2417b7-3f3a-452a-8594-b9af08af6d82] +description = "limit is prime" + +[92102a05-4c7c-47de-9ed0-b7d5fcd00f21] +description = "find primes up to 1000" diff --git a/exercises/practice/sieve/sieve.sml b/exercises/practice/sieve/sieve.sml new file mode 100644 index 00000000..99e595e0 --- /dev/null +++ b/exercises/practice/sieve/sieve.sml @@ -0,0 +1,2 @@ +fun primes (limit: int): int list = + raise Fail "'primes' is not implemented" \ No newline at end of file diff --git a/exercises/practice/sieve/test.sml b/exercises/practice/sieve/test.sml new file mode 100644 index 00000000..d5e05b80 --- /dev/null +++ b/exercises/practice/sieve/test.sml @@ -0,0 +1,25 @@ +use "testlib.sml"; +use "sieve.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "sieve" [ + test "no primes under two" + (fn _ => primes 1 |> Expect.equalTo []), + + test "find first prime" + (fn _ => primes 2 |> Expect.equalTo [2]), + + test "find primes up to 10" + (fn _ => primes 10 |> Expect.equalTo [2, 3, 5, 7]), + + test "limit is prime" + (fn _ => primes 13 |> Expect.equalTo [2, 3, 5, 7, 11, 13]), + + test "find primes up to 1000" + (fn _ => primes 1000 |> Expect.equalTo [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997]) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/sieve/testlib.sml b/exercises/practice/sieve/testlib.sml new file mode 100644 index 00000000..0c8370c0 --- /dev/null +++ b/exercises/practice/sieve/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)