Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* `Address`: optimize `functionCall` by calling `functionCallWithValue` directly. ([#3468](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3468))
* `Address`: optimize `functionCall` functions by checking contract size only if there is no returned data. ([#3469](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3469))
* `GovernorCompatibilityBravo`: remove unused `using` statements ([#3506](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3506))
* `Arrays`: add `sort` function

## 4.7.0 (2022-06-29)

Expand Down
5 changes: 5 additions & 0 deletions contracts/mocks/ArraysImpl.sol
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,9 @@ contract ArraysImpl {
function findUpperBound(uint256 element) external view returns (uint256) {
return _array.findUpperBound(element);
}

function sort(uint256[] memory array) external pure returns (uint256[] memory sorted) {
array.sort();
return (array);
}
}
80 changes: 80 additions & 0 deletions contracts/utils/Arrays.sol
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,84 @@ library Arrays {
return low;
}
}

/**
* @dev Sorts `array` of integers in an ascending order.
*
* Sorting is done in-place using the heap sort algorithm.
* Examples of gas cost with optimizer enabled for 200 runs:
* - 10 random items: ~8K gas
* - 100 random items: ~156K gas
*/
function sort(uint256[] memory array) internal pure {
unchecked {
uint256 length = array.length;
if (length < 2) return;
// Heapify the array
for (uint256 i = length / 2; i-- > 0; ) {
_siftDown(array, length, i, _arrayLoad(array, i));
}
// Drain all elements from highest to lowest and put them at the end of the array
while (--length != 0) {
uint256 val = _arrayLoad(array, 0);
_siftDown(array, length, 0, _arrayLoad(array, length));
_arrayStore(array, length, val);
}
}
}

/**
* @dev Insert a `inserted` value into an empty space in a binary heap.
* Makes sure that the space and all items below it still form a valid heap.
* Index `empty` is considered empty and will be overwritten.
*/
function _siftDown(
uint256[] memory array,
uint256 length,
uint256 emptyIdx,
uint256 inserted
) private pure {
unchecked {
while (true) {
// The first child of empty, one level deeper in the heap
uint256 childIdx = (emptyIdx << 1) + 1;
// Empty has no children
if (childIdx >= length) break;
uint256 childVal = _arrayLoad(array, childIdx);
uint256 otherChildIdx = childIdx + 1;
// Pick the larger child
if (otherChildIdx < length) {
uint256 otherChildVal = _arrayLoad(array, otherChildIdx);
if (otherChildVal > childVal) {
childIdx = otherChildIdx;
childVal = otherChildVal;
}
}
// No child is larger than the inserted value
if (childVal <= inserted) break;
// Move the larger child one level up and keep sifting down
_arrayStore(array, emptyIdx, childVal);
emptyIdx = childIdx;
}
_arrayStore(array, emptyIdx, inserted);
}
}

function _arrayLoad(uint256[] memory array, uint256 idx) private pure returns (uint256 val) {
/// @solidity memory-safe-assembly
assembly {
val := mload(add(32, add(array, shl(5, idx))))
}
}

function _arrayStore(
uint256[] memory array,
uint256 idx,
uint256 val
) private pure {
/// @solidity memory-safe-assembly
assembly {
mstore(add(32, add(array, shl(5, idx))), val)
}
}
}
77 changes: 77 additions & 0 deletions test/utils/Arrays.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require('@openzeppelin/test-helpers');

const { expect } = require('chai');
const { config } = require('hardhat');

const ArraysImpl = artifacts.require('ArraysImpl');

Expand Down Expand Up @@ -84,4 +85,80 @@ contract('Arrays', function (accounts) {
});
});
});

describe('sort', function () {
let arraysImpl;

before(async function () {
arraysImpl = await ArraysImpl.new([]);
});

async function testSort (array) {
const sorted = await arraysImpl.sort(array);
const sortedNum = sorted.map(val => val.toNumber());
array.sort((a, b) => a - b);
expect(sortedNum).to.deep.equal(array, 'Invalid sorting result');
}

function arrayOf (length, generator) {
return Array.from(Array(length), generator);
}

const randomItems = () => Math.floor(Math.random() * 1000);
const sameItems = () => 1;
const sortedItems = (_, idx) => idx;
const reverseSortedItems = (_, idx) => 1000 - idx;

it('accepts zero length arrays', async function () {
await testSort([]);
});

it('accepts one length arrays', async function () {
await testSort([1]);
});

it('handles sorted data', async function () {
await testSort([1, 2, 3]);
});

it('handles reverse sorted data', async function () {
await testSort([3, 2, 1]);
});

it('handles scrambled data', async function () {
await testSort([2, 1, 3]);
});

it('handles 10 random items', async function () {
await testSort(arrayOf(10, randomItems));
});

it('handles 10 same items', async function () {
await testSort(arrayOf(10, sameItems));
});

it('handles 10 sorted items', async function () {
await testSort(arrayOf(10, sortedItems));
});

it('handles 10 reverse sorted items', async function () {
await testSort(arrayOf(10, reverseSortedItems));
});

it('handles 100 random items', async function () {
await testSort(arrayOf(100, randomItems));
});

it('handles 100 same items', async function () {
await testSort(arrayOf(100, sameItems));
});

it('handles 100 sorted items', async function () {
await testSort(arrayOf(100, sortedItems));
});

it('handles 100 reverse sorted items', async function () {
await testSort(arrayOf(100, reverseSortedItems));
});
});
});