feat(List)
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
49
README.md
Normal file
49
README.md
Normal file
@ -0,0 +1,49 @@
|
||||
# iterable-js
|
||||
|
||||
A data structures library based on iterators, inspired by libstdc++
|
||||
|
||||
## Operators
|
||||
|
||||
| Operator | Naming |
|
||||
| ----------- | ------ |
|
||||
| `+` | add |
|
||||
| `-` | sub |
|
||||
| `++obj` | inc |
|
||||
| `--obj` | dec |
|
||||
| `*` | mul |
|
||||
| `/` | div |
|
||||
| `%` | mod |
|
||||
| `==` | eq |
|
||||
| `!=` | neq |
|
||||
| `<` | lt |
|
||||
| `>` | gt |
|
||||
| `<=` | lte |
|
||||
| `>=` | gte |
|
||||
| `&&` | and |
|
||||
| `=` const & | copy |
|
||||
| `=` && | move |
|
||||
| `*obj` | deref |
|
||||
| `&obj` | ref |
|
||||
| `\|\|` | or |
|
||||
| `!` | not |
|
||||
| `&` | band |
|
||||
| `\|` | bor |
|
||||
| `^` | bxor |
|
||||
| `~` | bnot |
|
||||
| `<<` | shl |
|
||||
| `>>` | shr |
|
||||
| `()` | call |
|
||||
| `,` | comma |
|
||||
| `obj++` | `X` |
|
||||
| `obj--` | `X` |
|
||||
| `->` | `X` |
|
||||
| `+=` | `X` |
|
||||
| `-=` | `X` |
|
||||
| `*=` | `X` |
|
||||
| `/=` | `X` |
|
||||
| `%=` | `X` |
|
||||
| `&=` | `X` |
|
||||
| `\|=` | `X` |
|
||||
| `^=` | `X` |
|
||||
| `<<=` | `X` |
|
||||
| `>>=` | `X` |
|
||||
1313
package-lock.json
generated
Normal file
1313
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
Normal file
18
package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "iterable",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "esbuild src/index.js --bundle --minify --outfile=dist/index.js",
|
||||
"test": "vitest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Vlad Litvinov <vlad@sek1.ro>",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.2",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
133
src/List.js
Normal file
133
src/List.js
Normal file
@ -0,0 +1,133 @@
|
||||
import { SequenceContainer } from "./containers/Sequence"
|
||||
import { BidirectionalIterator } from "./iterators/Bidirectional"
|
||||
import { mixinClasses } from "./utils/mixin"
|
||||
|
||||
export class List {
|
||||
|
||||
constructor(...args) {
|
||||
this._end = new ListIterator(new ListItem())
|
||||
this._begin = this._end
|
||||
this._size = 0
|
||||
if (args.length >= 1) {
|
||||
this.assign(...args)
|
||||
}
|
||||
}
|
||||
|
||||
move(rhs) {
|
||||
this._begin = rhs._begin
|
||||
this._end = rhs._end
|
||||
this._size = rhs._size
|
||||
rhs._begin = undefined
|
||||
rhs._end = undefined
|
||||
rhs._size = 0
|
||||
return this
|
||||
}
|
||||
|
||||
size() {
|
||||
return this._size
|
||||
}
|
||||
|
||||
begin() {
|
||||
return this._begin.clone()
|
||||
}
|
||||
|
||||
end() {
|
||||
return this._end.clone()
|
||||
}
|
||||
|
||||
_shiftRight(dest, generator) {
|
||||
let prev, res, new_item, item = dest.deref()
|
||||
|
||||
for (const value of generator) {
|
||||
prev = item._prev
|
||||
new_item = new ListItem(value)
|
||||
res = res ?? new_item
|
||||
|
||||
item._prev = new_item
|
||||
new_item._next = item
|
||||
new_item._prev = prev
|
||||
if (prev) {
|
||||
prev._next = new_item
|
||||
} else {
|
||||
this._begin = new ListIterator(res)
|
||||
}
|
||||
this._size++
|
||||
}
|
||||
|
||||
return new ListIterator(res ?? dest.deref())
|
||||
}
|
||||
|
||||
_shiftLeft(dest, generator) {
|
||||
let item = dest.deref(), start_item = item
|
||||
|
||||
for (const _ of generator) {
|
||||
item = item._prev
|
||||
this._size--
|
||||
}
|
||||
if (item._prev) {
|
||||
item._prev._next = start_item
|
||||
} else {
|
||||
this._begin = new ListIterator(start_item)
|
||||
}
|
||||
start_item._prev = item._prev
|
||||
|
||||
return dest
|
||||
}
|
||||
}
|
||||
|
||||
List.from = function (obj) {
|
||||
const list = new List()
|
||||
for (const value of obj) {
|
||||
list.push_back(value)
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
mixinClasses(List, SequenceContainer)
|
||||
|
||||
export class ListItem {
|
||||
|
||||
constructor(value) {
|
||||
this.value = value
|
||||
this._next = undefined
|
||||
this._prev = undefined
|
||||
}
|
||||
|
||||
get next() { return this._next }
|
||||
get prev() { return this._prev }
|
||||
}
|
||||
|
||||
export class ListIterator {
|
||||
constructor(item) {
|
||||
this._item = item
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new ListIterator(this._item)
|
||||
}
|
||||
|
||||
copy(rhs) {
|
||||
this._item = rhs._item
|
||||
return this
|
||||
}
|
||||
|
||||
deref() {
|
||||
return this._item
|
||||
}
|
||||
|
||||
eq(rhs) {
|
||||
return this._item == rhs._item
|
||||
}
|
||||
|
||||
inc() {
|
||||
this._item = this._item.next
|
||||
return this
|
||||
}
|
||||
|
||||
dec() {
|
||||
this._item = this._item.prev
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
mixinClasses(ListIterator, BidirectionalIterator)
|
||||
178
src/containers/Sequence.js
Normal file
178
src/containers/Sequence.js
Normal file
@ -0,0 +1,178 @@
|
||||
import { BidirectionalIterator } from "../iterators/Bidirectional"
|
||||
import { satisfiesConcept } from "../utils/concept"
|
||||
|
||||
export class SequenceContainer {
|
||||
clone() {
|
||||
return new this.constructor(this.begin(), this.end())
|
||||
}
|
||||
|
||||
copy(rhs) {
|
||||
this.assign(rhs.begin(), rhs.end())
|
||||
return this
|
||||
}
|
||||
|
||||
empty() {
|
||||
return this.size() == 0
|
||||
}
|
||||
|
||||
get front() {
|
||||
return this.begin().value
|
||||
}
|
||||
|
||||
set front(value) {
|
||||
return this.begin().value = value
|
||||
}
|
||||
|
||||
get back() {
|
||||
return this.end().dec().value
|
||||
}
|
||||
|
||||
set back(value) {
|
||||
return this.end().dec().value = value
|
||||
}
|
||||
|
||||
insert(dest, arg2, arg3) {
|
||||
if (
|
||||
BidirectionalIterator.is(arg2) &&
|
||||
BidirectionalIterator.is(arg3)
|
||||
) {
|
||||
return this._shiftRight(dest,
|
||||
{
|
||||
[Symbol.iterator]: () => {
|
||||
const local_begin = arg2.clone()
|
||||
return {
|
||||
next() {
|
||||
const res = {
|
||||
done: local_begin.eq(arg3),
|
||||
value: local_begin.value,
|
||||
}
|
||||
local_begin.inc()
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else if (
|
||||
typeof arg3 == 'number' && arg3 >= 0 ||
|
||||
arg3 === undefined
|
||||
) {
|
||||
return this._shiftRight(dest,
|
||||
{
|
||||
[Symbol.iterator]: () => {
|
||||
let local_n = arg3 ?? 1
|
||||
return {
|
||||
next: () => ({ done: local_n-- <= 0, value: arg2 })
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
throw new Error('incorrect args')
|
||||
}
|
||||
}
|
||||
|
||||
assign(...args) {
|
||||
if (this.empty()) {
|
||||
return this.insert(this.begin(), ...args)
|
||||
}
|
||||
this.erase(this.begin(), this.insert(this.end(), ...args))
|
||||
return this.begin()
|
||||
}
|
||||
|
||||
erase(begin, end = begin.clone().inc()) {
|
||||
return this._shiftLeft(end,
|
||||
{
|
||||
[Symbol.iterator]: () => {
|
||||
let local_begin = begin.clone()
|
||||
return {
|
||||
next() {
|
||||
const res = {
|
||||
done: local_begin.eq(end)
|
||||
}
|
||||
local_begin.inc()
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.erase(this.begin(), this.end())
|
||||
}
|
||||
|
||||
push_front(value) {
|
||||
this.insert(this.begin(), value)
|
||||
}
|
||||
|
||||
push_back(value) {
|
||||
this.insert(this.end(), value)
|
||||
}
|
||||
|
||||
pop_front() {
|
||||
this.erase(this.begin())
|
||||
}
|
||||
|
||||
pop_back() {
|
||||
this.erase(this.end().dec())
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
toString() {
|
||||
let res = []
|
||||
for (const value of this) {
|
||||
res.push(JSON.stringify(value))
|
||||
}
|
||||
return '[ ' + res.join(', ') + ' ]'
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
let it = this.begin()
|
||||
const it_end = this.end()
|
||||
return {
|
||||
next() {
|
||||
const res = {
|
||||
done: it.eq(it_end),
|
||||
value: it.value,
|
||||
}
|
||||
it.inc()
|
||||
return res
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SequenceContainer.is = function (obj, ...args) {
|
||||
const pure_virtual = [
|
||||
'move',
|
||||
'size',
|
||||
'begin',
|
||||
'end',
|
||||
'_shiftRight',
|
||||
'_shiftLeft',
|
||||
]
|
||||
const virtual = [
|
||||
'clone',
|
||||
'copy',
|
||||
'empty',
|
||||
'front',
|
||||
'back',
|
||||
'insert',
|
||||
'assign',
|
||||
'erase',
|
||||
'clear',
|
||||
'push_front',
|
||||
'push_back',
|
||||
'pop_front',
|
||||
'pop_back',
|
||||
Symbol.iterator,
|
||||
'toJSON',
|
||||
'toString',
|
||||
]
|
||||
return satisfiesConcept(obj, pure_virtual, virtual, ...args)
|
||||
}
|
||||
4
src/index.js
Normal file
4
src/index.js
Normal file
@ -0,0 +1,4 @@
|
||||
export { ForwardIterator } from "./iterators/Forward"
|
||||
export { BidirectionalIterator } from "./iterators/Bidirectional"
|
||||
export { SequenceContainer } from "./containers/Sequence"
|
||||
export { List, ListIterator, ListItem } from "./List"
|
||||
11
src/iterators/Bidirectional.js
Normal file
11
src/iterators/Bidirectional.js
Normal file
@ -0,0 +1,11 @@
|
||||
import { satisfiesConcept } from "../utils/concept"
|
||||
import { mixinClasses } from "../utils/mixin"
|
||||
import { ForwardIterator } from "./Forward"
|
||||
|
||||
export class BidirectionalIterator { }
|
||||
|
||||
mixinClasses(BidirectionalIterator, ForwardIterator)
|
||||
|
||||
BidirectionalIterator.is = function (obj, ...args) {
|
||||
return ForwardIterator.is(obj, ...args) && satisfiesConcept(obj, ['dec'], [], ...args)
|
||||
}
|
||||
35
src/iterators/Forward.js
Normal file
35
src/iterators/Forward.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { satisfiesConcept } from "../utils/concept"
|
||||
|
||||
export class ForwardIterator {
|
||||
move(rhs) {
|
||||
return this.copy(rhs)
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.deref().value
|
||||
}
|
||||
|
||||
set value(value) {
|
||||
return this.deref().value = value
|
||||
}
|
||||
|
||||
neq(rhs) {
|
||||
return !this.eq(rhs)
|
||||
}
|
||||
}
|
||||
|
||||
ForwardIterator.is = function (obj, ...args) {
|
||||
const pure_virtual = [
|
||||
'clone',
|
||||
'copy',
|
||||
'deref',
|
||||
'inc',
|
||||
'eq'
|
||||
]
|
||||
const virtual = [
|
||||
'move',
|
||||
'value',
|
||||
'neq',
|
||||
]
|
||||
return satisfiesConcept(obj, pure_virtual, virtual, ...args)
|
||||
}
|
||||
34
src/utils/concept.js
Normal file
34
src/utils/concept.js
Normal file
@ -0,0 +1,34 @@
|
||||
export function satisfiesConcept(obj, pure_virtual, virtual, debug) {
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return false;
|
||||
}
|
||||
if (debug) {
|
||||
const missingMethods = []
|
||||
|
||||
pure_virtual.forEach((method) => {
|
||||
if (typeof obj[method] !== 'function') {
|
||||
missingMethods.push(method)
|
||||
}
|
||||
})
|
||||
|
||||
virtual.forEach((method) => {
|
||||
if (!(method in obj)) {
|
||||
missingMethods.push(method)
|
||||
}
|
||||
})
|
||||
|
||||
if (missingMethods.length !== 0) {
|
||||
console.debug(obj.constructor.name, missingMethods)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if (!pure_virtual.every((method) => typeof obj[method] === 'function')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!virtual.every((method) => method in obj)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
7
src/utils/mixin.js
Normal file
7
src/utils/mixin.js
Normal file
@ -0,0 +1,7 @@
|
||||
export function mixinClasses(dest, ...sources) {
|
||||
for (const source of sources) {
|
||||
const properties = Object.getOwnPropertyDescriptors(source.prototype)
|
||||
delete properties.constructor
|
||||
Object.defineProperties(dest.prototype, properties)
|
||||
}
|
||||
}
|
||||
136
test/List.test.js
Normal file
136
test/List.test.js
Normal file
@ -0,0 +1,136 @@
|
||||
import { expect, test } from 'vitest'
|
||||
import { List, ListIterator } from '../src/List'
|
||||
import { SequenceContainer } from '../src/containers/Sequence'
|
||||
import { BidirectionalIterator } from '../src/iterators/Bidirectional'
|
||||
|
||||
function listEquArray(list, arr) {
|
||||
if (list.size() != Array.from(arr).length) {
|
||||
return false
|
||||
}
|
||||
let it = list.begin(), it_end = list.end(), idx = 0
|
||||
while (it.neq(it_end)) {
|
||||
if (arr[idx] !== it.value) {
|
||||
return false
|
||||
}
|
||||
idx++
|
||||
it.inc()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
test('@concepts', () => {
|
||||
expect(SequenceContainer.is(List.prototype, true)).toBeTruthy()
|
||||
expect(BidirectionalIterator.is(ListIterator.prototype, true)).toBeTruthy()
|
||||
})
|
||||
|
||||
test('from', () => {
|
||||
expect(listEquArray(List.from([]), [])).toBeTruthy()
|
||||
expect(listEquArray(List.from([1, 2, 3]), [1, 2, 3])).toBeTruthy()
|
||||
})
|
||||
|
||||
test('copy, clone', () => {
|
||||
const list = List.from([1, 2, 3])
|
||||
const a = new List(), b = list.clone()
|
||||
a.copy(list)
|
||||
expect(a.size()).toEqual(3)
|
||||
expect(b.size()).toEqual(3)
|
||||
list.clear()
|
||||
expect(a.size()).toEqual(3)
|
||||
expect(b.size()).toEqual(3)
|
||||
})
|
||||
|
||||
test('move, empty', () => {
|
||||
const a = List.from([1, 2, 3])
|
||||
const b = new List()
|
||||
b.move(a)
|
||||
expect(a.empty()).toBeTruthy()
|
||||
expect(b.size()).toEqual(3)
|
||||
})
|
||||
|
||||
test('constructor, assign', () => {
|
||||
expect(new List().size()).toEqual(0)
|
||||
expect(listEquArray(new List(null), [null])).toBeTruthy()
|
||||
expect(listEquArray(new List(null, 2), [null, null])).toBeTruthy()
|
||||
{
|
||||
const list = List.from([1, 2, 3])
|
||||
expect(listEquArray(new List(list.begin().inc(), list.end().dec()), [2])).toBeTruthy()
|
||||
list.assign(10)
|
||||
expect(list.size()).toEqual(1)
|
||||
}
|
||||
})
|
||||
|
||||
test('insert, clone', () => {
|
||||
{
|
||||
const list = List.from([1, 2])
|
||||
const it = list.insert(list.begin().inc(), null, 2)
|
||||
expect(listEquArray(list, [1, null, null, 2])).toBeTruthy()
|
||||
expect(list.size()).toEqual(4)
|
||||
expect(it.eq(list.begin().inc())).toBeTruthy()
|
||||
}
|
||||
{
|
||||
const a = List.from([1, 4])
|
||||
const b = List.from([2, 3])
|
||||
const it = a.insert(a.begin().inc(), b.begin(), b.end())
|
||||
expect(listEquArray(a, [1, 2, 3, 4])).toBeTruthy()
|
||||
expect(a.size()).toEqual(4)
|
||||
expect(it.eq(a.begin().inc())).toBeTruthy()
|
||||
}
|
||||
{
|
||||
const list = List.from([1, 2])
|
||||
expect(() => list.insert()).toThrowError()
|
||||
const it = list.insert(list.begin(), 0, 0)
|
||||
expect(it.eq(list.begin())).toBeTruthy()
|
||||
}
|
||||
{
|
||||
const a = List.from([1, 2])
|
||||
const b = a.clone()
|
||||
expect(listEquArray(b, [1, 2])).toBeTruthy()
|
||||
expect(b.size()).toEqual(2)
|
||||
expect(b.begin().deref().prev).toEqual(undefined)
|
||||
expect(b.begin().deref().next).toEqual(b.end().deref().prev)
|
||||
expect(b.end().deref().prev).toEqual(b.begin().deref().next)
|
||||
expect(b.end().deref().next).toEqual(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
test('erase, clear', () => {
|
||||
{
|
||||
const list = List.from([1, 2, 3, 4])
|
||||
const it = list.erase(list.begin(), list.end().dec().dec())
|
||||
expect(listEquArray(list, [3, 4])).toBeTruthy()
|
||||
expect(list.size()).toEqual(2)
|
||||
expect(it.eq(list.begin())).toBeTruthy()
|
||||
}
|
||||
{
|
||||
const list = List.from([1, 2, 3])
|
||||
const it = list.erase(list.begin().inc())
|
||||
expect(listEquArray(list, [1, 3])).toBeTruthy()
|
||||
expect(list.size()).toEqual(2)
|
||||
expect(it.deref()).toEqual(list.begin().inc().deref())
|
||||
}
|
||||
{
|
||||
const list = List.from([1, 2])
|
||||
list.clear()
|
||||
expect(list.size()).toEqual(0)
|
||||
}
|
||||
})
|
||||
|
||||
test('push_back, push_front, pop_back, pop_front, back, front', () => {
|
||||
const list = List.from([1, 2, 3])
|
||||
list.pop_front()
|
||||
list.push_front(5)
|
||||
list.pop_back()
|
||||
list.pop_back()
|
||||
list.push_back(10)
|
||||
expect(listEquArray(list, [5, 10])).toBeTruthy()
|
||||
expect(list.front).toEqual(5)
|
||||
expect(list.back).toEqual(10)
|
||||
list.front = 1
|
||||
list.back = 2
|
||||
expect(list.front).toEqual(1)
|
||||
expect(list.back).toEqual(2)
|
||||
})
|
||||
|
||||
test('toJSON, toString', () => {
|
||||
expect(List.from([1, 2]).toJSON()).toEqual('[ 1, 2 ]')
|
||||
})
|
||||
Reference in New Issue
Block a user