-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ON-846: implemented linked list library
- Loading branch information
Showing
5 changed files
with
412 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
// SPDX-License-Identifier: CC0-1.0 | ||
|
||
pragma solidity 0.8.9; | ||
|
||
/// @title Uint64SortedLinkedListLibrary | ||
/// @dev Implementation of a linked list of uint64 keys. The list is ordered in descending order, and allows duplicates. | ||
library Uint64SortedLinkedListLibrary { | ||
uint64 private constant EMPTY = 0; | ||
|
||
struct Item { | ||
uint64 prev; | ||
uint64 next; | ||
uint8 count; | ||
} | ||
|
||
struct List { | ||
uint64 head; | ||
mapping(uint64 => Item) items; | ||
} | ||
|
||
/// @notice Inserts a new item into the list. | ||
/// @dev It should maintain the descending order of the list. | ||
/// @param _self The list to insert into. | ||
/// @param _key The new item to be inserted. | ||
function insert(List storage _self, uint64 _key) internal { | ||
require(_key != EMPTY, 'Uint64SortedLinkedListLibrary: key cannot be zero'); | ||
|
||
// if _key already exists, only increase counter | ||
if (_self.items[_key].count > 0) { | ||
_self.items[_key].count++; | ||
return; | ||
} | ||
|
||
// if _key is the highest in the list, insert as head | ||
if (_key > _self.head) { | ||
_self.items[_key] = Item(EMPTY, _self.head, 1); | ||
|
||
// only update the previous head if list was not empty | ||
if (_self.head != EMPTY) _self.items[_self.head].prev = _key; | ||
|
||
_self.head = _key; | ||
return; | ||
} | ||
|
||
// loop until position to insert is found | ||
uint64 _itemKey = _self.head; | ||
Item storage _item = _self.items[_itemKey]; | ||
while (_key < _item.next && _item.next != EMPTY) { | ||
_itemKey = _item.next; | ||
_item = _self.items[_itemKey]; | ||
} | ||
|
||
// if found item is tail, next is EMPTY | ||
if (_item.next == EMPTY) { | ||
_self.items[_key] = Item(_itemKey, EMPTY, 1); | ||
_item.next = _key; | ||
return; | ||
} | ||
|
||
// if not tail, insert between two items | ||
_self.items[_key] = Item(_itemKey, _item.next, 1); | ||
_self.items[_item.next].prev = _key; | ||
_item.next = _key; | ||
} | ||
|
||
/// @notice Removes an item from the list. | ||
/// @dev It should maintain the descending order of the list. | ||
/// @param _self The list to remove from. | ||
/// @param _key The item to be removed. | ||
function remove(List storage _self, uint64 _key) internal { | ||
Item storage _itemToUpdate = _self.items[_key]; | ||
|
||
// if _key does not exist, return | ||
if (_itemToUpdate.count == 0) { | ||
return; | ||
} | ||
|
||
// if _key occurs more than once, just decrease counter | ||
if (_itemToUpdate.count > 1) { | ||
_itemToUpdate.count--; | ||
return; | ||
} | ||
|
||
// updating list | ||
|
||
// if _key is the head, update head to the next item | ||
if (_itemToUpdate.prev == EMPTY) { | ||
_self.head = _itemToUpdate.next; | ||
|
||
// only update next item if it exists (if it's not head and tail simultaneously) | ||
if (_itemToUpdate.next != EMPTY) _self.items[_itemToUpdate.next].prev = EMPTY; | ||
|
||
delete _self.items[_key]; | ||
return; | ||
} | ||
|
||
// if _key is not head, but it is tail, update the previous item's next pointer to EMPTY | ||
if (_itemToUpdate.next == EMPTY) { | ||
_self.items[_itemToUpdate.prev].next = EMPTY; | ||
delete _self.items[_key]; | ||
return; | ||
} | ||
|
||
// if not head nor tail, update both previous and next items | ||
_self.items[_itemToUpdate.next].prev = _itemToUpdate.prev; | ||
_self.items[_itemToUpdate.prev].next = _itemToUpdate.next; | ||
delete _self.items[_key]; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// SPDX-License-Identifier: CC0-1.0 | ||
|
||
pragma solidity 0.8.9; | ||
|
||
import { Uint64SortedLinkedListLibrary } from '../libraries/Uint64SortedLinkedListLibrary.sol'; | ||
|
||
contract MockSortedLinkedList { | ||
using Uint64SortedLinkedListLibrary for Uint64SortedLinkedListLibrary.List; | ||
using Uint64SortedLinkedListLibrary for Uint64SortedLinkedListLibrary.Item; | ||
|
||
Uint64SortedLinkedListLibrary.List internal list; | ||
|
||
function insert(uint64 _key) public { | ||
list.insert(_key); | ||
} | ||
|
||
function remove(uint64 _key) public { | ||
list.remove(_key); | ||
} | ||
|
||
function getHead() public view returns (uint256) { | ||
return list.head; | ||
} | ||
|
||
function getItem(uint64 _key) public view returns (uint64 prev_, uint64 next_, uint8 count_) { | ||
Uint64SortedLinkedListLibrary.Item storage item = list.items[_key]; | ||
prev_ = item.prev; | ||
next_ = item.next; | ||
count_ = item.count; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
import { beforeEach, it } from 'mocha' | ||
import { expect } from 'chai' | ||
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers' | ||
import { Contract } from 'ethers' | ||
import { ethers } from 'hardhat' | ||
import { checkList } from './helpers' | ||
import { generateRandomIntBetween } from '../helpers' | ||
|
||
describe('Uint64SortedLinkedListLibrary', async () => { | ||
let MockedList: Contract | ||
|
||
async function deployContracts() { | ||
const MockedListFactory = await ethers.getContractFactory('MockSortedLinkedList') | ||
MockedList = await MockedListFactory.deploy() | ||
} | ||
|
||
beforeEach(async () => { | ||
await loadFixture(deployContracts) | ||
}) | ||
|
||
describe('insert', async () => { | ||
it('should revert when key is zero', async () => { | ||
await expect(MockedList.insert(0)).to.be.revertedWith('Uint64SortedLinkedListLibrary: key cannot be zero') | ||
}) | ||
|
||
it('should insert as head and tail when list is empty', async () => { | ||
expect(await MockedList.insert(1)).to.not.be.reverted | ||
await checkList(MockedList, [1], [1]) | ||
}) | ||
|
||
it('should insert as head when list is not empty', async () => { | ||
expect(await MockedList.insert(10)).to.not.be.reverted | ||
expect(await MockedList.insert(11)).to.not.be.reverted | ||
await checkList(MockedList, [11, 10], [1, 1]) | ||
}) | ||
|
||
it('should insert as tail when list is not empty', async () => { | ||
expect(await MockedList.insert(12)).to.not.be.reverted | ||
expect(await MockedList.insert(10)).to.not.be.reverted | ||
await checkList(MockedList, [12, 10], [1, 1]) | ||
}) | ||
|
||
it('should insert in the middle of the list', async () => { | ||
expect(await MockedList.insert(20)).to.not.be.reverted | ||
expect(await MockedList.insert(10)).to.not.be.reverted | ||
expect(await MockedList.insert(15)).to.not.be.reverted | ||
await checkList(MockedList, [20, 15, 10], [1, 1, 1]) | ||
}) | ||
|
||
it('should insert the same item', async () => { | ||
await checkList(MockedList, [], []) | ||
expect(await MockedList.insert(100)).to.not.be.reverted | ||
await checkList(MockedList, [100], [1]) | ||
expect(await MockedList.insert(100)).to.not.be.reverted | ||
await checkList(MockedList, [100], [2]) | ||
expect(await MockedList.insert(100)).to.not.be.reverted | ||
await checkList(MockedList, [100], [3]) | ||
}) | ||
|
||
it('should insert as head multiple times', async () => { | ||
expect(await MockedList.insert(10)).to.not.be.reverted | ||
expect(await MockedList.insert(20)).to.not.be.reverted | ||
expect(await MockedList.insert(30)).to.not.be.reverted | ||
expect(await MockedList.insert(40)).to.not.be.reverted | ||
expect(await MockedList.insert(50)).to.not.be.reverted | ||
await checkList(MockedList, [50, 40, 30, 20, 10], [1, 1, 1, 1, 1]) | ||
}) | ||
|
||
it('should insert as tail multiple times', async () => { | ||
expect(await MockedList.insert(50)).to.not.be.reverted | ||
expect(await MockedList.insert(40)).to.not.be.reverted | ||
expect(await MockedList.insert(30)).to.not.be.reverted | ||
expect(await MockedList.insert(20)).to.not.be.reverted | ||
expect(await MockedList.insert(10)).to.not.be.reverted | ||
await checkList(MockedList, [50, 40, 30, 20, 10], [1, 1, 1, 1, 1]) | ||
}) | ||
|
||
it('should insert as middle item multiple times', async () => { | ||
expect(await MockedList.insert(50)).to.not.be.reverted | ||
expect(await MockedList.insert(10)).to.not.be.reverted | ||
|
||
expect(await MockedList.insert(40)).to.not.be.reverted | ||
expect(await MockedList.insert(30)).to.not.be.reverted | ||
expect(await MockedList.insert(20)).to.not.be.reverted | ||
|
||
await checkList(MockedList, [50, 40, 30, 20, 10], [1, 1, 1, 1, 1]) | ||
}) | ||
}) | ||
|
||
describe('remove', async () => { | ||
it('should not do anything when list is empty', async () => { | ||
expect(await MockedList.remove(1)).to.not.be.reverted | ||
await checkList(MockedList, [], []) | ||
}) | ||
|
||
it('should not do anything when list has only one different element', async () => { | ||
expect(await MockedList.insert(1)).to.not.be.reverted | ||
expect(await MockedList.remove(2)).to.not.be.reverted | ||
await checkList(MockedList, [1], [1]) | ||
}) | ||
|
||
it('should not do anything when list has two different elements', async () => { | ||
expect(await MockedList.insert(1)).to.not.be.reverted | ||
expect(await MockedList.insert(2)).to.not.be.reverted | ||
expect(await MockedList.remove(3)).to.not.be.reverted | ||
await checkList(MockedList, [2, 1], [1, 1]) | ||
}) | ||
|
||
it('should decrease item count when item has more than one occurrence', async () => { | ||
expect(await MockedList.insert(1)).to.not.be.reverted | ||
expect(await MockedList.insert(1)).to.not.be.reverted | ||
expect(await MockedList.insert(2)).to.not.be.reverted | ||
expect(await MockedList.insert(2)).to.not.be.reverted | ||
expect(await MockedList.insert(2)).to.not.be.reverted | ||
expect(await MockedList.remove(1)).to.not.be.reverted | ||
expect(await MockedList.remove(2)).to.not.be.reverted | ||
await checkList(MockedList, [2, 1], [2, 1]) | ||
}) | ||
|
||
it('should remove head when list has only one element', async () => { | ||
expect(await MockedList.insert(1)).to.not.be.reverted | ||
expect(await MockedList.remove(1)).to.not.be.reverted | ||
await checkList(MockedList, [], []) | ||
}) | ||
|
||
it('should remove head when list has more than one element', async () => { | ||
expect(await MockedList.insert(10)).to.not.be.reverted | ||
expect(await MockedList.insert(20)).to.not.be.reverted | ||
expect(await MockedList.remove(20)).to.not.be.reverted | ||
await checkList(MockedList, [10], [1]) | ||
}) | ||
|
||
it('should remove tail when list has more than one element', async () => { | ||
expect(await MockedList.insert(10)).to.not.be.reverted | ||
expect(await MockedList.insert(20)).to.not.be.reverted | ||
expect(await MockedList.remove(10)).to.not.be.reverted | ||
await checkList(MockedList, [20], [1]) | ||
}) | ||
|
||
it('should remove middle element when list has more than two elements', async () => { | ||
expect(await MockedList.insert(10)).to.not.be.reverted | ||
expect(await MockedList.insert(20)).to.not.be.reverted | ||
expect(await MockedList.insert(30)).to.not.be.reverted | ||
expect(await MockedList.insert(40)).to.not.be.reverted | ||
expect(await MockedList.remove(30)).to.not.be.reverted | ||
await checkList(MockedList, [40, 20, 10], [1, 1, 1]) | ||
}) | ||
}) | ||
|
||
// takes up to 25s to run | ||
describe('Insert & Remove multiple times', async () => { | ||
it('should insert and remove 100 items', async () => { | ||
// insert | ||
const keyList = [] | ||
for (let i = 0; i < 100; i++) { | ||
const key = generateRandomIntBetween(1, 100) | ||
keyList.push(key) | ||
const { keys, counters } = computeLinkedList(keyList) | ||
expect(await MockedList.insert(key)).to.not.be.reverted | ||
await checkList(MockedList, keys, counters) | ||
} | ||
|
||
// remove | ||
for (let i = keyList.length; i > 0; i--) { | ||
const key = keyList[i - 1] | ||
keyList.pop() | ||
const { keys, counters } = computeLinkedList(keyList) | ||
expect(await MockedList.remove(key)).to.not.be.reverted | ||
await checkList(MockedList, keys, counters) | ||
} | ||
}) | ||
}) | ||
}) | ||
|
||
function computeLinkedList(list: number[]) { | ||
const keysMap = list.reduce((acc: { [key: string]: { key: number; count: number } }, key) => { | ||
if (acc[key.toString()]) { | ||
acc[key.toString()].count++ | ||
} else { | ||
acc[key.toString()] = { key, count: 1 } | ||
} | ||
return acc | ||
}, {}) | ||
|
||
const keysCounter = Object.keys(keysMap) | ||
.map(key => keysMap[key]) | ||
.sort((a, b) => b.key - a.key) | ||
|
||
const keys: number[] = [] | ||
const counters: number[] = [] | ||
keysCounter.forEach((item, i) => { | ||
keys[i] = item.key | ||
counters[i] = item.count | ||
}) | ||
|
||
return { keys, counters } | ||
} |
Oops, something went wrong.