From 1e0a09b6e3341ad92a153dfdd5a58a96e5f60ce9 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Sun, 4 Feb 2024 09:41:14 +1100 Subject: [PATCH 1/2] Add binary-search exercise --- config.json | 8 + .../binary-search/.docs/instructions.md | 29 ++++ .../binary-search/.docs/introduction.md | 13 ++ .../practice/binary-search/.meta/config.json | 19 +++ .../practice/binary-search/.meta/example.sml | 16 ++ .../practice/binary-search/.meta/tests.toml | 43 +++++ .../practice/binary-search/binary-search.sml | 2 + exercises/practice/binary-search/test.sml | 45 +++++ exercises/practice/binary-search/testlib.sml | 160 ++++++++++++++++++ 9 files changed, 335 insertions(+) create mode 100644 exercises/practice/binary-search/.docs/instructions.md create mode 100644 exercises/practice/binary-search/.docs/introduction.md create mode 100644 exercises/practice/binary-search/.meta/config.json create mode 100644 exercises/practice/binary-search/.meta/example.sml create mode 100644 exercises/practice/binary-search/.meta/tests.toml create mode 100644 exercises/practice/binary-search/binary-search.sml create mode 100644 exercises/practice/binary-search/test.sml create mode 100644 exercises/practice/binary-search/testlib.sml diff --git a/config.json b/config.json index d5a144db..8a54a9a8 100644 --- a/config.json +++ b/config.json @@ -368,6 +368,14 @@ "prerequisites": [], "difficulty": 1 }, + { + "slug": "binary-search", + "name": "Binary Search", + "uuid": "afa5857c-bc9f-492c-a49e-1fbc06a613ab", + "practices": [], + "prerequisites": [], + "difficulty": 3 + }, { "slug": "isogram", "name": "Isogram", diff --git a/exercises/practice/binary-search/.docs/instructions.md b/exercises/practice/binary-search/.docs/instructions.md new file mode 100644 index 00000000..12f4358e --- /dev/null +++ b/exercises/practice/binary-search/.docs/instructions.md @@ -0,0 +1,29 @@ +# Instructions + +Your task is to implement a binary search algorithm. + +A binary search algorithm finds an item in a list by repeatedly splitting it in half, only keeping the half which contains the item we're looking for. +It allows us to quickly narrow down the possible locations of our item until we find it, or until we've eliminated all possible locations. + +~~~~exercism/caution +Binary search only works when a list has been sorted. +~~~~ + +The algorithm looks like this: + +- Find the middle element of a _sorted_ list and compare it with the item we're looking for. +- If the middle element is our item, then we're done! +- If the middle element is greater than our item, we can eliminate that element and all the elements **after** it. +- If the middle element is less than our item, we can eliminate that element and all the elements **before** it. +- If every element of the list has been eliminated then the item is not in the list. +- Otherwise, repeat the process on the part of the list that has not been eliminated. + +Here's an example: + +Let's say we're looking for the number 23 in the following sorted list: `[4, 8, 12, 16, 23, 28, 32]`. + +- We start by comparing 23 with the middle element, 16. +- Since 23 is greater than 16, we can eliminate the left half of the list, leaving us with `[23, 28, 32]`. +- We then compare 23 with the new middle element, 28. +- Since 23 is less than 28, we can eliminate the right half of the list: `[23]`. +- We've found our item. diff --git a/exercises/practice/binary-search/.docs/introduction.md b/exercises/practice/binary-search/.docs/introduction.md new file mode 100644 index 00000000..03496599 --- /dev/null +++ b/exercises/practice/binary-search/.docs/introduction.md @@ -0,0 +1,13 @@ +# Introduction + +You have stumbled upon a group of mathematicians who are also singer-songwriters. +They have written a song for each of their favorite numbers, and, as you can imagine, they have a lot of favorite numbers (like [0][zero] or [73][seventy-three] or [6174][kaprekars-constant]). + +You are curious to hear the song for your favorite number, but with so many songs to wade through, finding the right song could take a while. +Fortunately, they have organized their songs in a playlist sorted by the title — which is simply the number that the song is about. + +You realize that you can use a binary search algorithm to quickly find a song given the title. + +[zero]: https://en.wikipedia.org/wiki/0 +[seventy-three]: https://en.wikipedia.org/wiki/73_(number) +[kaprekars-constant]: https://en.wikipedia.org/wiki/6174_(number) diff --git a/exercises/practice/binary-search/.meta/config.json b/exercises/practice/binary-search/.meta/config.json new file mode 100644 index 00000000..243d5e22 --- /dev/null +++ b/exercises/practice/binary-search/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "keiravillekode" + ], + "files": { + "solution": [ + "binary-search.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "Implement a binary search algorithm.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Binary_search_algorithm" +} diff --git a/exercises/practice/binary-search/.meta/example.sml b/exercises/practice/binary-search/.meta/example.sml new file mode 100644 index 00000000..ce578147 --- /dev/null +++ b/exercises/practice/binary-search/.meta/example.sml @@ -0,0 +1,16 @@ +fun find (haystack: int array, needle: int): int = + let + fun recurse (low: int, high: int): int = + if low >= high then raise Fail "value not in array" + else + let + val middle = (low + high) div 2 + val element = Array.sub (haystack, middle) + in + if needle < element then recurse (low, middle) + else if needle > element then recurse (middle + 1, high) + else middle + end + in + recurse (0, Array.length haystack) + end diff --git a/exercises/practice/binary-search/.meta/tests.toml b/exercises/practice/binary-search/.meta/tests.toml new file mode 100644 index 00000000..61e2b068 --- /dev/null +++ b/exercises/practice/binary-search/.meta/tests.toml @@ -0,0 +1,43 @@ +# 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. + +[b55c24a9-a98d-4379-a08c-2adcf8ebeee8] +description = "finds a value in an array with one element" + +[73469346-b0a0-4011-89bf-989e443d503d] +description = "finds a value in the middle of an array" + +[327bc482-ab85-424e-a724-fb4658e66ddb] +description = "finds a value at the beginning of an array" + +[f9f94b16-fe5e-472c-85ea-c513804c7d59] +description = "finds a value at the end of an array" + +[f0068905-26e3-4342-856d-ad153cadb338] +description = "finds a value in an array of odd length" + +[fc316b12-c8b3-4f5e-9e89-532b3389de8c] +description = "finds a value in an array of even length" + +[da7db20a-354f-49f7-a6a1-650a54998aa6] +description = "identifies that a value is not included in the array" + +[95d869ff-3daf-4c79-b622-6e805c675f97] +description = "a value smaller than the array's smallest value is not found" + +[8b24ef45-6e51-4a94-9eac-c2bf38fdb0ba] +description = "a value larger than the array's largest value is not found" + +[f439a0fa-cf42-4262-8ad1-64bf41ce566a] +description = "nothing is found in an empty array" + +[2c353967-b56d-40b8-acff-ce43115eed64] +description = "nothing is found when the left and right bounds cross" diff --git a/exercises/practice/binary-search/binary-search.sml b/exercises/practice/binary-search/binary-search.sml new file mode 100644 index 00000000..167402c8 --- /dev/null +++ b/exercises/practice/binary-search/binary-search.sml @@ -0,0 +1,2 @@ +fun find (haystack: int array, needle: int): int = + raise Fail "'find' is not implemented" diff --git a/exercises/practice/binary-search/test.sml b/exercises/practice/binary-search/test.sml new file mode 100644 index 00000000..129b2b0a --- /dev/null +++ b/exercises/practice/binary-search/test.sml @@ -0,0 +1,45 @@ +(* version 1.0.0 *) + +use "testlib.sml"; +use "binary-search.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "binary-search" [ + test "finds a value in an array with one element" + (fn _ => find (Array.fromList [6], 6) |> Expect.equalTo 0), + + test "finds a value in the middle of an array" + (fn _ => find (Array.fromList [1, 3, 4, 6, 8, 9, 11], 6) |> Expect.equalTo 3), + + test "finds a value at the beginning of an array" + (fn _ => find (Array.fromList [1, 3, 4, 6, 8, 9, 11], 1) |> Expect.equalTo 0), + + test "finds a value at the end of an array" + (fn _ => find (Array.fromList [1, 3, 4, 6, 8, 9, 11], 11) |> Expect.equalTo 6), + + test "finds a value in an array of odd length" + (fn _ => find (Array.fromList [1, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 634], 144) |> Expect.equalTo 9), + + test "finds a value in an array of even length" + (fn _ => find (Array.fromList [1, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377], 21) |> Expect.equalTo 5), + + test "identifies that a value is not included in the array" + (fn _ => (fn _ => find (Array.fromList [1, 3, 4, 6, 8, 9, 11], 7)) |> Expect.error (Fail "value not in array")), + + test "a value smaller than the array's smallest value is not found" + (fn _ => (fn _ => find (Array.fromList [1, 3, 4, 6, 8, 9, 11], 0)) |> Expect.error (Fail "value not in array")), + + test "a value larger than the array's largest value is not found" + (fn _ => (fn _ => find (Array.fromList [1, 3, 4, 6, 8, 9, 11], 13)) |> Expect.error (Fail "value not in array")), + + test "nothing is found in an empty array" + (fn _ => (fn _ => find (Array.fromList [], 1)) |> Expect.error (Fail "value not in array")), + + test "nothing is found when the left and right bounds cross" + (fn _ => (fn _ => find (Array.fromList [1, 2], 0)) |> Expect.error (Fail "value not in array")) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/binary-search/testlib.sml b/exercises/practice/binary-search/testlib.sml new file mode 100644 index 00000000..0c8370c0 --- /dev/null +++ b/exercises/practice/binary-search/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) From c4f19326229a8d841bf7b1830d25825aaccdfd76 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Wed, 28 Aug 2024 20:36:02 +1000 Subject: [PATCH 2/2] Skip test for empty array --- exercises/practice/binary-search/.meta/tests.toml | 1 + exercises/practice/binary-search/test.sml | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/exercises/practice/binary-search/.meta/tests.toml b/exercises/practice/binary-search/.meta/tests.toml index 61e2b068..f14108c4 100644 --- a/exercises/practice/binary-search/.meta/tests.toml +++ b/exercises/practice/binary-search/.meta/tests.toml @@ -38,6 +38,7 @@ description = "a value larger than the array's largest value is not found" [f439a0fa-cf42-4262-8ad1-64bf41ce566a] description = "nothing is found in an empty array" +include = false [2c353967-b56d-40b8-acff-ce43115eed64] description = "nothing is found when the left and right bounds cross" diff --git a/exercises/practice/binary-search/test.sml b/exercises/practice/binary-search/test.sml index 129b2b0a..a9f3c349 100644 --- a/exercises/practice/binary-search/test.sml +++ b/exercises/practice/binary-search/test.sml @@ -35,9 +35,6 @@ val testsuite = test "a value larger than the array's largest value is not found" (fn _ => (fn _ => find (Array.fromList [1, 3, 4, 6, 8, 9, 11], 13)) |> Expect.error (Fail "value not in array")), - test "nothing is found in an empty array" - (fn _ => (fn _ => find (Array.fromList [], 1)) |> Expect.error (Fail "value not in array")), - test "nothing is found when the left and right bounds cross" (fn _ => (fn _ => find (Array.fromList [1, 2], 0)) |> Expect.error (Fail "value not in array")) ]