1 module ringbuffer;
2 
3 /**
4 * a simple @safe and @nogc-compatible template ringbuffer.
5 * Authors: Susan
6 * Date: 2022-02-03
7 * Licence: AGPL-3.0 or later
8 * Copyright: Susan, 2022
9 * special thanks to my good friend flussence, whose advice while making this module was invaluable. <3
10 */
11 
12 struct RingBuffer(DataType, size_t maxLength)
13 {
14 	private size_t readIndex; //todo: size_t is overkill in 99.999% of cases
15 	private size_t writeIndex;
16 	private DataType[maxLength] data;
17 
18 	private auto _length() @safe @nogc pure nothrow const
19 	{
20 		//todo: there is almost certainly a better way to go about this
21 		if (writeIndex == readIndex)
22 		{
23 			return 0;
24 		}
25 
26 		immutable write = sanitize(writeIndex);
27 		immutable read = sanitize(readIndex);
28 
29 		if (write == read)
30 		{
31 			return maxLength;
32 		}
33 
34 		if (write > read)
35 		{
36 			return write - read;
37 		}
38 		else
39 		{
40 			return (write + maxLength) - read;
41 		}
42 	}
43 
44 	invariant(_length <= maxLength);
45 
46 	private auto next(in size_t rhs) @safe @nogc pure nothrow const
47 	{
48 		import std.math.traits : isPowerOf2;
49 		static if (isPowerOf2(maxLength))
50 		{
51 			return rhs & ((maxLength * 2) - 1);
52 		}
53 		else
54 		{
55 			return rhs % (maxLength * 2);
56 		}
57 	}
58 
59 	private auto sanitize(in size_t rhs) @safe @nogc pure nothrow const
60 	{
61 		import std.math.traits : isPowerOf2;
62 		static if (isPowerOf2(maxLength))
63 		{
64 			return rhs & (maxLength - 1);
65 		}
66 		else
67 		{
68 			return rhs % maxLength;
69 		}
70 	}
71 
72 	///
73 	@property auto length() @safe @nogc pure nothrow const
74 	{
75 		return _length;
76 	}
77 
78 	///
79 	@property auto capacity() @safe @nogc pure nothrow const
80 	{
81 		return maxLength - length;
82 	}
83 
84 	/// assignment
85 	void opAssign(in DataType[] rhs) @safe @nogc nothrow pure
86 	in (rhs.length <= maxLength)
87 	{
88 		data[0 .. rhs.length] = rhs;
89 		readIndex = 0;
90 		writeIndex = rhs.length;
91 	}
92 
93 	/// push to buffer
94 	void push(in DataType rhs) @safe @nogc pure nothrow
95 	{
96 		data[sanitize(writeIndex)] = rhs;
97 		writeIndex = next(writeIndex + 1);
98 	}
99 
100 	/// ditto
101 	void push(in DataType[] rhs) @safe @nogc pure nothrow
102 	{
103 		foreach(DataType d; rhs)
104 		{
105 			push(d);
106 		}
107 	}
108 
109 	/// ditto
110 	void opOpAssign(string op)(in DataType rhs) @safe @nogc nothrow pure
111 		if (op == "~")
112 	in (length + 1 <= maxLength)
113 	{
114 		push(rhs);
115 	}
116 
117 	/// ditto
118 	void opOpAssign(string op)(in DataType[] rhs) @safe @nogc nothrow pure
119 		if (op == "~")
120 	in (length + rhs.length <= maxLength)
121 	{
122 		push(rhs);
123 	}
124 
125 	/// retrieve item from buffer (fifo)
126 	DataType shift() @safe @nogc nothrow pure
127 	in (length > 0)
128 	{
129 		immutable result = data[sanitize(readIndex)];
130 		readIndex = next(readIndex + 1);
131 		return result;
132 	}
133 
134 	/// retrieve item from buffer (lifo)
135 	DataType pop() @safe @nogc nothrow pure
136 	in (length > 0)
137 	{
138 		writeIndex = next(writeIndex - 1);
139 		return data[sanitize(writeIndex)];
140 	}
141 
142 	/// empty the buffer
143 	void clear() @safe @nogc nothrow pure
144 	{
145 		data[] = DataType.init;
146 		writeIndex = readIndex;
147 	}
148 
149 	/// range interface
150 	bool empty() @safe @nogc nothrow pure const
151 	{
152 		return length == 0;
153 	}
154 
155 	/// ditto
156 	DataType front() @safe @nogc nothrow const pure
157 	{
158 		return data[sanitize(readIndex)];
159 	}
160 
161 	/// ditto
162 	void popFront() @safe @nogc nothrow pure
163 	{
164 		shift();
165 	}
166 }
167 
168 @safe @nogc nothrow unittest
169 {
170 	RingBuffer!(int, 8) foo;
171 	assert(foo.length == 0);
172 	assert(foo.capacity == 8);
173 
174 	foo.push(0); //0
175 	assert(foo.length == 1);
176 	assert(foo.capacity == 7);
177 
178 	import std.array : staticArray;
179 	import std.range : iota;
180 	immutable int[5] temp = staticArray!(iota(5));
181 
182 	foo ~= temp[1 .. 3]; //0, 1, 2
183 	foo.push(temp[3 .. 5]); //0, 1, 2, 3, 4
184 	assert(foo.length == 5);
185 	assert(foo.capacity == 3);
186 
187 	assert(foo.shift == 0); //1, 2, 3, 4
188 	assert(foo.pop == 4); //1, 2, 3
189 
190 	assert(foo.length == 3);
191 	assert(foo.capacity == 5);
192 
193 	foo.push(temp); //1, 2, 3, 0, 1, 2, 3, 4
194 	assert(foo.length == 8); //not empty. a nasty bug.
195 	assert(foo.capacity == 0);
196 	assert(foo.shift == 1);
197 
198 	foo.clear();
199 	assert(foo.length == 0);
200 	assert(foo.capacity == 8);
201 
202 	foo ~= temp;
203 	assert(foo.length == temp.length);
204 	assert(foo.shift == 0);
205 	assert(foo.pop == 4);
206 	assert(foo.length == 3);
207 	assert(foo.capacity == 5);
208 
209 	foo = temp;
210 	assert(foo.shift == 0);
211 	assert(foo.pop == 4);
212 	assert(foo.length == 3);
213 	assert(foo.capacity == 5);
214 
215 	foreach(i; 0 .. 100) //one last stomp
216 	{
217 		foo.push(i);
218 		foo.shift;
219 	}
220 
221 	// range test
222 	int i;
223 	foreach(val; foo)
224 	{
225 		switch(i)
226 		{
227 			case 0: assert(val == 97); break;
228 			case 1: assert(val == 98); break;
229 			case 2: assert(val == 99); break;
230 			default: assert(false);
231 		}
232 
233 		++i;
234 	}
235 }
236 
237 /// example
238 @safe @nogc nothrow unittest
239 {
240 	RingBuffer!(int, 5) buff;
241 	// non-power-of-two lengths are supported, though powers of two will be faster
242 	buff.push(69);
243 	buff ~= 420; // equivilent to the push syntax
244 	assert(buff.shift == 69);
245 	assert(buff.shift == 420);
246 
247 	import std.array : staticArray;
248 	import std.range : iota;
249 	immutable int[5] temp = staticArray!(iota(5));
250 
251 	buff.push(temp); // multiple items may be pushed in a single call
252 
253 	assert(buff.length == 5);
254 	assert(buff.capacity == 0);
255 
256 	assert(buff.pop == 4);
257 
258 	assert(buff.length == 4);
259 	assert(buff.capacity == 1);
260 
261 	buff.clear();
262 
263 	assert(buff.length == 0);
264 	assert(buff.capacity == 5);
265 }