Add CircularBuffer data structure (#4913)
Co-authored-by: ernestognw <ernestognw@gmail.com>
This commit is contained in:
@ -455,6 +455,7 @@ library Arrays {
|
||||
* WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
|
||||
*/
|
||||
function unsafeSetLength(address[] storage array, uint256 len) internal {
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
sstore(array.slot, len)
|
||||
}
|
||||
@ -466,6 +467,7 @@ library Arrays {
|
||||
* WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
|
||||
*/
|
||||
function unsafeSetLength(bytes32[] storage array, uint256 len) internal {
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
sstore(array.slot, len)
|
||||
}
|
||||
@ -477,6 +479,7 @@ library Arrays {
|
||||
* WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
|
||||
*/
|
||||
function unsafeSetLength(uint256[] storage array, uint256 len) internal {
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
sstore(array.slot, len)
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
|
||||
* {EnumerableMap}: A type like Solidity's https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`], but with key-value _enumeration_: this will let you know how many entries a mapping has, and iterate over them (which is not possible with `mapping`).
|
||||
* {EnumerableSet}: Like {EnumerableMap}, but for https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets]. Can be used to store privileged accounts, issued IDs, etc.
|
||||
* {DoubleEndedQueue}: An implementation of a https://en.wikipedia.org/wiki/Double-ended_queue[double ended queue] whose values can be removed added or remove from both sides. Useful for FIFO and LIFO structures.
|
||||
* {CircularBuffer}: A data structure to store the last N values pushed to it.
|
||||
* {Checkpoints}: A data structure to store values mapped to an strictly increasing key. Can be used for storing and accessing values over time.
|
||||
* {MerkleTree}: A library with https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures and helper functions.
|
||||
* {Create2}: Wrapper around the https://blog.openzeppelin.com/getting-the-most-out-of-create2/[`CREATE2` EVM opcode] for safe use without having to deal with low-level assembly.
|
||||
@ -95,6 +96,8 @@ Ethereum contracts have no native concept of an interface, so applications must
|
||||
|
||||
{{DoubleEndedQueue}}
|
||||
|
||||
{{CircularBuffer}}
|
||||
|
||||
{{Checkpoints}}
|
||||
|
||||
{{MerkleTree}}
|
||||
|
||||
130
contracts/utils/structs/CircularBuffer.sol
Normal file
130
contracts/utils/structs/CircularBuffer.sol
Normal file
@ -0,0 +1,130 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* @dev A fixed-size buffer for keeping `bytes32` items in storage.
|
||||
*
|
||||
* This data structure allows for pushing elements to it, and when its length exceeds the specified fixed size,
|
||||
* new items take the place of the oldest element in the buffer, keeping at most `N` elements in the
|
||||
* structure.
|
||||
*
|
||||
* Elements can't be removed but the data structure can be cleared. See {clear}.
|
||||
*
|
||||
* Complexity:
|
||||
* - insertion ({push}): O(1)
|
||||
* - lookup ({last}): O(1)
|
||||
* - inclusion ({includes}): O(N) (worst case)
|
||||
* - reset ({clear}): O(1)
|
||||
*
|
||||
* * The struct is called `Bytes32CircularBuffer`. Other types can be cast to and from `bytes32`. This data structure
|
||||
* can only be used in storage, and not in memory.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* ```solidity
|
||||
* contract Example {
|
||||
* // Add the library methods
|
||||
* using CircularBuffer for CircularBuffer.Bytes32CircularBuffer;
|
||||
*
|
||||
* // Declare a buffer storage variable
|
||||
* CircularBuffer.Bytes32CircularBuffer private myBuffer;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
library CircularBuffer {
|
||||
/**
|
||||
* @dev Counts the number of items that have been pushed to the buffer. The residuo modulo _data.length indicates
|
||||
* where the next value should be stored.
|
||||
*
|
||||
* Struct members have an underscore prefix indicating that they are "private" and should not be read or written to
|
||||
* directly. Use the functions provided below instead. Modifying the struct manually may violate assumptions and
|
||||
* lead to unexpected behavior.
|
||||
*
|
||||
* The last item is at data[(index - 1) % data.length] and the last item is at data[index % data.length]. This
|
||||
* range can wrap around.
|
||||
*/
|
||||
struct Bytes32CircularBuffer {
|
||||
uint256 _count;
|
||||
bytes32[] _data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Initialize a new CircularBuffer of given size.
|
||||
*
|
||||
* If the CircularBuffer was already setup and used, calling that function again will reset it to a blank state.
|
||||
*
|
||||
* NOTE: The size of the buffer will affect the execution of {includes} function, as it has a complexity of O(N).
|
||||
* Consider a large buffer size may render the function unusable.
|
||||
*/
|
||||
function setup(Bytes32CircularBuffer storage self, uint256 size) internal {
|
||||
clear(self);
|
||||
Arrays.unsafeSetLength(self._data, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Clear all data in the buffer without resetting memory, keeping the existing size.
|
||||
*/
|
||||
function clear(Bytes32CircularBuffer storage self) internal {
|
||||
self._count = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Push a new value to the buffer. If the buffer is already full, the new value replaces the oldest value in
|
||||
* the buffer.
|
||||
*/
|
||||
function push(Bytes32CircularBuffer storage self, bytes32 value) internal {
|
||||
uint256 index = self._count++;
|
||||
uint256 modulus = self._data.length;
|
||||
Arrays.unsafeAccess(self._data, index % modulus).value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Number of values currently in the buffer. This value is 0 for an empty buffer, and cannot exceed the size of
|
||||
* the buffer.
|
||||
*/
|
||||
function count(Bytes32CircularBuffer storage self) internal view returns (uint256) {
|
||||
return Math.min(self._count, self._data.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Length of the buffer. This is the maximum number of elements kepts in the buffer.
|
||||
*/
|
||||
function length(Bytes32CircularBuffer storage self) internal view returns (uint256) {
|
||||
return self._data.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Getter for the i-th value in the buffer, from the end.
|
||||
*
|
||||
* Reverts with {Panic-ARRAY_OUT_OF_BOUNDS} if trying to access an element that was not pushed, or that was
|
||||
* dropped to make room for newer elements.
|
||||
*/
|
||||
function last(Bytes32CircularBuffer storage self, uint256 i) internal view returns (bytes32) {
|
||||
uint256 index = self._count;
|
||||
uint256 modulus = self._data.length;
|
||||
uint256 total = Math.min(index, modulus); // count(self)
|
||||
if (i >= total) {
|
||||
Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
|
||||
}
|
||||
return Arrays.unsafeAccess(self._data, (index - i - 1) % modulus).value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Check if a given value is in the buffer.
|
||||
*/
|
||||
function includes(Bytes32CircularBuffer storage self, bytes32 value) internal view returns (bool) {
|
||||
uint256 index = self._count;
|
||||
uint256 modulus = self._data.length;
|
||||
uint256 total = Math.min(index, modulus); // count(self)
|
||||
for (uint256 i = 0; i < total; ++i) {
|
||||
if (Arrays.unsafeAccess(self._data, (index - i - 1) % modulus).value == value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user