Skip to content

Commit

Permalink
add circular buffer
Browse files Browse the repository at this point in the history
  • Loading branch information
Amxx committed Feb 20, 2024
1 parent 088fa8c commit 9ecdee5
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-cheetahs-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`CircularBuffer`: add a datastructure that stored the last N values pushed to it.
57 changes: 57 additions & 0 deletions contracts/utils/structs/CircularBuffer.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Math} from "../math/Math.sol";
import {Arrays} from "../Arrays.sol";
import {Panic} from "../Panic.sol";

library CircularBuffer {
struct Bytes32CircularBuffer {
uint256 index;
bytes32[] data;
}

function setup(Bytes32CircularBuffer storage self, uint256 length) internal {
clear(self);
Arrays.unsafeSetLength(self.data, length);
}

function clear(Bytes32CircularBuffer storage self) internal {
self.index = 0;
}

function push(Bytes32CircularBuffer storage self, bytes32 value) internal {
uint256 index = self.index++;
uint256 length = self.data.length;
Arrays.unsafeAccess(self.data, index % length).value = value;
}

function count(Bytes32CircularBuffer storage self) internal view returns (uint256) {
return Math.min(self.index, self.data.length);
}

function size(Bytes32CircularBuffer storage self) internal view returns (uint256) {
return self.data.length;
}

function last(Bytes32CircularBuffer storage self, uint256 i) internal view returns (bytes32) {
uint256 index = self.index;
if (index <= i) {
Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
}
return Arrays.unsafeAccess(self.data, (index - i - 1) % self.data.length).value;
}

function includes(Bytes32CircularBuffer storage self, bytes32 value) internal view returns (bool) {
uint256 index = self.index;
uint256 length = self.data.length;
for (uint256 i = 1; i <= length; ++i) {
if (i > index) {
return false;
} else if (Arrays.unsafeAccess(self.data, (index - i) % length).value == value) {
return true;
}
}
return false;
}
}
71 changes: 71 additions & 0 deletions test/utils/structs/CircularBuffer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');

const { generators } = require('../../helpers/random');

const LENGTH = 4;

async function fixture() {
const mock = await ethers.deployContract('$CircularBuffer');
await mock.$setup(0, LENGTH);
return { mock };
}

describe('CircularBuffer', function () {
beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});

it('starts empty', async function () {
expect(await this.mock.$count(0)).to.equal(0n);
expect(await this.mock.$size(0)).to.equal(LENGTH);
await expect(this.mock.$last(0, 0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
});

it('push', async function () {
const values = Array.from({ length: LENGTH + 3 }, generators.bytes32);

for (const [i, value] of values.map((v, i) => [i, v])) {
// push value
await this.mock.$push(0, value);

// view of the values
const pushed = values.slice(0, i + 1);
const stored = pushed.slice(-LENGTH);
const dropped = pushed.slice(0, -LENGTH);

// check count
expect(await this.mock.$size(0)).to.equal(LENGTH);
expect(await this.mock.$count(0)).to.equal(stored.length);

// check last
for (const i in stored) {
expect(await this.mock.$last(0, i)).to.equal(stored.at(-i - 1));
}

// check included and non-included values
for (const v of stored) {
expect(await this.mock.$includes(0, v)).to.be.true;
}
for (const v of dropped) {
expect(await this.mock.$includes(0, v)).to.be.false;
}
}
});

it('clear', async function () {
await this.mock.$push(0, generators.bytes32());

expect(await this.mock.$count(0)).to.equal(1n);
expect(await this.mock.$size(0)).to.equal(LENGTH);
await this.mock.$last(0, 0); // not revert

await this.mock.$clear(0);

expect(await this.mock.$count(0)).to.equal(0n);
expect(await this.mock.$size(0)).to.equal(LENGTH);
await expect(this.mock.$last(0, 0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS);
});
});

0 comments on commit 9ecdee5

Please sign in to comment.