Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d78ae17
Migrate 'arrays'
RenanSouza2 Nov 12, 2023
52b3bd8
fix findUpperBound and add findLowerBound
Amxx Nov 17, 2023
9a1411b
add memory variants
Amxx Nov 20, 2023
c4726c8
Merge branch 'master' into feature/array-bound-with-duplicates
Amxx Jan 18, 2024
a6ec616
fix merge
Amxx Jan 18, 2024
23e8db9
fix lint
Amxx Jan 18, 2024
c72591f
minimize change
Amxx Jan 18, 2024
9162e42
add changeset
Amxx Jan 18, 2024
ed1de5b
Apply suggestions from code review
Amxx Jan 18, 2024
4c1c7f4
add Arrays.sort
Amxx Jan 18, 2024
abd07b6
add sort test
Amxx Jan 18, 2024
1e89815
fix lint
Amxx Jan 18, 2024
b73989d
codespell
Amxx Jan 19, 2024
79bf367
add fuzzing tests for Arrays.sort
Amxx Jan 19, 2024
f2d49ef
add unsafeMemoryAccess tests
Amxx Jan 22, 2024
c043453
Merge branch 'master' into feature/quicksort
Amxx Jan 29, 2024
c75de32
fix lint
Amxx Jan 29, 2024
f823bee
lint
Amxx Jan 30, 2024
c90f12b
Update contracts/utils/Arrays.sol
Amxx Feb 2, 2024
180a969
Update contracts/utils/Arrays.sol
Amxx Feb 2, 2024
708972f
Apply suggestions from code review
Amxx Feb 2, 2024
533e6cd
Merge branch 'master' into feature/quicksort
Amxx Feb 2, 2024
3f1f0a5
Update contracts/utils/Arrays.sol
ernestognw Feb 2, 2024
5a0ad7f
Add comments to `_quickSort`
ernestognw Feb 3, 2024
a8e6f54
Lint
ernestognw Feb 3, 2024
7600291
Update contracts/utils/Arrays.sol
Amxx Feb 5, 2024
6f163d2
Update contracts/utils/Arrays.sol
Amxx Feb 5, 2024
8983066
cache the pivot and improve doc
Amxx Feb 5, 2024
8704763
Apply suggestions from code review
Amxx Feb 5, 2024
4336e2e
Merge branch 'master' into feature/quicksort
Amxx Feb 6, 2024
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
Prev Previous commit
Next Next commit
add memory variants
  • Loading branch information
Amxx committed Nov 20, 2023
commit 9a1411bda117f35cbea507d56f7e60ed6e469363
20 changes: 16 additions & 4 deletions contracts/mocks/ArraysMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,24 @@ contract Uint256ArraysMock {
_array = array;
}

