From 92f0f77c47199b0e28ac2c2dfe40ad1ad0549297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Geron?= Date: Tue, 17 Sep 2024 07:39:32 +1200 Subject: [PATCH] Add circular-buffer exercise (#95) --- config.json | 8 + .../circular-buffer/.docs/instructions.md | 58 ++++ .../circular-buffer/.meta/Example.roc | 41 +++ .../circular-buffer/.meta/config.json | 19 ++ .../circular-buffer/.meta/template.j2 | 46 +++ .../practice/circular-buffer/.meta/tests.toml | 52 ++++ .../circular-buffer/CircularBuffer.roc | 23 ++ .../circular-buffer/circular-buffer-test.roc | 288 ++++++++++++++++++ 8 files changed, 535 insertions(+) create mode 100644 exercises/practice/circular-buffer/.docs/instructions.md create mode 100644 exercises/practice/circular-buffer/.meta/Example.roc create mode 100644 exercises/practice/circular-buffer/.meta/config.json create mode 100644 exercises/practice/circular-buffer/.meta/template.j2 create mode 100644 exercises/practice/circular-buffer/.meta/tests.toml create mode 100644 exercises/practice/circular-buffer/CircularBuffer.roc create mode 100644 exercises/practice/circular-buffer/circular-buffer-test.roc diff --git a/config.json b/config.json index 26423a4..20d7da5 100644 --- a/config.json +++ b/config.json @@ -359,6 +359,14 @@ "prerequisites": [], "difficulty": 5 }, + { + "slug": "circular-buffer", + "name": "Circular Buffer", + "uuid": "521c505e-edb6-4a9c-82e8-44eed527e9ff", + "practices": [], + "prerequisites": [], + "difficulty": 5 + }, { "slug": "flatten-array", "name": "Flatten Array", diff --git a/exercises/practice/circular-buffer/.docs/instructions.md b/exercises/practice/circular-buffer/.docs/instructions.md new file mode 100644 index 0000000..2ba1fda --- /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/Example.roc b/exercises/practice/circular-buffer/.meta/Example.roc new file mode 100644 index 0000000..ea5b7a7 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/Example.roc @@ -0,0 +1,41 @@ +module [create, read, write, overwrite, clear] + +CircularBuffer : { data : List I64, start : U64, length : U64 } + +create : { capacity : U64 } -> CircularBuffer +create = \{ capacity } -> + { data: List.repeat 0 capacity, start: 0, length: 0 } + +read : CircularBuffer -> Result { newBuffer : CircularBuffer, value : I64 } [BufferEmpty] +read = \{ data, start, length } -> + if length == 0 then + Err BufferEmpty + else + incrementStart = (start + 1) % List.len data + newBuffer = { data, start: incrementStart, length: length - 1 } + when data |> List.get start is + Ok value -> Ok { newBuffer, value } + Err OutOfBounds -> crash "Unreachable: start should never be out of bounds" + +write : CircularBuffer, I64 -> Result CircularBuffer [BufferFull] +write = \{ data, start, length }, value -> + if length == List.len data then + Err BufferFull + else + index = (start + length) % List.len data + newData = data |> List.replace index value |> .list + Ok { data: newData, start, length: length + 1 } + +overwrite : CircularBuffer, I64 -> CircularBuffer +overwrite = \{ data, start, length }, value -> + index = (start + length) % List.len data + newData = data |> List.replace index value |> .list + if length == List.len data then + incStart = (start + 1) % List.len data + { data: newData, start: incStart, length: length } + else + { data: newData, start, length: length + 1 } + +clear : CircularBuffer -> CircularBuffer +clear = \circularBuffer -> + { data: circularBuffer.data, start: 0, length: 0 } diff --git a/exercises/practice/circular-buffer/.meta/config.json b/exercises/practice/circular-buffer/.meta/config.json new file mode 100644 index 0000000..39ba9a7 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "ageron" + ], + "files": { + "solution": [ + "CircularBuffer.roc" + ], + "test": [ + "circular-buffer-test.roc" + ], + "example": [ + ".meta/Example.roc" + ] + }, + "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/template.j2 b/exercises/practice/circular-buffer/.meta/template.j2 new file mode 100644 index 0000000..af554c1 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/template.j2 @@ -0,0 +1,46 @@ +{%- import "generator_macros.j2" as macros with context -%} +{{ macros.canonical_ref() }} +{{ macros.header() }} + +import {{ exercise | to_pascal }} exposing [create, read, write, overwrite, clear] + +{% for case in cases -%} +# {{ case["description"] }} +runOperations{{ loop.index }} = + result = + create { capacity: {{ case["input"]["capacity"] }} } + {%- for op in case["input"]["operations"] -%} + {%- if op["operation"] == "clear" %} + |> clear + {%- elif op["operation"] == "overwrite" %} + |> overwrite {{ op["item"] }} + {%- elif op["operation"] == "write" %} + {%- if op["should_succeed"] %} + |> write? {{ op["item"] }} + {%- else %} + |> \bufferBeforeWrite -> + writeResult = bufferBeforeWrite |> write {{ op["item"] }} + expect writeResult == Err BufferFull + bufferBeforeWrite + {%- endif %} + {%- elif op["operation"] == "read" %} + {%- if op["should_succeed"] %} + |> read? |> \readResult -> + expect readResult.value == {{ op["expected"] }} + readResult.newBuffer + {%- else %} + |> \bufferBeforeRead -> + readResult = bufferBeforeRead |> read + expect readResult == Err BufferEmpty + bufferBeforeRead + {%- endif %} + {%- endif %} + {%- endfor %} + Ok result + +expect + + result = runOperations{{ loop.index }} + result |> Result.isOk + +{% endfor %} diff --git a/exercises/practice/circular-buffer/.meta/tests.toml b/exercises/practice/circular-buffer/.meta/tests.toml new file mode 100644 index 0000000..0fb3143 --- /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/CircularBuffer.roc b/exercises/practice/circular-buffer/CircularBuffer.roc new file mode 100644 index 0000000..668f047 --- /dev/null +++ b/exercises/practice/circular-buffer/CircularBuffer.roc @@ -0,0 +1,23 @@ +module [create, read, write, overwrite, clear] + +CircularBuffer : { data : List I64, start : U64, length : U64 } + +create : { capacity : U64 } -> CircularBuffer +create = \{ capacity } -> + crash "Please implement the 'create' function" + +read : CircularBuffer -> Result { newBuffer : CircularBuffer, value : I64 } [BufferEmpty] +read = \{ data, start, length } -> + crash "Please implement the 'read' function" + +write : CircularBuffer, I64 -> Result CircularBuffer [BufferFull] +write = \circularBuffer, value -> + crash "Please implement the 'write' function" + +overwrite : CircularBuffer, I64 -> CircularBuffer +overwrite = \{ data, start, length }, value -> + crash "Please implement the 'overwrite' function" + +clear : CircularBuffer -> CircularBuffer +clear = \circularBuffer -> + crash "Please implement the 'clear' function" diff --git a/exercises/practice/circular-buffer/circular-buffer-test.roc b/exercises/practice/circular-buffer/circular-buffer-test.roc new file mode 100644 index 0000000..38f6713 --- /dev/null +++ b/exercises/practice/circular-buffer/circular-buffer-test.roc @@ -0,0 +1,288 @@ +# These tests are auto-generated with test data from: +# https://github.com/exercism/problem-specifications/tree/main/exercises/circular-buffer/canonical-data.json +# File last updated on 2024-09-15 +app [main] { + pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.15.0/SlwdbJ-3GR7uBWQo6zlmYWNYOxnvo8r6YABXD-45UOw.tar.br", +} + +main = + Task.ok {} + +import CircularBuffer exposing [create, read, write, overwrite, clear] + +# reading empty buffer should fail +runOperations1 = + result = + create { capacity: 1 } + |> \bufferBeforeRead -> + readResult = bufferBeforeRead |> read + expect readResult == Err BufferEmpty + bufferBeforeRead + Ok result + +expect + result = runOperations1 + result |> Result.isOk + +# can read an item just written +runOperations2 = + result = + create { capacity: 1 } + |> write? 1 + |> read? + |> \readResult -> + expect readResult.value == 1 + readResult.newBuffer + Ok result + +expect + result = runOperations2 + result |> Result.isOk + +# each item may only be read once +runOperations3 = + result = + create { capacity: 1 } + |> write? 1 + |> read? + |> \readResult -> + expect readResult.value == 1 + readResult.newBuffer + |> \bufferBeforeRead -> + readResult = bufferBeforeRead |> read + expect readResult == Err BufferEmpty + bufferBeforeRead + Ok result + +expect + result = runOperations3 + result |> Result.isOk + +# items are read in the order they are written +runOperations4 = + result = + create { capacity: 2 } + |> write? 1 + |> write? 2 + |> read? + |> \readResult -> + expect readResult.value == 1 + readResult.newBuffer + |> read? + |> \readResult -> + expect readResult.value == 2 + readResult.newBuffer + Ok result + +expect + result = runOperations4 + result |> Result.isOk + +# full buffer can't be written to +runOperations5 = + result = + create { capacity: 1 } + |> write? 1 + |> \bufferBeforeWrite -> + writeResult = bufferBeforeWrite |> write 2 + expect writeResult == Err BufferFull + bufferBeforeWrite + Ok result + +expect + result = runOperations5 + result |> Result.isOk + +# a read frees up capacity for another write +runOperations6 = + result = + create { capacity: 1 } + |> write? 1 + |> read? + |> \readResult -> + expect readResult.value == 1 + readResult.newBuffer + |> write? 2 + |> read? + |> \readResult -> + expect readResult.value == 2 + readResult.newBuffer + Ok result + +expect + result = runOperations6 + result |> Result.isOk + +# read position is maintained even across multiple writes +runOperations7 = + result = + create { capacity: 3 } + |> write? 1 + |> write? 2 + |> read? + |> \readResult -> + expect readResult.value == 1 + readResult.newBuffer + |> write? 3 + |> read? + |> \readResult -> + expect readResult.value == 2 + readResult.newBuffer + |> read? + |> \readResult -> + expect readResult.value == 3 + readResult.newBuffer + Ok result + +expect + result = runOperations7 + result |> Result.isOk + +# items cleared out of buffer can't be read +runOperations8 = + result = + create { capacity: 1 } + |> write? 1 + |> clear + |> \bufferBeforeRead -> + readResult = bufferBeforeRead |> read + expect readResult == Err BufferEmpty + bufferBeforeRead + Ok result + +expect + result = runOperations8 + result |> Result.isOk + +# clear frees up capacity for another write +runOperations9 = + result = + create { capacity: 1 } + |> write? 1 + |> clear + |> write? 2 + |> read? + |> \readResult -> + expect readResult.value == 2 + readResult.newBuffer + Ok result + +expect + result = runOperations9 + result |> Result.isOk + +# clear does nothing on empty buffer +runOperations10 = + result = + create { capacity: 1 } + |> clear + |> write? 1 + |> read? + |> \readResult -> + expect readResult.value == 1 + readResult.newBuffer + Ok result + +expect + result = runOperations10 + result |> Result.isOk + +# overwrite acts like write on non-full buffer +runOperations11 = + result = + create { capacity: 2 } + |> write? 1 + |> overwrite 2 + |> read? + |> \readResult -> + expect readResult.value == 1 + readResult.newBuffer + |> read? + |> \readResult -> + expect readResult.value == 2 + readResult.newBuffer + Ok result + +expect + result = runOperations11 + result |> Result.isOk + +# overwrite replaces the oldest item on full buffer +runOperations12 = + result = + create { capacity: 2 } + |> write? 1 + |> write? 2 + |> overwrite 3 + |> read? + |> \readResult -> + expect readResult.value == 2 + readResult.newBuffer + |> read? + |> \readResult -> + expect readResult.value == 3 + readResult.newBuffer + Ok result + +expect + result = runOperations12 + result |> Result.isOk + +# overwrite replaces the oldest item remaining in buffer following a read +runOperations13 = + result = + create { capacity: 3 } + |> write? 1 + |> write? 2 + |> write? 3 + |> read? + |> \readResult -> + expect readResult.value == 1 + readResult.newBuffer + |> write? 4 + |> overwrite 5 + |> read? + |> \readResult -> + expect readResult.value == 3 + readResult.newBuffer + |> read? + |> \readResult -> + expect readResult.value == 4 + readResult.newBuffer + |> read? + |> \readResult -> + expect readResult.value == 5 + readResult.newBuffer + Ok result + +expect + result = runOperations13 + result |> Result.isOk + +# initial clear does not affect wrapping around +runOperations14 = + result = + create { capacity: 2 } + |> clear + |> write? 1 + |> write? 2 + |> overwrite 3 + |> overwrite 4 + |> read? + |> \readResult -> + expect readResult.value == 3 + readResult.newBuffer + |> read? + |> \readResult -> + expect readResult.value == 4 + readResult.newBuffer + |> \bufferBeforeRead -> + readResult = bufferBeforeRead |> read + expect readResult == Err BufferEmpty + bufferBeforeRead + Ok result + +expect + result = runOperations14 + result |> Result.isOk +