lamed_table
Version:
Light table implementation
386 lines (362 loc) • 15.2 kB
JavaScript
// Comment out in test functions
console.log(`Starting ${__filename}...`) // comment line to remove simple logging
/* ------------------------------------------------------
* tableCols.js
* Purpose: The purpose of this module is to manage table columns
* Date Created: 2019/12/12
* Created by : Perez Lamed van Niekerk
--------------------------------------------------------- */
/* jshint esversion: 6 */
const _test = require('lamed_test')
const { Ok, notOk, notOk_Then, Equal, notEqual, con, testAND, unZip } = _test // eslint-disable-line
// con.setupChalk(require('chalk'))
// con.traceSet(0
require('ltype_string')
const _convert = require('ltype_convert')
const _array = require('ltype_array')
/**
* Returns the col number from the name
* @param {array} cols - The cols array of the table object
* @param {string / number} colNameOrNumber - The column name
* @param {bool} throwError - If true and colname is not found throw error else return -1
*/
function colsNumber (cols, colNameOrNumber, throwError = true) {
// Use cols as parameter to allow bigger scope
// if (notOk(colNameOrNumber)) throw new Error(`colNameOrNumber not defined: '${colNameOrNumber}'`) // will return -1
if (typeof colNameOrNumber === 'number') {
// Number col
if (colNameOrNumber < -1 || colNameOrNumber >= cols.length) {
if (throwError) throw new Error(`in colsNumber; Column number ${colNameOrNumber} does not exist.\n`)
return -1
}
return colNameOrNumber
}
const test = colNameOrNumber.toLowerCase()
// con.log({ test })
// }
const col = cols.indexOf(test)
if (col === -1) {
if (throwError) {
throw new Error(`in colsNumber; Column '${test}' does not exist in ['${cols.join("','")}'].\n`)
}
return -1
}
return col
}
/**
* Return array of colNames
* @param {array} cols - The cols array of the table object
* @param {array} colNamesOrNumbers - The col names or numbers array. Numbers start at 1
* @returns {array} - Of column names
*/
function colsNames (cols, colNamesOrNumbers) {
const result = []
for (let ii = 0; ii < colNamesOrNumbers.length; ii++) {
let item = colNamesOrNumbers[ii]
if (typeof item !== 'string') item = cols[item]
result.push(item)
}
return result
}
unZip(() => colsNames(['col1', 'col2', 'col3'], [0, 1, 2]), ['col1', 'col2', 'col3'])
unZip(() => colsNames(['col1', 'col2', 'col3'], [0, 'col3', 1]), ['col1', 'col3', 'col2'])
unZip(() => colsNames(['col1', 'col2', 'col3'], []), [])
unZip(() => colsNames(['col1', 'col2', 'col3'], [1]), ['col2'])
/**
* Set the columns to the table
* @param {object} table - the table object
* @param {array} colNames - string array of column names
*/
function colsCreate (table, colNames) {
if (table.cols.length !== 0) throw new Error('in colSet(). Cannot set columns if there already exist columns')
table.cols = colNames.toLowerCase()
table.cols = table.cols.map(x => x.trim()) // Make sure the cols are trimed
}
/**
* Add columns to a table object
* @param {object} table - the table object
* @param {array} colNames - string array of column names
* @param {string} defaultValue - the default value for the new columns
*/
function colsAddColumns (table, colNames, defaultValue = '') { // eslint-disable-line
const result = []
for (let ii = 0; ii < colNames.length; ii++) {
const colName = colNames[ii].toLowerCase() // columns are in lower case
const pos1 = table.cols.indexOf(colName)
if (pos1 === -1) {
const colNo = table.cols.push(colName) - 1
result.push(colNo)
for (let jj = 0; jj < table.rows.length; jj++) {
table.rows[jj][colNo] = defaultValue
}
} else result.push(pos1) // reuse existing column
}
if (result.length === 1) return result[0]
else return result
}
/**
* Add a column to a table object
* @param {object} table - the table object
* @param {array} colNames - string array of column names
* @param {string} defaultValue - the defaultValue for the new column
*/
function colsAddColumn (table, colName, defaultValue = '') {
return colsAddColumns(table, [colName], defaultValue)
}
/**
* Drop a column from the table object
* @param {object} table - the table object
* @param {array of number / string} colNameOrNumbers - The Column names and/or numbers to drop
*/
function colsDropColumn (table, colNameOrNumbers) { // eslint-disable-line
if (Array.isArray(colNameOrNumbers) === false) colNameOrNumbers = [colNameOrNumbers]
for (let ii = 0; ii < colNameOrNumbers.length; ii++) {
const colNameOrNumber = colNameOrNumbers[ii]
const colNo = colsNumber(table.cols, colNameOrNumber)
// con.log({ colNameOrNumber, colNo })
table.cols.splice(colNo, 1)
for (let jj = 0; jj < table.rows.length; jj++) {
const row = table.rows[jj]
row.splice(colNo, 1)
}
}
}
/**
* Change a column name from the table object
* @param {object} table - the table object
* @param {number / string} colNameOrNumber - The Column name of number
* @param {string} newColName - The new name of the column
*/
function colsNameDef (table, colNameOrNumber, newColName) {
if (notOk(newColName)) throw new Error(`newColName invalid value: ${newColName}`)
if (colNameOrNumber.trim().toLowerCase() === newColName.trim().toLowerCase()) return
const colNo = colsNumber(table.cols, colNameOrNumber.trim())
table.cols[colNo] = newColName.trim().toLowerCase()
}
/**
* Change a column names from the table object
* @param {object} table - the table object
* @param {string} commands - The name change commands to be executed. oldname-->newName <-> etc.
* @param {bool} debug - If true, show debug information
*/
function colsNameDefs (table, commands, debug = false) {
const nameDefs = commands
if (debug) con.log({ commands })
for (let ii = 0; ii < nameDefs.length; ii++) {
const name = nameDefs[ii].trim().toLowerCase()
if (name.indexOf('-->') === -1) throw new Error(`Setting '${name}' does not have "-->" definition.\n`)
const oldName = name.split0('-->')
const newName = name.split99('-->')
if (notOk(newName)) throw new Error(`Setting '${name}' does not include valid value after "-->"\n`)
if (debug) con.log({ name, oldName, newName })
colsNameDef(table, oldName, newName)
}
}
/**
* Return array of column values
* @param {object} table - The table object
* @param {string / number} colNameOrNumber - The column name or number
* @param {int} rows - The number of rows to return
* @returns {array} - Array of column values
*/
function col2Array (table, colNameOrNumber, rows = -1) {
const colNo = colsNumber(table.cols, colNameOrNumber)
let result = table.rows.map(function (value, index) { return value[colNo] })
if (rows !== -1) result = result.slice(0, rows)
return result
}
/**
* Return sample values from the column
* @param {table} table - The table
* @param {string / number} colNameOrNumber - The column name or number
* @param {int} maxLength - The maximum length of the sample
* @returns {array} - Array of string (Eg. [str...])
*/
function colSamples (table, colNameOrNumber, maxLength = 45) {
let samples = col2Array(table, colNameOrNumber).join(', ')
if (maxLength !== -1 && samples.length > maxLength) samples = `[${samples.substr(0, maxLength)}...]`
else samples = `[${samples.substr(0, maxLength)}]`
return samples
}
/**
* Add a column to a table and load the list items to the column
* @param {table} table - The talbe to load the items from
* @param {array} list - Array of items. Total must be equal to row count
* @param {string} colName - The column name to add
*/
function TableArray2Col (table, list, colName) { // eslint-disable-line
if (list.length !== table.rows.length) {
if (Array.isArray(list) === false) throw new Error(`List '${list}' is not a list.\n`)
throw new Error(`List (${list.length}) and table (${table.rows.length}) must have same row size.\n`)
}
const colNo = colsAddColumn(table, colName)
// dtFiles -> [id, file, level]
for (let ii = 0; ii < table.rows.length; ii++) {
const row = table.rows[ii]
row[colNo] = list[ii]
}
}
/**
* Merge two columns using the function provided
* @param {string} colResult - The column where the result shall be stored
* @param {table} table - The table
* @param {string} col1 - The first column to use
* @param {string} col2 - The second column to use
* @param {function} func - The function to use
*/
function mergeColumns (colResult, table, col1, col2, func = (x, y) => { return x + y }) {
const list1 = col2Array(table, col1) // Get list of col1
const list2 = col2Array(table, col2) // Get list of col2
const listMerge = _array.mergeArrays(list1, list2, func) // Merge the two lists
TableArray2Col(table, listMerge, colResult) // Replace / add result column
if (colResult === col1) colsDropColumn(table, col2) // If the result is part of the merge -> remove the other column
else if (colResult === col2) colsDropColumn(table, col1)
}
/**
* Execute a function on a column
* @param {table} table - The table
* @param {array} colNames - The columns to execute the function on
* @param {function} func - The function to use
* @param {string} - colNew - If one colName provided then copy results to colNew
*/
function calcColumn (table, colNames, func = (x, index) => `${index}. ${x}`, colNew = '') {
if (Array.isArray(colNames) === false) colNames = [colNames] // Make it an array
for (let ii = 0; ii < colNames.length; ii++) {
let colName = colNames[ii]
// con.log({ colName })
const list1 = col2Array(table, colName) // Get list of colName
const result = list1.map(func)
if (colNames.length === 1 && colNew !== '') colName = colNew // Only new column if one colName provided
TableArray2Col(table, result, colName) // Update the column
}
}
function calcDuration (table, fromColNameOrNumber, toColNameOrNumber, durationCol = 'duration') {
const colNo1 = colsNumber(table.cols, fromColNameOrNumber)
const colNo2 = colsNumber(table.cols, toColNameOrNumber)
const result = []
for (let jj = 0; jj < table.rows.length; jj++) {
const row = table.rows[jj]
const from = row[colNo1]
const to = row[colNo2]
const duration = _convert.Convert.duration_AsHour(from, to) // duration in hours
// con.log({ from, to, duration })
result.push(duration)
}
// con.log({ result })
TableArray2Col(table, result, durationCol) // Update the column
}
function compress (table, fromColNameOrNumber, compressItems = []) {
if (Array.isArray(compressItems) === false) compressItems = [compressItems] // Ensure compressItems is array
compressItems = compressItems.map(x => x.toLowerCase())
let list = col2Array(table, fromColNameOrNumber)
// con.log({ compressItems, list })
for (let ii = 0; ii < compressItems.length; ii++) {
let item = compressItems[ii]
let itemSmall = item
const pos = item.indexOf('-->')
if (pos !== -1) {
itemSmall = item.substr(pos + 3)
item = item.substr(0, pos)
// con.log({ item, itemSmall })
}
list = list.map(x => (x.toLowerCase().includes(item)) ? itemSmall : x)
}
// con.log({ compressItems, list })
TableArray2Col(table, list, fromColNameOrNumber) // Update the column
}
// -------------------------------------------------------------------------------
/**
* Manages the table columns
* @param {object} table - The table object
*/
function Cols (data) {
this.DATA = data
this.Convert = new Convert(data)
}
/* mergeColumns */
Cols.prototype.Merge = function (colResult, col1, col2, func = (x, y) => { return x + y }) {
mergeColumns(colResult, this.DATA, col1, col2, func)
}
/* calcColumn */
Cols.prototype.Calculate = function (colName, func = (x, index) => `${index}. ${x}`, colNew = '') {
calcColumn(this.DATA, colName, func, colNew)
}
/* {{colsNumber}} */
Cols.prototype.Number = function (colNameOrNumber, throwError = true) {
return colsNumber(this.DATA.cols, colNameOrNumber, throwError)
}
/* {{colsNames}} */
Cols.prototype.Names = function (colNamesOrNumbers) {
return colsNames(this.DATA, colNamesOrNumbers)
}
/* {{colsCreate}} */
Cols.prototype.Create = function (colNames) {
colsCreate(this.DATA, colNames)
}
/* {{colsAddColumn}} */
Cols.prototype.Add = function (colNames, defaultValue) {
if (Array.isArray(colNames)) return colsAddColumns(this.DATA, colNames, defaultValue)
else return colsAddColumn(this.DATA, colNames, defaultValue)
}
/* colsDropColumn */
Cols.prototype.Drop = function (colNameOrNumbers) {
colsDropColumn(this.DATA, colNameOrNumbers)
}
/* colsNameDef */
Cols.prototype.NameDef = function (colNameOrNumber, newColName) {
colsNameDef(this.DATA, colNameOrNumber, newColName)
}
/* colsNameDefs */
Cols.prototype.NameDefs = function (commands, debug = false) {
colsNameDefs(this.DATA, commands, debug)
}
/* {{col2Array}} */
Cols.prototype.toArray = function (colNameOrNumber, rows = -1) {
return col2Array(this.DATA, colNameOrNumber, rows)
}
Cols.prototype.samples = function (colNameOrNumber, maxLength = 45) {
return colSamples(this.DATA, colNameOrNumber, maxLength)
}
/**
* Convert columns to other types
* @param {table} data - The table data
*/
function Convert (data) {
this.DATA = data
}
/**
* Convert columns to integer
*/
Convert.prototype.toInt = function (colNamesOrNumbers) {
calcColumn(this.DATA, colNamesOrNumbers, (x) => parseInt(x))
}
Convert.prototype.toFloat = function (colNamesOrNumbers) {
calcColumn(this.DATA, colNamesOrNumbers, (x) => parseFloat(x))
}
Convert.prototype.toDate = function (colNamesOrNumbers) {
calcColumn(this.DATA, colNamesOrNumbers, (x) => _convert.Convert.dateFromStr(x))
}
Convert.prototype.toBool = function (colNamesOrNumbers) {
calcColumn(this.DATA, colNamesOrNumbers, (x) => _convert.Convert.boolFromStr(x))
}
Convert.prototype.addDuration = function (fromColNameOrNumber, toColNameOrNumber, durationCol = 'duration') {
calcDuration(this.DATA, fromColNameOrNumber, toColNameOrNumber, durationCol)
}
Convert.prototype.addMonth = function (colNamesOrNumbers) {
calcColumn(this.DATA, colNamesOrNumbers, (x) => _convert.Convert.date2MonthStr(x), 'month')
}
Convert.prototype.addWeek = function (colNamesOrNumbers) {
calcColumn(this.DATA, colNamesOrNumbers, (x) => _convert.Convert.date2Week(x), 'week')
}
Convert.prototype.compress = function (fromColNameOrNumber, compressItems = []) {
compress(this.DATA, fromColNameOrNumber, compressItems)
}
/**
* Expose private functions for testing & internal use
*/
function Private () {
this.col2Array = col2Array
}
const _private = new Private()
// Exports --------------------------
module.exports = { Cols, colsNumber, colsNames, colsCreate, colsAddColumn, TableArray2Col, _private }