2 Commits eedf5e7312 ... 97955498da

Author SHA1 Message Date
  Frédéric G. MARAND 97955498da Merge branch '08-circular_buffer' 1 month ago
  Frédéric G. MARAND 7ce4448672 08: Circular buffer. 1 month ago

+ 19 - 0
circular-buffer/.exercism/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"
+}

+ 1 - 0
circular-buffer/.exercism/metadata.json

@@ -0,0 +1 @@
+{"track":"php","exercise":"circular-buffer","id":"c82d27ce5d0c4675b0784f03c76bc2ec","url":"https://exercism.org/tracks/php/exercises/circular-buffer","handle":"Fairgame","is_requester":true,"auto_approve":false}

+ 99 - 0
circular-buffer/CircularBuffer.php

@@ -0,0 +1,99 @@
+<?php
+
+/*
+ * By adding type hints and enabling strict type checking, code can become
+ * easier to read, self-documenting and reduce the number of potential bugs.
+ * By default, type declarations are non-strict, which means they will attempt
+ * to change the original type to match the type specified by the
+ * type-declaration.
+ *
+ * In other words, if you pass a string to a function requiring a float,
+ * it will attempt to convert the string value to a float.
+ *
+ * To enable strict mode, a single declare directive must be placed at the top
+ * of the file.
+ * This means that the strictness of typing is configured on a per-file basis.
+ * This directive not only affects the type declarations of parameters, but also
+ * a function's return type.
+ *
+ * For more info review the Concept on strict type checking in the PHP track
+ * <link>.
+ *
+ * To disable strict typing, comment out the directive below.
+ */
+
+declare(strict_types=1);
+
+class CircularBuffer {
+
+  /**
+   * @var int Array capacity.
+   */
+  private int $cap = 0;
+
+  /**
+   * @var \SplFixedArray Storage, faster than plain array and fixed size.
+   */
+  private SplFixedArray $data;
+
+  /**
+   * @var int Start of data with $this->>data.
+   */
+  private int $base = 0;
+
+  /**
+   * @var int Number of data items in $this->>data.
+   */
+  private int $length = 0;
+
+  public function __construct(int $length) {
+    $this->cap = $length;
+    $this->data = new SplFixedArray($length);
+  }
+
+  public function clear(): void {
+    $this->data = new SplFixedArray($this->cap);
+    $this->base = 0;
+    $this->length = 0;
+  }
+
+  /**
+   * @throws \BufferEmptyError
+   */
+  public function read() {
+    if ($this->length === 0) {
+      throw new BufferEmptyError();
+    }
+    $res = $this->data[$this->base];
+    $this->base = ($this->base + 1) % $this->cap;
+    $this->length--;
+    return $res;
+  }
+
+  /**
+   * @throws \BufferFullError
+   */
+  public function write($item): void {
+    if ($this->length == $this->cap) {
+      throw new BufferFullError();
+    }
+    $this->data[($this->base+$this->length)%$this->cap] = $item;
+    $this->length++;
+  }
+
+  public function forceWrite($item): void {
+    $this->data[($this->base+$this->length)%$this->cap] = $item;
+    if ($this->length === $this->cap) {
+      $this->base = ($this->base+1)%$this->cap;
+    }
+    $this->length = min($this->cap, $this->length+1);
+  }
+}
+
+class BufferFullError extends Exception {
+
+}
+
+class BufferEmptyError extends Exception {
+
+}

+ 185 - 0
circular-buffer/CircularBufferTest.php

@@ -0,0 +1,185 @@
+<?php
+
+declare(strict_types=1);
+
+require_once 'CircularBuffer.php';
+
+use PHPUnit\Framework\TestCase;
+
+class CircularBufferTest extends TestCase
+{
+    /**
+     * uuid: 28268ed4-4ff3-45f3-820e-895b44d53dfa
+     */
+    public function testReadingEmptyBufferShouldFail(): void
+    {
+        $buffer = new CircularBuffer(1);
+        $this->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();
+    }
+}

+ 52 - 0
circular-buffer/HELP.md

@@ -0,0 +1,52 @@
+# Help
+
+## Running the tests
+
+## Running the tests
+
+1. Go to the root of your PHP exercise directory, which is `<EXERCISM_WORKSPACE>/php`.
+   To find the Exercism workspace run
+
+       ➜ exercism debug | grep Workspace
+
+1. Get [PHPUnit] if you don't have it already.
+
+       ➜ wget -O phpunit https://phar.phpunit.de/phpunit-9.phar
+       ➜ chmod +x phpunit
+       ➜ ./phpunit --version
+
+2. Execute the tests:
+
+       ➜ ./phpunit file_to_test.php
+
+   For example, to run the tests for the Hello World exercise, you would run:
+
+       ➜ ./phpunit HelloWorldTest.php
+
+[PHPUnit]: https://phpunit.de
+
+## Submitting your solution
+
+You can submit your solution using the `exercism submit CircularBuffer.php` command.
+This command will upload your solution to the Exercism website and print the solution page's URL.
+
+It's possible to submit an incomplete solution which allows you to:
+
+- See how others have completed the exercise
+- Request help from a mentor
+
+## Need to get help?
+
+If you'd like help solving the exercise, check the following pages:
+
+- The [PHP track's documentation](https://exercism.org/docs/tracks/php)
+- The [PHP track's programming category on the forum](https://forum.exercism.org/c/programming/php)
+- [Exercism's programming category on the forum](https://forum.exercism.org/c/programming/5)
+- The [Frequently Asked Questions](https://exercism.org/docs/using/faqs)
+
+Should those resources not suffice, you could submit your (incomplete) solution to request mentoring.
+
+To get help if you're having trouble, you can use one of the following resources:
+
+ - [/r/php](https://www.reddit.com/r/php) is the PHP subreddit.
+ - [StackOverflow](https://stackoverflow.com/questions/tagged/php) can be used to search for your problem and see if it has been answered already. You can also ask and answer questions.

+ 73 - 0
circular-buffer/README.md

@@ -0,0 +1,73 @@
+# Circular Buffer
+
+Welcome to Circular Buffer on Exercism's PHP Track.
+If you need help running the tests or submitting your code, check out `HELP.md`.
+
+## 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]
+```
+
+## Source
+
+### Created by
+
+- @tomasnorre
+
+### Based on
+
+Wikipedia - https://en.wikipedia.org/wiki/Circular_buffer