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 }