Skip to content

Commit 9ecdee5

Browse files
committed
add circular buffer
1 parent 088fa8c commit 9ecdee5

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

.changeset/cold-cheetahs-check.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'openzeppelin-solidity': minor
3+
---
4+
5+
`CircularBuffer`: add a datastructure that stored the last N values pushed to it.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import {Math} from "../math/Math.sol";
5+
import {Arrays} from "../Arrays.sol";
6+
import {Panic} from "../Panic.sol";
7+
8+
library CircularBuffer {
9+
struct Bytes32CircularBuffer {
10+
uint256 index;
11+
bytes32[] data;
12+
}
13+
14+
function setup(Bytes32CircularBuffer storage self, uint256 length) internal {
15+
clear(self);
16+
Arrays.unsafeSetLength(self.data, length);
17+
}
18+
19+
function clear(Bytes32CircularBuffer storage self) internal {
20+
self.index = 0;
21+
}
22+
23+
function push(Bytes32CircularBuffer storage self, bytes32 value) internal {
24+
uint256 index = self.index++;
25+
uint256 length = self.data.length;
26+
Arrays.unsafeAccess(self.data, index % length).value = value;
27+
}
28+
29+
function count(Bytes32CircularBuffer storage self) internal view returns (uint256) {
30+
return Math.min(self.index, self.data.length);
31+
}
32+
33+
function size(Bytes32CircularBuffer storage self) internal view returns (uint256) {
34+
return self.data.length;
35+
}
36+
37+
function last(Bytes32CircularBuffer storage self, uint256 i) internal view returns (bytes32) {
38+
uint256 index = self.index;
39+
if (index <= i) {
40+
Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
41+
}
42+
return Arrays.unsafeAccess(self.data, (index - i - 1) % self.data.length).value;
43+
}
44+
45+
function includes(Bytes32CircularBuffer storage self, bytes32 value) internal view returns (bool) {
46+
uint256 index = self.index;
47+
uint256 length = self.data.length;
48+
for (uint256 i = 1; i <= length; ++i) {
49+
if (i > index) {
50+
return false;
51+
} else if (Arrays.unsafeAccess(self.data, (index - i) % length).value == value) {
52+
return true;
53+
}
54+
}
55+
return false;
56+
}
57+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const { ethers } = require('hardhat');
2+
const { expect } = require('chai');
3+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
4+
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');
5+
6+
const { generators } = require('../../helpers/random');
7+
8+
const LENGTH = 4;
9+
10+
async function fixture() {
11+
const mock = await ethers.deployContract('$CircularBuffer');
12+
await mock.$setup(0, LENGTH);
13+
return { mock };
14+
}
15+
16+
describe('CircularBuffer', function () {
17+
beforeEach(async function () {
18+
Object.assign(this, await loadFixture(fixture));
19+
});
20+
21+
it('starts empty', async function () {
22+
expect(await this.mock.$count(0)).to.equal(0n);
23+
expect(await this.mock.$size(0)).to.equal(LENGTH);
24+
await expect(this.mock.$last(0, 0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
25+
});
26+
27+
it('push', async function () {
28+
const values = Array.from({ length: LENGTH + 3 }, generators.bytes32);
29+
30+
for (const [i, value] of values.map((v, i) => [i, v])) {
31+
// push value
32+
await this.mock.$push(0, value);
33+
34+
// view of the values
35+
const pushed = values.slice(0, i + 1);
36+
const stored = pushed.slice(-LENGTH);
37+
const dropped = pushed.slice(0, -LENGTH);
38+
39+
// check count
40+
expect(await this.mock.$size(0)).to.equal(LENGTH);
41+
expect(await this.mock.$count(0)).to.equal(stored.length);
42+
43+
// check last
44+
for (const i in stored) {
45+
expect(await this.mock.$last(0, i)).to.equal(stored.at(-i - 1));
46+
}
47+
48+
// check included and non-included values
49+
for (const v of stored) {
50+
expect(await this.mock.$includes(0, v)).to.be.true;
51+
}
52+
for (const v of dropped) {
53+
expect(await this.mock.$includes(0, v)).to.be.false;
54+
}
55+
}
56+
});
57+
58+
it('clear', async function () {
59+
await this.mock.$push(0, generators.bytes32());
60+
61+
expect(await this.mock.$count(0)).to.equal(1n);
62+
expect(await this.mock.$size(0)).to.equal(LENGTH);
63+
await this.mock.$last(0, 0); // not revert
64+
65+
await this.mock.$clear(0);
66+
67+
expect(await this.mock.$count(0)).to.equal(0n);
68+
expect(await this.mock.$size(0)).to.equal(LENGTH);
69+
await expect(this.mock.$last(0, 0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
70+
});
71+
});

0 commit comments

Comments
 (0)