diff --git a/config.json b/config.json index 1ebbd42..3adb808 100644 --- a/config.json +++ b/config.json @@ -616,6 +616,14 @@ "prerequisites": [], "difficulty": 4 }, + { + "slug": "saddle-points", + "name": "Saddle Points", + "uuid": "c3244c98-8419-4178-9a94-80374d2ebd3f", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "sieve", "name": "Sieve", diff --git a/exercises/practice/saddle-points/.docs/instructions.md b/exercises/practice/saddle-points/.docs/instructions.md new file mode 100644 index 0000000..f69cdab --- /dev/null +++ b/exercises/practice/saddle-points/.docs/instructions.md @@ -0,0 +1,27 @@ +# Instructions + +Your task is to find the potential trees where you could build your tree house. + +The data company provides the data as grids that show the heights of the trees. +The rows of the grid represent the east-west direction, and the columns represent the north-south direction. + +An acceptable tree will be the largest in its row, while being the smallest in its column. + +A grid might not have any good trees at all. +Or it might have one, or even several. + +Here is a grid that has exactly one candidate tree. + +```text + ↓ + 1 2 3 4 + |----------- + 1 | 9 8 7 8 +→ 2 |[5] 3 2 4 + 3 | 6 6 7 1 +``` + +- Row 2 has values 5, 3, 2, and 4. The largest value is 5. +- Column 1 has values 9, 5, and 6. The smallest value is 5. + +So the point at `[2, 1]` (row: 2, column: 1) is a great spot for a tree house. diff --git a/exercises/practice/saddle-points/.docs/introduction.md b/exercises/practice/saddle-points/.docs/introduction.md new file mode 100644 index 0000000..34b2c77 --- /dev/null +++ b/exercises/practice/saddle-points/.docs/introduction.md @@ -0,0 +1,11 @@ +# Introduction + +You plan to build a tree house in the woods near your house so that you can watch the sun rise and set. + +You've obtained data from a local survey company that show the height of every tree in each rectangular section of the map. +You need to analyze each grid on the map to find good trees for your tree house. + +A good tree is both: + +- taller than every tree to the east and west, so that you have the best possible view of the sunrises and sunsets. +- shorter than every tree to the north and south, to minimize the amount of tree climbing. diff --git a/exercises/practice/saddle-points/.meta/config.json b/exercises/practice/saddle-points/.meta/config.json new file mode 100644 index 0000000..f92e812 --- /dev/null +++ b/exercises/practice/saddle-points/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "saddle-points.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "Detect saddle points in a matrix.", + "source": "J Dalbey's Programming Practice problems", + "source_url": "https://users.csc.calpoly.edu/~jdalbey/103/Projects/ProgrammingPractice.html" +} diff --git a/exercises/practice/saddle-points/.meta/example.sml b/exercises/practice/saddle-points/.meta/example.sml new file mode 100644 index 0000000..33ae192 --- /dev/null +++ b/exercises/practice/saddle-points/.meta/example.sml @@ -0,0 +1,29 @@ +type point = {row: int, column: int}; + +fun saddlePoints ([] : int list list): point list = [] + | saddlePoints ([] :: _): point list = [] + | saddlePoints (matrix: int list list): point list = + let + fun rowMax (row: int list): int = foldl Int.max (hd row) (tl row) + val rowMaximums = Vector.fromList (map rowMax matrix) + + fun transpose ([] :: _) = [] + | transpose rows = map hd rows :: transpose (map tl rows) + fun colMin (col: int list): int = foldl Int.min (hd col) (tl col) + val columnMinimums = Vector.fromList (map colMin (transpose matrix)) + + fun checkCol rowIdx (element, (colIdx, acc)) = + (colIdx + 1, + if element = Vector.sub (rowMaximums, rowIdx) + andalso element = Vector.sub (columnMinimums, colIdx) + then {row = rowIdx + 1, column = colIdx + 1} :: acc + else acc) + + fun check (row, rowIdx, acc) = + #2 (foldl (checkCol rowIdx) (0, acc) row) + + fun checkRow (row, (rowIdx, acc)) = + (rowIdx + 1, check (row, rowIdx, acc)) + in + rev (#2 (foldl checkRow (0, []) matrix)) + end diff --git a/exercises/practice/saddle-points/.meta/tests.toml b/exercises/practice/saddle-points/.meta/tests.toml new file mode 100644 index 0000000..ca00852 --- /dev/null +++ b/exercises/practice/saddle-points/.meta/tests.toml @@ -0,0 +1,37 @@ +# 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. + +[3e374e63-a2e0-4530-a39a-d53c560382bd] +description = "Can identify single saddle point" + +[6b501e2b-6c1f-491f-b1bb-7f278f760534] +description = "Can identify that empty matrix has no saddle points" + +[8c27cc64-e573-4fcb-a099-f0ae863fb02f] +description = "Can identify lack of saddle points when there are none" + +[6d1399bd-e105-40fd-a2c9-c6609507d7a3] +description = "Can identify multiple saddle points in a column" + +[3e81dce9-53b3-44e6-bf26-e328885fd5d1] +description = "Can identify multiple saddle points in a row" + +[88868621-b6f4-4837-bb8b-3fad8b25d46b] +description = "Can identify saddle point in bottom right corner" + +[5b9499ca-fcea-4195-830a-9c4584a0ee79] +description = "Can identify saddle points in a non square matrix" + +[ee99ccd2-a1f1-4283-ad39-f8c70f0cf594] +description = "Can identify that saddle points in a single column matrix are those with the minimum value" + +[63abf709-a84b-407f-a1b3-456638689713] +description = "Can identify that saddle points in a single row matrix are those with the maximum value" diff --git a/exercises/practice/saddle-points/saddle-points.sml b/exercises/practice/saddle-points/saddle-points.sml new file mode 100644 index 0000000..bb2aeb2 --- /dev/null +++ b/exercises/practice/saddle-points/saddle-points.sml @@ -0,0 +1,4 @@ +type point = {row: int, column: int}; + +fun saddlePoints (matrix: int list list): point list = + raise Fail "'saddlePoints' is not implemented" diff --git a/exercises/practice/saddle-points/test.sml b/exercises/practice/saddle-points/test.sml new file mode 100644 index 0000000..a24a0f1 --- /dev/null +++ b/exercises/practice/saddle-points/test.sml @@ -0,0 +1,84 @@ +(* version 1.0.0 *) + +use "testlib.sml"; +use "saddle-points.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "saddle-points" [ + test "Can identify single saddle point" + (fn _ => let + val matrix = [[9, 8, 7], [5, 3, 2], [6, 6, 7]] + val expected = [{row = 2, column = 1}] + in + saddlePoints matrix |> Expect.equalTo expected + end), + + test "Can identify that empty matrix has no saddle points" + (fn _ => let + val matrix = [[]] + val expected = [] + in + saddlePoints matrix |> Expect.equalTo expected + end), + + test "Can identify lack of saddle points when there are none" + (fn _ => let + val matrix = [[1, 2, 3], [3, 1, 2], [2, 3, 1]] + val expected = [] + in + saddlePoints matrix |> Expect.equalTo expected + end), + + test "Can identify multiple saddle points in a column" + (fn _ => let + val matrix = [[4, 5, 4], [3, 5, 5], [1, 5, 4]] + val expected = [{row = 1, column = 2}, {row = 2, column = 2}, {row = 3, column = 2}] + in + saddlePoints matrix |> Expect.equalTo expected + end), + + test "Can identify multiple saddle points in a row" + (fn _ => let + val matrix = [[6, 7, 8], [5, 5, 5], [7, 5, 6]] + val expected = [{row = 2, column = 1}, {row = 2, column = 2}, {row = 2, column = 3}] + in + saddlePoints matrix |> Expect.equalTo expected + end), + + test "Can identify saddle point in bottom right corner" + (fn _ => let + val matrix = [[8, 7, 9], [6, 7, 6], [3, 2, 5]] + val expected = [{row = 3, column = 3}] + in + saddlePoints matrix |> Expect.equalTo expected + end), + + test "Can identify saddle points in a non square matrix" + (fn _ => let + val matrix = [[3, 1, 3], [3, 2, 4]] + val expected = [{row = 1, column = 1}, {row = 1, column = 3}] + in + saddlePoints matrix |> Expect.equalTo expected + end), + + test "Can identify that saddle points in a single column matrix are those with the minimum value" + (fn _ => let + val matrix = [[2], [1], [4], [1]] + val expected = [{row = 2, column = 1}, {row = 4, column = 1}] + in + saddlePoints matrix |> Expect.equalTo expected + end), + + test "Can identify that saddle points in a single row matrix are those with the maximum value" + (fn _ => let + val matrix = [[2, 5, 3, 5]] + val expected = [{row = 1, column = 2}, {row = 1, column = 4}] + in + saddlePoints matrix |> Expect.equalTo expected + end) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/saddle-points/testlib.sml b/exercises/practice/saddle-points/testlib.sml new file mode 100644 index 0000000..0c8370c --- /dev/null +++ b/exercises/practice/saddle-points/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)