function findUpperBound(uint256 element) external view returns (uint256) {
return _array.findUpperBound(element);
function findUpperBound(uint256 value) external view returns (uint256) {
return _array.findUpperBound(value);
}

function findLowerBound(uint256 element) external view returns (uint256) {
return _array.findLowerBound(element);
function lowerBound(uint256 value) external view returns (uint256) {
return _array.lowerBound(value);
}

function upperBound(uint256 value) external view returns (uint256) {
return _array.upperBound(value);
}

function lowerBoundMemory(uint256[] memory array, uint256 value) external pure returns (uint256) {
return array.lowerBoundMemory(value);
}

function upperBoundMemory(uint256[] memory array, uint256 value) external pure returns (uint256) {
return array.upperBoundMemory(value);
}

function unsafeAccess(uint256 pos) external view returns (uint256) {
Expand Down
154 changes: 141 additions & 13 deletions contracts/utils/Arrays.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ library Arrays {
*
* `array` is expected to be sorted in ascending order, and to contain no
* repeated elements.
*
* Deprecated in favor of `lowerBound` and `findUpperBound`. Note that this actually implements a lower bound
* search, and should be replaced with `lowerBound`
*/
function findUpperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {
uint256 low = 0;
Expand All @@ -29,6 +32,44 @@ library Arrays {
return 0;
}

while (low < high) {
uint256 mid = Math.average(low, high);

// Note that mid will always be strictly less than high (i.e. it will be a valid array index)
// because Math.average rounds towards zero (it does integer division with truncation).
if (unsafeAccess(array, mid).value > element) {
high = mid;
} else {
low = mid + 1;
}
}

// At this point `low` is the exclusive upper bound. We will return the inclusive upper bound.
if (low > 0 && unsafeAccess(array, low - 1).value == element) {
return low - 1;
} else {
return low;
}
}

/**
* @dev Searches a sorted `array` and returns the first index that contains
* a value greater or equal to `element`. If no such index exists (i.e. all
* values in the array are strictly less than `element`), the array length is
* returned. Time complexity O(log n).
*
* `array` is expected to be sorted in ascending order,
*
* See https://en.cppreference.com/w/cpp/algorithm/ranges/lower_bound
*/
function lowerBound(uint256[] storage array, uint256 element) internal view returns (uint256) {
uint256 low = 0;
uint256 high = array.length;

if (high == 0) {
return 0;
}

while (low < high) {
uint256 mid = Math.average(low, high);

Expand All @@ -48,24 +89,111 @@ library Arrays {
}

/**
* @dev Searches a sorted `array` and returns the last index that contains
* a value smaller or equal to `element`. If no such index exists (i.e. all
* values in the array are strictly greater than `element`), the array length is
* @dev Searches a sorted `array` and returns the first index that contains
* a value strictly greater to `element`. If no such index exists (i.e. all
* values in the array are strictly less than `element`), the array length is
* returned. Time complexity O(log n).
*
* `array` is expected to be sorted in ascending order, and to contain no
* repeated elements.
* `array` is expected to be sorted in ascending order,
*
* See https://en.cppreference.com/w/cpp/algorithm/upper_bound
*/
function findLowerBound(uint256[] storage array, uint256 element) internal view returns (uint256) {
// following math cannot overflow
unchecked {
uint256 length = array.length;
if (element == type(uint256).max) {
return length == 0 ? 0 : length - 1;
function upperBound(uint256[] storage array, uint256 element) internal view returns (uint256) {
uint256 low = 0;
uint256 high = array.length;

if (high == 0) {
return 0;
}

while (low < high) {
uint256 mid = Math.average(low, high);

// Note that mid will always be strictly less than high (i.e. it will be a valid array index)
// because Math.average rounds towards zero (it does integer division with truncation).
if (unsafeAccess(array, mid).value > element) {
high = mid;
} else {
// this cannot overflow because mid < high
unchecked {
low = mid + 1;
}
}
uint256 upperBoundForNext = findUpperBound(array, element + 1);
return upperBoundForNext == 0 ? length : upperBoundForNext - 1;
}

return low;
}

/**
* @dev Searches a sorted `array` and returns the first index that contains
* a value greater or equal to `element`. If no such index exists (i.e. all
* values in the array are strictly less than `element`), the array length is
* returned. Time complexity O(log n).
*
* `array` is expected to be sorted in ascending order,
*
* See https://en.cppreference.com/w/cpp/algorithm/ranges/lower_bound
*/
function lowerBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {
uint256 low = 0;
uint256 high = array.length;

if (high == 0) {
return 0;
}

while (low < high) {
uint256 mid = Math.average(low, high);

// Note that mid will always be strictly less than high (i.e. it will be a valid array index)
// because Math.average rounds towards zero (it does integer division with truncation).
if (unsafeMemoryAccess(array, mid) < element) {
// this cannot overflow because mid < high
unchecked {
low = mid + 1;
}
} else {
high = mid;
}
}

return low;
}

/**
* @dev Searches a sorted `array` and returns the first index that contains
* a value strictly greater to `element`. If no such index exists (i.e. all
* values in the array are strictly less than `element`), the array length is
* returned. Time complexity O(log n).
*
* `array` is expected to be sorted in ascending order,
*
* See https://en.cppreference.com/w/cpp/algorithm/upper_bound
*/
function upperBoundMemory(uint256[] memory array, uint256 element) internal pure returns (uint256) {
uint256 low = 0;
uint256 high = array.length;

if (high == 0) {
return 0;
}

while (low < high) {
uint256 mid = Math.average(low, high);

// Note that mid will always be strictly less than high (i.e. it will be a valid array index)
// because Math.average rounds towards zero (it does integer division with truncation).
if (unsafeMemoryAccess(array, mid) > element) {
high = mid;
} else {
// this cannot overflow because mid < high
unchecked {
low = mid + 1;
}
}
}

return low;
}

/**
Expand Down
127 changes: 37 additions & 90 deletions test/utils/Arrays.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,61 +2,65 @@ const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');

const findUpperBound = (array, value) => {
const i = array.findIndex(x => x >= value);
// See https://en.cppreference.com/w/cpp/algorithm/ranges/lower_bound
const lowerBound = (array, value) => {
const i = array.findIndex(element => value <= element);
return i == -1 ? array.length : i;
};

const findLowerBound = (array, value) => {
const i = array.findLastIndex(x => x <= value);
// See https://en.cppreference.com/w/cpp/algorithm/upper_bound
const upperBound = (array, value) => {
const i = array.findIndex(element => value < element);
return i == -1 ? array.length : i;
};

const hasDuplicates = array => array.some((v, i) => array.indexOf(v) != i);

describe('Arrays', function () {
describe('findUpperBound', function () {
describe('search', function () {
for (const [title, { array, tests }] of Object.entries({
'Even number of elements': {
array: [11n, 12n, 13n, 14n, 15n, 16n, 17n, 18n, 19n, 20n],
tests: {
'returns correct index for the basic case': 16n,
'returns 0 for the first element': 11n,
'returns index of the last element': 20n,
'returns first index after last element if searched value is over the upper boundary': 32n,
'returns 0 for the element under the lower boundary': 2n,
'basic case': 16n,
'first element': 11n,
'last element': 20n,
'searched value is over the upper boundary': 32n,
'searched value is under the lower boundary': 2n,
},
},
'Odd number of elements': {
array: [11n, 12n, 13n, 14n, 15n, 16n, 17n, 18n, 19n, 20n, 21n],
tests: {
'returns correct index for the basic case': 16n,
'returns 0 for the first element': 11n,
'returns index of the last element': 20n,
'returns first index after last element if searched value is over the upper boundary': 32n,
'returns 0 for the element under the lower boundary': 2n,
'basic case': 16n,
'first element': 11n,
'last element': 21n,
'searched value is over the upper boundary': 32n,
'searched value is under the lower boundary': 2n,
},
},
'Array with gap': {
array: [11n, 12n, 13n, 14n, 15n, 20n, 21n, 22n, 23n, 24n],
tests: {
'returns index of first element in next filled range': 17n,
'search value in gap': 17n,
},
},
'Array with duplicated elements': {
array: [0n, 10n, 10n, 10n, 10n, 10n, 10n, 10n, 20n],
tests: {
'returns index of first occurence': 10n,
'search value is duplicated': 10n,
},
},
'Array with duplicated first element': {
array: [10n, 10n, 10n, 10n, 10n, 10n, 10n, 20n],
tests: {
'returns index of first occurence': 10n,
'search value is duplicated first element': 10n,
},
},
'Array with duplicated last element': {
array: [0n, 10n, 10n, 10n, 10n, 10n, 10n, 10n],
tests: {
'returns index of first occurence': 10n,
'search value is duplicated last element': 10n,
},
},
'Empty array': {
Expand All @@ -76,79 +80,22 @@ describe('Arrays', function () {
});

for (const [name, input] of Object.entries(tests)) {
it(name, async function () {
expect(await this.mock.findUpperBound(input)).to.be.equal(findUpperBound(array, input));
});
}
});
}
});
describe(name, function () {
it('[deprecated] findUpperBound', async function () {
// findUpperBound does not support duplicated
if (hasDuplicates(array)) this.skip();
expect(await this.mock.findUpperBound(input)).to.be.equal(lowerBound(array, input));
});

describe('findLowerBound', function () {
for (const [title, { array, tests }] of Object.entries({
'Even number of elements': {
array: [11n, 12n, 13n, 14n, 15n, 16n, 17n, 18n, 19n, 20n],
tests: {
'returns correct index for the basic case': 16n,
'returns 0 for the first element': 11n,
'returns index of the last element': 20n,
'returns index of last element if searched value is over the upper boundary': 32n,
'returns length for the element under the lower boundary': 2n,
},
},
'Odd number of elements': {
array: [11n, 12n, 13n, 14n, 15n, 16n, 17n, 18n, 19n, 20n, 21n],
tests: {
'returns correct index for the basic case': 16n,
'returns 0 for the first element': 11n,
'returns index of the last element': 20n,
'returns index of last element if searched value is over the upper boundary': 32n,
'returns length for the element under the lower boundary': 2n,
},
},
'Array with gap': {
array: [11n, 12n, 13n, 14n, 15n, 20n, 21n, 22n, 23n, 24n],
tests: {
'returns index of first element in next filled range': 17n,
},
},
'Array with duplicated elements': {
array: [0n, 10n, 10n, 10n, 10n, 10n, 10n, 10n, 20n],
tests: {
'returns index of last occurence': 10n,
},
},
'Array with duplicated first element': {
array: [10n, 10n, 10n, 10n, 10n, 10n, 10n, 20n],
tests: {
'returns index of last occurence': 10n,
},
},
'Array with duplicated last element': {
array: [0n, 10n, 10n, 10n, 10n, 10n, 10n, 10n],
tests: {
'returns index of last occurence': 10n,
},
},
'Empty array': {
array: [],
tests: {
'always returns 0 for empty array': 10n,
},
},
})) {
describe(title, function () {
const fixture = async () => {
return { mock: await ethers.deployContract('Uint256ArraysMock', [array]) };
};

beforeEach(async function () {
Object.assign(this, await loadFixture(fixture));
});
it('lowerBound', async function () {
expect(await this.mock.lowerBound(input)).to.be.equal(lowerBound(array, input));
expect(await this.mock.lowerBoundMemory(array, input)).to.be.equal(lowerBound(array, input));
});

for (const [name, input] of Object.entries(tests)) {
it(name, async function () {
expect(await this.mock.findLowerBound(input)).to.be.equal(findLowerBound(array, input));
it('upperBound', async function () {
expect(await this.mock.upperBound(input)).to.be.equal(upperBound(array, input));
expect(await this.mock.upperBoundMemory(array, input)).to.be.equal(upperBound(array, input));
});
});
}
});
Expand Down