Skip to content

Commit be5c60a

Browse files
ernestognwclaude
andcommitted
Add values(start, end) slice accessor to DoubleEndedQueue
Mirrors the paginated `values` accessor in `EnumerableSet`. Clamps `end` to `length(deque)` and `start` to `end` (both with `Math.min`), then reads the slice through the wraparound-aware `_begin` offset. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cd05883 commit be5c60a

3 files changed

Lines changed: 67 additions & 0 deletions

File tree

.changeset/noisy-dragons-paint.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+
`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.

contracts/utils/structs/DoubleEndedQueue.sol

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
pragma solidity ^0.8.20;
55

6+
import {Math} from "../math/Math.sol";
67
import {Panic} from "../Panic.sol";
78

89
/**
@@ -209,6 +210,31 @@ library DoubleEndedQueue {
209210
}
210211
}
211212

213+
/**
214+
* @dev Return a slice of the queue in an array, with the first item at `start` (inclusive) and the last item at
215+
* `end` (exclusive). Out-of-bound values for `start` and `end` are clamped to the queue length.
216+
*
217+
* WARNING: This operation will copy a portion of the storage to memory, which can be quite expensive. This is
218+
* designed to mostly be used by view accessors that are queried without any gas fees. Developers should keep in
219+
* mind that this function has an unbounded cost, and using it as part of a state-changing function may render the
220+
* function uncallable if the queue grows to a point where copying to memory consumes too much gas to fit in a
221+
* block.
222+
*/
223+
function values(Bytes32Deque storage deque, uint256 start, uint256 end) internal view returns (bytes32[] memory) {
224+
unchecked {
225+
end = Math.min(end, length(deque));
226+
start = Math.min(start, end);
227+
228+
uint256 len = end - start;
229+
bytes32[] memory result = new bytes32[](len);
230+
uint128 begin = deque._begin;
231+
for (uint256 i = 0; i < len; ++i) {
232+
result[i] = deque._data[begin + uint128(start + i)];
233+
}
234+
return result;
235+
}
236+
}
237+
212238
/**
213239
* @dev Resets the queue back to being empty.
214240
*

test/utils/structs/DoubleEndedQueue.test.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ describe('DoubleEndedQueue', function () {
5454

5555
await expect(this.getContent()).to.eventually.have.ordered.members([bytesA, bytesB]);
5656
});
57+
58+
it('values returns empty array', async function () {
59+
await expect(this.mock.$values(0, 0, 0)).to.eventually.deep.equal([]);
60+
await expect(this.mock.$values(0, 0, 10)).to.eventually.deep.equal([]);
61+
await expect(this.mock.$values(0, 5, 10)).to.eventually.deep.equal([]);
62+
});
5763
});
5864

5965
describe('when not empty', function () {
@@ -140,5 +146,35 @@ describe('DoubleEndedQueue', function () {
140146
await expect(this.mock.$empty(0)).to.eventually.be.true;
141147
await expect(this.getContent()).to.eventually.have.ordered.members([]);
142148
});
149+
150+
describe('values', function () {
151+
it('returns the full content for [0, length)', async function () {
152+
await expect(this.mock.$values(0, 0, this.content.length)).to.eventually.deep.equal(this.content);
153+
});
154+
155+
it('paginates across all begin/end combinations', async function () {
156+
for (const begin of [0, 1, 2, 3, 4])
157+
for (const end of [0, 1, 2, 3, 4]) {
158+
await expect(this.mock.$values(0, begin, end)).to.eventually.deep.equal(this.content.slice(begin, end));
159+
}
160+
});
161+
162+
it('clamps end to length', async function () {
163+
await expect(this.mock.$values(0, 0, ethers.MaxUint256)).to.eventually.deep.equal(this.content);
164+
await expect(this.mock.$values(0, 1, ethers.MaxUint256)).to.eventually.deep.equal(this.content.slice(1));
165+
});
166+
167+
it('clamps start to end', async function () {
168+
await expect(this.mock.$values(0, ethers.MaxUint256, ethers.MaxUint256)).to.eventually.deep.equal([]);
169+
await expect(this.mock.$values(0, 2, 1)).to.eventually.deep.equal([]);
170+
});
171+
172+
it('reflects pushFront/pushBack ordering (wraparound indices)', async function () {
173+
await this.mock.$pushFront(0, bytesD);
174+
const expected = [bytesD, ...this.content];
175+
await expect(this.mock.$values(0, 0, expected.length)).to.eventually.deep.equal(expected);
176+
await expect(this.mock.$values(0, 1, 3)).to.eventually.deep.equal(expected.slice(1, 3));
177+
});
178+
});
143179
});
144180
});

0 commit comments

Comments
 (0)