diff --git a/config.json b/config.json index 25db6a1a..a42ba39a 100644 --- a/config.json +++ b/config.json @@ -742,6 +742,15 @@ "prerequisites": [], "difficulty": 1, "topics": [] + }, + { + "slug": "circular-buffer", + "name": "Circular Buffer", + "uuid": "2e709c96-56e3-4a48-8948-e180b210e86d", + "practices": [], + "prerequisites": [], + "difficulty": 1, + "topics": [] } ] }, 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..31e13210 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/config.json @@ -0,0 +1,22 @@ +{ + "authors": [ + "m-dango" + ], + "contributors": [ + "rcmlz" + ], + "files": { + "solution": [ + "lib/CircularBuffer.rakumod" + ], + "test": [ + "t/circular-buffer.rakutest" + ], + "example": [ + ".meta/solutions/lib/CircularBuffer.rakumod" + ] + }, + "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/solutions/lib/CircularBuffer.rakumod b/exercises/practice/circular-buffer/.meta/solutions/lib/CircularBuffer.rakumod new file mode 100644 index 00000000..9bb93662 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/solutions/lib/CircularBuffer.rakumod @@ -0,0 +1,74 @@ +my class X::CircularBuffer::BufferIsEmpty is Exception { + method message {'Buffer is empty'} +} + +my class X::CircularBuffer::BufferIsFull is Exception { + method message {'Buffer is full'} +} + +role Circular-Buffer-Interface is export { + has UInt $.capacity is required; + multi method clear( --> Bool) {...} + multi method read( --> Any ) {...} + multi method read(UInt $count --> Seq ) {...} + multi method write(**@items --> UInt) {...} + multi method overwrite(**@items --> UInt) {...} +} + +class CircularBuffer does Circular-Buffer-Interface is export { + + has UInt $.elems = 0; + has UInt $!read-pointer = 0; + has UInt $!write-pointer = 0; + has @!buffer = []; + + multi method clear( --> Bool) { + $!elems = 0; + $!read-pointer = 0; + $!write-pointer = 0; + @!buffer = []; + True + } + + method !is-empty( --> Bool) { $!elems == 0 } + method !is-full( --> Bool) { $!elems == $!capacity } + method !has-space-for( UInt $count, --> Bool) { $count <= ($!capacity - $!elems) } + method !tic( $pointer is rw, --> UInt) { $pointer = ($pointer + 1) % $!capacity } + + multi method read(--> Any) { self!get-item } + multi method read(UInt $count --> Seq){ + PRE { $count <= $!elems } + gather { take self!get-item for ^$count } + } + multi method write { 0 } + multi method write(**@items --> UInt){ + PRE { +@items and self!has-space-for(+@items) or X::CircularBuffer::BufferIsFull.new.throw } + + self!write-item($_) for @items; + +@items + } + multi method overwrite(**@items --> UInt){ + PRE { +@items } + + for @items -> $item { + self!tic($!read-pointer) if self!is-full; + self!write-item( $item ) + } + +@items + } + method !get-item { + PRE { not self!is-empty or X::CircularBuffer::BufferIsEmpty.new.throw } + + LEAVE { $!elems-- unless self!is-empty; + @!buffer[ $!read-pointer ] = Nil; + self!tic($!read-pointer) + } + @!buffer[ $!read-pointer ] + } + method !write-item($item) { + LEAVE { $!elems++ unless self!is-full; + self!tic($!write-pointer) + } + @!buffer[ $!write-pointer ] = $item.clone # clone: slow but prevents reference capturing/leaking! + } +} diff --git a/exercises/practice/circular-buffer/.meta/solutions/t/circular-buffer.rakutest b/exercises/practice/circular-buffer/.meta/solutions/t/circular-buffer.rakutest new file mode 120000 index 00000000..c70be607 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/solutions/t/circular-buffer.rakutest @@ -0,0 +1 @@ +../../../t/circular-buffer.rakutest \ No newline at end of file diff --git a/exercises/practice/circular-buffer/.meta/template-data.yaml b/exercises/practice/circular-buffer/.meta/template-data.yaml new file mode 100644 index 00000000..71976419 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/template-data.yaml @@ -0,0 +1,173 @@ +properties: + run: + test: |- + my $ops = %case.map(-> %op { + given %op { + when 'read' { + if %op { + sprintf(q:to/CASE/, %op).trim-trailing; + cmp-ok( + $buffer.read, + "==", + %s, + "read buffer", + ); + CASE + } + else { + sprintf(q:to/CASE/).trim-trailing; + throws-like( + { $buffer.read }, + X::CircularBuffer::BufferIsEmpty, + "read error", + ); + CASE + } + } + when 'write' { + if %op { + sprintf(q:to/CASE/, %op).trim-trailing; + lives-ok( + { $buffer.write(%s) }, + "write buffer", + ); + CASE + } + else { + sprintf(q:to/CASE/, %op).trim-trailing; + throws-like( + { $buffer.write(%s) }, + X::CircularBuffer::BufferIsFull, + "write error", + ); + CASE + } + } + when 'clear' { + sprintf(q:to/CASE/).trim-trailing; + lives-ok( + { $buffer.clear }, + "clear buffer", + ); + CASE + } + when 'overwrite' { + sprintf(q:to/CASE/, %op).trim-trailing; + lives-ok( + { $buffer.overwrite(%s) }, + "overwrite buffer", + ); + CASE + } + default { + ' flunk "NYI";' + } + } + }).join("\n\n"); + + sprintf(q:to/END/, %case.raku, %case, $ops); + subtest %s => { + my $buffer := CircularBuffer.new( :capacity(%s) ); + + %s + }; + END + +unit: false + +example: |- + my class X::CircularBuffer::BufferIsEmpty is Exception { + method message {'Buffer is empty'} + } + + my class X::CircularBuffer::BufferIsFull is Exception { + method message {'Buffer is full'} + } + + role Circular-Buffer-Interface is export { + has UInt $.capacity is required; + multi method clear( --> Bool) {...} + multi method read( --> Any ) {...} + multi method read(UInt $count --> Seq ) {...} + multi method write(**@items --> UInt) {...} + multi method overwrite(**@items --> UInt) {...} + } + + class CircularBuffer does Circular-Buffer-Interface is export { + + has UInt $.elems = 0; + has UInt $!read-pointer = 0; + has UInt $!write-pointer = 0; + has @!buffer = []; + + multi method clear( --> Bool) { + $!elems = 0; + $!read-pointer = 0; + $!write-pointer = 0; + @!buffer = []; + True + } + + method !is-empty( --> Bool) { $!elems == 0 } + method !is-full( --> Bool) { $!elems == $!capacity } + method !has-space-for( UInt $count, --> Bool) { $count <= ($!capacity - $!elems) } + method !tic( $pointer is rw, --> UInt) { $pointer = ($pointer + 1) % $!capacity } + + multi method read(--> Any) { self!get-item } + multi method read(UInt $count --> Seq){ + PRE { $count <= $!elems } + gather { take self!get-item for ^$count } + } + multi method write { 0 } + multi method write(**@items --> UInt){ + PRE { +@items and self!has-space-for(+@items) or X::CircularBuffer::BufferIsFull.new.throw } + + self!write-item($_) for @items; + +@items + } + multi method overwrite(**@items --> UInt){ + PRE { +@items } + + for @items -> $item { + self!tic($!read-pointer) if self!is-full; + self!write-item( $item ) + } + +@items + } + method !get-item { + PRE { not self!is-empty or X::CircularBuffer::BufferIsEmpty.new.throw } + + LEAVE { $!elems-- unless self!is-empty; + @!buffer[ $!read-pointer ] = Nil; + self!tic($!read-pointer) + } + @!buffer[ $!read-pointer ] + } + method !write-item($item) { + LEAVE { $!elems++ unless self!is-full; + self!tic($!write-pointer) + } + @!buffer[ $!write-pointer ] = $item.clone # clone: slow but prevents reference capturing/leaking! + } + } + +stub: |- + my class X::CircularBuffer::BufferIsEmpty is Exception { + method message {'Buffer is empty'} + } + + my class X::CircularBuffer::BufferIsFull is Exception { + method message {'Buffer is full'} + } + + class CircularBuffer { + has $.capacity; + + method read () {} + + method write ($item) {} + + method clear () {} + + method overwrite ($item) {} + } 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/lib/CircularBuffer.rakumod b/exercises/practice/circular-buffer/lib/CircularBuffer.rakumod new file mode 100644 index 00000000..6d2a635c --- /dev/null +++ b/exercises/practice/circular-buffer/lib/CircularBuffer.rakumod @@ -0,0 +1,19 @@ +my class X::CircularBuffer::BufferIsEmpty is Exception { + method message {'Buffer is empty'} +} + +my class X::CircularBuffer::BufferIsFull is Exception { + method message {'Buffer is full'} +} + +class CircularBuffer { + has $.capacity; + + method read () {} + + method write ($item) {} + + method clear () {} + + method overwrite ($item) {} +} diff --git a/exercises/practice/circular-buffer/t/circular-buffer.rakutest b/exercises/practice/circular-buffer/t/circular-buffer.rakutest new file mode 100755 index 00000000..a9377e36 --- /dev/null +++ b/exercises/practice/circular-buffer/t/circular-buffer.rakutest @@ -0,0 +1,399 @@ +#!/usr/bin/env raku +use Test; +use lib $?FILE.IO.parent(2).add('lib'); +use CircularBuffer; + +subtest "reading empty buffer should fail" => { # begin: 28268ed4-4ff3-45f3-820e-895b44d53dfa + my $buffer := CircularBuffer.new( :capacity(1) ); + + throws-like( + { $buffer.read }, + X::CircularBuffer::BufferIsEmpty, + "read error", + ); +}; # end: 28268ed4-4ff3-45f3-820e-895b44d53dfa + +subtest "can read an item just written" => { # begin: 2e6db04a-58a1-425d-ade8-ac30b5f318f3 + my $buffer := CircularBuffer.new( :capacity(1) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 1, + "read buffer", + ); +}; # end: 2e6db04a-58a1-425d-ade8-ac30b5f318f3 + +subtest "each item may only be read once" => { # begin: 90741fe8-a448-45ce-be2b-de009a24c144 + my $buffer := CircularBuffer.new( :capacity(1) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 1, + "read buffer", + ); + + throws-like( + { $buffer.read }, + X::CircularBuffer::BufferIsEmpty, + "read error", + ); +}; # end: 90741fe8-a448-45ce-be2b-de009a24c144 + +subtest "items are read in the order they are written" => { # begin: be0e62d5-da9c-47a8-b037-5db21827baa7 + my $buffer := CircularBuffer.new( :capacity(2) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + lives-ok( + { $buffer.write(2) }, + "write buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 1, + "read buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 2, + "read buffer", + ); +}; # end: be0e62d5-da9c-47a8-b037-5db21827baa7 + +subtest "full buffer can't be written to" => { # begin: 2af22046-3e44-4235-bfe6-05ba60439d38 + my $buffer := CircularBuffer.new( :capacity(1) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + throws-like( + { $buffer.write(2) }, + X::CircularBuffer::BufferIsFull, + "write error", + ); +}; # end: 2af22046-3e44-4235-bfe6-05ba60439d38 + +subtest "a read frees up capacity for another write" => { # begin: 547d192c-bbf0-4369-b8fa-fc37e71f2393 + my $buffer := CircularBuffer.new( :capacity(1) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 1, + "read buffer", + ); + + lives-ok( + { $buffer.write(2) }, + "write buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 2, + "read buffer", + ); +}; # end: 547d192c-bbf0-4369-b8fa-fc37e71f2393 + +subtest "read position is maintained even across multiple writes" => { # begin: 04a56659-3a81-4113-816b-6ecb659b4471 + my $buffer := CircularBuffer.new( :capacity(3) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + lives-ok( + { $buffer.write(2) }, + "write buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 1, + "read buffer", + ); + + lives-ok( + { $buffer.write(3) }, + "write buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 2, + "read buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 3, + "read buffer", + ); +}; # end: 04a56659-3a81-4113-816b-6ecb659b4471 + +subtest "items cleared out of buffer can't be read" => { # begin: 60c3a19a-81a7-43d7-bb0a-f07242b1111f + my $buffer := CircularBuffer.new( :capacity(1) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + lives-ok( + { $buffer.clear }, + "clear buffer", + ); + + throws-like( + { $buffer.read }, + X::CircularBuffer::BufferIsEmpty, + "read error", + ); +}; # end: 60c3a19a-81a7-43d7-bb0a-f07242b1111f + +subtest "clear frees up capacity for another write" => { # begin: 45f3ae89-3470-49f3-b50e-362e4b330a59 + my $buffer := CircularBuffer.new( :capacity(1) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + lives-ok( + { $buffer.clear }, + "clear buffer", + ); + + lives-ok( + { $buffer.write(2) }, + "write buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 2, + "read buffer", + ); +}; # end: 45f3ae89-3470-49f3-b50e-362e4b330a59 + +subtest "clear does nothing on empty buffer" => { # begin: e1ac5170-a026-4725-bfbe-0cf332eddecd + my $buffer := CircularBuffer.new( :capacity(1) ); + + lives-ok( + { $buffer.clear }, + "clear buffer", + ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 1, + "read buffer", + ); +}; # end: e1ac5170-a026-4725-bfbe-0cf332eddecd + +subtest "overwrite acts like write on non-full buffer" => { # begin: 9c2d4f26-3ec7-453f-a895-7e7ff8ae7b5b + my $buffer := CircularBuffer.new( :capacity(2) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + lives-ok( + { $buffer.overwrite(2) }, + "overwrite buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 1, + "read buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 2, + "read buffer", + ); +}; # end: 9c2d4f26-3ec7-453f-a895-7e7ff8ae7b5b + +subtest "overwrite replaces the oldest item on full buffer" => { # begin: 880f916b-5039-475c-bd5c-83463c36a147 + my $buffer := CircularBuffer.new( :capacity(2) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + lives-ok( + { $buffer.write(2) }, + "write buffer", + ); + + lives-ok( + { $buffer.overwrite(3) }, + "overwrite buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 2, + "read buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 3, + "read buffer", + ); +}; # end: 880f916b-5039-475c-bd5c-83463c36a147 + +subtest "overwrite replaces the oldest item remaining in buffer following a read" => { # begin: bfecab5b-aca1-4fab-a2b0-cd4af2b053c3 + my $buffer := CircularBuffer.new( :capacity(3) ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + lives-ok( + { $buffer.write(2) }, + "write buffer", + ); + + lives-ok( + { $buffer.write(3) }, + "write buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 1, + "read buffer", + ); + + lives-ok( + { $buffer.write(4) }, + "write buffer", + ); + + lives-ok( + { $buffer.overwrite(5) }, + "overwrite buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 3, + "read buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 4, + "read buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 5, + "read buffer", + ); +}; # end: bfecab5b-aca1-4fab-a2b0-cd4af2b053c3 + +subtest "initial clear does not affect wrapping around" => { # begin: 9cebe63a-c405-437b-8b62-e3fdc1ecec5a + my $buffer := CircularBuffer.new( :capacity(2) ); + + lives-ok( + { $buffer.clear }, + "clear buffer", + ); + + lives-ok( + { $buffer.write(1) }, + "write buffer", + ); + + lives-ok( + { $buffer.write(2) }, + "write buffer", + ); + + lives-ok( + { $buffer.overwrite(3) }, + "overwrite buffer", + ); + + lives-ok( + { $buffer.overwrite(4) }, + "overwrite buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 3, + "read buffer", + ); + + cmp-ok( + $buffer.read, + "==", + 4, + "read buffer", + ); + + throws-like( + { $buffer.read }, + X::CircularBuffer::BufferIsEmpty, + "read error", + ); +}; # end: 9cebe63a-c405-437b-8b62-e3fdc1ecec5a + +done-testing;