Skip to content

Commit 647ce53

Browse files
✨ feat: First draft.
1 parent cc3de52 commit 647ce53

File tree

10 files changed

+329
-8
lines changed

10 files changed

+329
-8
lines changed

package.json

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@
7575
"bugs": {
7676
"url": "https://github.com/aureooms/js-persistent-stack/issues"
7777
},
78-
"dependencies": {},
78+
"dependencies": {
79+
"@aureooms/js-error": "^4.0.1",
80+
"@aureooms/js-itertools": "^4.1.0"
81+
},
7982
"devDependencies": {
8083
"@babel/cli": "7.11.6",
8184
"@babel/core": "7.11.6",
@@ -101,7 +104,12 @@
101104
"lib"
102105
],
103106
"homepage": "https://aureooms.github.io/js-persistent-stack",
104-
"keywords": ["data", "persistent", "stack", "structures"],
107+
"keywords": [
108+
"data",
109+
"persistent",
110+
"stack",
111+
"structures"
112+
],
105113
"license": "AGPL-3.0",
106114
"main": "lib/index.js",
107115
"prettier": {
@@ -133,12 +141,7 @@
133141
"unicorn"
134142
],
135143
"rules": {
136-
"unicorn/filename-case": [
137-
"error",
138-
{
139-
"case": "camelCase"
140-
}
141-
]
144+
"unicorn/filename-case": "off"
142145
}
143146
}
144147
}

src/DONE_ITERATOR.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
function* _emptyGenerator() {}
2+
const DONE_ITERATOR = _emptyGenerator();
3+
export default DONE_ITERATOR;

src/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import DONE_ITERATOR from './DONE_ITERATOR';
2+
import withMethods from './withMethods';
3+
import withoutMethods from './withoutMethods';
4+
5+
/* eslint import/no-anonymous-default-export: [2, {"allowObject": true}] */
6+
export default withoutMethods;
7+
8+
export {DONE_ITERATOR, withMethods, withoutMethods};

src/withMethods.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {TypeError} from '@aureooms/js-error';
2+
import {_reduce} from '@aureooms/js-itertools';
3+
4+
export default function PersistentStack() {}
5+
PersistentStack.prototype.push = function (value) {
6+
return new Pushed(value, this);
7+
};
8+
9+
PersistentStack.prototype[Symbol.iterator] = function* () {
10+
let current = this;
11+
while (!current.isEmpty()) {
12+
yield current.peek();
13+
current = current.pop();
14+
}
15+
};
16+
17+
function Empty() {}
18+
Empty.prototype = new PersistentStack();
19+
20+
Empty.prototype.isEmpty = function () {
21+
return true;
22+
};
23+
24+
Empty.prototype.pop = function () {
25+
throw new TypeError('Cannot call pop on empty stack.');
26+
};
27+
28+
Empty.prototype.peek = function () {
29+
throw new TypeError('Cannot call peek on empty stack.');
30+
};
31+
32+
function Pushed(value, next) {
33+
this.value = value;
34+
this.next = next;
35+
}
36+
37+
Pushed.prototype = new PersistentStack();
38+
Pushed.prototype.isEmpty = function () {
39+
return false;
40+
};
41+
42+
Pushed.prototype.pop = function () {
43+
return this.next;
44+
};
45+
46+
Pushed.prototype.peek = function () {
47+
return this.value;
48+
};
49+
50+
const EMPTY = new Empty();
51+
PersistentStack.empty = () => EMPTY;
52+
PersistentStack.push = (stack, value) => new Pushed(value, stack);
53+
PersistentStack.isEmpty = (stack) => stack.isEmpty();
54+
PersistentStack.pop = (stack) => stack.pop();
55+
PersistentStack.peek = (stack) => stack.peek();
56+
PersistentStack.iter = (stack) => stack[Symbol.iterator]();
57+
PersistentStack.from = (iterable) =>
58+
_reduce(PersistentStack.push, iterable, EMPTY);

