diff --git a/config.json b/config.json index 884e89d..d0831b0 100644 --- a/config.json +++ b/config.json @@ -496,6 +496,15 @@ "topics": [ "math" ] + }, + { + "slug": "palindrome-products", + "name": "Palindrome Products", + "uuid": "95b2f13e-e59f-477c-905a-8cfa652607b3", + "practices": [], + "prerequisites": [], + "difficulty": 6, + "topics": [] } ] }, diff --git a/exercises/practice/palindrome-products/.docs/instructions.md b/exercises/practice/palindrome-products/.docs/instructions.md new file mode 100644 index 0000000..aac6652 --- /dev/null +++ b/exercises/practice/palindrome-products/.docs/instructions.md @@ -0,0 +1,36 @@ +# Instructions + +Detect palindrome products in a given range. + +A palindromic number is a number that remains the same when its digits are reversed. +For example, `121` is a palindromic number but `112` is not. + +Given a range of numbers, find the largest and smallest palindromes which +are products of two numbers within that range. + +Your solution should return the largest and smallest palindromes, along with the factors of each within the range. +If the largest or smallest palindrome has more than one pair of factors within the range, then return all the pairs. + +## Example 1 + +Given the range `[1, 9]` (both inclusive)... + +And given the list of all possible products within this range: +`[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 15, 21, 24, 27, 20, 28, 32, 36, 25, 30, 35, 40, 45, 42, 48, 54, 49, 56, 63, 64, 72, 81]` + +The palindrome products are all single digit numbers (in this case): +`[1, 2, 3, 4, 5, 6, 7, 8, 9]` + +The smallest palindrome product is `1`. +Its factors are `(1, 1)`. +The largest palindrome product is `9`. +Its factors are `(1, 9)` and `(3, 3)`. + +## Example 2 + +Given the range `[10, 99]` (both inclusive)... + +The smallest palindrome product is `121`. +Its factors are `(11, 11)`. +The largest palindrome product is `9009`. +Its factors are `(91, 99)`. diff --git a/exercises/practice/palindrome-products/.meta/config.json b/exercises/practice/palindrome-products/.meta/config.json new file mode 100644 index 0000000..304b4d9 --- /dev/null +++ b/exercises/practice/palindrome-products/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "palindrome-products.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "Detect palindrome products in a given range.", + "source": "Problem 4 at Project Euler", + "source_url": "https://projecteuler.net/problem=4" +} diff --git a/exercises/practice/palindrome-products/.meta/example.sml b/exercises/practice/palindrome-products/.meta/example.sml new file mode 100644 index 0000000..4df8e00 --- /dev/null +++ b/exercises/practice/palindrome-products/.meta/example.sml @@ -0,0 +1,55 @@ +local + fun maximum (x: int, y: int): int = + if x > y then x + else y + + fun isPalindrome (n: int): bool = + let + val s = Int.toString n + in + s = String.implode (List.rev (String.explode s)) + end + + (* Returns the first non-none return value of f, + * where f is called with start, then start + step, ..., + * up to but not including stop. + *) + fun firstResult (start: int, stop: int, step: int) (f: int -> 'a option): 'a option = + if start = stop then NONE + else case f(start) of + result as (SOME _) => result + | NONE => firstResult (start + step, stop, step) f + + (* Returns a list of factor pairs of n, where + * each factor is at least min, and at most max. + *) + fun factorPairs (min: int, max: int) (n: int): {value: int, factors: (int * int) list} option = + let + (* Preconditions: first >= min, and n/first <= max *) + fun recurse (first: int): (int * int) list = + let + val second = n div first + in + if second < first then nil + else if n mod first <> 0 then recurse (first + 1) + else (first, second) :: (recurse (first + 1)) + end + val pairs: (int * int) list = recurse (maximum(min, (n + max - 1) div max)) + in + case pairs of + nil => NONE + | r as ((first, second) :: _) => SOME {value = first * second, factors = r} + end + + fun palindromeFactorPairs (min: int, max: int) (n: int): {value: int, factors: (int * int) list} option = + if isPalindrome n then factorPairs (min, max) n + else NONE +in + fun smallest (min: int, max: int): {value: int, factors: (int * int) list} option = + if min > max then raise Fail "min must be <= max" + else firstResult (min * min, max * max + 1, 1) (palindromeFactorPairs (min, max)) + + fun largest (min: int, max: int): {value: int, factors: (int * int) list} option = + if min > max then raise Fail "min must be <= max" + else firstResult (max * max, min * min - 1, ~1) (palindromeFactorPairs (min, max)) +end diff --git a/exercises/practice/palindrome-products/.meta/tests.toml b/exercises/practice/palindrome-products/.meta/tests.toml new file mode 100644 index 0000000..a3bc417 --- /dev/null +++ b/exercises/practice/palindrome-products/.meta/tests.toml @@ -0,0 +1,49 @@ +# 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. + +[5cff78fe-cf02-459d-85c2-ce584679f887] +description = "find the smallest palindrome from single digit factors" + +[0853f82c-5fc4-44ae-be38-fadb2cced92d] +description = "find the largest palindrome from single digit factors" + +[66c3b496-bdec-4103-9129-3fcb5a9063e1] +description = "find the smallest palindrome from double digit factors" + +[a10682ae-530a-4e56-b89d-69664feafe53] +description = "find the largest palindrome from double digit factors" + +[cecb5a35-46d1-4666-9719-fa2c3af7499d] +description = "find the smallest palindrome from triple digit factors" + +[edab43e1-c35f-4ea3-8c55-2f31dddd92e5] +description = "find the largest palindrome from triple digit factors" + +[4f802b5a-9d74-4026-a70f-b53ff9234e4e] +description = "find the smallest palindrome from four digit factors" + +[787525e0-a5f9-40f3-8cb2-23b52cf5d0be] +description = "find the largest palindrome from four digit factors" + +[58fb1d63-fddb-4409-ab84-a7a8e58d9ea0] +description = "empty result for smallest if no palindrome in the range" + +[9de9e9da-f1d9-49a5-8bfc-3d322efbdd02] +description = "empty result for largest if no palindrome in the range" + +[12e73aac-d7ee-4877-b8aa-2aa3dcdb9f8a] +description = "error result for smallest if min is more than max" + +[eeeb5bff-3f47-4b1e-892f-05829277bd74] +description = "error result for largest if min is more than max" + +[16481711-26c4-42e0-9180-e2e4e8b29c23] +description = "smallest product does not use the smallest factor" diff --git a/exercises/practice/palindrome-products/palindrome-products.sml b/exercises/practice/palindrome-products/palindrome-products.sml new file mode 100644 index 0000000..b101be2 --- /dev/null +++ b/exercises/practice/palindrome-products/palindrome-products.sml @@ -0,0 +1,5 @@ +fun smallest (min: int, max: int): {value: int, factors: (int * int) list} option = + raise Fail "'smallest' is not implemented" + +fun largest (min: int, max: int): {value: int, factors: (int * int) list} option = + raise Fail "'largest' is not implemented" diff --git a/exercises/practice/palindrome-products/test.sml b/exercises/practice/palindrome-products/test.sml new file mode 100644 index 0000000..a5a76f2 --- /dev/null +++ b/exercises/practice/palindrome-products/test.sml @@ -0,0 +1,51 @@ +(* version 1.0.0 *) + +use "testlib.sml"; +use "palindrome-products.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "palindrome-products" [ + test "find the smallest palindrome from single digit factors" + (fn _ => smallest (1, 9) |> Expect.equalTo (SOME {value = 1, factors = [(1, 1)]})), + + test "find the largest palindrome from single digit factors" + (fn _ => largest (1, 9) |> Expect.equalTo (SOME {value = 9, factors = [(1, 9), (3, 3)]})), + + test "find the smallest palindrome from double digit factors" + (fn _ => smallest (10, 99) |> Expect.equalTo (SOME {value = 121, factors = [(11, 11)]})), + + test "find the largest palindrome from double digit factors" + (fn _ => largest (10, 99) |> Expect.equalTo (SOME {value = 9009, factors = [(91, 99)]})), + + test "find the smallest palindrome from triple digit factors" + (fn _ => smallest (100, 999) |> Expect.equalTo (SOME {value = 10201, factors = [(101, 101)]})), + + test "find the largest palindrome from triple digit factors" + (fn _ => largest (100, 999) |> Expect.equalTo (SOME {value = 906609, factors = [(913, 993)]})), + + test "find the smallest palindrome from four digit factors" + (fn _ => smallest (1000, 9999) |> Expect.equalTo (SOME {value = 1002001, factors = [(1001, 1001)]})), + + test "find the largest palindrome from four digit factors" + (fn _ => largest (1000, 9999) |> Expect.equalTo (SOME {value = 99000099, factors = [(9901, 9999)]})), + + test "empty result for smallest if no palindrome in the range" + (fn _ => smallest (1002, 1003) |> Expect.equalTo NONE), + + test "empty result for largest if no palindrome in the range" + (fn _ => largest (15, 15) |> Expect.equalTo NONE), + + test "error result for smallest if min is more than max" + (fn _ => (fn _ => smallest (10000, 1)) |> Expect.error (Fail "min must be <= max")), + + test "error result for largest if min is more than max" + (fn _ => (fn _ => largest (2, 1)) |> Expect.error (Fail "min must be <= max")), + + test "smallest product does not use the smallest factor" + (fn _ => smallest (3215, 4000) |> Expect.equalTo (SOME {value = 10988901, factors = [(3297, 3333)]})) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/palindrome-products/testlib.sml b/exercises/practice/palindrome-products/testlib.sml new file mode 100644 index 0000000..0c8370c --- /dev/null +++ b/exercises/practice/palindrome-products/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)