From 06b2f7e3a901fa1ff69d1e687f7aaaf32fced427 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Sat, 21 Oct 2017 15:32:10 +0200
Subject: [PATCH] collections

---
 package-lock.json                         | Bin 165468 -> 154251 bytes
 package.json                              |  10 ++--
 src/structure/collections/hash-set.ts     |  50 +++++++++++++++++++
 src/structure/collections/linked-index.ts |  58 ++++++++++++++++++++++
 src/structure/collections/linked-set.ts   |   1 -
 src/structure/collections/range-set.ts    |  41 +++++++++++++++
 src/structure/spec/collections.spec.ts    |   6 +++
 7 files changed, 160 insertions(+), 6 deletions(-)
 create mode 100644 src/structure/collections/hash-set.ts
 create mode 100644 src/structure/collections/linked-index.ts
 delete mode 100644 src/structure/collections/linked-set.ts

diff --git a/package-lock.json b/package-lock.json
index fbf52d4b90d0f4e49c73635a1e0e111e5f3bff45..47e6bce95bb6fce12ea990a40b6cdc0d9bde06e8 100644
GIT binary patch
delta 1724
zcmZ9MYfO`86vuNOgqAx@E~0X+D6}Z;+Y8iH=!IKp>4gHNxMibM+d|8wv_QLP6hACF
z*_0=G5}ld4IkQNdj?8YF8TV<%OlP)a%O)RoNfwiBio58%EqmG`OZMf>$(!?>^FP1y
zf1ZW?X|H{ldM*U?L^^ly29KVFT%4W-L61%!G;P;K6eHsr^OVbD?{IcIoVJMA+tF$W
zO*OTJYQ^6EvElJngVm+1H#@lY`etpg$2u_*8IZbrCoCS$Aq{B;yIS32j*0p}^PplZ
zqM_Ec0%`&tb&@hr(DD>Xtm!3^OAo$RN-eo7x&oa;7TJ(f-tN$E*Nk);`kR~sE=!+w
zpvz=-M{JtWaKvLVDYq#Co*u8QU1PVl`fR=WUb%aqPVVg#bA#R?&uAnp4ULDSlF5!X
zpKm+;^xIWbU6?iRO==gyAfEpL)=;%Vg~tk^k{*!e#yaDzLg=T(r-am8mx;TyWF6kt
zl6?B}nS452lR|x>^!dvh=Ssjya~F&7x4V#y$A5(?tXhQfSP?wl2Px=UgqQHxk03(x
zW8lrE@~DcJh6VSa1nciX6%`3m(SMIozWNAO;;Yk;iIaPw46ns9zWxXD(V%CoO^=~0
zy5SDI3-dLCi-3{6kdD56aDDcA#sxoF2bl9a`(w<8YE&PBEZp@sT%~Vyq|6T+7xLz_
zoe%2K^8^a8VGqf`r++{eoikOTgAg&bw5-IPe?ldG{tl!wE}8Qyj<&yoIR%g&y<7@A
zk`E`5d@Qp-GPYDe4h`+eqod+F6xXspZ<esVJwx_ONdg&8mqH;fe93ggj~OZ<mp=U|
z6U+MGd7P_+s@ZUUG}{CpCt-mQR???EnQQ@yM9HuaYL-i07D7MnC?c!SZ)22ht_OZ}
z!3wGsxIzqh)a=UmPm4zko0bp0FJ=d4s)-Q4$s%Pax(MayZiEVK+sZz-BuwCa2^7<<
zGfN^bH8Ox(IN(#Cc}eLZ4r-WE{ElOD?cXPgs_n3fk5^s<9{N?VitgBxXn#`$`sL2t
zmL&>|U^2aOQkZ~{+Q9V3@vAMM!oh|(1w7NhbTWeoW584Jjuuwq7iJdu8lf1^#iGwD
zm>1%a(F_8-Tfs(&r(~*OZ6exaW5<jafsYO~<e*;-YyLOhQ30gUGs--ScNc0v7@N$m
zVOn1(j+s0K)3i`TM`b+R6bp84<YAcEp4JHXn5|+P?5(a!aJ;Q$4vi~Vmd57H$6D4S
zA@s-wri@80r~32~d{Re5_)rH$SkM$ReiN+6(>kKU`e|}^iB2!`-e0Un+#-*g65!R%
z&)}%7Y`{0n3~24i(&#5I!-g5WaRf3C^GFu<JY-mC4&m@lGRee`9>NCvDu)QVJ0#;a
zgIlJMPs$`c?Gr8H!ANL8EFPceZ<S~bHg|U<Fe)}%ZI+SRdJpFl`MC*;+&yM%u>|D9
zoz9k)R*$`JYFKYK*7Z$Uea1SCUGJv3rxdXZ$&Vg-1lN);B#{8bwOrtl(}3oOY-Q$j
zl7oxwB#SQ2$zrND$FEsq3aLa@3R|`%jU`;a&n&_GSbzsI2oH~pL00rQpUfo6MK5KL
z6EYeC4qw-hLY!XDCh~N3>L;emgW;jJZJ~aXPT4!;G7X7b&OlINazuKhIz_F{B<l(*
z{kovm9%wR}blt(>u+nW9(YM>g-NDJ=wvf%=>>4q;1Dw-q6giu4cQsKgTL8b;68`Ck
zmL%f`X&}S1t>ihVr@N&UtJ}o>pgk;?PH~e8rAca#_ZUK5gASWwisnwqV-;qr%2CND
zf@RlWppD!B36>T>ZFEFW`VvnS3694gRS*e>ng>vaoPp%AwDTNP7E3lNO}+tHWN5(K
p+cxC%bu~vO>zq1oo68=y(i4?xEYg!~e7gk-VsBG)!9aFx{uf$1HB$fp

delta 3599
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

diff --git a/package.json b/package.json
index a024c3177..6e12b2ee1 100644
--- a/package.json
+++ b/package.json
@@ -30,19 +30,19 @@
   "license": "MIT",
   "devDependencies": {
     "@types/benchmark": "^1.0.30",
-    "@types/jest": "^21.1.3",
-    "@types/node": "^8.0.41",
+    "@types/jest": "^21.1.4",
+    "@types/node": "^8.0.46",
     "benchmark": "^2.1.4",
     "download-cli": "^1.0.5",
     "jest": "^21.2.1",
     "rollup": "^0.50.0",
     "rollup-plugin-buble": "^0.16.0",
-    "rollup-plugin-commonjs": "^8.2.1",
+    "rollup-plugin-commonjs": "^8.2.4",
     "rollup-plugin-json": "^2.3.0",
     "rollup-plugin-node-resolve": "^3.0.0",
     "rollup-watch": "^4.3.1",
-    "ts-jest": "^21.1.2",
-    "tslint": "^5.7.0",
+    "ts-jest": "^21.1.3",
+    "tslint": "^5.8.0",
     "typescript": "^2.5.3",
     "uglify-js": "^3.1.4",
     "util.promisify": "^1.0.0"
diff --git a/src/structure/collections/hash-set.ts b/src/structure/collections/hash-set.ts
new file mode 100644
index 000000000..8cf25df55
--- /dev/null
+++ b/src/structure/collections/hash-set.ts
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+interface SetLike<T> {
+    readonly size: number;
+    add(a: T): boolean;
+    has(a: T): boolean;
+}
+
+class HashSetImpl<T> implements SetLike<T> {
+    size: number = 0;
+    private byHash: { [hash: number]: T[] } = Object.create(null);
+
+    add(a: T) {
+        const hash = this.getHash(a);
+        if (this.byHash[hash]) {
+            const xs = this.byHash[hash];
+            for (const x of xs) {
+                if (this.areEqual(a, x)) return false;
+            }
+            xs[xs.length] = a;
+            this.size++;
+            return true;
+        } else {
+            this.byHash[hash] = [a];
+            this.size++;
+            return true;
+        }
+    }
+
+    has(v: T) {
+        const hash = this.getHash(v);
+        if (!this.byHash[hash]) return false;
+        for (const x of this.byHash[hash]) {
+            if (this.areEqual(v, x)) return true;
+        }
+        return false;
+    }
+
+    constructor(private getHash: (v: T) => any, private areEqual: (a: T, b: T) => boolean) { }
+}
+
+function HashSet<T>(getHash: (v: T) => any, areEqual: (a: T, b: T) => boolean): SetLike<T> {
+    return new HashSetImpl<T>(getHash, areEqual);
+}
+
+export default HashSet;
\ No newline at end of file
diff --git a/src/structure/collections/linked-index.ts b/src/structure/collections/linked-index.ts
new file mode 100644
index 000000000..0f40f2d0d
--- /dev/null
+++ b/src/structure/collections/linked-index.ts
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+/** A data structure useful for graph traversal */
+interface LinkedIndex {
+    readonly head: number,
+    has(i: number): boolean,
+    remove(i: number): void
+}
+
+function LinkedIndex(size: number): LinkedIndex {
+    return new LinkedIndexImpl(size);
+}
+
+class LinkedIndexImpl implements LinkedIndex {
+    private prev: Int32Array;
+    private next: Int32Array;
+    head: number;
+
+    remove(i: number) {
+        const { prev, next } = this;
+        const p = prev[i], n = next[i];
+        if (p >= 0) {
+            next[p] = n;
+            prev[i] = -1;
+        }
+        if (n >= 0) {
+            prev[n] = p;
+            next[i] = -1;
+        }
+        if (i === this.head) {
+            if (p < 0) this.head = n;
+            else this.head = p;
+        }
+    }
+
+    has(i: number) {
+        return this.prev[i] >= 0 || this.next[i] >= 0;
+    }
+
+    constructor(size: number) {
+        this.head = size > 0 ? 0 : -1;
+        this.prev = new Int32Array(size);
+        this.next = new Int32Array(size);
+
+        for (let i = 0; i < size; i++) {
+            this.next[i] = i + 1;
+            this.prev[i] = i - 1;
+        }
+        this.prev[0] = -1;
+        this.next[size - 1] = -1;
+    }
+}
+
+export default LinkedIndex;
\ No newline at end of file
diff --git a/src/structure/collections/linked-set.ts b/src/structure/collections/linked-set.ts
deleted file mode 100644
index 9749ede22..000000000
--- a/src/structure/collections/linked-set.ts
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: fixed length doubly linked list used for graph traversal
\ No newline at end of file
diff --git a/src/structure/collections/range-set.ts b/src/structure/collections/range-set.ts
index cd243cc37..6543f9428 100644
--- a/src/structure/collections/range-set.ts
+++ b/src/structure/collections/range-set.ts
@@ -21,6 +21,29 @@ namespace RangeSet {
         toArray(): ArrayLike<number>
     }
 
+    export function hashCode(a: RangeSet) {
+        // hash of tuple (size, min, max, mid)
+        const { size } = a;
+        let hash = 23;
+        if (!size) return hash;
+        hash = 31 * hash + size;
+        hash = 31 * hash + a.elementAt(0);
+        hash = 31 * hash + a.elementAt(size - 1);
+        if (size > 2) hash = 31 * hash + a.elementAt(size >> 1);
+        return hash;
+    }
+
+    export function areEqual(a: RangeSet, b: RangeSet) {
+        if (a === b) return true;
+        if (a instanceof RangeImpl) {
+            if (b instanceof RangeImpl) return a.min === b.min && a.max === b.max;
+            return equalAR(b as ArrayImpl, a);
+        } else if (b instanceof RangeImpl) {
+            return equalAR(a as ArrayImpl, b);
+        }
+        return equalAA(a as ArrayImpl, b as ArrayImpl);
+    }
+
     export function union(a: RangeSet, b: RangeSet) {
         if (a instanceof RangeImpl) {
             if (b instanceof RangeImpl) return unionRR(a, b);
@@ -107,6 +130,20 @@ namespace RangeSet {
         return -1;
     }
 
+    function equalAR(a: ArrayImpl, b: RangeImpl) {
+        return a.size === b.size && a.min === b.min && a.max === b.max;
+    }
+
+    function equalAA(a: ArrayImpl, b: ArrayImpl) {
+        if (a.size !== b.size || a.min !== b.min || a.max !== b.max) return false;
+        const { size, values: xs } = a;
+        const { values: ys } = b;
+        for (let i = 0; i < size; i++) {
+            if (xs[i] !== ys[i]) return false;
+        }
+        return true;
+    }
+
     function areRangesIntersecting(a: Impl, b: Impl) {
         return a.size > 0 && b.size > 0 && a.max >= b.min && a.min <= b.max;
     }
@@ -150,6 +187,8 @@ namespace RangeSet {
     function unionAA(xs: ArrayLike<number>, ys: ArrayLike<number>) {
         const la = xs.length, lb = ys.length;
 
+        // sorted list merge.
+
         let i = 0, j = 0, resultSize = 0;
         while (i < la && j < lb) {
             const x = xs[i], y = ys[j];
@@ -203,6 +242,8 @@ namespace RangeSet {
     function intersectAA(xs: ArrayLike<number>, ys: ArrayLike<number>) {
         const la = xs.length, lb = ys.length;
 
+        // a variation on sorted list merge.
+
         let i = 0, j = 0, resultSize = 0;
         while (i < la && j < lb) {
             const x = xs[i], y = ys[j];
diff --git a/src/structure/spec/collections.spec.ts b/src/structure/spec/collections.spec.ts
index 7506936f3..848ec4803 100644
--- a/src/structure/spec/collections.spec.ts
+++ b/src/structure/spec/collections.spec.ts
@@ -137,6 +137,12 @@ describe('range set', () => {
     testEq('range', range, [1, 2, 3, 4]);
     testEq('sorted array', arr, [1, 3, 6]);
 
+    expect(RangeSet.areEqual(empty, singleton)).toBe(false);
+    expect(RangeSet.areEqual(singleton, singleton)).toBe(true);
+    expect(RangeSet.areEqual(range, singleton)).toBe(false);
+    expect(RangeSet.areEqual(arr, RangeSet.ofSortedArray([1, 3, 6]))).toBe(true);
+    expect(RangeSet.areEqual(arr, RangeSet.ofSortedArray([1, 4, 6]))).toBe(false);
+
     expect(empty.has(10)).toBe(false);
     expect(empty.indexOf(10)).toBe(-1);
 
-- 
GitLab