diff --git a/src/structure/collections/iterator.ts b/src/structure/collections/iterator.ts index 2fc26b10b3bf2f955b21ba3076a7ceebac06f0b0..4e6072234a90439ed573a5d2a7dbca45eafd91f5 100644 --- a/src/structure/collections/iterator.ts +++ b/src/structure/collections/iterator.ts @@ -10,28 +10,25 @@ * "Idiomatic" usage: * * const it = ...; - * for (let v = it.reset(data).nextValue(); !it.done; v = it.nextValue()) { ... } + * for (let v = it.nextValue(); !it.done; v = it.nextValue()) { ... } */ -interface Iterator<T, Data = any> { - [Symbol.iterator](): Iterator<T, Data>, +interface Iterator<T> { + [Symbol.iterator](): Iterator<T>, readonly done: boolean, readonly value: T, next(): { done: boolean, value: T }, - - reset(data: Data): Iterator<T, Data>, nextValue(): T } -class __EmptyIterator implements Iterator<any, any> { // tslint:disable-line:class-name +class EmptyIteratorImpl implements Iterator<any> { [Symbol.iterator]() { return this; } done = true; value = void 0; next() { return this; } nextValue() { return this.value; } - reset(value: undefined) { return this; } } -class __SingletonIterator<T> implements Iterator<T, T> { // tslint:disable-line:class-name +class SingletonIteratorImpl<T> implements Iterator<T> { private yielded = false; [Symbol.iterator]() { return this; } @@ -39,13 +36,10 @@ class __SingletonIterator<T> implements Iterator<T, T> { // tslint:disable-line: value: T; next() { this.done = this.yielded; this.yielded = true; return this; } nextValue() { return this.next().value; } - reset(value: T) { this.value = value; this.done = false; this.yielded = false; return this; } - constructor(value: T) { this.value = value; } } - -class __ArrayIterator<T> implements Iterator<T, ArrayLike<T>> { // tslint:disable-line:class-name +class ArrayIteratorImpl<T> implements Iterator<T> { private xs: ArrayLike<T> = []; private index: number = -1; private length: number = 0; @@ -63,7 +57,7 @@ class __ArrayIterator<T> implements Iterator<T, ArrayLike<T>> { // tslint:disabl nextValue() { return this.next().value; } - reset(xs: ArrayLike<T>) { + constructor(xs: ArrayLike<T>) { this.length = xs.length; this.done = false; this.xs = xs; @@ -72,47 +66,37 @@ class __ArrayIterator<T> implements Iterator<T, ArrayLike<T>> { // tslint:disabl } } -type Range = { min: number, max: number } -class __RangeIterator implements Iterator<number, Range> { // tslint:disable-line:class-name - private min: number; - private max: number; - +class RangeIteratorImpl implements Iterator<number> { [Symbol.iterator]() { return this; }; done = true; value: number; next() { ++this.value; - this.done = this.value >= this.max; + this.done = this.value > this.max; return this; } nextValue() { return this.next().value; } - reset({ min, max}: Range) { - this.min = min; - this.max = max; + constructor(min: number, private max: number) { this.value = min - 1; this.done = false; return this; } - - constructor(bounds: Range) { this.reset(bounds); } } -export const EmptyIterator: Iterator<any> = new __EmptyIterator(); -export function SingletonIterator<T>(value: T): Iterator<T, T> { return new __SingletonIterator(value); } -export function ArrayIterator<T>(xs?: ArrayLike<T>): Iterator<T, ArrayLike<T>> { - const ret = new __ArrayIterator<T>(); - if (xs) ret.reset(xs); - return ret; -} -export function RangeIterator(bounds?: Range): Iterator<number, Range> { return new __RangeIterator(bounds || { min: 0, max: 0 }); } +namespace Iterator { + export const Empty: Iterator<any> = new EmptyIteratorImpl(); + export function Singleton<T>(value: T): Iterator<T> { return new SingletonIteratorImpl(value); } + export function Array<T>(xs: ArrayLike<T>): Iterator<T> { return new ArrayIteratorImpl<T>(xs); } + export function Range(min: number, max: number): Iterator<number> { return new RangeIteratorImpl(min, max); } -export function toArray<T>(it: Iterator<T>): T[] { - const ret = []; - for (let v = it.nextValue(); !it.done; v = it.nextValue()) ret[ret.length] = v; - return ret; + export function toArray<T>(it: Iterator<T>): T[] { + const ret = []; + for (let v = it.nextValue(); !it.done; v = it.nextValue()) ret[ret.length] = v; + return ret; + } } export default Iterator \ No newline at end of file diff --git a/src/structure/collections/linear-set.ts b/src/structure/collections/linear-set.ts deleted file mode 100644 index 0ffdd02fcbce683e436c0030ffe0517135c6ceda..0000000000000000000000000000000000000000 --- a/src/structure/collections/linear-set.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO \ No newline at end of file diff --git a/src/structure/collections/range-set.ts b/src/structure/collections/range-set.ts new file mode 100644 index 0000000000000000000000000000000000000000..93a95a1611bcd970254bf098e86ebcff7fb307eb --- /dev/null +++ b/src/structure/collections/range-set.ts @@ -0,0 +1,228 @@ +/** + * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import Iterator from './iterator' + +interface RangeSet { + readonly size: number, + has(x: number): boolean, + indexOf(x: number): number, + elements(): Iterator<number> +} + +namespace RangeSet { + interface Impl extends RangeSet { + readonly min: number, + readonly max: number, + toArray(): ArrayLike<number> + } + + export function union(a: RangeSet, b: RangeSet) { + if (a instanceof RangeImpl) { + if (b instanceof RangeImpl) return unionRR(a, b); + return unionAR(b as ArrayImpl, a); + } else if (b instanceof RangeImpl) { + return unionAR(a as ArrayImpl, b); + } else return unionAA((a as Impl).toArray(), (b as Impl).toArray()); + } + + export function intersect(a: RangeSet, b: RangeSet) { + if (a instanceof RangeImpl) { + if (b instanceof RangeImpl) return intersectRR(a, b); + return intersectAR(b as ArrayImpl, a); + } else if (b instanceof RangeImpl) { + return intersectAR(a as ArrayImpl, b); + } else { + const ai = a as Impl, bi = b as Impl; + if (!areRangesIntersecting(ai, bi)) return Empty; + return intersectAA(ai.toArray(), bi.toArray()); + } + } + + class RangeImpl implements Impl { + size: number; + has(x: number) { return x >= this.min && x <= this.max; } + indexOf(x: number) { return x >= this.min && x <= this.max ? x - this.min : -1; } + toArray() { + const ret = new Array(this.size); + for (let i = 0; i < this.size; i++) ret[i] = i + this.min; + return ret; + } + elements() { return Iterator.Range(this.min, this.max); } + + constructor(public min: number, public max: number) { + this.size = max - min + 1; + } + } + + class ArrayImpl implements Impl { + size: number; + public min: number; + public max: number; + has(x: number) { return x >= this.min && x <= this.max && binarySearch(this.values, x) >= 0; } + indexOf(x: number) { return x >= this.min && x <= this.max ? binarySearch(this.values, x) : -1; } + toArray() { return this.values; } + elements() { return Iterator.Array(this.values); } + + constructor(public values: ArrayLike<number>) { + this.min = values[0]; + this.max = values[values.length - 1]; + this.size = values.length; + } + } + + export function ofSingleton(value: number): RangeSet { return new RangeImpl(value, value); } + export function ofRange(min: number, max: number): RangeSet { return new RangeImpl(min, max); } + /** It is the responsibility of the caller to ensure the array is sorted and contains unique values. */ + export function ofSortedArray(xs: ArrayLike<number>): RangeSet { + if (!xs.length) return Empty; + // check if the array is just a range + if (xs[xs.length - 1] - xs[0] + 1 === xs.length) return ofRange(xs[0], xs[xs.length - 1]); + return new ArrayImpl(xs); + } + export const Empty = ofRange(0, -1); + + function binarySearch(xs: ArrayLike<number>, value: number) { + let min = 0, max = xs.length - 1; + while (min <= max) { + if (min + 11 > max) { + for (let i = min; i <= max; i++) { + if (value === xs[i]) return i; + } + return -1; + } + + const mid = (min + max) >> 1; + const v = xs[mid]; + if (value < v) max = mid - 1; + else if (value > v) min = mid + 1; + else return mid; + } + return -1; + } + + function areRangesIntersecting(a: Impl, b: Impl) { + return a.size > 0 && b.size > 0 && a.max >= b.min && a.min <= b.max; + } + + function unionRR(a: RangeImpl, b: RangeImpl) { + if (!a.size) return b; + if (!b.size) return a; + if (areRangesIntersecting(a, b)) return ofRange(Math.min(a.min, b.min), Math.max(a.max, b.max)); + let l, r; + if (a.min < b.min) { l = a; r = b; } + else { l = b; r = a; } + const arr = new Int32Array(a.size + b.size); + for (let i = 0; i < l.size; i++) arr[i] = i + l.min; + for (let i = 0; i < r.size; i++) arr[i + l.size] = i + r.min; + return ofSortedArray(arr); + } + + function unionAR(a: ArrayImpl, b: RangeImpl) { + if (!b.size) return a; + // is the array fully contained in the range? + if (a.min >= b.min && a.max <= b.max) return b; + + const xs = a.values; + const { min, max } = b; + + let start = 0, end = xs.length - 1; + while (xs[start] < min) { start++; } + while (xs[end] > max) { end--; } + end++; + + const size = start + (xs.length - end) + b.size; + const indices = new Int32Array(size); + let offset = 0; + for (let i = 0; i < start; i++) indices[offset++] = xs[i]; + for (let i = min; i <= max; i++) indices[offset++] = i; + for (let i = end, _i = xs.length; i < _i; i++) indices[offset] = xs[i]; + + return ofSortedArray(indices); + } + + function unionAA(xs: ArrayLike<number>, ys: ArrayLike<number>) { + const la = xs.length, lb = ys.length; + + let i = 0, j = 0, resultSize = 0; + while (i < la && j < lb) { + const x = xs[i], y = ys[j]; + resultSize++; + if (x < y) { i++; } + else if (x > y) { j++; } + else { i++; j++; } + } + resultSize += Math.max(la - i, lb - j); + + const indices = new Int32Array(resultSize); + let offset = 0; + i = 0; + j = 0; + while (i < la && j < lb) { + const x = xs[i], y = ys[j]; + if (x < y) { indices[offset++] = x; i++; } + else if (x > y) { indices[offset++] = y; j++; } + else { indices[offset++] = x; i++; j++; } + } + for (; i < la; i++) { indices[offset++] = xs[i]; } + for (; j < lb; j++) { indices[offset++] = ys[j]; } + + return ofSortedArray(indices); + } + + function intersectRR(a: RangeImpl, b: RangeImpl) { + if (!areRangesIntersecting(a, b)) return Empty; + return ofRange(Math.max(a.min, b.min), Math.min(a.max, b.max)); + } + + function intersectAR(a: ArrayImpl, r: RangeImpl) { + const xs = a.values; + let resultSize = 0; + for (let i = 0, _i = xs.length; i < _i; i++) { + if (r.has(xs[i])) resultSize++; + } + + if (!resultSize) return Empty; + + const indices = new Int32Array(resultSize); + let offset = 0; + + for (let i = 0, _i = xs.length; i < _i; i++) { + if (r.has(xs[i])) indices[offset++] = xs[i]; + } + + return ofSortedArray(indices); + } + + function intersectAA(xs: ArrayLike<number>, ys: ArrayLike<number>) { + const la = xs.length, lb = ys.length; + + let i = 0, j = 0, resultSize = 0; + while (i < la && j < lb) { + const x = xs[i], y = ys[j]; + if (x < y) { i++; } + else if (x > y) { j++; } + else { i++; j++; resultSize++; } + } + + if (!resultSize) return Empty; + + const indices = new Int32Array(resultSize); + let offset = 0; + i = 0; + j = 0; + while (i < la && j < lb) { + const x = xs[i], y = ys[j]; + if (x < y) { i++; } + else if (x > y) { j++; } + else { indices[offset++] = x; i++; j++; } + } + + return ofSortedArray(indices); + } +} + +export default RangeSet \ No newline at end of file diff --git a/src/structure/spec/collections.spec.ts b/src/structure/spec/collections.spec.ts index c23fee8decaddb6da83a4f8e631f2bb5a2267a15..57e130ac83d53b26aadfd71d5f568d159a6ddc83 100644 --- a/src/structure/spec/collections.spec.ts +++ b/src/structure/spec/collections.spec.ts @@ -4,24 +4,22 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import Iterator, * as I from '../collections/iterator' +import Iterator from '../collections/iterator' import IntPair from '../collections/int-pair' import * as Sort from '../collections/sort' +import RangeSet from '../collections/range-set' describe('basic iterators', () => { function check<T>(name: string, iter: Iterator<T>, expected: T[]) { it(name, () => { - expect(I.toArray(iter)).toEqual(expected); + expect(Iterator.toArray(iter)).toEqual(expected); }); } - check('empty', I.EmptyIterator, []); - check('singleton', I.SingletonIterator(10), [10]); - check('singleton reset', I.SingletonIterator(10).reset(13), [13]); - check('array', I.ArrayIterator([1, 2, 3]), [1, 2, 3]); - check('array reset', I.ArrayIterator([1, 2, 3]).reset([4]), [4]); - check('range', I.RangeIterator({ min: 0, max: 3 }), [0, 1, 2]); - check('range reset', I.RangeIterator().reset({ min: 1, max: 2 }), [1]); + check('empty', Iterator.Empty, []); + check('singleton', Iterator.Singleton(10), [10]); + check('array', Iterator.Array([1, 2, 3]), [1, 2, 3]); + check('range', Iterator.Range(0, 3), [0, 1, 2, 3]); }); describe('int pair', () => { @@ -117,4 +115,66 @@ describe('qsort-dual array', () => { } test('sorted', data, false); test('shuffled', data, true); -}) \ No newline at end of file +}) + +describe('range set', () => { + function testEq(name: string, set: RangeSet, expected: number[]) { + it(name, () => { + // copy the arrays to ensure "compatibility" between typed and native arrays + expect(Array.prototype.slice.call(Iterator.toArray(set.elements()))).toEqual(Array.prototype.slice.call(expected)); + }); + } + + const empty = RangeSet.Empty; + const singleton = RangeSet.ofSingleton(10); + const range = RangeSet.ofRange(1, 4); + const arr = RangeSet.ofSortedArray([1, 3, 6]); + + testEq('empty', empty, []); + testEq('singleton', singleton, [10]); + testEq('range', range, [1, 2, 3, 4]); + testEq('sorted array', arr, [1, 3, 6]); + + expect(empty.has(10)).toBe(false); + expect(empty.indexOf(10)).toBe(-1); + + expect(singleton.has(10)).toBe(true); + expect(singleton.has(11)).toBe(false); + expect(singleton.indexOf(10)).toBe(0); + expect(singleton.indexOf(11)).toBe(-1); + + expect(range.has(4)).toBe(true); + expect(range.has(5)).toBe(false); + expect(range.indexOf(4)).toBe(3); + expect(range.indexOf(11)).toBe(-1); + + expect(arr.has(3)).toBe(true); + expect(arr.has(4)).toBe(false); + expect(arr.indexOf(3)).toBe(1); + expect(arr.indexOf(11)).toBe(-1); + + testEq('union ES', RangeSet.union(empty, singleton), [10]); + testEq('union ER', RangeSet.union(empty, range), [1, 2, 3, 4]); + testEq('union EA', RangeSet.union(empty, arr), [1, 3, 6]); + testEq('union SS', RangeSet.union(singleton, RangeSet.ofSingleton(16)), [10, 16]); + testEq('union SR', RangeSet.union(range, singleton), [1, 2, 3, 4, 10]); + testEq('union SA', RangeSet.union(arr, singleton), [1, 3, 6, 10]); + testEq('union SA1', RangeSet.union(arr, RangeSet.ofSingleton(3)), [1, 3, 6]); + testEq('union RR', RangeSet.union(range, range), [1, 2, 3, 4]); + testEq('union RR1', RangeSet.union(range, RangeSet.ofRange(6, 7)), [1, 2, 3, 4, 6, 7]); + testEq('union RR2', RangeSet.union(range, RangeSet.ofRange(3, 5)), [1, 2, 3, 4, 5]); + testEq('union RA', RangeSet.union(range, arr), [1, 2, 3, 4, 6]); + testEq('union AA', RangeSet.union(arr, RangeSet.ofSortedArray([2, 4, 6, 7])), [1, 2, 3, 4, 6, 7]); + testEq('union AA1', RangeSet.union(arr, RangeSet.ofSortedArray([2, 3, 4, 6, 7])), [1, 2, 3, 4, 6, 7]); + + testEq('intersect ES', RangeSet.intersect(empty, singleton), []); + testEq('intersect ER', RangeSet.intersect(empty, range), []); + testEq('intersect EA', RangeSet.intersect(empty, arr), []); + testEq('intersect SS', RangeSet.intersect(singleton, RangeSet.ofSingleton(16)), []); + testEq('intersect SS', RangeSet.intersect(singleton, singleton), [10]); + testEq('intersect SR', RangeSet.intersect(range, singleton), []); + testEq('intersect RR', RangeSet.intersect(range, range), [1, 2, 3, 4]); + testEq('intersect RR2', RangeSet.intersect(range, RangeSet.ofRange(3, 5)), [3, 4]); + testEq('intersect RA', RangeSet.intersect(range, arr), [1, 3]); + testEq('intersect AA', RangeSet.intersect(arr, RangeSet.ofSortedArray([2, 3, 4, 6, 7])), [3, 6]); +}); \ No newline at end of file