Quellcode durchsuchen

added database type

David Sehnal vor 7 Jahren
Ursprung
Commit
11a3aa165a

+ 12 - 0
src/mol-base/collections/database.ts

@@ -0,0 +1,12 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Database from './database/database'
+import Table from './database/table'
+import Column from './database/column'
+import * as ColumnHelpers from './database/column-helpers'
+
+export { Database, Table, Column, ColumnHelpers }

+ 122 - 0
src/mol-base/collections/database/_spec/table.spec.ts

@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as ColumnHelpers from '../column-helpers'
+import Column 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(ColumnHelpers.isTypedArray(typedWindow.toArray())).toBe(true);
+    });
+
+    it('numStr', () => {
+        expect(numStr.value(0)).toBe('1');
+        expect(numStr.toArray()).toEqual(['1', '2']);
+    });
+
+    it('view', () => {
+        expect(Column.view(arr, [1, 0, 3, 2]).toArray()).toEqual([2, 1, 4, 3]);
+        expect(Column.view(arr, [1, 3]).toArray()).toEqual([2, 4]);
+    });
+
+    it('map to array', () => {
+        expect(Column.mapToArray(arrWindow, x => x + 1)).toEqual([3, 4]);
+    });
+})
+
+describe('table', () => {
+    const schema = {
+        x: Column.Type.int,
+        n: Column.Type.str
+    };
+
+    it('ofRows', () => {
+        const t = Table.ofRows(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(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('ofArrays', () => {
+        const t = Table.ofArrays(schema, {
+            x: [10, -1],
+            n: ['row1', 'row2'],
+        });
+        expect(t.x.toArray()).toEqual([10, -1]);
+        expect(t.n.toArray()).toEqual(['row1', 'row2']);
+    });
+
+    it('pickColumns', () => {
+        const t = Table.ofColumns(schema, {
+            x: Column.ofArray({ array: [10, -1], type: Column.Type.int }),
+            n: Column.ofArray({ array: ['row1', 'row2'], type: Column.Type.str }),
+        });
+        const s = { x: Column.Type.int, y: Column.Type.int };
+        const picked = Table.pickColumns(s, t, { y: Column.ofArray({ array: [3, 4], type: Column.Type.int })});
+        expect(picked._columns).toEqual(['x', 'y']);
+        expect(picked._rowCount).toEqual(2);
+        expect(picked.x.toArray()).toEqual([10, -1]);
+        expect(picked.y.toArray()).toEqual([3, 4]);
+    });
+
+    it('view', () => {
+        const t = Table.ofColumns(schema, {
+            x: Column.ofArray({ array: [10, -1], type: Column.Type.int }),
+            n: Column.ofArray({ array: ['row1', 'row2'], type: Column.Type.str }),
+        });
+        const s = { x: Column.Type.int };
+        const view = Table.view(t, s, [1]);
+        expect(view._columns).toEqual(['x']);
+        expect(view._rowCount).toEqual(1);
+        expect(view.x.toArray()).toEqual([-1]);
+    });
+
+    it('sort', () => {
+        const t = Table.ofColumns<typeof schema>(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']);
+    });
+});

+ 40 - 0
src/mol-base/collections/database/column-helpers.ts

@@ -0,0 +1,40 @@
+/**
+ * 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'
+
+export function getArrayBounds(rowCount: number, params?: Column.ToArrayParams<any>) {
+    const start = params && typeof params.start !== 'undefined' ? Math.max(Math.min(params.start, rowCount - 1), 0) : 0;
+    const end = params && typeof params.end !== 'undefined' ? Math.min(params.end, rowCount) : rowCount;
+    return { start, end };
+}
+
+export function createArray(rowCount: number, params?: Column.ToArrayParams<any>) {
+    const c = params && typeof params.array !== 'undefined' ? params.array : Array;
+    const { start, end } = getArrayBounds(rowCount, params);
+    return { array: new c(end - start) as any[], start, end };
+}
+
+export function fillArrayValues(value: (row: number) => any, target: any[], start: number) {
+    for (let i = 0, _e = target.length; i < _e; i++) target[i] = value(start + i);
+    return target;
+}
+
+export function createAndFillArray(rowCount: number, value: (row: number) => any, params?: Column.ToArrayParams<any>) {
+    const { array, start } = createArray(rowCount, params);
+    return fillArrayValues(value, array, start);
+}
+
+export function isTypedArray(data: any): boolean {
+    return !!data.buffer && typeof data.byteLength === 'number' && typeof data.BYTES_PER_ELEMENT === 'number';
+}
+
+export function typedArrayWindow(data: any, params?: Column.ToArrayParams<any>): ReadonlyArray<number> {
+    const { constructor, buffer, length, byteOffset, BYTES_PER_ELEMENT } = data;
+    const { start, end } = getArrayBounds(length, params);
+    if (start === 0 && end === length) return data;
+    return new constructor(buffer, byteOffset + BYTES_PER_ELEMENT * start, Math.min(length, end - start));
+}

+ 309 - 0
src/mol-base/collections/database/column.ts

@@ -0,0 +1,309 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as ColumnHelpers from './column-helpers'
+
+interface Column<T> {
+    readonly '@type': Column.Type,
+    readonly '@array': ArrayLike<any> | undefined,
+
+    readonly isDefined: boolean,
+    readonly rowCount: number,
+    value(row: number): T,
+    valueKind(row: number): Column.ValueKind,
+    toArray(params?: Column.ToArrayParams<T>): ReadonlyArray<T>,
+    areValuesEqual(rowA: number, rowB: number): boolean
+}
+
+namespace Column {
+    export type Type<T = any> = Type.Str | Type.Int | Type.Float | Type.Vector | Type.Matrix | Type.Aliased<T>
+    export type ArrayCtor<T> = { new(size: number): ArrayLike<T> }
+
+    export namespace Type {
+        export type Str = { T: string, kind: 'str' }
+        export type Int = { T: number, kind: 'int' }
+        export type Float = { T: number, kind: 'float' }
+        export type Vector = { T: number[], dim: number, kind: 'vector' };
+        export type Matrix = { T: number[][], rows: number, cols: number, kind: 'matrix' };
+        export type Aliased<T> = { T: T } & { kind: 'str' | 'int' | 'float' }
+
+        export const str: Str = { T: '', kind: 'str' };
+        export const int: Int = { T: 0, kind: 'int' };
+        export const float: Float = { T: 0, kind: 'float' };
+
+        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 function aliased<T>(t: Type): Aliased<T> { return t as any as Aliased<T>; }
+    }
+
+    export interface ToArrayParams<T> {
+        array?: ArrayCtor<T>,
+        start?: number,
+        /** Last row (exclusive) */
+        end?: number
+    }
+
+    export interface LambdaSpec<T extends Type> {
+        value: (row: number) => T['T'],
+        rowCount: number,
+        type: T,
+        valueKind?: (row: number) => ValueKind,
+    }
+
+    export interface ArraySpec<T extends 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['T']> {
+        return constColumn(type['T'], rowCount, type, ValueKind.NotPresent);
+    }
+
+    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['T']> {
+        return lambdaColumn(spec);
+    }
+
+    export function ofArray<T extends Column.Type>(spec: Column.ArraySpec<T>): Column<T['T']> {
+        return arrayColumn(spec);
+    }
+
+    export function ofIntArray(array: ArrayLike<number>) {
+        return arrayColumn({ array, type: Type.int });
+    }
+
+    export function ofFloatArray(array: ArrayLike<number>) {
+        return arrayColumn({ array, type: Type.float });
+    }
+
+    export function ofStringArray(array: ArrayLike<string>) {
+        return arrayColumn({ array, type: Type.str });
+    }
+
+    export function window<T>(column: Column<T>, start: number, end: number) {
+        return windowColumn(column, start, end);
+    }
+
+    export function view<T>(column: Column<T>, indices: ArrayLike<number>, checkIndentity = true) {
+        return columnView(column, indices, checkIndentity);
+    }
+
+    /** A map of the 1st occurence of each value. */
+    export function createFirstIndexMap<T>(column: Column<T>) {
+        return createFirstIndexMapOfColumn(column);
+    }
+
+    export function mapToArray<T, S>(column: Column<T>, f: (v: T) => S, ctor?: ArrayCtor<S>): ArrayLike<S> {
+        return mapToArrayImpl(column, f, ctor || Array);
+    }
+
+    export function areEqual<T>(a: Column<T>, b: Column<T>) {
+        return areColumnsEqual(a, b);
+    }
+
+    /** Makes the column backned by an array. Useful for columns that accessed often. */
+    export function asArrayColumn<T>(c: Column<T>, array?: ArrayCtor<T>): Column<T> {
+        if (c['@array']) return c;
+        if (!c.isDefined) return Undefined(c.rowCount, c['@type']) as any as Column<T>;
+        return arrayColumn({ array: c.toArray({ array }), type: c['@type'] as any, valueKind: c.valueKind });
+    }
+}
+
+export default Column;
+
+function createFirstIndexMapOfColumn<T>(c: Column<T>): Map<T, number> {
+    const map = new Map<T, number>();
+    for (let i = 0, _i = c.rowCount; i < _i; i++) {
+        const v = c.value(i);
+        if (!map.has(v)) return map.set(c.value(i), i);
+    }
+    return map;
+}
+
+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,
+        isDefined: valueKind === Column.ValueKind.Present,
+        rowCount,
+        value,
+        valueKind: row => valueKind,
+        toArray: params => {
+            const { array } = ColumnHelpers.createArray(rowCount, params);
+            for (let i = 0, _i = array.length; i < _i; i++) array[i] = v;
+            return array;
+        },
+        areValuesEqual: (rowA, rowB) => true
+    }
+}
+
+function lambdaColumn<T extends Column.Type>({ value, valueKind, rowCount, type }: Column.LambdaSpec<T>): Column<T['T']> {
+    return {
+        '@type': type,
+        '@array': void 0,
+        isDefined: true,
+        rowCount,
+        value,
+        valueKind: valueKind ? valueKind : row => Column.ValueKind.Present,
+        toArray: params => {
+            const { array, start } = ColumnHelpers.createArray(rowCount, params);
+            for (let i = 0, _i = array.length; i < _i; i++) array[i] = value(i + start);
+            return array;
+        },
+        areValuesEqual: (rowA, rowB) => value(rowA) === value(rowB)
+    }
+}
+
+function arrayColumn<T extends Column.Type>({ array, type, valueKind }: Column.ArraySpec<T>): Column<T['T']> {
+    const rowCount = array.length;
+    const value: Column<T['T']>['value'] = type.kind === 'str'
+        ? row => { const v = array[row]; return typeof v === 'string' ? v : '' + v; }
+        : row => array[row];
+
+    const isTyped = ColumnHelpers.isTypedArray(array);
+    return {
+        '@type': type,
+        '@array': array,
+        isDefined: true,
+        rowCount,
+        value,
+        valueKind: valueKind ? valueKind : row => Column.ValueKind.Present,
+        toArray: type.kind === 'str'
+            ? params => {
+                const { start, end } = ColumnHelpers.getArrayBounds(rowCount, params);
+                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++) {
+                    const v = array[start + i];
+                    ret[i] = typeof v === 'string' ? v : '' + v;
+                }
+                return ret;
+            }
+            : isTyped
+            ? params => ColumnHelpers.typedArrayWindow(array, params) as any as ReadonlyArray<T>
+            : params => {
+                const { start, end } = ColumnHelpers.getArrayBounds(rowCount, params);
+                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;
+            },
+        areValuesEqual: (rowA, rowB) => array[rowA] === array[rowB]
+    }
+}
+
+function windowColumn<T>(column: Column<T>, start: number, end: number) {
+    if (!column.isDefined) return Column.Undefined(end - start, column['@type']);
+    if (!!column['@array'] && ColumnHelpers.isTypedArray(column['@array'])) return windowTyped(column, start, end);
+    return windowFull(column, start, end);
+}
+
+function windowTyped<T>(c: Column<T>, start: number, end: number): Column<T> {
+    const array = ColumnHelpers.typedArrayWindow(c['@array'], { start, end });
+    return arrayColumn({ array, type: c['@type'], valueKind: c.valueKind }) as any;
+}
+
+function windowFull<T>(c: Column<T>, start: number, end: number): Column<T> {
+    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 {
+        '@type': c['@type'],
+        '@array': void 0,
+        isDefined: c.isDefined,
+        rowCount,
+        value,
+        valueKind: start === 0 ? vk : row => vk(row + start),
+        toArray: params => {
+            const { array } = ColumnHelpers.createArray(rowCount, params);
+            for (let i = 0, _i = array.length; i < _i; i++) array[i] = v(i + start);
+            return array;
+        },
+        areValuesEqual: start === 0 ? ave : (rowA, rowB) => ave(rowA + start, rowB + start)
+    };
+}
+
+function isIdentity(map: ArrayLike<number>, rowCount: number) {
+    if (map.length !== rowCount) return false;
+    for (let i = 0, _i = map.length; i < _i; i++) {
+        if (map[i] !== i) return false;
+    }
+    return true;
+}
+
+function columnView<T>(c: Column<T>, map: ArrayLike<number>, checkIdentity: boolean): Column<T> {
+    if (!c.isDefined) return c;
+    if (checkIdentity && isIdentity(map, c.rowCount)) return c;
+    if (!!c['@array']) return arrayView(c, map);
+    return viewFull(c, map);
+}
+
+function arrayView<T>(c: Column<T>, map: ArrayLike<number>): Column<T> {
+    const array = c['@array']!;
+    const ret = new (array as any).constructor(map.length);
+    for (let i = 0, _i = map.length; i < _i; i++) ret[i] = array[map[i]];
+    return arrayColumn({ array: ret, type: c['@type'], valueKind: c.valueKind });
+}
+
+function viewFull<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 = map.length;
+    return {
+        '@type': c['@type'],
+        '@array': void 0,
+        isDefined: c.isDefined,
+        rowCount,
+        value,
+        valueKind: row => vk(map[row]),
+        toArray: params => {
+            const { array } = ColumnHelpers.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])
+    };
+}
+
+function mapToArrayImpl<T, S>(c: Column<T>, f: (v: T) => S, ctor: Column.ArrayCtor<S>): ArrayLike<S> {
+    const ret = new ctor(c.rowCount) as any;
+    for (let i = 0, _i = c.rowCount; i < _i; i++) ret[i] = f(c.value(i));
+    return ret;
+}
+
+function areColumnsEqual(a: Column<any>, b: Column<any>) {
+    if (a.rowCount !== b.rowCount || a.isDefined !== b.isDefined || a['@type'].kind !== b['@type'].kind) return false;
+    if (!!a['@array'] && !!b['@array']) return areArraysEqual(a, b);
+    return areValuesEqual(a, b);
+}
+
+function areArraysEqual(a: Column<any>, b: Column<any>) {
+    const xs = a['@array']!, ys = b['@array']!;
+    for (let i = 0, _i = a.rowCount; i < _i; i++) {
+        if (xs[i] !== ys[i]) return false;
+    }
+    return true;
+}
+
+function areValuesEqual(a: Column<any>, b: Column<any>) {
+    const va = a.value, vb = b.value;
+    for (let i = 0, _i = a.rowCount; i < _i; i++) {
+        if (va(i) !== vb(i)) return false;
+    }
+    return true;
+}

