From 13e5387d34ba3f6940e5b942855377369d0478ae Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Sun, 8 Dec 2024 12:19:35 -0500 Subject: [PATCH 1/4] Add circular-buffer exercise --- config.json | 8 + .../circular-buffer/.docs/instructions.md | 58 +++++++ .../circular-buffer/.meta/config.json | 19 +++ .../circular-buffer/.meta/example.sml | 59 +++++++ .../practice/circular-buffer/.meta/tests.toml | 52 ++++++ .../circular-buffer/circular-buffer.sml | 33 ++++ exercises/practice/circular-buffer/test.sml | 129 ++++++++++++++ .../practice/circular-buffer/testlib.sml | 160 ++++++++++++++++++ 8 files changed, 518 insertions(+) create mode 100644 exercises/practice/circular-buffer/.docs/instructions.md create mode 100644 exercises/practice/circular-buffer/.meta/config.json create mode 100644 exercises/practice/circular-buffer/.meta/example.sml create mode 100644 exercises/practice/circular-buffer/.meta/tests.toml create mode 100644 exercises/practice/circular-buffer/circular-buffer.sml create mode 100644 exercises/practice/circular-buffer/test.sml create mode 100644 exercises/practice/circular-buffer/testlib.sml diff --git a/config.json b/config.json index bf4b3ec0..bcdba926 100644 --- a/config.json +++ b/config.json @@ -593,6 +593,14 @@ "prerequisites": [], "difficulty": 6 }, + { + "slug": "circular-buffer", + "name": "Circular Buffer", + "uuid": "6b01cbb4-6a20-4214-83a0-bb1691bbdb10", + "practices": [], + "prerequisites": [], + "difficulty": 8 + }, { "slug": "knapsack", "name": "Knapsack", diff --git a/exercises/practice/circular-buffer/.docs/instructions.md b/exercises/practice/circular-buffer/.docs/instructions.md new file mode 100644 index 00000000..2ba1fda2 --- /dev/null +++ b/exercises/practice/circular-buffer/.docs/instructions.md @@ -0,0 +1,58 @@ +# Instructions + +A circular buffer, cyclic buffer or ring buffer is a data structure that uses a single, fixed-size buffer as if it were connected end-to-end. + +A circular buffer first starts empty and of some predefined length. +For example, this is a 7-element buffer: + +```text +[ ][ ][ ][ ][ ][ ][ ] +``` + +Assume that a 1 is written into the middle of the buffer (exact starting location does not matter in a circular buffer): + +```text +[ ][ ][ ][1][ ][ ][ ] +``` + +Then assume that two more elements are added — 2 & 3 — which get appended after the 1: + +```text +[ ][ ][ ][1][2][3][ ] +``` + +If two elements are then removed from the buffer, the oldest values inside the buffer are removed. +The two elements removed, in this case, are 1 & 2, leaving the buffer with just a 3: + +```text +[ ][ ][ ][ ][ ][3][ ] +``` + +If the buffer has 7 elements then it is completely full: + +```text +[5][6][7][8][9][3][4] +``` + +When the buffer is full an error will be raised, alerting the client that further writes are blocked until a slot becomes free. + +When the buffer is full, the client can opt to overwrite the oldest data with a forced write. +In this case, two more elements — A & B — are added and they overwrite the 3 & 4: + +```text +[5][6][7][8][9][A][B] +``` + +3 & 4 have been replaced by A & B making 5 now the oldest data in the buffer. +Finally, if two elements are removed then what would be returned is 5 & 6 yielding the buffer: + +```text +[ ][ ][7][8][9][A][B] +``` + +Because there is space available, if the client again uses overwrite to store C & D then the space where 5 & 6 were stored previously will be used not the location of 7 & 8. +7 is still the oldest element and the buffer is once again full. + +```text +[C][D][7][8][9][A][B] +``` diff --git a/exercises/practice/circular-buffer/.meta/config.json b/exercises/practice/circular-buffer/.meta/config.json new file mode 100644 index 00000000..f2cf3a5e --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "circular-buffer.sml" + ], + "test": [ + "test.sml" + ], + "example": [ + ".meta/example.sml" + ] + }, + "blurb": "A data structure that uses a single, fixed-size buffer as if it were connected end-to-end.", + "source": "Wikipedia", + "source_url": "https://en.wikipedia.org/wiki/Circular_buffer" +} diff --git a/exercises/practice/circular-buffer/.meta/example.sml b/exercises/practice/circular-buffer/.meta/example.sml new file mode 100644 index 00000000..30d26234 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/example.sml @@ -0,0 +1,59 @@ +structure CircularBuffer :> sig + type buffer + + exception BufferFull + exception BufferEmpty + + val create : int -> buffer + val read : buffer -> int + val write : buffer -> int -> unit + val clear : buffer -> unit + val overwrite : buffer -> int -> unit +end = struct + type buffer = { + data : int Array.array, + size : int, + count : int ref, + readPtr : int ref, + writePtr : int ref + } + + exception BufferFull + exception BufferEmpty + + fun create size = { + data = Array.array (size, 0), + size = size, + count = ref 0, + readPtr = ref 0, + writePtr = ref 0 + } + + fun read buff = + if !(#count buff) = 0 + then raise BufferEmpty + else let val value = Array.sub (#data buff, !(#readPtr buff)) + in #readPtr buff := (!(#readPtr buff) + 1) mod #size buff; + #count buff := !(#count buff) - 1; + value + end + + fun write buff value = + if !(#count buff) = #size buff + then raise BufferFull + else ( Array.update (#data buff, !(#writePtr buff), value); + #writePtr buff := (!(#writePtr buff) + 1) mod #size buff; + #count buff := !(#count buff) + 1 ) + + fun clear buff = + ( #writePtr buff := !(#readPtr buff); + #count buff := 0 ) + + fun overwrite buff value = + let val _ = if !(#count buff) = #size buff + then read buff + else 0 + in write buff value + end +end + diff --git a/exercises/practice/circular-buffer/.meta/tests.toml b/exercises/practice/circular-buffer/.meta/tests.toml new file mode 100644 index 00000000..0fb3143d --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/tests.toml @@ -0,0 +1,52 @@ +# 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. + +[28268ed4-4ff3-45f3-820e-895b44d53dfa] +description = "reading empty buffer should fail" + +[2e6db04a-58a1-425d-ade8-ac30b5f318f3] +description = "can read an item just written" + +[90741fe8-a448-45ce-be2b-de009a24c144] +description = "each item may only be read once" + +[be0e62d5-da9c-47a8-b037-5db21827baa7] +description = "items are read in the order they are written" + +[2af22046-3e44-4235-bfe6-05ba60439d38] +description = "full buffer can't be written to" + +[547d192c-bbf0-4369-b8fa-fc37e71f2393] +description = "a read frees up capacity for another write" + +[04a56659-3a81-4113-816b-6ecb659b4471] +description = "read position is maintained even across multiple writes" + +[60c3a19a-81a7-43d7-bb0a-f07242b1111f] +description = "items cleared out of buffer can't be read" + +[45f3ae89-3470-49f3-b50e-362e4b330a59] +description = "clear frees up capacity for another write" + +[e1ac5170-a026-4725-bfbe-0cf332eddecd] +description = "clear does nothing on empty buffer" + +[9c2d4f26-3ec7-453f-a895-7e7ff8ae7b5b] +description = "overwrite acts like write on non-full buffer" + +[880f916b-5039-475c-bd5c-83463c36a147] +description = "overwrite replaces the oldest item on full buffer" + +[bfecab5b-aca1-4fab-a2b0-cd4af2b053c3] +description = "overwrite replaces the oldest item remaining in buffer following a read" + +[9cebe63a-c405-437b-8b62-e3fdc1ecec5a] +description = "initial clear does not affect wrapping around" diff --git a/exercises/practice/circular-buffer/circular-buffer.sml b/exercises/practice/circular-buffer/circular-buffer.sml new file mode 100644 index 00000000..4e56f724 --- /dev/null +++ b/exercises/practice/circular-buffer/circular-buffer.sml @@ -0,0 +1,33 @@ +structure CircularBuffer :> sig + type buffer + + exception BufferFull + exception BufferEmpty + + val create : int -> buffer + val read : buffer -> int + val write : buffer -> int -> unit + val clear : buffer -> unit + val overwrite : buffer -> int -> unit +end = struct + + type buffer = ??? (* TODO declare the data structure *) + + exception BufferFull + exception BufferEmpty + + fun create size = + raise Fail "'create' is not implemented" + + fun read buff = + raise Fail "'read' is not implemented" + + fun write buff value = + raise Fail "'write' is not implemented" + + fun clear buff = + raise Fail "'clear' is not implemented" + + fun overwrite buff value = + raise Fail "'overwrite' is not implemented" +end diff --git a/exercises/practice/circular-buffer/test.sml b/exercises/practice/circular-buffer/test.sml new file mode 100644 index 00000000..14499c02 --- /dev/null +++ b/exercises/practice/circular-buffer/test.sml @@ -0,0 +1,129 @@ +use "circular-buffer.sml"; +use "testlib.sml"; + +infixr |> +fun x |> f = f x + +val testsuite = + describe "circular-buffer" [ + test "reading empty buffer should fail" + (fn _ => (fn _ => let val buf = CircularBuffer.create 1 + in CircularBuffer.read buf + end + ) |> Expect.error CircularBuffer.BufferEmpty), + + test "can read an item just written" + (fn _ => let val buf = CircularBuffer.create 1 + in CircularBuffer.write buf 100; + CircularBuffer.read buf |> Expect.equalTo 100 + end), + + test "each item may only be read once" + (fn _ => (fn _ => let val buf = CircularBuffer.create 1 + in CircularBuffer.write buf 100; + CircularBuffer.read buf |> Expect.equalTo 100; + CircularBuffer.read buf + end + ) |> Expect.error CircularBuffer.BufferEmpty), + + test "items are read in the order they are written" + (fn _ => let val buf = CircularBuffer.create 2 + in CircularBuffer.write buf 100; + CircularBuffer.write buf 200; + CircularBuffer.read buf |> Expect.equalTo 100; + CircularBuffer.read buf |> Expect.equalTo 200 + end), + + test "full buffer can't be written to" + (fn _ => (fn _ => let val buf = CircularBuffer.create 1 + in CircularBuffer.write buf 100; + CircularBuffer.write buf 200 + end + ) |> Expect.error CircularBuffer.BufferFull), + + test "a read frees up capacity for another write" + (fn _ => let val buf = CircularBuffer.create 1 + in CircularBuffer.write buf 100; + CircularBuffer.read buf |> Expect.equalTo 100; + CircularBuffer.write buf 200; + CircularBuffer.read buf |> Expect.equalTo 200 + end), + + test "read position is maintained even across multiple writes" + (fn _ => let val buf = CircularBuffer.create 3 + in CircularBuffer.write buf 100; + CircularBuffer.write buf 200; + CircularBuffer.read buf |> Expect.equalTo 100; + CircularBuffer.write buf 300; + CircularBuffer.read buf |> Expect.equalTo 200; + CircularBuffer.read buf |> Expect.equalTo 300 + end), + + test "items cleared out of buffer can't be read" + (fn _ => (fn _ => let val buf = CircularBuffer.create 1 + in CircularBuffer.write buf 100; + CircularBuffer.clear buf; + CircularBuffer.read buf + end + ) |> Expect.error CircularBuffer.BufferEmpty), + + test "clear frees up capacity for another write" + (fn _ => let val buf = CircularBuffer.create 1 + in CircularBuffer.write buf 100; + CircularBuffer.clear buf; + CircularBuffer.write buf 200; + CircularBuffer.read buf |> Expect.equalTo 200 + end), + + test "clear does nothing on empty buffer" + (fn _ => let val buf = CircularBuffer.create 1 + in CircularBuffer.clear buf; + CircularBuffer.write buf 100; + CircularBuffer.read buf |> Expect.equalTo 100 + end), + + test "overwrite acts like write on non-full buffer" + (fn _ => let val buf = CircularBuffer.create 2 + in CircularBuffer.write buf 100; + CircularBuffer.overwrite buf 200; + CircularBuffer.read buf |> Expect.equalTo 100; + CircularBuffer.read buf |> Expect.equalTo 200 + end), + + test "overwrite replaces the oldest item on full buffer" + (fn _ => let val buf = CircularBuffer.create 2 + in CircularBuffer.write buf 100; + CircularBuffer.write buf 200; + CircularBuffer.overwrite buf 300; + CircularBuffer.read buf |> Expect.equalTo 200; + CircularBuffer.read buf |> Expect.equalTo 300 + end), + + test "overwrite replaces the oldest item remaining in buffer following a read" + (fn _ => let val buf = CircularBuffer.create 3 + in CircularBuffer.write buf 100; + CircularBuffer.write buf 200; + CircularBuffer.write buf 300; + CircularBuffer.read buf |> Expect.equalTo 100; + CircularBuffer.write buf 400; + CircularBuffer.overwrite buf 500; + CircularBuffer.read buf |> Expect.equalTo 300; + CircularBuffer.read buf |> Expect.equalTo 400; + CircularBuffer.read buf |> Expect.equalTo 500 + end), + + test "initial clear does not affect wrapping around" + (fn _ => (fn _ => let val buf = CircularBuffer.create 2 + in CircularBuffer.clear buf; + CircularBuffer.write buf 100; + CircularBuffer.write buf 200; + CircularBuffer.overwrite buf 300; + CircularBuffer.overwrite buf 400; + CircularBuffer.read buf |> Expect.equalTo 300; + CircularBuffer.read buf |> Expect.equalTo 400; + CircularBuffer.read buf + end + ) |> Expect.error CircularBuffer.BufferEmpty) + ] + +val _ = Test.run testsuite diff --git a/exercises/practice/circular-buffer/testlib.sml b/exercises/practice/circular-buffer/testlib.sml new file mode 100644 index 00000000..0c8370c0 --- /dev/null +++ b/exercises/practice/circular-buffer/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 68498061fa3a3c0ee831382ff9dd45518bdb45a8 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Sun, 8 Dec 2024 12:24:08 -0500 Subject: [PATCH 2/4] fix the syntax error in the stub --- exercises/practice/circular-buffer/circular-buffer.sml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/circular-buffer/circular-buffer.sml b/exercises/practice/circular-buffer/circular-buffer.sml index 4e56f724..9cc4e0d1 100644 --- a/exercises/practice/circular-buffer/circular-buffer.sml +++ b/exercises/practice/circular-buffer/circular-buffer.sml @@ -11,7 +11,7 @@ structure CircularBuffer :> sig val overwrite : buffer -> int -> unit end = struct - type buffer = ??? (* TODO declare the data structure *) + type buffer = () (* TODO declare the data structure *) exception BufferFull exception BufferEmpty From 62db691b7c8d1607c495ea1bb3e1be2c76532396 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Sun, 8 Dec 2024 12:24:08 -0500 Subject: [PATCH 3/4] fix the syntax error in the stub --- exercises/practice/circular-buffer/circular-buffer.sml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/circular-buffer/circular-buffer.sml b/exercises/practice/circular-buffer/circular-buffer.sml index 9cc4e0d1..d4cdc12f 100644 --- a/exercises/practice/circular-buffer/circular-buffer.sml +++ b/exercises/practice/circular-buffer/circular-buffer.sml @@ -11,7 +11,7 @@ structure CircularBuffer :> sig val overwrite : buffer -> int -> unit end = struct - type buffer = () (* TODO declare the data structure *) + type buffer = unit (* TODO declare the data structure *) exception BufferFull exception BufferEmpty From e14ad01970597c39a65b4dd947a4a693dad97f79 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Sun, 8 Dec 2024 12:29:18 -0500 Subject: [PATCH 4/4] "define" not "declare" --- exercises/practice/circular-buffer/circular-buffer.sml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/circular-buffer/circular-buffer.sml b/exercises/practice/circular-buffer/circular-buffer.sml index d4cdc12f..cf9153e5 100644 --- a/exercises/practice/circular-buffer/circular-buffer.sml +++ b/exercises/practice/circular-buffer/circular-buffer.sml @@ -11,7 +11,7 @@ structure CircularBuffer :> sig val overwrite : buffer -> int -> unit end = struct - type buffer = unit (* TODO declare the data structure *) + type buffer = unit (* TODO define the data structure *) exception BufferFull exception BufferEmpty