diff --git a/.changeset/noisy-dragons-paint.md b/.changeset/noisy-dragons-paint.md new file mode 100644 index 00000000000..92413abe841 --- /dev/null +++ b/.changeset/noisy-dragons-paint.md @@ -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. diff --git a/contracts/utils/structs/DoubleEndedQueue.sol b/contracts/utils/structs/DoubleEndedQueue.sol index 1d431d2edbe..83163fd718b 100644 --- a/contracts/utils/structs/DoubleEndedQueue.sol +++ b/contracts/utils/structs/DoubleEndedQueue.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; +import {Math} from "../math/Math.sol"; import {Panic} from "../Panic.sol"; /** @@ -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. * diff --git a/test/utils/structs/DoubleEndedQueue.test.js b/test/utils/structs/DoubleEndedQueue.test.js index 6f8235ccdd3..ba235616473 100644 --- a/test/utils/structs/DoubleEndedQueue.test.js +++ b/test/utils/structs/DoubleEndedQueue.test.js @@ -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 () { @@ -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)); + }); + }); }); });