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