From 8d6250cd5a66a79f0072f2c089510dda21f14830 Mon Sep 17 00:00:00 2001 From: Jakub Bogacz Date: Mon, 8 Oct 2018 16:01:33 +0200 Subject: [PATCH] Add Arrays library with unit tests (#1209) (#1375) * Add Arrays library with unit tests (#1209) * prepared due to snapshot token requirements * add library with method to find upper bound * add unit test for basic and edge cases * Imporove documentation for Arrays library Simplify Arrays.test.js to use short arrays as test date * Added comment for uint256 mid variable. * Explaned why uint256 mid variable calculated as Math.average is safe to use as index of array. (cherry picked from commit f7e53d90fa638553ffc93e93fe1b12fc081bb774) --- contracts/mocks/ArraysImpl.sol | 18 +++++++ contracts/utils/Arrays.sol | 56 ++++++++++++++++++++++ test/utils/Arrays.test.js | 87 ++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 contracts/mocks/ArraysImpl.sol create mode 100644 contracts/utils/Arrays.sol create mode 100644 test/utils/Arrays.test.js diff --git a/contracts/mocks/ArraysImpl.sol b/contracts/mocks/ArraysImpl.sol new file mode 100644 index 000000000..8a2b9ec51 --- /dev/null +++ b/contracts/mocks/ArraysImpl.sol @@ -0,0 +1,18 @@ +pragma solidity ^0.4.24; + +import "../utils/Arrays.sol"; + +contract ArraysImpl { + + using Arrays for uint256[]; + + uint256[] private array; + + constructor(uint256[] _array) public { + array = _array; + } + + function findUpperBound(uint256 _element) external view returns (uint256) { + return array.findUpperBound(_element); + } +} diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol new file mode 100644 index 000000000..6882ea995 --- /dev/null +++ b/contracts/utils/Arrays.sol @@ -0,0 +1,56 @@ +pragma solidity ^0.4.23; + +import "../math/Math.sol"; + + +/** + * @title Arrays + * @dev Utility library of inline array functions + */ +library Arrays { + + /** + * @dev Upper bound search function which is kind of binary search algoritm. It searches sorted + * array to find index of the element value. If element is found then returns it's index otherwise + * it returns index of first element which is grater than searched value. If searched element is + * bigger than any array element function then returns first index after last element (i.e. all + * values inside the array are smaller than the target). Complexity O(log n). + * @param array The array sorted in ascending order. + * @param element The element's value to be find. + * @return The calculated index value. Returns 0 for empty array. + */ + function findUpperBound( + uint256[] storage array, + uint256 element + ) + internal + view + returns (uint256) + { + if (array.length == 0) { + return 0; + } + + uint256 low = 0; + uint256 high = array.length; + + 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 down (it does integer division with truncation). + if (array[mid] > 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 && array[low - 1] == element) { + return low - 1; + } else { + return low; + } + } +} diff --git a/test/utils/Arrays.test.js b/test/utils/Arrays.test.js new file mode 100644 index 000000000..4bfc9eaba --- /dev/null +++ b/test/utils/Arrays.test.js @@ -0,0 +1,87 @@ +const ArraysImpl = artifacts.require('ArraysImpl'); + +const BigNumber = web3.BigNumber; + +require('chai') + .use(require('chai-bignumber')(BigNumber)) + .should(); + +contract('Arrays', function () { + context('Even number of elements', function () { + const EVEN_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + + beforeEach(async function () { + this.arrays = await ArraysImpl.new(EVEN_ELEMENTS_ARRAY); + }); + + it('should return correct index for the basic case', async function () { + (await this.arrays.findUpperBound(16)).should.be.bignumber.equal(5); + }); + + it('should return 0 for the first element', async function () { + (await this.arrays.findUpperBound(11)).should.be.bignumber.equal(0); + }); + + it('should return index of the last element', async function () { + (await this.arrays.findUpperBound(20)).should.be.bignumber.equal(9); + }); + + it('should return first index after last element if searched value is over the upper boundary', async function () { + (await this.arrays.findUpperBound(32)).should.be.bignumber.equal(10); + }); + + it('should return 0 for the element under the lower boundary', async function () { + (await this.arrays.findUpperBound(2)).should.be.bignumber.equal(0); + }); + }); + + context('Odd number of elements', function () { + const ODD_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]; + + beforeEach(async function () { + this.arrays = await ArraysImpl.new(ODD_ELEMENTS_ARRAY); + }); + + it('should return correct index for the basic case', async function () { + (await this.arrays.findUpperBound(16)).should.be.bignumber.equal(5); + }); + + it('should return 0 for the first element', async function () { + (await this.arrays.findUpperBound(11)).should.be.bignumber.equal(0); + }); + + it('should return index of the last element', async function () { + (await this.arrays.findUpperBound(21)).should.be.bignumber.equal(10); + }); + + it('should return first index after last element if searched value is over the upper boundary', async function () { + (await this.arrays.findUpperBound(32)).should.be.bignumber.equal(11); + }); + + it('should return 0 for the element under the lower boundary', async function () { + (await this.arrays.findUpperBound(2)).should.be.bignumber.equal(0); + }); + }); + + context('Array with gap', function () { + const WITH_GAP_ARRAY = [11, 12, 13, 14, 15, 20, 21, 22, 23, 24]; + + beforeEach(async function () { + this.arrays = await ArraysImpl.new(WITH_GAP_ARRAY); + }); + + it('should return index of first element in next filled range', async function () { + (await this.arrays.findUpperBound(17)).should.be.bignumber.equal(5); + }); + }); + + context('Empty array', function () { + beforeEach(async function () { + this.arrays = await ArraysImpl.new([]); + }); + + it('should always return 0 for empty array', async function () { + (await this.arrays.findUpperBound(10)).should.be.bignumber.equal(0); + }); + }); +});