From cd230ea756b0a755a453028cee4758f9681003b6 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Mon, 25 Dec 2023 09:31:27 +1100 Subject: [PATCH] Add armstrong-numbers exercise --- config.json | 11 ++ .../armstrong-numbers/.docs/instructions.md | 14 ++ .../armstrong-numbers/.meta/config.json | 19 +++ .../armstrong-numbers/.meta/example.sml | 26 +++ .../armstrong-numbers/.meta/tests.toml | 45 +++++ .../armstrong-numbers/armstrong-numbers.sml | 2 + exercises/practice/armstrong-numbers/test.sml | 39 +++++ .../practice/armstrong-numbers/testlib.sml | 160 ++++++++++++++++++ 8 files changed, 316 insertions(+) create mode 100644 exercises/practice/armstrong-numbers/.docs/instructions.md create mode 100644 exercises/practice/armstrong-numbers/.meta/config.json create mode 100644 exercises/practice/armstrong-numbers/.meta/example.sml create mode 100644 exercises/practice/armstrong-numbers/.meta/tests.toml create mode 100644 exercises/practice/armstrong-numbers/armstrong-numbers.sml create mode 100644 exercises/practice/armstrong-numbers/test.sml create mode 100644 exercises/practice/armstrong-numbers/testlib.sml diff --git a/config.json b/config.json index 15d55ed..5739774 100644 --- a/config.json +++ b/config.json @@ -287,6 +287,17 @@ "math" ] }, + { + "slug": "armstrong-numbers", + "name": "Armstrong Numbers", + "uuid": "e1131c5f-f0a1-4cce-aa24-6fb4e265824a", + "practices": [], + "prerequisites": [], + "difficulty": 2, + "topics": [ + "math" + ] + }, { "slug": "pop-count", "name": "Pop Count", diff --git a/exercises/practice/armstrong-numbers/.docs/instructions.md b/exercises/practice/armstrong-numbers/.docs/instructions.md new file mode 100644 index 0000000..5e56bbe --- /dev/null +++ b/exercises/practice/armstrong-numbers/.docs/instructions.md @@ -0,0 +1,14 @@ +# Instructions + +An [Armstrong number][armstrong-number] is a number that is the sum of its own digits each raised to the power of the number of digits. + +For example: + +- 9 is an Armstrong number, because `9 = 9^1 = 9` +- 10 is _not_ an Armstrong number, because `10 != 1^2 + 0^2 = 1` +- 153 is an Armstrong number, because: `153 = 1^3 + 5^3 + 3^3 = 1 + 125 + 27 = 153` +- 154 is _not_ an Armstrong number, because: `154 != 1^3 + 5^3 + 4^3 = 1 + 125 + 64 = 190` + +Write some code to determine whether a number is an Armstrong number. + +[armstrong-number]: https://en.wikipedia.org/wiki/Narcissistic_number diff --git a/exercises/practice/armstrong-numbers/.meta/config.json b/exercises/practice/armstrong-numbers/.meta/config.json new file mode 100644 index 0000000..dcd577f --- /dev/null +++ b/exercises/practice/armstrong-numbers/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "armstrong-numbers.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "Determine if a number is an Armstrong number.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Narcissistic_number" +} diff --git a/exercises/practice/armstrong-numbers/.meta/example.sml b/exercises/practice/armstrong-numbers/.meta/example.sml new file mode 100644 index 0000000..7e6936f --- /dev/null +++ b/exercises/practice/armstrong-numbers/.meta/example.sml @@ -0,0 +1,26 @@ +local + fun recurse (exponent: int) (base: int) (multiplier: int): int = + if exponent = 0 then multiplier + else recurse (exponent div 2) (base * base) (if exponent mod 2 = 0 then multiplier else base * multiplier) +in + fun power (exponent: int) (base: int): int = + recurse exponent base 1 +end + +fun isArmstrongNumber (number: int): bool = + let + val sum: int list -> int = + foldl op+ 0 + + fun recurse (digits: int list) (n: int): bool = + if n = 0 then + let + val count = length digits + in + number = sum (map (power count) digits) + end + else + recurse ((n mod 10) :: digits) (n div 10) + in + recurse [] number + end diff --git a/exercises/practice/armstrong-numbers/.meta/tests.toml b/exercises/practice/armstrong-numbers/.meta/tests.toml new file mode 100644 index 0000000..b956bdf --- /dev/null +++ b/exercises/practice/armstrong-numbers/.meta/tests.toml @@ -0,0 +1,45 @@ +# 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. + +[c1ed103c-258d-45b2-be73-d8c6d9580c7b] +description = "Zero is an Armstrong number" + +[579e8f03-9659-4b85-a1a2-d64350f6b17a] +description = "Single-digit numbers are Armstrong numbers" + +[2d6db9dc-5bf8-4976-a90b-b2c2b9feba60] +description = "There are no two-digit Armstrong numbers" + +[509c087f-e327-4113-a7d2-26a4e9d18283] +description = "Three-digit number that is an Armstrong number" + +[7154547d-c2ce-468d-b214-4cb953b870cf] +description = "Three-digit number that is not an Armstrong number" + +[6bac5b7b-42e9-4ecb-a8b0-4832229aa103] +description = "Four-digit number that is an Armstrong number" + +[eed4b331-af80-45b5-a80b-19c9ea444b2e] +description = "Four-digit number that is not an Armstrong number" + +[f971ced7-8d68-4758-aea1-d4194900b864] +description = "Seven-digit number that is an Armstrong number" + +[7ee45d52-5d35-4fbd-b6f1-5c8cd8a67f18] +description = "Seven-digit number that is not an Armstrong number" + +[5ee2fdf8-334e-4a46-bb8d-e5c19c02c148] +description = "Armstrong number containing seven zeroes" +include = false + +[12ffbf10-307a-434e-b4ad-c925680e1dd4] +description = "The largest and last Armstrong number" +include = false diff --git a/exercises/practice/armstrong-numbers/armstrong-numbers.sml b/exercises/practice/armstrong-numbers/armstrong-numbers.sml new file mode 100644 index 0000000..82c1e44 --- /dev/null +++ b/exercises/practice/armstrong-numbers/armstrong-numbers.sml @@ -0,0 +1,2 @@ +fun isArmstrongNumber (number: int): bool = + raise Fail "'isArmstrongNumber' is not implemented" diff --git a/exercises/practice/armstrong-numbers/test.sml b/exercises/practice/armstrong-numbers/test.sml new file mode 100644 index 0000000..9b72b4a --- /dev/null +++ b/exercises/practice/armstrong-numbers/test.sml @@ -0,0 +1,39 @@ +(* version 1.0.0 *) + +use "testlib.sml"; +use "armstrong-numbers.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "armstrong-numbers" [ + test "Zero is an Armstrong number" + (fn _ => isArmstrongNumber 0 |> Expect.truthy), + + test "Single-digit numbers are Armstrong numbers" + (fn _ => isArmstrongNumber 5 |> Expect.truthy), + + test "There are no two-digit Armstrong numbers" + (fn _ => isArmstrongNumber 10 |> Expect.falsy), + + test "Three-digit number that is an Armstrong number" + (fn _ => isArmstrongNumber 153 |> Expect.truthy), + + test "Three-digit number that is not an Armstrong number" + (fn _ => isArmstrongNumber 100 |> Expect.falsy), + + test "Four-digit number that is an Armstrong number" + (fn _ => isArmstrongNumber 9474 |> Expect.truthy), + + test "Four-digit number that is not an Armstrong number" + (fn _ => isArmstrongNumber 9475 |> Expect.falsy), + + test "Seven-digit number that is an Armstrong number" + (fn _ => isArmstrongNumber 9926315 |> Expect.truthy), + + test "Seven-digit number that is not an Armstrong number" + (fn _ => isArmstrongNumber 9926314 |> Expect.falsy) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/armstrong-numbers/testlib.sml b/exercises/practice/armstrong-numbers/testlib.sml new file mode 100644 index 0000000..0c8370c --- /dev/null +++ b/exercises/practice/armstrong-numbers/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)