src/withoutMethods.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {_reduce} from '@aureooms/js-itertools';
2+
import DONE_ITERATOR from './DONE_ITERATOR';
3+
4+
function Node(value, next) {
5+
this.value = value;
6+
this.next = next;
7+
}
8+
9+
Node.prototype[Symbol.iterator] = function* () {
10+
let current = this;
11+
while (current !== null) {
12+
yield current.value;
13+
current = current.next;
14+
}
15+
};
16+
17+
const empty = () => null;
18+
const from = (iterable) => _reduce(push, iterable, null);
19+
const isEmpty = (stack) => stack === null;
20+
const push = (stack, value) => new Node(value, stack);
21+
const pop = (stack) => stack.next;
22+
const peek = (stack) => stack.value;
23+
const iter = (stack) =>
24+
stack === null ? DONE_ITERATOR : stack[Symbol.iterator]();
25+
26+
const withoutMethods = {
27+
name: 'withoutMethods',
28+
empty,
29+
from,
30+
isEmpty,
31+
push,
32+
pop,
33+
peek,
34+
iter,
35+
};
36+
37+
export default withoutMethods;

test/src/api.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import test from 'ava';
2+
3+
import {product} from '@aureooms/js-itertools';
4+
5+
import {withMethods, withoutMethods} from '../../src';
6+
7+
function macro(t, {empty, isEmpty, push, pop, peek}, n) {
8+
const array = [];
9+
10+
let stack = empty();
11+
t.true(isEmpty(stack), 'At the begining, stack is empty.');
12+
t.throws(
13+
() => peek(stack),
14+
null,
15+
'At the beginning, calling peek on empty stack throws.',
16+
);
17+
t.throws(
18+
() => pop(stack),
19+
null,
20+
'At the beginning, calling pop on empty stack throws.',
21+
);
22+
23+
for (let i = 0; i < n; ++i) {
24+
const r = Math.random();
25+
26+
stack = push(stack, r);
27+
array.push(r);
28+
29+
t.false(isEmpty(stack), i + 'The stack is not empty.');
30+
31+
t.deepEqual(peek(stack), r, i + ' Peek returns the last value.');
32+
}
33+
34+
for (let i = 0; i < n; ++i) {
35+
t.false(isEmpty(stack), i + 'The stack is not empty.');
36+
37+
t.deepEqual(peek(stack), array.pop(), i + ' Pop works.');
38+
stack = pop(stack);
39+
}
40+
41+
t.true(isEmpty(stack), 'At the end, stack is empty.');
42+
t.throws(
43+
() => peek(stack),
44+
null,
45+
'At the end, calling peek on empty stack throws.',
46+
);
47+
t.throws(
48+
() => pop(stack),
49+
null,
50+
'At the end, calling pop on empty stack throws.',
51+
);
52+
}
53+
54+
const implementations = [withMethods, withoutMethods];
55+
const sizes = [0, 1, 2, 10, 42, 100, 121, 197, 1024, 2500];
56+
57+
for (const [implementation, n] of product([implementations, sizes])) {
58+
test(`api: ${implementation.name} <${n}>`, macro, implementation, n);
59+
}

test/src/errors.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import test from 'ava';
2+
3+
import {withMethods, withoutMethods} from '../../src';
4+
5+
function macro(t, {empty, peek, pop}) {
6+
t.throws(() => peek(empty()), null, 'Cannot peek empty.');
7+
t.throws(() => pop(empty()), null, 'Cannot pop empty.');
8+
}
9+
10+
const implementations = [withMethods, withoutMethods];
11+
12+
for (const implementation of implementations) {
13+
test(`errors: ${implementation.name}`, macro, implementation);
14+
}

test/src/from.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import test from 'ava';
2+
3+
import {product, range, list, reversed} from '@aureooms/js-itertools';
4+
5+
import {withMethods, withoutMethods} from '../../src';
6+
7+
function macro(t, {from, iter, isEmpty}, n) {
8+
const stack = from(range(n));
9+
if (n === 0) t.true(isEmpty(stack), 'The stack is empty.');
10+
else t.false(isEmpty(stack), 'The stack is not empty.');
11+
12+
t.deepEqual(
13+
list(reversed(range(n))),
14+
list(iter(stack)),
15+
'All items are in the stack in order.',
16+
);
17+
t.deepEqual(
18+
list(reversed(range(n))),
19+
list(iter(stack)),
20+
'Can be iterated two times.',
21+
);
22+
}
23+
24+
const implementations = [withMethods, withoutMethods];
25+
const sizes = [0, 1, 2, 10, 42, 100, 121, 197, 1024, 2500];
26+
27+
for (const [implementation, n] of product([implementations, sizes])) {
28+
test(`from: ${implementation.name} <${n}>`, macro, implementation, n);
29+
}

