Skip to content

Commit fbe89d5

Browse files
Add circular-buffer exercise (#515)
1 parent e503b12 commit fbe89d5

File tree

7 files changed

+350
-0
lines changed

7 files changed

+350
-0
lines changed

config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,14 @@
10591059
"prerequisites": [],
10601060
"difficulty": 6
10611061
},
1062+
{
1063+
"slug": "circular-buffer",
1064+
"name": "Circular Buffer",
1065+
"uuid": "29e1dff8-b3bc-4b8a-b50b-bd05f9a9a42f",
1066+
"practices": [],
1067+
"prerequisites": [],
1068+
"difficulty": 6
1069+
},
10621070
{
10631071
"slug": "flower-field",
10641072
"name": "Flower Field",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Instructions
2+
3+
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.
4+
5+
A circular buffer first starts empty and of some predefined length.
6+
For example, this is a 7-element buffer:
7+
8+
```text
9+
[ ][ ][ ][ ][ ][ ][ ]
10+
```
11+
12+
Assume that a 1 is written into the middle of the buffer (exact starting location does not matter in a circular buffer):
13+
14+
```text
15+
[ ][ ][ ][1][ ][ ][ ]
16+
```
17+
18+
Then assume that two more elements are added — 2 & 3 — which get appended after the 1:
19+
20+
```text
21+
[ ][ ][ ][1][2][3][ ]
22+
```
23+
24+
If two elements are then removed from the buffer, the oldest values inside the buffer are removed.
25+
The two elements removed, in this case, are 1 & 2, leaving the buffer with just a 3:
26+
27+
```text
28+
[ ][ ][ ][ ][ ][3][ ]
29+
```
30+
31+
If the buffer has 7 elements then it is completely full:
32+
33+
```text
34+
[5][6][7][8][9][3][4]
35+
```
36+
37+
When the buffer is full an error will be raised, alerting the client that further writes are blocked until a slot becomes free.
38+
39+
When the buffer is full, the client can opt to overwrite the oldest data with a forced write.
40+
In this case, two more elements — A & B — are added and they overwrite the 3 & 4:
41+
42+
```text
43+
[5][6][7][8][9][A][B]
44+
```
45+
46+
3 & 4 have been replaced by A & B making 5 now the oldest data in the buffer.
47+
Finally, if two elements are removed then what would be returned is 5 & 6 yielding the buffer:
48+
49+
```text
50+
[ ][ ][7][8][9][A][B]
51+
```
52+
53+
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.
54+
7 is still the oldest element and the buffer is once again full.
55+
56+
```text
57+
[C][D][7][8][9][A][B]
58+
```
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"authors": [
3+
"keiravillekode"
4+
],
5+
"files": {
6+
"solution": [
7+
"circular_buffer.zig"
8+
],
9+
"test": [
10+
"test_circular_buffer.zig"
11+
],
12+
"example": [
13+
".meta/example.zig"
14+
]
15+
},
16+
"blurb": "A data structure that uses a single, fixed-size buffer as if it were connected end-to-end.",
17+
"source": "Wikipedia",
18+
"source_url": "https://en.wikipedia.org/wiki/Circular_buffer"
19+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
pub const BufferError = error{BufferOverflow};
2+
3+
pub fn CircularBuffer(comptime T: type, comptime capacity: usize) type {
4+
return struct {
5+
const Self = @This();
6+
7+
items: [capacity]T = undefined,
8+
read_index: usize = 0,
9+
write_index: usize = 0,
10+
11+
/// Initializes a CircularBuffer
12+
pub fn init() Self {
13+
return .{};
14+
}
15+
16+
/// Discards all items in the buffer.
17+
pub fn clear(self: *Self) void {
18+
self.write_index = self.read_index;
19+
}
20+
21+
/// Extracts the oldest item from the buffer.
22+
pub fn read(self: *Self) ?T {
23+
if (self.read_index == self.write_index) {
24+
return null;
25+
}
26+
const result = self.items[self.read_index];
27+
self.read_index += 1;
28+
if (self.read_index == capacity) {
29+
self.read_index -= capacity;
30+
self.write_index -= capacity;
31+
}
32+
return result;
33+
}
34+
35+
/// Write `item` into the buffer.
36+
pub fn write(self: *Self, item: T) BufferError!void {
37+
if (self.write_index == self.read_index + capacity) {
38+
return BufferError.BufferOverflow;
39+
}
40+
self.overwrite(item);
41+
}
42+
43+
/// Write `item` into the buffer, replacing the oldest item if necessary.
44+
pub fn overwrite(self: *Self, item: T) void {
45+
if (self.write_index == self.read_index + capacity) {
46+
_ = self.read();
47+
}
48+
self.items[self.write_index % capacity] = item;
49+
self.write_index += 1;
50+
}
51+
};
52+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[28268ed4-4ff3-45f3-820e-895b44d53dfa]
13+
description = "reading empty buffer should fail"
14+
15+
[2e6db04a-58a1-425d-ade8-ac30b5f318f3]
16+
description = "can read an item just written"
17+
18+
[90741fe8-a448-45ce-be2b-de009a24c144]
19+
description = "each item may only be read once"
20+
21+
[be0e62d5-da9c-47a8-b037-5db21827baa7]
22+
description = "items are read in the order they are written"
23+
24+
[2af22046-3e44-4235-bfe6-05ba60439d38]
25+
description = "full buffer can't be written to"
26+
27+
[547d192c-bbf0-4369-b8fa-fc37e71f2393]
28+
description = "a read frees up capacity for another write"
29+
30+
[04a56659-3a81-4113-816b-6ecb659b4471]
31+
description = "read position is maintained even across multiple writes"
32+
33+
[60c3a19a-81a7-43d7-bb0a-f07242b1111f]
34+
description = "items cleared out of buffer can't be read"
35+
36+
[45f3ae89-3470-49f3-b50e-362e4b330a59]
37+
description = "clear frees up capacity for another write"
38+
39+
[e1ac5170-a026-4725-bfbe-0cf332eddecd]
40+
description = "clear does nothing on empty buffer"
41+
42+
[9c2d4f26-3ec7-453f-a895-7e7ff8ae7b5b]
43+
description = "overwrite acts like write on non-full buffer"
44+
45+
[880f916b-5039-475c-bd5c-83463c36a147]
46+
description = "overwrite replaces the oldest item on full buffer"
47+
48+
[bfecab5b-aca1-4fab-a2b0-cd4af2b053c3]
49+
description = "overwrite replaces the oldest item remaining in buffer following a read"
50+
51+
[9cebe63a-c405-437b-8b62-e3fdc1ecec5a]
52+
description = "initial clear does not affect wrapping around"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
pub const BufferError = error{BufferOverflow};
2+
3+
pub fn CircularBuffer(comptime T: type, comptime capacity: usize) type {
4+
_ = capacity;
5+
return struct {
6+
const Self = @This();
7+
8+
// This struct, as well as its fields and methods, needs to be implemented.
9+
10+
/// Initializes a CircularBuffer
11+
pub fn init() Self {
12+
@compileError("please implement the init function");
13+
}
14+
15+
/// Discards all items in the buffer.
16+
pub fn clear(self: *Self) void {
17+
_ = self;
18+
@compileError("please implement the clear function");
19+
}
20+
21+
/// Extracts the oldest item from the buffer.
22+
pub fn read(self: *Self) ?T {
23+
_ = self;
24+
@compileError("please implement the read function");
25+
}
26+
27+
/// Write `item` into the buffer.
28+
pub fn write(self: *Self, item: T) BufferError!void {
29+
_ = self;
30+
_ = item;
31+
@compileError("please implement the write function");
32+
}
33+
34+
/// Write `item` into the buffer, replacing the oldest item if necessary.
35+
pub fn overwrite(self: *Self, item: T) void {
36+
_ = self;
37+
_ = item;
38+
@compileError("please implement the overwrite function");
39+
}
40+
};
41+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
const std = @import("std");
2+
const testing = std.testing;
3+
4+
const circular_buffer = @import("circular_buffer.zig");
5+
const CircularBuffer = circular_buffer.CircularBuffer;
6+
const BufferError = circular_buffer.BufferError;
7+
8+
test "reading empty buffer should fail" {
9+
var cb = CircularBuffer(i16, 1).init();
10+
try testing.expectEqual(null, cb.read());
11+
}
12+
13+
test "can read an item just written" {
14+
var cb = CircularBuffer(i16, 1).init();
15+
try cb.write(1);
16+
try testing.expectEqual(1, cb.read());
17+
}
18+
19+
test "each item may only be read once" {
20+
var cb = CircularBuffer(i16, 1).init();
21+
try cb.write(1);
22+
try testing.expectEqual(1, cb.read());
23+
try testing.expectEqual(null, cb.read());
24+
}
25+
26+
test "items are read in the order they are written" {
27+
var cb = CircularBuffer(i16, 2).init();
28+
try cb.write(1);
29+
try cb.write(2);
30+
try testing.expectEqual(1, cb.read());
31+
try testing.expectEqual(2, cb.read());
32+
}
33+
34+
test "full buffer can't be written to" {
35+
var cb = CircularBuffer(i16, 1).init();
36+
try cb.write(1);
37+
try testing.expectError(BufferError.BufferOverflow, cb.write(2));
38+
}
39+
40+
test "a read frees up capacity for another write" {
41+
var cb = CircularBuffer(i16, 1).init();
42+
try cb.write(1);
43+
try testing.expectEqual(1, cb.read());
44+
try cb.write(2);
45+
try testing.expectEqual(2, cb.read());
46+
}
47+
48+
test "read position is maintained even across multiple writes" {
49+
var cb = CircularBuffer(i16, 3).init();
50+
try cb.write(1);
51+
try cb.write(2);
52+
try testing.expectEqual(1, cb.read());
53+
try cb.write(3);
54+
try testing.expectEqual(2, cb.read());
55+
try testing.expectEqual(3, cb.read());
56+
}
57+
58+
test "items cleared out of buffer can't be read" {
59+
var cb = CircularBuffer(i16, 1).init();
60+
try cb.write(1);
61+
cb.clear();
62+
try testing.expectEqual(null, cb.read());
63+
}
64+
65+
test "clear frees up capacity for another write" {
66+
var cb = CircularBuffer(i16, 1).init();
67+
try cb.write(1);
68+
cb.clear();
69+
try cb.write(2);
70+
try testing.expectEqual(2, cb.read());
71+
}
72+
73+
test "clear does nothing on empty buffer" {
74+
var cb = CircularBuffer(i16, 1).init();
75+
cb.clear();
76+
try cb.write(1);
77+
try testing.expectEqual(1, cb.read());
78+
}
79+
80+
test "overwrite acts like write on non-full buffer" {
81+
var cb = CircularBuffer(i16, 2).init();
82+
try cb.write(1);
83+
cb.overwrite(2);
84+
try testing.expectEqual(1, cb.read());
85+
try testing.expectEqual(2, cb.read());
86+
}
87+
88+
test "overwrite replaces the oldest item on full buffer" {
89+
var cb = CircularBuffer(i16, 2).init();
90+
try cb.write(1);
91+
try cb.write(2);
92+
cb.overwrite(3);
93+
try testing.expectEqual(2, cb.read());
94+
try testing.expectEqual(3, cb.read());
95+
}
96+
97+
test "overwrite replaces the oldest item remaining in buffer following a read" {
98+
var cb = CircularBuffer(i16, 3).init();
99+
try cb.write(1);
100+
try cb.write(2);
101+
try cb.write(3);
102+
try testing.expectEqual(1, cb.read());
103+
try cb.write(4);
104+
cb.overwrite(5);
105+
try testing.expectEqual(3, cb.read());
106+
try testing.expectEqual(4, cb.read());
107+
try testing.expectEqual(5, cb.read());
108+
}
109+
110+
test "initial clear does not affect wrapping around" {
111+
var cb = CircularBuffer(i16, 2).init();
112+
cb.clear();
113+
try cb.write(1);
114+
try cb.write(2);
115+
cb.overwrite(3);
116+
cb.overwrite(4);
117+
try testing.expectEqual(3, cb.read());
118+
try testing.expectEqual(4, cb.read());
119+
try testing.expectEqual(null, cb.read());
120+
}

0 commit comments

Comments
 (0)