+ 36 - 0
src/mol-base/collections/database/database.ts

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import Table from './table'
+
+/** A collection of tables */
+type Database<Schema extends Database.Schema> = {
+    readonly _name: string,
+    readonly _tableNames: string[],
+    readonly _schema: Schema
+} & Database.Tables<Schema>
+
+namespace Database {
+    export type Tables<S extends Schema> = { [T in keyof Schema]: Table<S[T]> }
+    export type Schema = { [table: string]: Table.Schema }
+
+    export function ofTables<S extends Schema, Db = Database<S>>(name: string, schema: Schema, tables: Tables<S>): Db {
+        const keys = Object.keys(tables);
+        const ret = Object.create(null);
+        const tableNames: string[] = [];
+        ret._name = name;
+        ret._tableNames = tableNames;
+        ret._schema = schema;
+        for (const k of keys) {
+            if (!Table.is(tables[k])) continue;
+            ret[k] = tables[k];
+            tableNames[tableNames.length] = k;
+        }
+        return ret;
+    }
+}
+
+export default Database

+ 135 - 0
src/mol-base/collections/database/table.ts

@@ -0,0 +1,135 @@
+/**
+ * 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'
+
+/** A collection of columns */
+type Table<Schema extends Table.Schema> = {
+    readonly _rowCount: number,
+    readonly _columns: ReadonlyArray<string>,
+    readonly _schema: Schema
+} & Table.Columns<Schema>
+
+/** An immutable table */
+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 type Arrays<S extends Schema> = { [C in keyof S]: ArrayLike<S[C]['T']> }
+    export type PartialTable<S extends Table.Schema> = { readonly _rowCount: number, readonly _columns: ReadonlyArray<string> } & { [C in keyof S]?: Column<S[C]['T']> }
+
+    export function is(t: any): t is Table<any> {
+        return t && typeof t._rowCount === 'number' && !!t._columns && !!t._schema;
+    }
+
+    export function pickColumns<S extends Schema>(schema: S, table: PartialTable<S>, guard: Partial<Columns<S>> = {}): Table<S> {
+        const ret = Object.create(null);
+        const keys = Object.keys(schema);
+        ret._rowCount = table._rowCount;
+        ret._columns = keys;
+        ret._schema = schema;
+        for (const k of keys) {
+            if (!!table[k]) ret[k] = table[k];
+            else if (!!guard[k]) ret[k] = guard[k];
+            else throw Error(`Cannot find column '${k}'.`);
+        }
+        return ret;
+    }
+
+    export function ofColumns<S extends Schema, R extends Table<S> = Table<S>>(schema: S, columns: Columns<S>): R {
+        const _columns = Object.keys(columns);
+        const _rowCount = columns[_columns[0]].rowCount;
+        return { _rowCount, _columns, _schema: schema, ...(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;
+        ret._schema = schema;
+        for (const k of columns) {
+            (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;
+    }
+
+    export function ofArrays<S extends Schema, R extends Table<S> = Table<S>>(schema: Schema, arrays: Arrays<S>): R {
+        const ret = Object.create(null);
+        const columns = Object.keys(schema);
+        ret._rowCount = arrays[columns[0]].length;
+        ret._columns = columns;
+        ret._schema = schema;
+        for (const k of columns) {
+            (ret as any)[k] = Column.ofArray({ array: arrays[k], type: schema[k] })
+        }
+        return ret as R;
+    }
+
+    export function view<S extends R, R extends Schema>(table: Table<S>, schema: R, view: ArrayLike<number>) {
+        const ret = Object.create(null);
+        const columns = Object.keys(schema);
+        ret._rowCount = view.length;
+        ret._columns = columns;
+        ret._schema = schema;
+        for (const k of columns) {
+            (ret as any)[k] = Column.view(table[k], view);
+        }
+        return ret as Table<R>;
+    }
+
+    export function columnToArray<S extends Schema>(table: Table<S>, name: keyof S, array?: Column.ArrayCtor<any>) {
+        table[name] = Column.asArrayColumn(table[name], array);
+    }
+
+    /** 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;
+        ret._schema = table._schema;
+        for (const c of table._columns) {
+            ret[c] = Column.view((table as any)[c], indices, false);
+        }
+        return ret;
+    }
+
+    export function areEqual<T extends Table<Schema>>(a: T, b: T) {
+        if (a._rowCount !== b._rowCount) return false;
+        if (a._columns.length !== b._columns.length) return false;
+        for (const c of a._columns) {
+            if (!b[c]) return false;
+        }
+
+        for (const c of a._columns) {
+            if (!Column.areEqual(a[c], b[c])) return false;
+        }
+
+        return true;
+    }
+}
+
+export default Table

+ 1 - 0
src/mol-io/writer/cif/TODO

@@ -0,0 +1 @@
+- Make a writer that takes a database and produces a CIF/BinaryCIF file.