From a81464ddc4c266c453a45fd1dd411a6d5686e980 Mon Sep 17 00:00:00 2001
From: David Sehnal <david.sehnal@gmail.com>
Date: Mon, 30 Oct 2017 20:42:23 +0100
Subject: [PATCH] Table collection

---
 src/mol-base/collections/_spec/column.spec.ts |  41 -------
 src/mol-base/collections/_spec/table.spec.ts  |  82 ++++++++++++++
 src/mol-base/collections/column.ts            | 106 ++++++++++++------
 src/mol-base/collections/table.ts             |  65 +++++++++++
 src/mol-io/reader/cif/schema.ts               |   5 +-
 src/mol-io/reader/common/text/column/fixed.ts |   5 +-
 src/mol-io/reader/common/text/column/token.ts |  14 +--
 7 files changed, 222 insertions(+), 96 deletions(-)
 delete mode 100644 src/mol-base/collections/_spec/column.spec.ts
 create mode 100644 src/mol-base/collections/_spec/table.spec.ts
 create mode 100644 src/mol-base/collections/table.ts

diff --git a/src/mol-base/collections/_spec/column.spec.ts b/src/mol-base/collections/_spec/column.spec.ts
deleted file mode 100644
index 6f1aa50b6..000000000
--- a/src/mol-base/collections/_spec/column.spec.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import Column, { isTypedArray } from '../column'
-
-describe('column', () => {
-    const cc = Column.ofConst(10, 2, Column.Type.int);
-    const arr = Column.ofArray({ array: [1, 2, 3, 4], type: Column.Type.int });
-    const arrWindow = Column.window(arr, 1, 3);
-
-    const typed = Column.ofArray({ array: new Int32Array([1, 2, 3, 4]), type: Column.Type.int });
-    const typedWindow = Column.window(typed, 1, 3);
-
-    const numStr = Column.ofArray({ array: [1, 2] as any, type: Column.Type.str });
-
-    it('constant', () => {
-        expect(cc.rowCount).toBe(2);
-        expect(cc.value(0)).toBe(10);
-    });
-
-    it('arr', () => {
-        expect(arr.rowCount).toBe(4);
-        expect(arr.value(1)).toBe(2);
-        expect(arrWindow.value(0)).toBe(2);
-        expect(arrWindow.rowCount).toBe(2);
-    });
-
-    it('typed', () => {
-        expect(typedWindow.value(0)).toBe(2);
-        expect(typedWindow.rowCount).toBe(2);
-        expect(isTypedArray(typedWindow.toArray())).toBe(true);
-    });
-
-    it('numStr', () => {
-        expect(numStr.value(0)).toBe('1');
-        expect(numStr.toArray()).toEqual(['1', '2']);
-    });
-})
\ No newline at end of file
diff --git a/src/mol-base/collections/_spec/table.spec.ts b/src/mol-base/collections/_spec/table.spec.ts
new file mode 100644
index 000000000..a41ce9f11
--- /dev/null
+++ b/src/mol-base/collections/_spec/table.spec.ts
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Column, { isTypedArray } from '../column'
+import Table from '../table'
+
+describe('column', () => {
+    const cc = Column.ofConst(10, 2, Column.Type.int);
+    const arr = Column.ofArray({ array: [1, 2, 3, 4], type: Column.Type.int });
+    const arrWindow = Column.window(arr, 1, 3);
+
+    const typed = Column.ofArray({ array: new Int32Array([1, 2, 3, 4]), type: Column.Type.int });
+    const typedWindow = Column.window(typed, 1, 3);
+
+    const numStr = Column.ofArray({ array: [1, 2] as any, type: Column.Type.str });
+
+    it('constant', () => {
+        expect(cc.rowCount).toBe(2);
+        expect(cc.value(0)).toBe(10);
+    });
+
+    it('arr', () => {
+        expect(arr.rowCount).toBe(4);
+        expect(arr.value(1)).toBe(2);
+        expect(arrWindow.value(0)).toBe(2);
+        expect(arrWindow.rowCount).toBe(2);
+    });
+
+    it('typed', () => {
+        expect(typedWindow.value(0)).toBe(2);
+        expect(typedWindow.rowCount).toBe(2);
+        expect(isTypedArray(typedWindow.toArray())).toBe(true);
+    });
+
+    it('numStr', () => {
+        expect(numStr.value(0)).toBe('1');
+        expect(numStr.toArray()).toEqual(['1', '2']);
+    });
+
+    it('permutation', () => {
+        expect(Column.permutation(arr, [1, 0, 3, 2]).toArray()).toEqual([2, 1, 4, 3]);
+    });
+})
+
+describe('table', () => {
+    const schema = {
+        x: Column.Type.int,
+        n: Column.Type.str
+    };
+
+    it('ofRows', () => {
+        const t = Table.ofRows<typeof schema>(schema, [
+            { x: 10, n: 'row1' },
+            { x: -1, n: 'row2' },
+        ]);
+        expect(t.x.toArray()).toEqual([10, -1]);
+        expect(t.n.toArray()).toEqual(['row1', 'row2']);
+    });
+
+    it('ofColumns', () => {
+        const t = Table.ofColumns<typeof schema>({
+            x: Column.ofArray({ array: [10, -1], type: Column.Type.int }),
+            n: Column.ofArray({ array: ['row1', 'row2'], type: Column.Type.str }),
+        });
+        expect(t.x.toArray()).toEqual([10, -1]);
+        expect(t.n.toArray()).toEqual(['row1', 'row2']);
+    });
+
+    it('sort', () => {
+        const t = Table.ofColumns<typeof schema>({
+            x: Column.ofArray({ array: [10, -1], type: Column.Type.int }),
+            n: Column.ofArray({ array: ['row1', 'row2'], type: Column.Type.str }),
+        });
+        const { x } = t;
+        const sorted = Table.sort(t, (i, j) => x.value(i) - x.value(j))
+        expect(sorted.x.toArray()).toEqual([-1, 10]);
+        expect(sorted.n.toArray()).toEqual(['row2', 'row1']);
+    });
+});
\ No newline at end of file
diff --git a/src/mol-base/collections/column.ts b/src/mol-base/collections/column.ts
index b9ec618e0..4650e8a37 100644
--- a/src/mol-base/collections/column.ts
+++ b/src/mol-base/collections/column.ts
@@ -13,19 +13,22 @@ interface Column<T> {
     value(row: number): T,
     valueKind(row: number): Column.ValueKind,
     toArray(params?: Column.ToArrayParams): ReadonlyArray<T>,
-    stringEquals(row: number, value: string): boolean,
     areValuesEqual(rowA: number, rowB: number): boolean
 }
 
 namespace Column {
-    export type Type = typeof Type.str | typeof Type.int | typeof Type.float | typeof Type.vector | typeof Type.matrix
+    export type Type = typeof Type.str | typeof Type.int | typeof Type.float | Type.Vector | Type.Matrix
 
     export namespace Type {
-        export const str = { '@type': '' as string, kind: 'str' as 'str' };
-        export const int = { '@type': 0 as number, kind: 'int' as 'int' };
-        export const float = { '@type': 0 as number, kind: 'float' as 'float' };
-        export const vector = { '@type': [] as number[], kind: 'vector' as 'vector' };
-        export const matrix = { '@type': [] as number[][], kind: 'matrix' as 'matrix' };
+        export const str = { T: '' as string, kind: 'str' as 'str' };
+        export const int = { T: 0 as number, kind: 'int' as 'int' };
+        export const float = { T: 0 as number, kind: 'float' as 'float' };
+
+        export type Vector = { T: number[], dim: number, kind: 'vector' };
+        export type Matrix = { T: number[][], rows: number, cols: number, kind: 'matrix' };
+
+        export function vector(dim: number): Vector { return { T: [] as number[], dim, kind: 'vector' }; }
+        export function matrix(rows: number, cols: number): Matrix { return { T: [] as number[][], rows, cols, kind: 'matrix' }; }
     }
 
     export interface ToArrayParams {
@@ -36,33 +39,39 @@ namespace Column {
     }
 
     export interface LambdaSpec<T extends Type> {
-        value: (row: number) => T['@type'],
+        value: (row: number) => T['T'],
         rowCount: number,
         type: T,
         valueKind?: (row: number) => ValueKind,
     }
 
     export interface ArraySpec<T extends Type> {
-        array: ArrayLike<T['@type']>,
+        array: ArrayLike<T['T']>,
         type: T,
         valueKind?: (row: number) => ValueKind
     }
 
+    export interface MapSpec<S extends Type, T extends Type> {
+        f: (v: S['T']) => T['T'],
+        type: T,
+        valueKind?: (row: number) => ValueKind,
+    }
+
     export const enum ValueKind { Present = 0, NotPresent = 1, Unknown = 2 }
 
-    export function Undefined<T extends Type>(rowCount: number, type: T): Column<T['@type']> {
-        return constColumn(type['@type'], rowCount, type, ValueKind.NotPresent);
+    export function Undefined<T extends Type>(rowCount: number, type: T): Column<T['T']> {
+        return constColumn(type['T'], rowCount, type, ValueKind.NotPresent);
     }
 
-    export function ofConst<T extends Type>(v: T['@type'], rowCount: number, type: T): Column<T['@type']> {
+    export function ofConst<T extends Type>(v: T['T'], rowCount: number, type: T): Column<T['T']> {
         return constColumn(v, rowCount, type, ValueKind.Present);
     }
 
-    export function ofLambda<T extends Type>(spec: LambdaSpec<T>): Column<T['@type']> {
+    export function ofLambda<T extends Type>(spec: LambdaSpec<T>): Column<T['T']> {
         return lambdaColumn(spec);
     }
 
-    export function ofArray<T extends Column.Type>(spec: Column.ArraySpec<T>): Column<T['@type']> {
+    export function ofArray<T extends Column.Type>(spec: Column.ArraySpec<T>): Column<T['T']> {
         return arrayColumn(spec);
     }
 
@@ -70,6 +79,10 @@ namespace Column {
         return windowColumn(column, start, end);
     }
 
+    export function permutation<T>(column: Column<T>, indices: ArrayLike<number>, checkIndentity = true) {
+        return columnPermutation(column, indices, checkIndentity);
+    }
+
     /** Makes the column backned by an array. Useful for columns that accessed often. */
     export function asArrayColumn<T>(c: Column<T>, array?: ToArrayParams['array']): Column<T> {
         if (c['@array']) return c;
@@ -80,8 +93,8 @@ namespace Column {
 
 export default Column;
 
-function constColumn<T extends Column.Type>(v: T['@type'], rowCount: number, type: T, valueKind: Column.ValueKind): Column<T['@type']> {
-    const value: Column<T['@type']>['value'] = row => v;
+function constColumn<T extends Column.Type>(v: T['T'], rowCount: number, type: T, valueKind: Column.ValueKind): Column<T['T']> {
+    const value: Column<T['T']>['value'] = row => v;
     return {
         '@type': type,
         '@array': void 0,
@@ -94,16 +107,11 @@ function constColumn<T extends Column.Type>(v: T['@type'], rowCount: number, typ
             for (let i = 0, _i = array.length; i < _i; i++) array[i] = v;
             return array;
         },
-        stringEquals: type.kind === 'str'
-            ? (row, value) => value === v
-            : type.kind === 'float' || type.kind === 'int'
-                ? (row, value) => +value === v
-                : (row, value) => false,
         areValuesEqual: (rowA, rowB) => true
     }
 }
 
-function lambdaColumn<T extends Column.Type>({ value, valueKind, rowCount, type }: Column.LambdaSpec<T>): Column<T['@type']> {
+function lambdaColumn<T extends Column.Type>({ value, valueKind, rowCount, type }: Column.LambdaSpec<T>): Column<T['T']> {
     return {
         '@type': type,
         '@array': void 0,
@@ -116,18 +124,13 @@ function lambdaColumn<T extends Column.Type>({ value, valueKind, rowCount, type
             for (let i = 0, _i = array.length; i < _i; i++) array[i] = value(i + start);
             return array;
         },
-        stringEquals: type.kind === 'str'
-            ? (row, v) => value(row) === v
-            : type.kind === 'float' || type.kind === 'int'
-            ? (row, v) => value(row) === +v
-            : (row, value) => false,
         areValuesEqual: (rowA, rowB) => value(rowA) === value(rowB)
     }
 }
 
-function arrayColumn<T extends Column.Type>({ array, type, valueKind }: Column.ArraySpec<T>): Column<T['@type']> {
+function arrayColumn<T extends Column.Type>({ array, type, valueKind }: Column.ArraySpec<T>): Column<T['T']> {
     const rowCount = array.length;
-    const value: Column<T['@type']>['value'] = type.kind === 'str'
+    const value: Column<T['T']>['value'] = type.kind === 'str'
         ? row => { const v = array[row]; return typeof v === 'string' ? v : '' + v; }
         : row => array[row];
 
@@ -153,16 +156,11 @@ function arrayColumn<T extends Column.Type>({ array, type, valueKind }: Column.A
             ? params => typedArrayWindow(array, params) as any as ReadonlyArray<T>
             : params => {
                 const { start, end } = getArrayBounds(rowCount, params);
-                if (start === 0 && end === array.length) return array as ReadonlyArray<T['@type']>;
+                if (start === 0 && end === array.length) return array as ReadonlyArray<T['T']>;
                 const ret = new (params && typeof params.array !== 'undefined' ? params.array : (array as any).constructor)(end - start) as any;
                 for (let i = 0, _i = end - start; i < _i; i++) ret[i] = array[start + i];
                 return ret;
             },
-        stringEquals: type.kind === 'int' || type.kind === 'float'
-            ? (row, value) => (array as any)[row] === +value
-            : type.kind === 'str'
-            ? (row, value) => { const v = array[row]; return typeof v === 'string' ? v === value : +v === +value; }
-            : (row, value) => false,
         areValuesEqual: (rowA, rowB) => array[rowA] === array[rowB]
     }
 }
@@ -179,7 +177,7 @@ function windowTyped<T>(c: Column<T>, start: number, end: number): Column<T> {
 }
 
 function windowFull<T>(c: Column<T>, start: number, end: number): Column<T> {
-    const v = c.value, vk = c.valueKind, se = c.stringEquals, ave = c.areValuesEqual;
+    const v = c.value, vk = c.valueKind, ave = c.areValuesEqual;
     const value: Column<T>['value'] = start === 0 ? v : row => v(row + start);
     const rowCount = end - start;
     return {
@@ -194,11 +192,45 @@ function windowFull<T>(c: Column<T>, start: number, end: number): Column<T> {
             for (let i = 0, _i = array.length; i < _i; i++) array[i] = v(i + start);
             return array;
         },
-        stringEquals: start === 0 ? se : (row, value) => se(row + start, value),
         areValuesEqual: start === 0 ? ave : (rowA, rowB) => ave(rowA + start, rowB + start)
     };
 }
 
+function columnPermutation<T>(c: Column<T>, map: ArrayLike<number>, checkIdentity: boolean): Column<T> {
+    if (!c.isDefined) return c;
+    if (checkIdentity) {
+        let isIdentity = true;
+        for (let i = 0, _i = map.length; i < _i; i++) {
+            if (map[i] !== i) {
+                isIdentity = false;
+                break;
+            }
+        }
+        if (isIdentity) return c;
+    }
+    return permutationFull(c, map);
+}
+
+function permutationFull<T>(c: Column<T>, map: ArrayLike<number>): Column<T> {
+    const v = c.value, vk = c.valueKind, ave = c.areValuesEqual;
+    const value: Column<T>['value'] = row => v(map[row]);
+    const rowCount = c.rowCount;
+    return {
+        '@type': c['@type'],
+        '@array': void 0,
+        isDefined: c.isDefined,
+        rowCount,
+        value,
+        valueKind: row => vk(map[row]),
+        toArray: params => {
+            const { array } = createArray(rowCount, params);
+            for (let i = 0, _i = array.length; i < _i; i++) array[i] = v(map[i]);
+            return array;
+        },
+        areValuesEqual: (rowA, rowB) => ave(map[rowA], map[rowB])
+    };
+}
+
 /** A helped function for Column.toArray */
 export function getArrayBounds(rowCount: number, params?: Column.ToArrayParams) {
     const start = params && typeof params.start !== 'undefined' ? Math.max(Math.min(params.start, rowCount - 1), 0) : 0;
diff --git a/src/mol-base/collections/table.ts b/src/mol-base/collections/table.ts
new file mode 100644
index 000000000..152a894a2
--- /dev/null
+++ b/src/mol-base/collections/table.ts
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Column from './column'
+import { sortArray } from './sort'
+
+type Table<Schema extends Table.Schema> = { readonly _rowCount: number, readonly _columns: ReadonlyArray<string> } & Table.Columns<Schema>
+
+namespace Table {
+    export type Schema = { [field: string]: Column.Type }
+    export type Columns<S extends Schema> = { [C in keyof S]: Column<S[C]['T']> }
+    export type Row<S extends Schema> = { [C in keyof S]: S[C]['T'] }
+
+    export function ofColumns<S extends Schema, R extends Table<S> = Table<S>>(columns: Columns<S>): R {
+        const _columns = Object.keys(columns);
+        const _rowCount = columns[_columns[0]].rowCount;
+        return { _rowCount, _columns, ...(columns as any) };
+    }
+
+    export function ofRows<S extends Schema, R extends Table<S> = Table<S>>(schema: Schema, rows: ArrayLike<Row<S>>): R {
+        const ret = Object.create(null);
+        const rowCount = rows.length;
+        const columns = Object.keys(schema);
+        ret._rowCount = rowCount;
+        ret._columns = columns;
+        for (const k of Object.keys(schema)) {
+            (ret as any)[k] = Column.ofLambda({
+                rowCount,
+                type: schema[k],
+                value: r => rows[r][k],
+                valueKind: r => typeof rows[r][k] === 'undefined' ? Column.ValueKind.NotPresent : Column.ValueKind.Present
+            })
+        }
+        return ret as R;
+    }
+
+    /** Sort and return a new table */
+    export function sort<T extends Table<S>, S extends Schema>(table: T, cmp: (i: number, j: number) => number) {
+        const indices = new Int32Array(table._rowCount);
+        for (let i = 0, _i = indices.length; i < _i; i++) indices[i] = i;
+        sortArray(indices, (_, i, j) => cmp(i, j));
+
+        let isIdentity = true;
+        for (let i = 0, _i = indices.length; i < _i; i++) {
+            if (indices[i] !== i) {
+                isIdentity = false;
+                break;
+            }
+        }
+        if (isIdentity) return table;
+
+        const ret = Object.create(null);
+        ret._rowCount = table._rowCount;
+        ret._columns = table._columns;
+        for (const c of table._columns) {
+            ret[c] = Column.permutation((table as any)[c], indices, false);
+        }
+        return ret;
+    }
+}
+
+export default Table
\ No newline at end of file
diff --git a/src/mol-io/reader/cif/schema.ts b/src/mol-io/reader/cif/schema.ts
index 368aaa110..adf421e3b 100644
--- a/src/mol-io/reader/cif/schema.ts
+++ b/src/mol-io/reader/cif/schema.ts
@@ -66,7 +66,6 @@ export namespace Field {
             rowCount: field.rowCount,
             value,
             valueKind: field.valueKind,
-            stringEquals: field.stringEquals,
             areValuesEqual: field.areValuesEqual,
             toArray
         };
@@ -79,14 +78,14 @@ export namespace Field {
     function Vector(rows: number) {
         return function(field: Data.Field, category: Data.Category, key: string) {
             const value = (row: number) => Data.getVector(category, key, rows, row);
-            return create(Column.Type.vector, field, value, params => createAndFillArray(field.rowCount, value, params));
+            return create(Column.Type.vector(rows), field, value, params => createAndFillArray(field.rowCount, value, params));
         }
     }
 
     function Matrix(rows: number, cols: number) {
         return function(field: Data.Field, category: Data.Category, key: string) {
             const value = (row: number) => Data.getMatrix(category, key, rows, cols, row);
-            return create(Column.Type.matrix, field, value, params => createAndFillArray(field.rowCount, value, params));
+            return create(Column.Type.matrix(rows, cols), field, value, params => createAndFillArray(field.rowCount, value, params));
         }
     }
 
diff --git a/src/mol-io/reader/common/text/column/fixed.ts b/src/mol-io/reader/common/text/column/fixed.ts
index 26d1c969d..0dceba7ac 100644
--- a/src/mol-io/reader/common/text/column/fixed.ts
+++ b/src/mol-io/reader/common/text/column/fixed.ts
@@ -14,11 +14,11 @@ export default function FixedColumnProvider(lines: Tokens) {
     }
 }
 
-export function FixedColumn<T extends Column.Type>(lines: Tokens, offset: number, width: number, type: T): Column<T['@type']> {
+export function FixedColumn<T extends Column.Type>(lines: Tokens, offset: number, width: number, type: T): Column<T['T']> {
     const { data, indices, count: rowCount } = lines;
     const { kind } = type;
 
-    const value: Column<T['@type']>['value'] = kind === 'str' ? row => {
+    const value: Column<T['T']>['value'] = kind === 'str' ? row => {
         let s = indices[2 * row] + offset, le = indices[2 * row + 1];
         if (s >= le) return '';
         let e = s + width;
@@ -41,7 +41,6 @@ export function FixedColumn<T extends Column.Type>(lines: Tokens, offset: number
         value,
         valueKind: row => Column.ValueKind.Present,
         toArray: params => createAndFillArray(rowCount, value, params),
-        stringEquals: (row, v) => value(row) === v,
         areValuesEqual: (rowA, rowB) => value(rowA) === value(rowB)
     };
 }
\ No newline at end of file
diff --git a/src/mol-io/reader/common/text/column/token.ts b/src/mol-io/reader/common/text/column/token.ts
index 447752cc1..89e6ac6ff 100644
--- a/src/mol-io/reader/common/text/column/token.ts
+++ b/src/mol-io/reader/common/text/column/token.ts
@@ -14,11 +14,11 @@ export default function TokenColumnProvider(tokens: Tokens) {
     }
 }
 
-export function TokenColumn<T extends Column.Type>(tokens: Tokens, type: T): Column<T['@type']> {
+export function TokenColumn<T extends Column.Type>(tokens: Tokens, type: T): Column<T['T']> {
     const { data, indices, count: rowCount } = tokens;
     const { kind } = type;
 
-    const value: Column<T['@type']>['value'] =
+    const value: Column<T['T']>['value'] =
           kind === 'str'
         ? row => data.substring(indices[2 * row], indices[2 * row + 1])
         : kind === 'int'
@@ -33,16 +33,6 @@ export function TokenColumn<T extends Column.Type>(tokens: Tokens, type: T): Col
         value,
         valueKind: row => Column.ValueKind.Present,
         toArray: params => createAndFillArray(rowCount, value, params),
-        stringEquals: (row, v) => {
-            const s = indices[2 * row];
-            const value = v || '';
-            const len = value.length;
-            if (len !== indices[2 * row + 1] - s) return false;
-            for (let i = 0; i < len; i++) {
-                if (data.charCodeAt(i + s) !== value.charCodeAt(i)) return false;
-            }
-            return true;
-        },
         areValuesEqual: areValuesEqualProvider(tokens)
     };
 }
-- 
GitLab