diff --git a/config.json b/config.json index 607f8df..56bced4 100644 --- a/config.json +++ b/config.json @@ -315,6 +315,17 @@ "difficulty": 1, "topics": [] }, + { + "slug": "pythagorean-triplet", + "name": "Pythagorean Triplet", + "uuid": "203470a2-f80c-4d6c-8ea4-7d09d7bfb509", + "practices": [], + "prerequisites": [], + "difficulty": 5, + "topics": [ + "math" + ] + }, { "slug": "roman-numerals", "name": "Roman Numerals", diff --git a/exercises/practice/pythagorean-triplet/.docs/instructions.md b/exercises/practice/pythagorean-triplet/.docs/instructions.md new file mode 100644 index 0000000..1c1a8ae --- /dev/null +++ b/exercises/practice/pythagorean-triplet/.docs/instructions.md @@ -0,0 +1,23 @@ +# Instructions + +A Pythagorean triplet is a set of three natural numbers, {a, b, c}, for which, + +```text +a² + b² = c² +``` + +and such that, + +```text +a < b < c +``` + +For example, + +```text +3² + 4² = 5². +``` + +Given an input integer N, find all Pythagorean triplets for which `a + b + c = N`. + +For example, with N = 1000, there is exactly one Pythagorean triplet for which `a + b + c = 1000`: `{200, 375, 425}`. diff --git a/exercises/practice/pythagorean-triplet/.meta/config.json b/exercises/practice/pythagorean-triplet/.meta/config.json new file mode 100644 index 0000000..2c31228 --- /dev/null +++ b/exercises/practice/pythagorean-triplet/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "pythagorean-triplet.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "There exists exactly one Pythagorean triplet for which a + b + c = 1000. Find the triplet.", + "source": "Problem 9 at Project Euler", + "source_url": "https://projecteuler.net/problem=9" +} diff --git a/exercises/practice/pythagorean-triplet/.meta/example.sml b/exercises/practice/pythagorean-triplet/.meta/example.sml new file mode 100644 index 0000000..fc8c417 --- /dev/null +++ b/exercises/practice/pythagorean-triplet/.meta/example.sml @@ -0,0 +1,25 @@ +(* For every Pythagorean triplet with total a + b + c = n, + * a² + b² = c² + * <=> a² + b² = (n - a - b)², substituting c + * <=> 0 = n² - 2*n*a - 2*n*b + 2*a*b + * <=> (2*n - 2*a) b = (n² - 2*n*a) + * <=> b = (n² - 2*n*a) / (2*n - 2*a) + * + * The denominator is never 0, as perimeter exceeds a side length. + *) +fun tripletsWithSum (n: int): (int * int * int) list = + let + fun recurse (a: int): (int * int * int) list = + let + val numerator = n * (n - 2 * a) + val denominator = 2 * (n - a) + val b = numerator div denominator + in + if b <= a then nil + else if numerator mod denominator <> 0 then recurse (a + 1) + else (a, b, n - a - b) :: recurse (a + 1) + end + in + if n < 2 then nil + else recurse 1 + end diff --git a/exercises/practice/pythagorean-triplet/.meta/tests.toml b/exercises/practice/pythagorean-triplet/.meta/tests.toml new file mode 100644 index 0000000..719620a --- /dev/null +++ b/exercises/practice/pythagorean-triplet/.meta/tests.toml @@ -0,0 +1,31 @@ +# 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. + +[a19de65d-35b8-4480-b1af-371d9541e706] +description = "triplets whose sum is 12" + +[48b21332-0a3d-43b2-9a52-90b2a6e5c9f5] +description = "triplets whose sum is 108" + +[dffc1266-418e-4daa-81af-54c3e95c3bb5] +description = "triplets whose sum is 1000" + +[5f86a2d4-6383-4cce-93a5-e4489e79b186] +description = "no matching triplets for 1001" + +[bf17ba80-1596-409a-bb13-343bdb3b2904] +description = "returns all matching triplets" + +[9d8fb5d5-6c6f-42df-9f95-d3165963ac57] +description = "several matching triplets" + +[f5be5734-8aa0-4bd1-99a2-02adcc4402b4] +description = "triplets for large number" diff --git a/exercises/practice/pythagorean-triplet/pythagorean-triplet.sml b/exercises/practice/pythagorean-triplet/pythagorean-triplet.sml new file mode 100644 index 0000000..62cc62a --- /dev/null +++ b/exercises/practice/pythagorean-triplet/pythagorean-triplet.sml @@ -0,0 +1,2 @@ +fun tripletsWithSum (n: int): (int * int * int) list = + raise Fail "'tripletsWithSum' is not implemented" diff --git a/exercises/practice/pythagorean-triplet/test.sml b/exercises/practice/pythagorean-triplet/test.sml new file mode 100644 index 0000000..7337c3e --- /dev/null +++ b/exercises/practice/pythagorean-triplet/test.sml @@ -0,0 +1,33 @@ +(* version 1.0.0 *) + +use "testlib.sml"; +use "pythagorean-triplet.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "pythagorean-triplet" [ + test "triplets whose sum is 12" + (fn _ => tripletsWithSum 12 |> Expect.equalTo [(3, 4, 5)]), + + test "triplets whose sum is 108" + (fn _ => tripletsWithSum 108 |> Expect.equalTo [(27, 36, 45)]), + + test "triplets whose sum is 1000" + (fn _ => tripletsWithSum 1000 |> Expect.equalTo [(200, 375, 425)]), + + test "no matching triplets for 1001" + (fn _ => tripletsWithSum 1001 |> Expect.equalTo []), + + test "returns all matching triplets" + (fn _ => tripletsWithSum 90 |> Expect.equalTo [(9, 40, 41), (15, 36, 39)]), + + test "several matching triplets" + (fn _ => tripletsWithSum 840 |> Expect.equalTo [(40, 399, 401), (56, 390, 394), (105, 360, 375), (120, 350, 370), (140, 336, 364), (168, 315, 357), (210, 280, 350), (240, 252, 348)]), + + test "triplets for large number" + (fn _ => tripletsWithSum 30000 |> Expect.equalTo [(1200, 14375, 14425), (1875, 14000, 14125), (5000, 12000, 13000), (6000, 11250, 12750), (7500, 10000, 12500)]) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/pythagorean-triplet/testlib.sml b/exercises/practice/pythagorean-triplet/testlib.sml new file mode 100644 index 0000000..0c8370c --- /dev/null +++ b/exercises/practice/pythagorean-triplet/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)