@analys/table
Version:
A cross-table analytics tool
411 lines (349 loc) • 14.1 kB
JavaScript
import { tableDivide, tableJoin, tableAcquire, tableMerge } from '@analys/table-algebra';
import { tableChips } from '@analys/table-chips';
import { tableFind } from '@analys/table-find';
import { tableFormula } from '@analys/table-formula';
import { tableGroup } from '@analys/table-group';
import { slice, shallow } from '@analys/table-init';
import { lookupCached, lookup, lookupMany } from '@analys/table-lookup';
import { tablePivot } from '@analys/table-pivot';
import { tableToObject, tableFilter } from '@analys/table-select';
import { inferTypes } from '@analys/table-types';
import { selectTabularToSamples, tabularToSamples, selectTabular, sortTabularByKeys } from '@analys/tabular';
import { NUM_ASC } from '@aryth/comparer';
import { DistinctCount, Distinct } from '@aryth/distinct-column';
import { distinctByColumn } from '@aryth/distinct-matrix';
import { column } from '@vect/column-getter';
import { mutate } from '@vect/column-mapper';
import { push, unshift, pop, shift, splices as splices$1 } from '@vect/columns-update';
import { transpose } from '@vect/matrix-algebra';
import { size } from '@vect/matrix-index';
import { mapper as mapper$1, mutate as mutate$2, selectMutate } from '@vect/matrix-mapper';
import { wind } from '@vect/object-init';
import { difference, intersect } from '@vect/vector-algebra';
import { gather } from '@vect/vector-init';
import { mapper, mutate as mutate$1, iterate } from '@vect/vector-mapper';
import { splices } from '@vect/vector-update';
class Table {
/** @type {*[]} */ head
/** @type {*[][]} */ rows
/** @type {string} */ title
/** @type {string[]} */ types
/**
* @param {*[]} [head]
* @param {*[][]} [rows]
* @param {string} [title]
* @param {string[]} [types]
*/
constructor(head, rows, title, types) {
this.head = head || [];
this.rows = rows || [];
this.title = title || '';
this.types = types;
}
static build(head, rows, title, types) { return new Table(head, rows, title, types) }
static gather(head, iter, title, types) { return new Table(head, gather(iter), title, types) }
static from(o) { return new Table(o.head || o.banner, o.rows || o.matrix, o.title, o.types) }
toSamples(fields) { return fields ? selectTabularToSamples.call(this, fields) : tabularToSamples.call(this) }
toObject(mutate = false) { return mutate ? slice(this) : shallow(this) }
setTitle(title) { return this.title = title, this }
get size() { return size(this.rows) }
get ht() { return this.rows?.length }
get wd() { return this.head?.length }
get height() { return this.rows?.length }
get width() { return this.head?.length }
get columns() { return transpose(this.rows) }
cell(x, field) { return (x in this.rows) ? this.rows[x][this.coin(field)] : undefined }
coin(field) { return this.head.indexOf(field) }
columnIndexes(fields) { return fields.map(this.coin, this) }
row(field, value, objectify) {
const vector = this.rows.find(row => row[this.coin(field)] === value);
return vector && objectify ? wind(this.head, vector) : vector
}
column(field) { return column(this.rows, this.coin(field), this.height) }
setColumn(field, column) { return mutate(this.rows, this.coin(field), (_, i) => column[i], this.height), this }
mutateColumn(field, fn) { return mutate(this.rows, this.coin(field), (x, i) => fn(x, i), this.height), this }
pushRow(row) { return this.rows.push(row), this }
unshiftRow(row) { return this.rows.unshift(row), this }
pushColumn(label, column) { return this.head.push(label), push(this.rows, column), this }
unshiftColumn(label, column) { return this.head.unshift(label), unshift(this.rows, column), this }
popRow() { return this.rows.pop() }
shiftRow() { return this.rows.shift() }
popColumn() { return [ this.head.pop(), pop(this.rows) ] }
shiftColumn() { return [ this.head.shift(), shift(this.rows) ] }
renameColumn(field, newName) {
const ci = this.coin(field);
if (ci >= 0) this.head[ci] = newName;
return this
}
mapHead(fn, { mutate = true } = {}) { return this.boot({ head: mapper(this.head, fn) }, mutate) }
mutateHead(fn) { return mutate$1(this.head, fn), this }
map(fn, { mutate = true } = {}) { return this.boot({ rows: mapper$1(this.rows, fn, this.height, this.width) }, mutate) }
mutate(fn, { fields, exclusive } = {}) {
if (!fields && !exclusive) return mutate$2(this.rows, fn, this.height, this.width), this
fields = fields ?? this.head;
fields = exclusive ? difference(fields, exclusive) : fields;
return selectMutate(this.rows, this.columnIndexes(fields), fn, this.height), this
}
lookupOne(valueToFind, key, field, cached = true) { return (cached ? lookupCached : lookup).call(this, valueToFind, key, field) }
lookupMany(valuesToFind, key, field) { return lookupMany.call(this, valuesToFind, key, field) }
/**
*
* @param {string} key
* @param {string|string[]|[string,string][]} [field]
* @param {boolean} [objectify=true]
* @return {Object|Array}
*/
lookupTable(key, field, objectify = true) { return tableToObject.call(this, key, field, objectify) }
/**
*
* @param {*[]|[*,*][]} fields
* @param {boolean=true} [mutate]
* @returns {Table}
*/
select(fields, { mutate = false } = {}) {
let o = mutate ? this : slice(this);
selectTabular.call(o, fields);
return mutate ? this : this.copy(o)
}
/**
*
* @param {*} field
* @param {Function} splitter
* @param {*} [fields] - new names of divided fields
* @param {boolean=true} [mutate]
* @returns {Table}
*/
divideColumn(field, splitter, { fields, mutate = false } = {}) {
const
o = mutate ? this : shallow(this),
y = this.coin(field);
o.head.splice(y, 1, ...(fields ?? splitter(field)));
iterate(o.rows, row => row.splice(y, 1, ...splitter(row[y])));
return mutate ? this : Table.from(o)
}
/**
*
* @param {{key,to,as}|{key,to,as}[]} fieldSpec
* @param {*} [nextTo] - the existing field after which the newField inserts
* @param {boolean=true} [mutate]
* @returns {Table}
*/
proliferateColumn(fieldSpec, { nextTo, mutate = false } = {}) {
if (!Array.isArray(fieldSpec)) fieldSpec = [ fieldSpec ];
const
o = mutate ? this : shallow(this),
{ head, rows } = o,
y = nextTo ? (this.coin(nextTo) + 1) : 0;
fieldSpec.forEach(o => o.index = this.coin(o.key));
if (fieldSpec?.length === 1) {
const [ { index, to, as } ] = fieldSpec;
head.splice(y, 0, as);
iterate(rows,
row => row.splice(y, 0, to(row[index])),
);
} else {
head.splice(y, 0, ...fieldSpec.map(({ as }) => as));
iterate(rows,
row => row.splice(y, 0, ...fieldSpec.map(({ index, to }) => to(row[index]))),
);
}
return mutate ? this : Table.from(o)
}
/**
*
* @param {*|*[]} newField
* @param {*[]|*[][]} column
* @param {*} [nextTo] - the existing field after which the newField inserts
* @param {boolean=true} [mutate]
* @returns {Table}
*/
insertColumn(newField, column, { nextTo, mutate = false } = {}) {
const o = mutate ? this : shallow(this), y = nextTo ? (this.coin(nextTo) + 1) : 0;
if (Array.isArray(newField)) {
o.head.splice(y, 0, ...newField);
iterate(o.rows, (row, i) => row.splice(y, 0, ...column[i]));
} else {
o.head.splice(y, 0, newField);
iterate(o.rows, (row, i) => row.splice(y, 0, column[i]));
}
return mutate ? this : Table.from(o)
}
/**
*
* @param {*|*[]} field
* @param {boolean=true} [mutate]
* @returns {Table}
*/
deleteColumn(field, { mutate = false } = {}) {
const o = mutate ? this : shallow(this);
const { head, rows } = o;
if (Array.isArray(field)) {
const indexes = this.columnIndexes(field).filter(i => i >= 0).sort(NUM_ASC);
splices(head, indexes);
splices$1(rows, indexes);
} else {
const index = this.coin(field);
head.splice(index, 1);
rows.forEach(row => row.splice(index, 1));
}
return mutate ? this : Table.from(o)
}
divide(fields, { mutate = false } = {}) {
const o = mutate ? this : shallow(this);
const { pick, rest } = tableDivide.call(o, fields);
return { pick: Table.from(pick), rest: mutate ? this : Table.from(rest) }
}
/**
*
* @param {Object|Filter[]|Filter} filterCollection
* @param {boolean} [mutate=true]
* @return {Table}
*/
filter(filterCollection, { mutate = true } = {}) {
const o = mutate ? this : slice(this);
tableFilter.call(o, filterCollection);
return mutate ? this : this.copy(o)
}
/**
*
* @param {Object<*,function(*?):boolean>} filter
* @param {boolean} [mutate=true]
* @return {Table}
*/
find(filter, { mutate = true } = {}) {
const o = mutate ? this : slice(this);
tableFind.call(o, filter);
return mutate ? this : this.copy(o)
}
//TODO: supposedly returns rows, currently only returns specific column
distinct(fields, { mutate = true } = {}) {
const o = mutate ? this : slice(this);
for (let field of fields) o.rows = distinctByColumn.call(o.rows, this.coin(field));
return mutate ? this : this.copy(o)
}
/**
*
* @param {*} field
* @param {boolean} [count=false]
* @param {string|boolean} [sort=false] - When sort is function, sort must be a comparer between two point element.
* @returns {[any, any][]|[]|any[]|*}
*/
distinctOnColumn(field, { count = false, sort = false } = {}) {
return count
? DistinctCount(this.coin(field))(this.rows, { l: this.height, sort })
: Distinct(this.coin(field))(this.rows, this.height)
}
/**
*
* @param field
* @param comparer
* @param mutate
* @returns {Table} - 'this' Table rows is mutated by sort function
*/
sort(field, comparer, { mutate = true } = {}) {
const y = this.coin(field);
const rowComparer = (a, b) => comparer(a[y], b[y]);
const o = mutate ? this : slice(this);
o.rows.sort(rowComparer);
return mutate ? this : this.copy(o)
}
/**
*
* @param {function(*,*):number} comparer - Comparer of head elements
* @param {boolean} mutate
* @returns {Table|*}
*/
sortLabel(comparer, { mutate = true } = {}) {
let o = mutate ? this : slice(this);
sortTabularByKeys.call(o, comparer);
return mutate ? this : this.copy(o)
}
join(another, fields, joinType, fillEmpty) { return Table.from(tableJoin(this, another, fields, joinType, fillEmpty)) }
/**
*
* @param {Table} another
* @param {boolean} [mutate=true]
* @return {Table}
*/
union(another, { mutate = true } = {}) {
const self = mutate ? this : this.copy();
const shared = intersect(self.head, another.head);
if (shared.length) {
for (let label of shared) self.setColumn(label, another.column(label));
another = another.deleteColumn(shared, { mutate });
}
return mutate
? tableAcquire(self, another)
: this.copy(tableMerge(self, another))
}
/**
* @param {Object} options
* @param {*} options.key
* @param {*} [options.field]
* @param {number} [options.mode=ACCUM] - MERGE, ACCUM, INCRE, COUNT
* @param {boolean} [options.objectify=true]
* @return {[*,*][]|{}}
*/
chips(options = {}) { return tableChips.call(this, options)}
/**
* @param {Object} options
* @param {*} options.key
* @param {*} [options.field]
* @param {Function} [options.filter]
* @param {Object|Array} [options.alias]
* @return {Table}
*/
group(options = {}) { return Table.from(tableGroup.call(this, options)) }
/**
* @param {Object} options
* @param {Object|Array} [formulae]
* @param {Function} [options.filter]
* @param {boolean} [options.append=true]
* @return {Table}
*/
formula(formulae, options = {}) { return Table.from(tableFormula.call(this, formulae, options)) }
/**
* @param {Object} options
* @param {str|str[]|Object<str,Function>|[string,Function][]} options.side
* @param {str|str[]|Object<str,Function>|[string,Function][]} options.banner
* @param {Object|*[]|string|number} [options.field]
* @param {Object<string|number,function(*?):boolean>} [options.filter]
* @param {function(...*):number} [options.formula] - formula is valid only when cell is CubeCell array.
* @returns {Crostab|CrostabObject}
*/
crosTab(options = {}) {
const table = slice(this);
if (options.filter) { tableFind.call(table, options.filter); }
return tablePivot.call(options, table)
}
inferTypes({ inferType, omitNull = true, mutate = false } = {}) {
const types = inferTypes.call(this, { inferType, omitNull });
return mutate ? (this.types = types) : types
}
/** @returns {Table} */
boot({ head, rows, types } = {}, mutate = true) {
if (mutate) {
if (head) this.head = head;
if (rows) this.rows = rows;
if (types) this.types = types;
return this
} else {
return this.copy({ head, rows, types })
}
}
/** @returns {Table} */
copy({ head, rows, types } = {}) {
if (!head) head = this.head.slice();
if (!rows) rows = this.rows.map(row => row.slice());
if (!types) types = this.types?.slice();
return new Table(head, rows, this.title, types)
}
}
/**
* @param {str|str[]|Object<str,Function>|[string,Function][]} side
* @param {str|str[]|Object<str,Function>|[string,Function][]} banner
* @param {Object|*[]|string|number} [field]
* @param {Object<string|number,function(*?):boolean>} [filter]
* @param {function(...*):number} [formula] - formula is valid only when cell is CubeCell array.
*/
export { Table };