From f354c9a5e27ede17968dcc95280303d7b2f84e45 Mon Sep 17 00:00:00 2001 From: Glenn Jackman Date: Thu, 14 Nov 2024 19:08:56 -0500 Subject: [PATCH] Add circular-buffer exercise --- config.json | 8 + .../circular-buffer/.docs/instructions.md | 58 +++++++ .../circular-buffer/.meta/config.json | 19 +++ .../.meta/solutions/lib/CircularBuffer.pm | 33 ++++ .../.meta/solutions/t/circular-buffer.t | 1 + .../circular-buffer/.meta/template-data.yaml | 109 +++++++++++++ .../practice/circular-buffer/.meta/tests.toml | 52 +++++++ .../circular-buffer/lib/CircularBuffer.pm | 24 +++ .../circular-buffer/t/circular-buffer.t | 143 ++++++++++++++++++ 9 files changed, 447 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/solutions/lib/CircularBuffer.pm create mode 120000 exercises/practice/circular-buffer/.meta/solutions/t/circular-buffer.t create mode 100644 exercises/practice/circular-buffer/.meta/template-data.yaml create mode 100644 exercises/practice/circular-buffer/.meta/tests.toml create mode 100644 exercises/practice/circular-buffer/lib/CircularBuffer.pm create mode 100755 exercises/practice/circular-buffer/t/circular-buffer.t diff --git a/config.json b/config.json index 80564110..87af4333 100644 --- a/config.json +++ b/config.json @@ -223,6 +223,14 @@ "prerequisites": [], "difficulty": 1 }, + { + "slug": "circular-buffer", + "name": "Circular Buffer", + "uuid": "2135af8f-5849-4611-8492-29a8c5b63629", + "practices": [], + "prerequisites": [], + "difficulty": 4 + }, { "slug": "clock", "name": "Clock", 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..b8bfcb48 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "lib/CircularBuffer.pm" + ], + "test": [ + "t/circular-buffer.t" + ], + "example": [ + ".meta/solutions/lib/CircularBuffer.pm" + ] + }, + "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.pm b/exercises/practice/circular-buffer/.meta/solutions/lib/CircularBuffer.pm new file mode 100644 index 00000000..b05d3a66 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/solutions/lib/CircularBuffer.pm @@ -0,0 +1,33 @@ +use strict; +use warnings; +use experimental qw; +use Feature::Compat::Class; + +class CircularBuffer; + +field $capacity :reader :param; +field @data = (); # kind of cheating to use a non-fixed size array. + +method _isFull { scalar @data == $capacity; } +method _isEmpty { scalar @data == 0; } + +method write ($item) { + die 'buffer full' if $self->_isFull; + push @data, $item; +} + +method read () { + die 'buffer empty' if $self->_isEmpty; + return shift @data; +} + +method clear () { + @data = (); +} + +method overwrite ($item) { + $self->read() if $self->_isFull; + $self->write($item); +} + +1; diff --git a/exercises/practice/circular-buffer/.meta/solutions/t/circular-buffer.t b/exercises/practice/circular-buffer/.meta/solutions/t/circular-buffer.t new file mode 120000 index 00000000..13ae3fca --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/solutions/t/circular-buffer.t @@ -0,0 +1 @@ +../../../t/circular-buffer.t \ 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..354cf20a --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/template-data.yaml @@ -0,0 +1,109 @@ +class: true + +properties: + run: + test: |- + use Data::Dmp; + (my $_t_name = $case->{description}) =~ s/'//g; + $_t_name =~ s/\W+/_/g; + my $_t = sprintf(<<'END1', $_t_name, $case->{input}{capacity}); + subtest %s => sub { + my $buff = CircularBuffer->new(capacity => %s); + END1 + + my $n = 1; + foreach my $_op (@{$case->{input}{operations}}) { + if ($_op->{operation} eq 'clear') { + $_t .= sprintf(<<'END2', $n); + ok(lives { $buff->clear() }, "check %d"); + END2 + } + + elsif ($_op->{operation} eq 'write') { + if ($_op->{should_succeed}) { + $_t .= sprintf(<<'END3', $_op->{item}, $n); + ok(lives { $buff->write(%s) }, "check %d"); + END3 + } + else { + $_t .= sprintf(<<'END4', $_op->{item}, $n); + like( + dies { $buff->write(%s) }, + qr/buffer full/, + "check %d" + ); + END4 + } + } + + elsif ($_op->{operation} eq 'read') { + if ($_op->{should_succeed}) { + $_t .= sprintf(<<'END5', $_op->{expected}, $n); + is($buff->read(), %s, "check %d"); + END5 + } + else { + $_t .= sprintf(<<'END6', $n); + like( + dies { $buff->read() }, + qr/buffer empty/, + "check %d" + ); + END6 + } + } + + elsif ($_op->{operation} eq 'overwrite') { + $_t .= sprintf(<<'END7', $_op->{item}, $n); + ok(lives { $buff->overwrite(%s) }, "check %d"); + END7 + } + + ++$n; + } + $_t . "};\n" + +stub: |- + field $capacity :reader :param; + + method write ($item) { + return undef; + } + + method read () { + return undef; + } + + method clear () { + return undef; + } + + method overwrite ($item) { + return undef; + } + +example: |- + field $capacity :reader :param; + field @data = (); # kind of cheating to use a non-fixed size array. + + method _isFull { scalar @data == $capacity; } + method _isEmpty { scalar @data == 0; } + + method write ($item) { + die 'buffer full' if $self->_isFull; + push @data, $item; + } + + method read () { + die 'buffer empty' if $self->_isEmpty; + return shift @data; + } + + method clear () { + @data = (); + } + + method overwrite ($item) { + $self->read() if $self->_isFull; + $self->write($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.pm b/exercises/practice/circular-buffer/lib/CircularBuffer.pm new file mode 100644 index 00000000..ac7c1702 --- /dev/null +++ b/exercises/practice/circular-buffer/lib/CircularBuffer.pm @@ -0,0 +1,24 @@ +use v5.40; +use experimental qw; + +class CircularBuffer; + +field $capacity :reader :param; + +method write ($item) { + return undef; +} + +method read () { + return undef; +} + +method clear () { + return undef; +} + +method overwrite ($item) { + return undef; +} + +1; diff --git a/exercises/practice/circular-buffer/t/circular-buffer.t b/exercises/practice/circular-buffer/t/circular-buffer.t new file mode 100755 index 00000000..b19e18ea --- /dev/null +++ b/exercises/practice/circular-buffer/t/circular-buffer.t @@ -0,0 +1,143 @@ +#!/usr/bin/env perl +use Test2::V0; + +use FindBin qw<$Bin>; +use lib "$Bin/../lib", "$Bin/../local/lib/perl5"; + +use CircularBuffer (); + +subtest reading_empty_buffer_should_fail => sub { # begin: 28268ed4-4ff3-45f3-820e-895b44d53dfa + my $buff = CircularBuffer->new( capacity => 1 ); + like( + dies { $buff->read() }, + qr/buffer empty/, + "check 1" + ); +}; # end: 28268ed4-4ff3-45f3-820e-895b44d53dfa + +subtest can_read_an_item_just_written => sub { # begin: 2e6db04a-58a1-425d-ade8-ac30b5f318f3 + my $buff = CircularBuffer->new( capacity => 1 ); + ok( lives { $buff->write(1) }, "check 1" ); + is( $buff->read(), 1, "check 2" ); +}; # end: 2e6db04a-58a1-425d-ade8-ac30b5f318f3 + +subtest each_item_may_only_be_read_once => sub { # begin: 90741fe8-a448-45ce-be2b-de009a24c144 + my $buff = CircularBuffer->new( capacity => 1 ); + ok( lives { $buff->write(1) }, "check 1" ); + is( $buff->read(), 1, "check 2" ); + like( + dies { $buff->read() }, + qr/buffer empty/, + "check 3" + ); +}; # end: 90741fe8-a448-45ce-be2b-de009a24c144 + +subtest items_are_read_in_the_order_they_are_written => sub { # begin: be0e62d5-da9c-47a8-b037-5db21827baa7 + my $buff = CircularBuffer->new( capacity => 2 ); + ok( lives { $buff->write(1) }, "check 1" ); + ok( lives { $buff->write(2) }, "check 2" ); + is( $buff->read(), 1, "check 3" ); + is( $buff->read(), 2, "check 4" ); +}; # end: be0e62d5-da9c-47a8-b037-5db21827baa7 + +subtest full_buffer_cant_be_written_to => sub { # begin: 2af22046-3e44-4235-bfe6-05ba60439d38 + my $buff = CircularBuffer->new( capacity => 1 ); + ok( lives { $buff->write(1) }, "check 1" ); + like( + dies { $buff->write(2) }, + qr/buffer full/, + "check 2" + ); +}; # end: 2af22046-3e44-4235-bfe6-05ba60439d38 + +subtest a_read_frees_up_capacity_for_another_write => sub { # begin: 547d192c-bbf0-4369-b8fa-fc37e71f2393 + my $buff = CircularBuffer->new( capacity => 1 ); + ok( lives { $buff->write(1) }, "check 1" ); + is( $buff->read(), 1, "check 2" ); + ok( lives { $buff->write(2) }, "check 3" ); + is( $buff->read(), 2, "check 4" ); +}; # end: 547d192c-bbf0-4369-b8fa-fc37e71f2393 + +subtest read_position_is_maintained_even_across_multiple_writes => sub { # begin: 04a56659-3a81-4113-816b-6ecb659b4471 + my $buff = CircularBuffer->new( capacity => 3 ); + ok( lives { $buff->write(1) }, "check 1" ); + ok( lives { $buff->write(2) }, "check 2" ); + is( $buff->read(), 1, "check 3" ); + ok( lives { $buff->write(3) }, "check 4" ); + is( $buff->read(), 2, "check 5" ); + is( $buff->read(), 3, "check 6" ); +}; # end: 04a56659-3a81-4113-816b-6ecb659b4471 + +subtest items_cleared_out_of_buffer_cant_be_read => sub { # begin: 60c3a19a-81a7-43d7-bb0a-f07242b1111f + my $buff = CircularBuffer->new( capacity => 1 ); + ok( lives { $buff->write(1) }, "check 1" ); + ok( lives { $buff->clear() }, "check 2" ); + like( + dies { $buff->read() }, + qr/buffer empty/, + "check 3" + ); +}; # end: 60c3a19a-81a7-43d7-bb0a-f07242b1111f + +subtest clear_frees_up_capacity_for_another_write => sub { # begin: 45f3ae89-3470-49f3-b50e-362e4b330a59 + my $buff = CircularBuffer->new( capacity => 1 ); + ok( lives { $buff->write(1) }, "check 1" ); + ok( lives { $buff->clear() }, "check 2" ); + ok( lives { $buff->write(2) }, "check 3" ); + is( $buff->read(), 2, "check 4" ); +}; # end: 45f3ae89-3470-49f3-b50e-362e4b330a59 + +subtest clear_does_nothing_on_empty_buffer => sub { # begin: e1ac5170-a026-4725-bfbe-0cf332eddecd + my $buff = CircularBuffer->new( capacity => 1 ); + ok( lives { $buff->clear() }, "check 1" ); + ok( lives { $buff->write(1) }, "check 2" ); + is( $buff->read(), 1, "check 3" ); +}; # end: e1ac5170-a026-4725-bfbe-0cf332eddecd + +subtest overwrite_acts_like_write_on_non_full_buffer => sub { # begin: 9c2d4f26-3ec7-453f-a895-7e7ff8ae7b5b + my $buff = CircularBuffer->new( capacity => 2 ); + ok( lives { $buff->write(1) }, "check 1" ); + ok( lives { $buff->overwrite(2) }, "check 2" ); + is( $buff->read(), 1, "check 3" ); + is( $buff->read(), 2, "check 4" ); +}; # end: 9c2d4f26-3ec7-453f-a895-7e7ff8ae7b5b + +subtest overwrite_replaces_the_oldest_item_on_full_buffer => sub { # begin: 880f916b-5039-475c-bd5c-83463c36a147 + my $buff = CircularBuffer->new( capacity => 2 ); + ok( lives { $buff->write(1) }, "check 1" ); + ok( lives { $buff->write(2) }, "check 2" ); + ok( lives { $buff->overwrite(3) }, "check 3" ); + is( $buff->read(), 2, "check 4" ); + is( $buff->read(), 3, "check 5" ); +}; # end: 880f916b-5039-475c-bd5c-83463c36a147 + +subtest overwrite_replaces_the_oldest_item_remaining_in_buffer_following_a_read => sub { # begin: bfecab5b-aca1-4fab-a2b0-cd4af2b053c3 + my $buff = CircularBuffer->new( capacity => 3 ); + ok( lives { $buff->write(1) }, "check 1" ); + ok( lives { $buff->write(2) }, "check 2" ); + ok( lives { $buff->write(3) }, "check 3" ); + is( $buff->read(), 1, "check 4" ); + ok( lives { $buff->write(4) }, "check 5" ); + ok( lives { $buff->overwrite(5) }, "check 6" ); + is( $buff->read(), 3, "check 7" ); + is( $buff->read(), 4, "check 8" ); + is( $buff->read(), 5, "check 9" ); +}; # end: bfecab5b-aca1-4fab-a2b0-cd4af2b053c3 + +subtest initial_clear_does_not_affect_wrapping_around => sub { # begin: 9cebe63a-c405-437b-8b62-e3fdc1ecec5a + my $buff = CircularBuffer->new( capacity => 2 ); + ok( lives { $buff->clear() }, "check 1" ); + ok( lives { $buff->write(1) }, "check 2" ); + ok( lives { $buff->write(2) }, "check 3" ); + ok( lives { $buff->overwrite(3) }, "check 4" ); + ok( lives { $buff->overwrite(4) }, "check 5" ); + is( $buff->read(), 3, "check 6" ); + is( $buff->read(), 4, "check 7" ); + like( + dies { $buff->read() }, + qr/buffer empty/, + "check 8" + ); +}; # end: 9cebe63a-c405-437b-8b62-e3fdc1ecec5a + +done_testing;