Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/noisy-dragons-paint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`DoubleEndedQueue`: Add `values(deque, start, end)` to return a slice of the queue as an array, mirroring the paginated `values` accessor in `EnumerableSet`. Out-of-bound values for `start` and `end` are clamped to the queue length.
27 changes: 27 additions & 0 deletions contracts/utils/structs/DoubleEndedQueue.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

pragma solidity ^0.8.20;

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

/**
Expand Down Expand Up @@ -209,6 +210,32 @@ library DoubleEndedQueue {
}
}

/**
* @dev Return a slice of the queue in an array, with the first item at `start` (inclusive) and the last item at
* `end` (exclusive). Out-of-bound values for `start` and `end` are clamped to the queue length.
*
* WARNING: This operation will copy a portion of the storage to memory, which can be quite expensive. This is
* designed to mostly be used by view accessors that are queried without any gas fees. Developers should keep in
* mind that this function has an unbounded cost, and using it as part of a state-changing function may render the
* function uncallable if the queue grows to a point where copying to memory consumes too much gas to fit in a
* block.
*/
function values(Bytes32Deque storage deque, uint256 start, uint256 end) internal view returns (bytes32[] memory) {
unchecked {
end = Math.min(end, length(deque));
start = Math.min(start, end);

uint256 len = end - start;
bytes32[] memory result = new bytes32[](len);

uint128 offset = deque._begin + uint128(start);
for (uint128 i = 0; i < len; ++i) {
result[i] = deque._data[offset + i];
}
return result;
}
}

/**
* @dev Resets the queue back to being empty.
*
Expand Down
36 changes: 36 additions & 0 deletions test/utils/structs/DoubleEndedQueue.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ describe('DoubleEndedQueue', function () {

await expect(this.getContent()).to.eventually.have.ordered.members([bytesA, bytesB]);
});

it('values returns empty array', async function () {
await expect(this.mock.$values(0, 0, 0)).to.eventually.deep.equal([]);
await expect(this.mock.$values(0, 0, 10)).to.eventually.deep.equal([]);
await expect(this.mock.$values(0, 5, 10)).to.eventually.deep.equal([]);
});
});

describe('when not empty', function () {
Expand Down Expand Up @@ -140,5 +146,35 @@ describe('DoubleEndedQueue', function () {
await expect(this.mock.$empty(0)).to.eventually.be.true;
await expect(this.getContent()).to.eventually.have.ordered.members([]);
});

describe('values', function () {
it('returns the full content for [0, length)', async function () {
await expect(this.mock.$values(0, 0, this.content.length)).to.eventually.deep.equal(this.content);
});

it('paginates across all begin/end combinations', async function () {
for (const begin of [0, 1, 2, 3, 4])
for (const end of [0, 1, 2, 3, 4]) {
await expect(this.mock.$values(0, begin, end)).to.eventually.deep.equal(this.content.slice(begin, end));
}
});

it('clamps end to length', async function () {
await expect(this.mock.$values(0, 0, ethers.MaxUint256)).to.eventually.deep.equal(this.content);
await expect(this.mock.$values(0, 1, ethers.MaxUint256)).to.eventually.deep.equal(this.content.slice(1));
});

it('clamps start to end', async function () {
await expect(this.mock.$values(0, ethers.MaxUint256, ethers.MaxUint256)).to.eventually.deep.equal([]);
await expect(this.mock.$values(0, 2, 1)).to.eventually.deep.equal([]);
});

it('reflects pushFront/pushBack ordering (wraparound indices)', async function () {
await this.mock.$pushFront(0, bytesD);
const expected = [bytesD, ...this.content];
await expect(this.mock.$values(0, 0, expected.length)).to.eventually.deep.equal(expected);
await expect(this.mock.$values(0, 1, 3)).to.eventually.deep.equal(expected.slice(1, 3));
});
});
});
});