test/src/immutability.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import test from 'ava';
2+
3+
import {product, enumerate, list, zip} from '@aureooms/js-itertools';
4+
5+
import {withMethods, withoutMethods} from '../../src';
6+
7+
function macro(t, {empty, push, pop, iter}, n) {
8+
const array = [];
9+
let stack = empty();
10+
11+
const pushed = [stack];
12+
for (let i = 0; i < n; ++i) {
13+
const r = Math.random();
14+
stack = push(stack, r);
15+
pushed.push(stack);
16+
array.push(r);
17+
}
18+
19+
const popped = [stack];
20+
for (let i = 0; i < n; ++i) {
21+
stack = pop(stack);
22+
popped.push(stack);
23+
}
24+
25+
popped.reverse();
26+
27+
for (const [i, [A, B]] of enumerate(zip(pushed, popped))) {
28+
const values = array.slice(0, i).reverse();
29+
30+
t.deepEqual(
31+
values,
32+
list(iter(A)),
33+
'All items are in order in the pushed stack.',
34+
);
35+
36+
t.deepEqual(
37+
values,
38+
list(iter(B)),
39+
'All items are in order in the popped stack.',
40+
);
41+
}
42+
}
43+
44+
const implementations = [withMethods, withoutMethods];
45+
const sizes = [0, 1, 2, 10, 42, 100, 121, 197, 1024, 2500];
46+
47+
for (const [implementation, n] of product([implementations, sizes])) {
48+
test(`immutability: ${implementation.name} <${n}>`, macro, implementation, n);
49+
}

test/src/methods.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import test from 'ava';
2+
3+
import {TypeError} from '@aureooms/js-error';
4+
import {product} from '@aureooms/js-itertools';
5+
6+
import {withMethods} from '../../src';
7+
8+
function macro(t, {empty}, n) {
9+
const array = [];
10+
11+
let stack = empty();
12+
t.true(stack.isEmpty(), 'At the begining, stack is empty.');
13+
t.throws(
14+
() => stack.peek(),
15+
{instanceOf: TypeError},
16+
'At the beginning, calling peek on empty stack throws type error.',
17+
);
18+
t.throws(
19+
() => stack.pop(),
20+
{instanceOf: TypeError},
21+
'At the beginning, calling pop on empty stack throws type error.',
22+
);
23+
24+
for (let i = 0; i < n; ++i) {
25+
const r = Math.random();
26+
27+
stack = stack.push(r);
28+
array.push(r);
29+
30+
t.false(stack.isEmpty(), i + 'The stack is not empty.');
31+
32+
t.deepEqual(stack.peek(), r, i + ' Peek returns the last value.');
33+
}
34+
35+
for (let i = 0; i < n; ++i) {
36+
t.false(stack.isEmpty(), i + 'The stack is not empty.');
37+
38+
t.deepEqual(stack.peek(), array.pop(), i + ' Pop works.');
39+
stack = stack.pop();
40+
}
41+
42+
t.true(stack.isEmpty(), 'At the end, stack is empty.');
43+
44+
t.throws(
45+
() => stack.peek(),
46+
{instanceOf: TypeError},
47+
'At the end, calling peek on empty stack throws type error.',
48+
);
49+
t.throws(
50+
() => stack.pop(),
51+
{instanceOf: TypeError},
52+
'At the end, calling pop on empty throws type error.',
53+
);
54+
}
55+
56+
const implementations = [withMethods];
57+
const sizes = [0, 1, 2, 10, 42, 100, 121, 197, 1024, 2500];
58+
59+
for (const [implementation, n] of product([implementations, sizes])) {
60+
test(`methods: ${implementation.name} <${n}>`, macro, implementation, n);
61+
}

0 commit comments

Comments
 (0)