class CircularArray {
constructor(...args) {
const capacity = Math.max(16, 2 ** Math.ceil(Math.log2(args.length + 1)));
const valKind = args.length > 0 ? args[0] : 0;
const data = [valKind];
let i = 1;
while (i < args.length) data.push(args[i++]);
while (i++ < capacity) {
data.push(valKind);
}
this.data = data;
this.length = args.length;
this.offset = 0;
this.autoshrink = true;
}
static from(array) {
return new this(...array);
}
push(...args) {
if (args.length === 0) return 0;
this.maybeGrow(args.length);
let i = this.offset + this.length;
const mod = this.data.length - 1;
// TODO: let version to not skip holes in the array
for (const val of args) {
this.data[i++ & mod] = val;
}
return (this.length += args.length);
}
unshift(...args) {
if (args.length === 0) return 0;
this.maybeGrow(args.length);
const mod = this.data.length - 1;
const offset = (this.offset - args.length + this.data.length) & mod;
this.offset = offset;
for (let i = offset; i < args.length + offset; i++) {
this.data[i & mod] = args[i];
}
return (this.length += args.length);
}
pop() {
if (this.length > 0) {
// Note: we're not overwriting the removed element, so this
// is NOT a secure implementation of a circular buffer!
// 1. we need the last element of the buffer (so length-1),
// *and* we need to shrink the length by one because
// we're removing that element. So we might as well
// do --this.length to inline that operation.
// 2. capacity is always a power of two, so instead of using
// modulo we can us a bitmask: by subtracting one from
// the capacity (data.length) we get a 0b111... mask.
const idx = (--this.length + this.offset) & (this.data.length - 1);
const val = this.data[idx];
if (this.autoshrink) this.maybeShrink();
return val;
}
}
shift() {
if (this.length > 0) {
// Note: we're not overwriting the removed element, so this
// is NOT a secure implementation of a circular buffer!
// We have to:
// - return the first element of the buffer (so this.offset)
// - "remove" it from the buffer by:
// - increasing the offset by one
// - decreasing the length by one
const val = this.data[this.offset];
this.offset = (this.offset + 1) & (this.data.length - 1);
this.length--;
if (this.autoshrink) this.maybeShrink();
return val;
}
}
slice(begin = 0, end = this.length) {
begin = Math.max(begin | 0, 0);
end = Math.min(end | 0, this.length);
if (begin >= end) return new CircularArray();
const ret = [];
const { offset, data } = this;
const mod = data.length - 1;
for (let i = begin; i < end; i++) {
ret.push(data[(i + offset) & mod]);
}
return CircularArray.from(ret);
}
map(callback) {
const ret = [];
const { offset, length, data } = this;
const mod = data.length - 1;
for (let i = 0; i < length; i++) {
ret.push(callback(data[(i + offset) & mod], i, this));
}
return CircularArray.from(ret);
}
filter(callback) {
const ret = [];
const { offset, length, data } = this;
const mod = data.length - 1;
for (let i = 0; i < length; i++) {
const val = data[(i + offset) & mod];
if (callback(val, i, this)) ret.push(val);
}
return CircularArray.from(ret);
}
/**
* Convenience function to replace chained calls to `.map`,
* `.filter` and `.slice`, avoiding the allocation of
* intermediate arrays that are immediately discarded.
*
* The `callback` is passed a `SKIP` symbol as its first
* argument. To filter out an element, return this SKIP
* symbol. Example:
*
* ```js
* // extract `obj.value` from a given section of a circular
* // array, but filter out undefined entries of `obj`.
* const callback = (SKIP, obj) => obj ? obj.value : SKIP;
* const values = cArray.mapFilter(array, callback, begin, end);
* ```
*/
mapFilter(callback, begin = 0, end = this.length) {
const ret = [];
const { offset, data } = this;
const mod = data.length - 1;
const SKIP = Symbol("mapFilter.SKIP");
for (let i = begin; i < end; i++) {
const v = callback(SKIP, data[(i + offset) & mod], i);
if (v !== SKIP) ret.push(v);
}
return CircularArray.from(ret);
}
maybeGrow(addedLength) {
if (addedLength + this.length > this.data.length) {
// We have to grow our backing array to be able to
// hold more data. We double its size whenever
// it is filled to capacity.
let { data, length, offset } = this;
const capacity = data.length;
const valKind = data[offset];
for (let i = 0; i < offset; i++) {
data.push(valKind);
}
for (let i = offset; i < capacity; i++) {
data.push(data[i]);
}
offset += capacity;
}
}
maybeShrink() {
// We wish to shrink the backing array if the buffer gets too small,
// but we also want to avoid the situation where a poorly timed
// alternating push/pop will trigger constant re-allocation of
// the backing array. So instead of shrinking the array at
// half capacity (mimicking the doubling of growing
// the backing array in push) we shrink at 3/8 capacity, but shrink
// capacity in half. This results in the new buffer being 3/4
// full, reducing the odds of this disastrous re-allocation,
// while maintaining the fast bitmasked modulo behavior.
if (this.length < (this.data.length * 0.375 && this.data.length > 16)) {
const { data, length, offset } = this;
const valKind = data[offset];
const nData = [valKind];
const mod = data.length - 1;
for (let i = 1; i < length; i++) {
nData.push(data[(i + offset) & mod]);
}
for (let i = length; i < data.length >> 1; i++) {
nData.push(valKind);
}
this.data = nData;
this.offset = 0;
}
}
*[Symbol.iterator]() {
const { data, length, offset } = this;
const mod = data.length - 1;
for (let i = 0; i < length; i++) {
yield data[(i + offset) & mod];
}
}
}