From 1144087932991cd6c48c9cb02c00f0f67f868e84 Mon Sep 17 00:00:00 2001 From: Tomas Norre Mikkelsen Date: Thu, 22 Feb 2024 10:49:17 +0100 Subject: [PATCH] Add Circular Buffer Exercise (#624) --- config.json | 8 + .../circular-buffer/.docs/instructions.md | 58 ++++++ .../circular-buffer/.meta/config.json | 19 ++ .../circular-buffer/.meta/example.php | 104 ++++++++++ .../practice/circular-buffer/.meta/tests.toml | 52 +++++ .../circular-buffer/CircularBuffer.php | 56 ++++++ .../circular-buffer/CircularBufferTest.php | 185 ++++++++++++++++++ 7 files changed, 482 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.php create mode 100644 exercises/practice/circular-buffer/.meta/tests.toml create mode 100644 exercises/practice/circular-buffer/CircularBuffer.php create mode 100644 exercises/practice/circular-buffer/CircularBufferTest.php diff --git a/config.json b/config.json index 178fd1b8..27e0cfcf 100644 --- a/config.json +++ b/config.json @@ -1171,6 +1171,14 @@ "practices": [], "prerequisites": [], "difficulty": 7 + }, + { + "slug": "circular-buffer", + "name": "Circular Buffer", + "uuid": "3f077d57-472a-469a-885d-ebc6aaccd3d7", + "practices": [], + "prerequisites": [], + "difficulty": 6 } ] }, 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..874f3dc8 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "tomasnorre" + ], + "files": { + "solution": [ + "CircularBuffer.php" + ], + "test": [ + "CircularBufferTest.php" + ], + "example": [ + ".meta/example.php" + ] + }, + "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.php b/exercises/practice/circular-buffer/.meta/example.php new file mode 100644 index 00000000..d97f4989 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/example.php @@ -0,0 +1,104 @@ +. + * + * To disable strict typing, comment out the directive below. + */ + +declare(strict_types=1); + +class BufferFullError extends Exception +{ +} + +class BufferEmptyError extends Exception +{ +} + +class CircularBuffer +{ + private int $capacity; + private array $buffer; + private int $readPosition; + private int $writePosition; + + public function __construct($capacity) + { + $this->capacity = $capacity; + $this->buffer = array_fill(0, $capacity, null); + $this->readPosition = 0; + $this->writePosition = 0; + } + + /** + * @throws BufferEmptyError + */ + public function read() + { + if ($this->isEmpty()) { + throw new BufferEmptyError(); + } + $value = $this->buffer[$this->readPosition]; + $this->buffer[$this->readPosition] = null; + $this->readPosition = ($this->readPosition + 1) % $this->capacity; + return $value; + } + + /** + * @throws BufferFullError + */ + public function write($item): void + { + if ($this->isFull()) { + throw new BufferFullError(); + } + $this->buffer[$this->writePosition] = $item; + $this->writePosition = ($this->writePosition + 1) % $this->capacity; + } + + /** + * @throws BufferEmptyError + * @throws BufferFullError + */ + public function forceWrite($item): void + { + if ($this->isFull()) { + $this->read(); + } + $this->write($item); + } + + public function clear(): void + { + $this->buffer = array_fill(0, $this->capacity, null); + $this->readPosition = 0; + $this->writePosition = 0; + } + + private function isEmpty(): bool + { + return $this->buffer[$this->readPosition] === null; + } + + private function isFull(): bool + { + return $this->buffer[$this->writePosition] !== null; + } +} 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/CircularBuffer.php b/exercises/practice/circular-buffer/CircularBuffer.php new file mode 100644 index 00000000..caa89057 --- /dev/null +++ b/exercises/practice/circular-buffer/CircularBuffer.php @@ -0,0 +1,56 @@ +. + * + * To disable strict typing, comment out the directive below. + */ + +declare(strict_types=1); + +class BufferFullError extends Exception +{ +} + +class BufferEmptyError extends Exception +{ +} + +class CircularBuffer +{ + public function read() + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } + + public function write($item): void + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } + + public function forceWrite($item): void + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } + + public function clear(): void + { + throw new \BadMethodCallException(sprintf('Implement the %s method', __FUNCTION__)); + } +} diff --git a/exercises/practice/circular-buffer/CircularBufferTest.php b/exercises/practice/circular-buffer/CircularBufferTest.php new file mode 100644 index 00000000..95421042 --- /dev/null +++ b/exercises/practice/circular-buffer/CircularBufferTest.php @@ -0,0 +1,185 @@ +expectException(BufferEmptyError::class); + $buffer->read(); + } + + /** + * uuid: 2e6db04a-58a1-425d-ade8-ac30b5f318f3 + */ + public function testCanReadAnItemJustWritten(): void + { + $buffer = new CircularBuffer(1); + $buffer->write('1'); + $this->assertSame('1', $buffer->read()); + } + + /** + * uuid: 90741fe8-a448-45ce-be2b-de009a24c144 + */ + public function testEachItemMayOnlyBeReadOnce(): void + { + $buffer = new CircularBuffer(1); + $buffer->write('1'); + $this->assertSame('1', $buffer->read()); + $this->expectException(BufferEmptyError::class); + $buffer->read(); + } + + /** + * uuid: be0e62d5-da9c-47a8-b037-5db21827baa7 + */ + public function testItemsAreReadInTheOrderTheyAreWritten(): void + { + $buffer = new CircularBuffer(2); + $buffer->write('1'); + $buffer->write('2'); + $this->assertSame('1', $buffer->read()); + $this->assertSame('2', $buffer->read()); + } + + /** + * uuid: 2af22046-3e44-4235-bfe6-05ba60439d38 + */ + public function testFullBufferCantBeWrittenTo(): void + { + $buffer = new CircularBuffer(1); + $buffer->write('1'); + $this->expectException(BufferFullError::class); + $buffer->write('2'); + } + + /** + * uuid: 547d192c-bbf0-4369-b8fa-fc37e71f2393 + */ + public function testAReadFreesUpCapacityForAnotherWrite(): void + { + $buffer = new CircularBuffer(1); + $buffer->write('1'); + $this->assertSame('1', $buffer->read()); + $buffer->write('2'); + $this->assertSame('2', $buffer->read()); + } + + /** + * uuid: 04a56659-3a81-4113-816b-6ecb659b4471 + */ + public function testReadPositionIsMaintainedEvenAcrossMultipleWrites(): void + { + $buffer = new CircularBuffer(3); + $buffer->write('1'); + $buffer->write('2'); + $this->assertSame('1', $buffer->read()); + $buffer->write('3'); + $this->assertSame('2', $buffer->read()); + $this->assertSame('3', $buffer->read()); + } + + /** + * uuid: 60c3a19a-81a7-43d7-bb0a-f07242b1111f + */ + public function testItemsClearedOutOfBufferCantBeRead(): void + { + $buffer = new CircularBuffer(1); + $buffer->write('1'); + $buffer->clear(); + $this->expectException(BufferEmptyError::class); + $buffer->read(); + } + + /** + * uuid: 45f3ae89-3470-49f3-b50e-362e4b330a59 + */ + public function testClearFreesUpCapacityForAnotherWrite(): void + { + $buffer = new CircularBuffer(1); + $buffer->write('1'); + $buffer->clear(); + $buffer->write('2'); + $this->assertSame('2', $buffer->read()); + } + + /** + * uuid: e1ac5170-a026-4725-bfbe-0cf332eddecd + */ + public function testClearDoesNothingOnEmptyBuffer(): void + { + $buffer = new CircularBuffer(1); + $buffer->clear(); + $buffer->write('1'); + $this->assertSame('1', $buffer->read()); + } + + /** + * uuid: 9c2d4f26-3ec7-453f-a895-7e7ff8ae7b5b + */ + public function testForceWriteActsLikeWriteOnNonFullBuffer(): void + { + $buffer = new CircularBuffer(2); + $buffer->write('1'); + $buffer->forceWrite('2'); + $this->assertSame('1', $buffer->read()); + $this->assertSame('2', $buffer->read()); + } + + /** + * uuid: 880f916b-5039-475c-bd5c-83463c36a147 + */ + public function testForceWriteReplacesTheOldestItemOnFullBuffer(): void + { + $buffer = new CircularBuffer(2); + $buffer->write('1'); + $buffer->write('2'); + $buffer->forceWrite('3'); + $this->assertSame('2', $buffer->read()); + $this->assertSame('3', $buffer->read()); + } + + /** + * uuid: bfecab5b-aca1-4fab-a2b0-cd4af2b053c3 + */ + public function testForceWriteReplacesTheOldestItemRemainingInBufferFollowingARead(): void + { + $buffer = new CircularBuffer(3); + $buffer->write('1'); + $buffer->write('2'); + $buffer->write('3'); + $this->assertSame('1', $buffer->read()); + $buffer->write('4'); + $buffer->forceWrite('5'); + $this->assertSame('3', $buffer->read()); + $this->assertSame('4', $buffer->read()); + $this->assertSame('5', $buffer->read()); + } + + /** + * uuid: 9cebe63a-c405-437b-8b62-e3fdc1ecec5a + */ + public function testInitialClearDoesNotAffectWrappingAround(): void + { + $buffer = new CircularBuffer(2); + $buffer->clear(); + $buffer->write('1'); + $buffer->write('2'); + $buffer->forceWrite('3'); + $buffer->forceWrite('4'); + $this->assertSame('3', $buffer->read()); + $this->assertSame('4', $buffer->read()); + $this->expectException(BufferEmptyError::class); + $buffer->read(); + } +}