/**
* @file FlexTable.
* @author nozalr <nozalr@group4layers.com> (Group4Layers®).
* @copyright 2017 nozalr (Group4Layers®).
* @license MIT
* @version 0.3.0
* @description
*
* This docs are generated from the source code, and therefore, they are concise and simple.
*
* **For a complete documentation full of examples, use cases, general overview and the most
* important points of the API, please, see the README.md in [flextable](https://github.com/Group4Layers/flextable)**.
*/
const sorter = require('./sorter');
const sorters = sorter.sorters;
const sortChain = sorter.sortChain;
const chainHeaderToIdx = sorter.chainHeaderToIdx;
const formatter = require('./formatter');
/**
* @typedef {Object} Table - Data structure.
* @type FlexTable
* @prop {string[]} headers - can be empty if no headers.
* @prop {int} _hlength - number of headers.
* @prop {any[]} values - can be empty if no values.
* @prop {int} _vlength - number of values (rows).
* @prop {Object.<string,int>} idx - indices of headers.
* @description
*
* Table object data structure (an instantiation of FlexTable).
*
* A valid Table could be:
* ```js
* let table = new FlexTable();
* ```
*
* The table can be modified in a fine-grained way like this:
* ```js
* let idx = table.idx;
* // increment in 1.05 the value of the second row with column header 'time'
* table.values[1][idx['time']] += 1.05;
* ```
*/
/**
* @typedef {Object} TableSkel - Data structure.
* @prop {string[]} headers - can be empty if no headers.
* @prop {any[]} values - can be empty if no values.
* @description
*
* Table object skeleton data structure.
*
* A valid TableSkel could be:
* ```js
* let table = new FlexTable();
* ```
* and also `{headers: [], values: []}`.
*/
/**
* @typedef {Object} Format - Data structure.
* @prop {string} header - Headers format.
* @prop {string} float - Float format.
* @prop {string} integer - Integer format.
* @prop {string} string - String format.
* @prop {string[]} columns - Column specific format.
* @description
*
* Formatter object. It accepts these type of formats:
* - `%s`: string.
* - `%d`: integer.
* - `%f`: float.
* - `%-s`: string left aligned.
* - `%-d`: integer left aligned.
* - `%-f`: float left aligned.
* - `%.4f`: float with 4 decimals (and other number of decimals).
*
* The column object receives the an array of formats. If a value is null it doesn't overwrite the format. It should provide as many formats as headers.
*/
/**
* @typedef {('markdown'|'md'|'csv')} Formatter - Formatting style.
*/
/**
* @class
*/
class FlexTable {
/**
* Constructor. It accepts an another FlexTable-like object (will be copied by reference).
* It also accepts 2 arrays: headers and values.
* @param {(string[]|TableSkel|null)} headers - array of strings (headers) or an object with headers and values.
* @param {(string[]|null)} values - rows.
*/
constructor(headers, values){
this.reset(headers, values);
}
/**
* It accepts an another FlexTable-like object (will be copied by reference).
* It also accepts 2 arrays: headers and values.
* @param {(string[]|TableSkel|null)} headers - array of strings (headers) or an object with headers and values.
* @param {(string[]|null)} values - rows.
*/
reset(headers, values){
if (headers != null && values == null && !Array.isArray(headers)){
values = headers.values;
headers = headers.headers;
}
if (!Array.isArray(headers)){
headers = [];
}
this.headers = headers;
this._hlength = headers.length;
this.values = [];
this._vlength = 0;
if (Array.isArray(values)){
this.appendRows(values);
}
this._setHeadersIndex();
}
_setHeadersIndex(){
let headers = this.headers;
let idx = {};
let i = 0;
for (let h of headers){
idx[h] = i++;
}
this.idx = idx;
}
// append
// i == -1 the last
/**
* @param {row[]} rows - Rows to append to the values.
* @description
*
* Append rows.
*
* It calls `#appendRow` for every row passed in the `rows` param.
*/
appendRows(rows){
let i = 0;
for (let row of rows){
this.appendRow(row, i);
i++;
}
}
/**
* @param {col[]} row - Row to append to the values.
* @throws if the number of cols doesn't match the number of headers.
* @description
*
* Append a row.
*
* ```js
* let table = new FlexTable(["col1"]);
* // 0 rows
* table.appendRow(["first"]);
* // 1 row
* table.appendRow(["last"]);
* // 2 rows
* ```
*/
appendRow(row, i = 0){
if (row.length !== this._hlength){
throw new Error(`row ${i} length (${row.length}) is not equal to headers length (${this._hlength})`);
// return; // err
}
this._vlength++;
this.values.push(row);
}
/**
* @param {int} i - Row number to be replaced/inserted.
* @param {col[]} row - Row to append to the values.
* @param {boolean} replace - If the row should replace the existing row.
* @description
*
* Set a row in the values.
*
* ```js
* // suppose there are at 2 rows at the beginning
* table.setRow(0, ["first"], false); // will insert ["first"] in the first row
* // now there are 3 rows
* table.setRow(2, ["third"], false); // will insert ["third"] in the third row
* // total: 3 rows
* table.setRow(2, ["none"], true); // will replace ["third"] with ["none"] in the third row
* // total: 3 rows
* ```
*
* `#appendRow` should be used to append a row or insert the first row in an empty table (no values)
*/
setRow(i, row, replace){
if (row && row.length !== this._hlength){
return; // err
}
let _vlength = this._vlength;
let rm = row == null;
if (!(i < _vlength && i >= -_vlength)){ // not valid
return;
}
if (i < 0){
i = _vlength - (-i);
}
if (rm){ // rm
this._vlength--;
this.values.splice(i, 1);
}else if (replace){ // replace
this.values.splice(i, 1, row);
}else{ // before or after
this._vlength++;
this.values.splice(i, 0, row);
}
}
/**
* @param {boolean} remove - Remove the row if the function returns true.
* @param {function} fn - Function to be executed per row.
* @description
*
* Modify rows by a function.
*
* ```js
* // remove all rows since the third row
* table.modifyRows(true, (row, i) => {
* return i >= 2;
* })
* // increment col2 from rows with col1 == "yes"
* let idx = table.idx;
* table.modifyRows(false, (row, i) => {
* return row[idx['col1']] === 'yes' ? row[idx['col2']]++ : null;
* })
* ```
*/
modifyRows(remove, fn){
let i=0;
let _hlength = this._hlength;
let values = this.values;
let _vlength = this._vlength;
let row = true;
while (row){
row = values[i];
if (row){
let nrow = fn(row, i);
if (!remove){
if (nrow != null && nrow.length === _hlength){ // leave unchanged
values.splice(i, 1, nrow);
}
i++;
}else if (nrow){ // true to be deleted
values.splice(i, 1);
_vlength--;
}else{
i++;
}
}
}
this._vlength = _vlength;
}
// can append, can set,
// name has to be a string, values an array
/**
* @param {string} name - Header/Column name.
* @param {col[]} values - Column values for every row in values.
* @description
*
* Set column with values.
*
* ```js
* let table = new FlexTable(["col1"], [[1], [2]]);
* table.setColumn("col2", ["a", "b"]);
* // table has headers ["col1", "col2"] and values [[1, "a"], [2, "b"]]
* table.setColumn("col1", [-1, null]);
* // table has headers ["col1", "col2"] and values [[-1, "a"], [null, "b"]]
* table.setColumn("col1", null);
* // table has headers ["col2"] and values [["a"], ["b"]]
* ```
*/
setColumn(name, values){
let fn;
if (typeof values === 'function'){
fn = values;
}
if (Array.isArray(name)){
return;
}
let headers = this.headers;
if (!this.idx){
this._setHeadersIndex();
}
let rm = values == null;
let modify = true;
let idx = this.idx;
let pos = idx[name];
if (rm){
if (pos != null){
this._hlength--;
// delete idx[name];
headers.splice(pos, 1);
this._setHeadersIndex();
idx = this.idx;
}else{
modify = false;
}
}else{
if (pos == null){ // append
pos = this._hlength;
this._hlength++;
// }else{ // replace
// this._hlength++;
}
idx[name] = pos;
headers.splice(pos, 1, name);
}
if (modify){
let rows = this.values;
let i=0;
if (rm){
for (let r of rows){
r.splice(pos, 1);
}
}else{
if (rows.length === 0 && values.length){
for (let col of values){
// special case
let v = fn ? fn(col, [col], 0, name) : col;
rows.push([v]);
}
}else if (!fn && rows.length !== values.length){
// throw error
return;
}else{
for (let r of rows){
// name = column
let v = fn ? fn(r[pos], r, i, name) : values[i];
if (v != null){ // null means leave it unchanged
r.splice(pos, 1, v);
}
i++;
}
}
}
if (!this._hlength){
this.values = [];
}
this._setHeadersIndex();
}
}
// names has to be a string[]
// values has to be a [any[], ...]
/**
* @param {string[]} names - Header/Column names.
* @param {col[][]} values - Column values for every row in values (an array per column).
* @description
*
* Set columns with values.
*
* ```js
* let table = new FlexTable(["col1"], [[1], [2]]);
* table.setColumns(["col2", "col1"], [["a", "b"], null]);
* // table has headers ["col2"] and values [["a"], ["b"]]
* ```
*/
setColumns(names, values){
let len = names.length;
if (values.length !== len){
return;
}
let i = 0;
while (i < len){
this.setColumn(names[i], values[i]);
i++;
}
}
/**
* @param {mapchain[]} mapchain - Sorting rules.
* @description
*
* Sorts the values. It modifies the rows order in the table.
*
* ```js
* let table = new FlexTable(["col1", "col2"], [[1, "a"], [2, "b"]]);
* table.sort(['col1', '>num']);
* // table has headers ["col1", "col2"] and values [[2, "b"], [1, "a"]]
* table.sort(['col1', '<num']);
* // table has headers ["col1", "col2"] and values [[1, "a"], [2, "b"]]
*
* table = new FlexTable(['ts', 'evname', 'time'],
* [
* [123, 'begin', 0.0],
* [123, 'start', 3.1],
* [123, 'end', 4.44],
* [124, 'begin', 0.0],
* [124, 'start', 2.5],
* [124, 'end', 4.1],
* ]);
* let mapchain = [
* ['ts', function(a, b, i){ // like <num in this case
* let ai = a[i];
* let bi = b[i];
* let less = ai < bi;
* let eq = ai === bi;
* if (eq){
* return 0;
* }else{
* return less ? -1 : 1;
* }
* }],
* ['time', '>num'],
* ];
* // table has:
* // {
* // headers: [ 'ts', 'evname', 'time' ],
* // values: [
* // [ 123, 'end', 4.44 ],
* // [ 123, 'start', 3.1 ],
* // [ 123, 'begin', 0 ],
* // [ 124, 'end', 4.1 ],
* // [ 124, 'start', 2.5 ],
* // [ 124, 'begin', 0 ]
* // ]
* // }
* ```
*/
sort(mapchain){
let idx = this.idx;
if (!idx){
this._setHeadersIndex();
idx = this.idx;
}
if (mapchain.length){
if (!Array.isArray(mapchain[0])){
mapchain = [mapchain];
}
}
let chain = chainHeaderToIdx(idx, mapchain);
this.values.sort(function(a, b){
return sortChain(a, b, chain);
});
this._setHeadersIndex();
}
/**
* Clone the table headers and values. It instantiates a new table (by value).
* @returns {Table} Cloned table instantiated.
* @see FlexTable
*/
clone(){
let o = JSON.parse(JSON.stringify(this));
let table = new FlexTable(o.headers, o.values);
return table;
}
/**
* @param {formatter} type - Formatter type.
* @param {format} fmt - Format style.
* @returns {string} Table formatted.
* @description
*
* Format the table as a string.
*
* ```js
* let table = new FlexTable(["a", "b", "c"], [[1.23, 1.02, "str"], [2, 2.335, ""]]);
* let str = table.format('markdown', { header: '%s', float: '%.1f'});
* // saves a markdown table in the variable. Every float value will be with 1 decimal.
* console.log(table.format('md', { float: '%.1f', columns: [null, '%.2f', null]}));
* // prints a markdown table. Every float is printed with 1 decimal, but the second column with 2 decimals.
* console.log(table.format('csv');
* // prints the table as CSV
* ```
*/
format(type, fmt){
let ret;
// fmt admits
// "%.f" -> round to integer rounding
// "%.5f" -> 5 decimals rounding
// "%d" -> truncate to integer
// "%s" -> string
// "%10s" -> string padding
// "%-10s" -> string padding left aligned
//
// [".4d", "d", "s"]
// { header: "%s", float: "%.4f", integer: "%d", string: "%s" }, if missing, normal behavior
// default fmt:
// { header: "%-s", float: "%.5f", integer: "%d", string: "%-s" }
fmt = formatter.parseFmt(fmt);
switch(type){
case 'md':
case 'markdown':
ret = formatter.printTableMarkdown(this, fmt);
break;
case 'csv':
ret = formatter.printTableCSV(this, fmt);
break;
}
return ret;
}
}
/**
* FlexTable module.
* @module flextable
*/
module.exports = {
/**
* FlexTable version.
*/
version: { major: 0, minor: 3, patch: 0 },
/**
* FlexTable class.
*/
FlexTable,
/**
* FlexTable sorters (expose global sorting functions).
*/
sorters,
};