Skip to content

Commit

Permalink
Implement circular-buffer (#696)
Browse files Browse the repository at this point in the history
  • Loading branch information
m-dango authored Mar 9, 2024
1 parent e52396a commit 2aec71e
Show file tree
Hide file tree
Showing 9 changed files with 807 additions and 0 deletions.
9 changes: 9 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": []
}
]
},
Expand Down
58 changes: 58 additions & 0 deletions exercises/practice/circular-buffer/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -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]
```
22 changes: 22 additions & 0 deletions exercises/practice/circular-buffer/.meta/config.json
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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!
}
}
173 changes: 173 additions & 0 deletions exercises/practice/circular-buffer/.meta/template-data.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
properties:
run:
test: |-
my $ops = %case<input><operations>.map(-> %op {
given %op<operation> {
when 'read' {
if %op<should_succeed> {
sprintf(q:to/CASE/, %op<expected>).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<should_succeed> {
sprintf(q:to/CASE/, %op<item>).trim-trailing;
lives-ok(
{ $buffer.write(%s) },
"write buffer",
);
CASE
}
else {
sprintf(q:to/CASE/, %op<item>).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<item>).trim-trailing;
lives-ok(
{ $buffer.overwrite(%s) },
"overwrite buffer",
);
CASE
}
default {
' flunk "NYI";'
}
}
}).join("\n\n");
sprintf(q:to/END/, %case<description>.raku, %case<input><capacity>, $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) {}
}
Loading

0 comments on commit 2aec71e

Please sign in to comment.