From 1cd88f37f934bf519308f215898c627c145b9e0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20B=20Nagy?= <20251272+BNAndras@users.noreply.github.com> Date: Mon, 4 Mar 2024 23:22:37 -0800 Subject: [PATCH] Add circular buffer WIP --- config.json | 10 +- .../circular-buffer/.docs/instructions.md | 58 ++++++++ .../circular-buffer/.meta/Example.cfc | 51 +++++++ .../circular-buffer/.meta/ExampleTest.cfc | 7 + .../circular-buffer/.meta/config.json | 19 +++ .../practice/circular-buffer/.meta/tests.toml | 52 ++++++++ .../circular-buffer/CircularBuffer.cfc | 40 ++++++ .../circular-buffer/CircularBufferTest.cfc | 126 ++++++++++++++++++ .../practice/circular-buffer/TestRunner.cfc | 103 ++++++++++++++ exercises/practice/circular-buffer/box.json | 8 ++ exercises/practice/circular-buffer/index.cfm | 37 +++++ 11 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 exercises/practice/circular-buffer/.docs/instructions.md create mode 100644 exercises/practice/circular-buffer/.meta/Example.cfc create mode 100644 exercises/practice/circular-buffer/.meta/ExampleTest.cfc create mode 100644 exercises/practice/circular-buffer/.meta/config.json create mode 100644 exercises/practice/circular-buffer/.meta/tests.toml create mode 100644 exercises/practice/circular-buffer/CircularBuffer.cfc create mode 100644 exercises/practice/circular-buffer/CircularBufferTest.cfc create mode 100644 exercises/practice/circular-buffer/TestRunner.cfc create mode 100644 exercises/practice/circular-buffer/box.json create mode 100644 exercises/practice/circular-buffer/index.cfm diff --git a/config.json b/config.json index 53b8a2a..68e1d8b 100644 --- a/config.json +++ b/config.json @@ -437,6 +437,14 @@ "practices": [], "prerequisites": [], "difficulty": 2 + }, + { + "slug": "circular-buffer", + "name": "Circular Buffer", + "uuid": "0b6ade33-0ca0-4bfe-8b7b-d4370cb6e7f1", + "practices": [], + "prerequisites": [], + "difficulty": 4 } ] }, @@ -482,4 +490,4 @@ "used_for/frontends", "used_for/web_development" ] -} +} \ No newline at end of file diff --git a/exercises/practice/circular-buffer/.docs/instructions.md b/exercises/practice/circular-buffer/.docs/instructions.md new file mode 100644 index 0000000..2ba1fda --- /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/Example.cfc b/exercises/practice/circular-buffer/.meta/Example.cfc new file mode 100644 index 0000000..73173c6 --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/Example.cfc @@ -0,0 +1,51 @@ +/** +* Here is an example solution for the CircularBuffer exercise +*/ +component { + + /** + * @returns + */ + CircularBuffer function init( required numeric capacity ) { + variables.capacity = arguments.capacity; + clear(); + + return this; + } + + numeric function read() { + if (variables.size == 0) { + throw( message="Empty buffer" ); + } + var value = variables.data[variables.readIndex] + variables.readIndex = ( variables.readIndex + 1) % variables.capacity + variables.size -= 1; + return value; + } + + void function write( required numeric value ) { + if (variables.size >= variables.capacity) { + throw( message="Full buffer" ); + } + + variables.data.set( variables.writeIndex, 1, arguments.value); + variables.writeIndex = ( variables.writeIndex + 1 ) % variables.capacity; + variables.size += 1; + } + + void function overwrite( required numeric value ) { + if (variables.size == variables.capacity) { + read(); + } + + write( arguments.value ); + } + + void function clear() { + variables.size = 0; + variables.data = arraySet( [], 1, variables.capacity, nullValue() ); + variables.readIndex = 1; + variables.writeIndex = 1; + } + +} \ No newline at end of file diff --git a/exercises/practice/circular-buffer/.meta/ExampleTest.cfc b/exercises/practice/circular-buffer/.meta/ExampleTest.cfc new file mode 100644 index 0000000..1238f7d --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/ExampleTest.cfc @@ -0,0 +1,7 @@ +component extends="CircularBufferTest" { + + function beforeAll(){ + SUT = createObject( 'Solution' ); + } + +} \ No newline at end of file diff --git a/exercises/practice/circular-buffer/.meta/config.json b/exercises/practice/circular-buffer/.meta/config.json new file mode 100644 index 0000000..dac23ac --- /dev/null +++ b/exercises/practice/circular-buffer/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "BNAndras" + ], + "files": { + "solution": [ + "CircularBuffer.cfc" + ], + "test": [ + "CircularBufferTest.cfc" + ], + "example": [ + ".meta/Example.cfc" + ] + }, + "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/tests.toml b/exercises/practice/circular-buffer/.meta/tests.toml new file mode 100644 index 0000000..0fb3143 --- /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.cfc b/exercises/practice/circular-buffer/CircularBuffer.cfc new file mode 100644 index 0000000..90d3c58 --- /dev/null +++ b/exercises/practice/circular-buffer/CircularBuffer.cfc @@ -0,0 +1,40 @@ +/** +* Your implementation of the CircularBuffer exercise +*/ +component { + + /** + * @returns + */ + function init( capacity ) { + // Implement me here + } + + /** + * @returns + */ + function read() { + // Implement me here + } + + /** + * @returns + */ + function write( value ) { + // Implement me here + } + + /** + * @returns + */ + function overwrite( value ) { + // Implement me here + } + + /** + * @returns + */ + function clear() { + // Implement me here + } +} \ No newline at end of file diff --git a/exercises/practice/circular-buffer/CircularBufferTest.cfc b/exercises/practice/circular-buffer/CircularBufferTest.cfc new file mode 100644 index 0000000..41d0a33 --- /dev/null +++ b/exercises/practice/circular-buffer/CircularBufferTest.cfc @@ -0,0 +1,126 @@ +component extends="testbox.system.BaseSpec" { + + + function run(){ + + describe( "My CircularBuffer class", function(){ + + it( "reading empty buffer should fail", function(){ + SUT = new CircularBuffer( capacity = 1 ); + expect( function(){ SUT.read(); } ).toThrow( message="Empty buffer" ); + }); + + it( "can read an item just written", function(){ + SUT = new CircularBuffer( capacity = 1 ); + SUT.write( 1 ); + expect( SUT.read() ).toBe( 1 ); + }); + + it( "each item may only be read once", function(){ + SUT = new CircularBuffer( capacity = 1 ); + SUT.write( 1 ); + expect( SUT.read() ).toBe( 1 ); + expect( function(){ SUT.read(); } ).toThrow( message="Empty buffer" ); + }); + + it( "items are read in the order they are written", function(){ + SUT = new CircularBuffer( capacity = 2 ); + SUT.write( 1 ); + SUT.write( 2 ); + expect( SUT.read() ).toBe( 1 ); + expect( SUT.read() ).toBe( 2 ); + }); + + it( "full buffer can't be written to", function(){ + SUT = new CircularBuffer( capacity = 1 ); + SUT.write( 1 ); + expect( function(){ SUT.write( 2 ); } ).toThrow( message="Full buffer" ); + }); + + it( "a read frees up capacity for another write", function(){ + SUT = new CircularBuffer( capacity = 1 ); + SUT.write( 1 ); + expect( SUT.read() ).toBe( 1 ); + SUT.write( 2 ); + expect( SUT.read() ).toBe( 2 ); + }); + + it( "read position is maintained even across multiple writes", function(){ + SUT = new CircularBuffer( capacity = 3 ); + SUT.write( 1 ); + SUT.write( 2 ); + expect( SUT.read() ).toBe( 1 ); + SUT.write( 3 ); + expect( SUT.read() ).toBe( 2 ); + expect( SUT.read() ).toBe( 3 ); + }); + + it( "items cleared out of buffer can't be read", function(){ + SUT = new CircularBuffer( capacity = 1 ); + SUT.write( 1 ); + SUT.clear(); + expect( function(){ SUT.read(); } ).toThrow( message="Empty buffer" ); + }); + + it( "clear frees up capacity for another write", function(){ + SUT = new CircularBuffer( capacity = 1 ); + SUT.write( 1 ); + SUT.clear(); + SUT.write( 2 ); + expect( SUT.read() ).toBe( 2 ); + }); + + it( "clear does nothing on empty buffer", function(){ + SUT = new CircularBuffer( capacity = 1 ); + SUT.clear(); + SUT.write( 1 ); + expect( SUT.read() ).toBe( 1 ); + }); + + it( "overwrite acts like write on non-full buffer", function(){ + SUT = new CircularBuffer( capacity = 2 ); + SUT.write( 1 ); + SUT.overwrite( 2 ); + expect( SUT.read() ).toBe( 1 ); + expect( SUT.read() ).toBe( 2 ); + }); + + it( "overwrite replaces the oldest item on full buffer", function(){ + SUT = new CircularBuffer( capacity = 2 ); + SUT.write( 1 ); + SUT.write( 2 ); + SUT.overwrite( 3 ); + expect( SUT.read() ).toBe( 2 ); + expect( SUT.read() ).toBe( 3 ); + }); + + it( "overwrite replaces the oldest item remaining in buffer following a read", function(){ + SUT = new CircularBuffer( capacity = 3 ); + SUT.write( 1 ); + SUT.write( 2 ); + SUT.write( 3 ); + expect( SUT.read() ).toBe( 1 ); + SUT.write( 4 ); + SUT.overwrite( 5 ); + expect( SUT.read() ).toBe( 3 ); + expect( SUT.read() ).toBe( 4 ); + expect( SUT.read() ).toBe( 5 ); + }); + + it( "initial clear does not affect wrapping around", function(){ + SUT = new CircularBuffer( capacity = 2 ); + SUT.clear(); + SUT.write( 1 ); + SUT.write( 2 ); + SUT.overwrite( 3 ); + SUT.overwrite( 4 ); + expect( SUT.read() ).toBe( 3 ); + expect( SUT.read() ).toBe( 4 ); + expect( function(){ SUT.read(); } ).toThrow( message="Empty buffer" ); + }); + + }); + + } + +} \ No newline at end of file diff --git a/exercises/practice/circular-buffer/TestRunner.cfc b/exercises/practice/circular-buffer/TestRunner.cfc new file mode 100644 index 0000000..762b153 --- /dev/null +++ b/exercises/practice/circular-buffer/TestRunner.cfc @@ -0,0 +1,103 @@ +/** +* I am a CommandBox task runner which you can use to test your implementation of this exercise against the +* provided test suite. To use me, open the CommandBox CLI and run this: +* +* CommandBox> task run TestRunner +* +* To start up a test watcher that will automatically rerun the test suite every time you save a file change, run this: +* +* CommandBox> task run TestRunner --watcher +* +*/ +component { + + /** + * @solution Runs the tests against the solution + * @watcher Start up a file watch that re-runs the tests on file changes. Use Ctrl-C to stop + */ + function run( boolean solution=false, boolean watcher=false ) { + + ensureTestBox(); + + if( watcher ) { + + // Tabula rasa + command( 'cls' ).run(); + + // Start watcher + watch() + .paths( '*.cfc' ) + .inDirectory( getCWD() ) + .withDelay( 500 ) + .onChange( function() { + + // Clear the screen + command( 'cls' ) + .run(); + + // This is neccessary so changes to tests get picked up right away. + pagePoolClear(); + + runTests( solution ); + + } ) + .start(); + + } else { + runTests( solution ); + } + + } + + /** + * Make sure the TestBox framework is installed + */ + private function ensureTestBox() { + var excerciseRoot = getCWD(); + var testBoxRoot = excerciseRoot & '/testbox'; + + if( !directoryExists( testBoxRoot ) ) { + + print.boldYellowLine( 'Installing some missing dependencies for you!' ).toConsole(); + command( 'install' ) + .inWorkingDirectory( excerciseRoot ) + .run(); + } + + // Bootstrap TestBox framework + filesystemUtil.createMapping( '/testbox', testBoxRoot ); + } + + /** + * Invoke TestBox to run the test suite + */ + private function runTests( boolean solution=false ) { + + // Create TestBox and run the tests + testData = new testbox.system.TestBox() + .runRaw( directory = { + // Find all CFCs... + mapping = filesystemUtil.makePathRelative( getCWD() ), + // ... in this directory ... + recurse = false, + // ... whose name ends in "test" + filter = function( path ) { + return path.reFind( ( solution ? 'Solution' : '' ) & 'Test.cfc$' ); + } + } ) + .getMemento(); + + // Print out the results with ANSI formatting for the CLI + getInstance( 'CLIRenderer@testbox-commands' ) + .render( print, testData, true ); + + print.toConsole(); + + // Set proper exit code + if( testData.totalFail || testData.totalError ) { + setExitCode( 1 ); + } + } + +} + diff --git a/exercises/practice/circular-buffer/box.json b/exercises/practice/circular-buffer/box.json new file mode 100644 index 0000000..4e5af44 --- /dev/null +++ b/exercises/practice/circular-buffer/box.json @@ -0,0 +1,8 @@ +{ + "dependencies":{ + "testbox":"^2.5.0+107" + }, + "installPaths":{ + "testbox":"testbox" + } +} \ No newline at end of file diff --git a/exercises/practice/circular-buffer/index.cfm b/exercises/practice/circular-buffer/index.cfm new file mode 100644 index 0000000..bb28af8 --- /dev/null +++ b/exercises/practice/circular-buffer/index.cfm @@ -0,0 +1,37 @@ + + + + + // get a list of all CFCs in this folder whose name looks like "XXXTest.cfc" + // And turn it into compnent path relative to the web root + url.bundles = directoryList( + path=expandPath( '/' ), + filter='*Test.cfc' ) + .map( function( path ) { + return path + .replaceNoCase( expandPath( '/' ), '' ) + .left( -4 ) + } ) + .toList(); + + + + + + + + Oops, you don't have TestBox installed yet! Please run box install from the root of your exercise folder and refresh this page. +