var ANSI_REGEX = require('ansi-regex')() var wcwidth try { wcwidth = require('wcwidth') } catch(e) {} module.exports = Table function Table() { this.rows = [] this.row = {__printers : {}} } /** * Push the current row to the table and start a new one * * @returns {Table} `this` */ Table.prototype.newRow = function() { this.rows.push(this.row) this.row = {__printers : {}} return this } /** * Write cell in the current row * * @param {String} col - Column name * @param {Any} val - Cell value * @param {Function} [printer] - Printer function to format the value * @returns {Table} `this` */ Table.prototype.cell = function(col, val, printer) { this.row[col] = val this.row.__printers[col] = printer || string return this } /** * String to separate columns */ Table.prototype.separator = ' ' function string(val) { return val === undefined ? '' : ''+val } function length(str) { var s = str.replace(ANSI_REGEX, '') return wcwidth == null ? s.length : wcwidth(s) } /** * Default printer */ Table.string = string /** * Create a printer which right aligns the content by padding with `ch` on the left * * @param {String} ch * @returns {Function} */ Table.leftPadder = leftPadder function leftPadder(ch) { return function(val, width) { var str = string(val) var len = length(str) var pad = width > len ? Array(width - len + 1).join(ch) : '' return pad + str } } /** * Printer which right aligns the content */ var padLeft = Table.padLeft = leftPadder(' ') /** * Create a printer which pads with `ch` on the right * * @param {String} ch * @returns {Function} */ Table.rightPadder = rightPadder function rightPadder(ch) { return function padRight(val, width) { var str = string(val) var len = length(str) var pad = width > len ? Array(width - len + 1).join(ch) : '' return str + pad } } var padRight = rightPadder(' ') /** * Create a printer for numbers * * Will do right alignment and optionally fix the number of digits after decimal point * * @param {Number} [digits] - Number of digits for fixpoint notation * @returns {Function} */ Table.number = function(digits) { return function(val, width) { if (val == null) return '' if (typeof val != 'number') throw new Error(''+val + ' is not a number') var str = digits == null ? val+'' : val.toFixed(digits) return padLeft(str, width) } } function each(row, fn) { for(var key in row) { if (key == '__printers') continue fn(key, row[key]) } } /** * Get list of columns in printing order * * @returns {string[]} */ Table.prototype.columns = function() { var cols = {} for(var i = 0; i < 2; i++) { // do 2 times this.rows.forEach(function(row) { var idx = 0 each(row, function(key) { idx = Math.max(idx, cols[key] || 0) cols[key] = idx idx++ }) }) } return Object.keys(cols).sort(function(a, b) { return cols[a] - cols[b] }) } /** * Format just rows, i.e. print the table without headers and totals * * @returns {String} String representaion of the table */ Table.prototype.print = function() { var cols = this.columns() var separator = this.separator var widths = {} var out = '' // Calc widths this.rows.forEach(function(row) { each(row, function(key, val) { var str = row.__printers[key].call(row, val) widths[key] = Math.max(length(str), widths[key] || 0) }) }) // Now print this.rows.forEach(function(row) { var line = '' cols.forEach(function(key) { var width = widths[key] var str = row.hasOwnProperty(key) ? ''+row.__printers[key].call(row, row[key], width) : '' line += padRight(str, width) + separator }) line = line.slice(0, -separator.length) out += line + '\n' }) return out } /** * Format the table * * @returns {String} */ Table.prototype.toString = function() { var cols = this.columns() var out = new Table() // copy options out.separator = this.separator // Write header cols.forEach(function(col) { out.cell(col, col) }) out.newRow() out.pushDelimeter(cols) // Write body out.rows = out.rows.concat(this.rows) // Totals if (this.totals && this.rows.length) { out.pushDelimeter(cols) this.forEachTotal(out.cell.bind(out)) out.newRow() } return out.print() } /** * Push delimeter row to the table (with each cell filled with dashs during printing) * * @param {String[]} [cols] * @returns {Table} `this` */ Table.prototype.pushDelimeter = function(cols) { cols = cols || this.columns() cols.forEach(function(col) { this.cell(col, undefined, leftPadder('-')) }, this) return this.newRow() } /** * Compute all totals and yield the results to `cb` * * @param {Function} cb - Callback function with signature `(column, value, printer)` */ Table.prototype.forEachTotal = function(cb) { for(var key in this.totals) { var aggr = this.totals[key] var acc = aggr.init var len = this.rows.length this.rows.forEach(function(row, idx) { acc = aggr.reduce.call(row, acc, row[key], idx, len) }) cb(key, acc, aggr.printer) } } /** * Format the table so that each row represents column and each column represents row * * @param {Object} [opts] * @param {String} [ops.separator] - Column separation string * @param {Function} [opts.namePrinter] - Printer to format column names * @returns {String} */ Table.prototype.printTransposed = function(opts) { opts = opts || {} var out = new Table out.separator = opts.separator || this.separator this.columns().forEach(function(col) { out.cell(0, col, opts.namePrinter) this.rows.forEach(function(row, idx) { out.cell(idx+1, row[col], row.__printers[col]) }) out.newRow() }, this) return out.print() } /** * Sort the table * * @param {Function|string[]} [cmp] - Either compare function or a list of columns to sort on * @returns {Table} `this` */ Table.prototype.sort = function(cmp) { if (typeof cmp == 'function') { this.rows.sort(cmp) return this } var keys = Array.isArray(cmp) ? cmp : this.columns() var comparators = keys.map(function(key) { var order = 'asc' var m = /(.*)\|\s*(asc|des)\s*$/.exec(key) if (m) { key = m[1] order = m[2] } return function (a, b) { return order == 'asc' ? compare(a[key], b[key]) : compare(b[key], a[key]) } }) return this.sort(function(a, b) { for (var i = 0; i < comparators.length; i++) { var order = comparators[i](a, b) if (order != 0) return order } return 0 }) } function compare(a, b) { if (a === b) return 0 if (a === undefined) return 1 if (b === undefined) return -1 if (a === null) return 1 if (b === null) return -1 if (a > b) return 1 if (a < b) return -1 return compare(String(a), String(b)) } /** * Add a total for the column * * @param {String} col - column name * @param {Object} [opts] * @param {Function} [opts.reduce = sum] - reduce(acc, val, idx, length) function to compute the total value * @param {Function} [opts.printer = padLeft] - Printer to format the total cell * @param {Any} [opts.init = 0] - Initial value for reduction * @returns {Table} `this` */ Table.prototype.total = function(col, opts) { opts = opts || {} this.totals = this.totals || {} this.totals[col] = { reduce: opts.reduce || Table.aggr.sum, printer: opts.printer || padLeft, init: opts.init == null ? 0 : opts.init } return this } /** * Predefined helpers for totals */ Table.aggr = {} /** * Create a printer which formats the value with `printer`, * adds the `prefix` to it and right aligns the whole thing * * @param {String} prefix * @param {Function} printer * @returns {printer} */ Table.aggr.printer = function(prefix, printer) { printer = printer || string return function(val, width) { return padLeft(prefix + printer(val), width) } } /** * Sum reduction */ Table.aggr.sum = function(acc, val) { return acc + val } /** * Average reduction */ Table.aggr.avg = function(acc, val, idx, len) { acc = acc + val return idx + 1 == len ? acc/len : acc } /** * Print the array or object * * @param {Array|Object} obj - Object to print * @param {Function|Object} [format] - Format options * @param {Function} [cb] - Table post processing and formating * @returns {String} */ Table.print = function(obj, format, cb) { var opts = format || {} format = typeof format == 'function' ? format : function(obj, cell) { for(var key in obj) { if (!obj.hasOwnProperty(key)) continue var params = opts[key] || {} cell(params.name || key, obj[key], params.printer) } } var t = new Table var cell = t.cell.bind(t) if (Array.isArray(obj)) { cb = cb || function(t) { return t.toString() } obj.forEach(function(item) { format(item, cell) t.newRow() }) } else { cb = cb || function(t) { return t.printTransposed({separator: ' : '}) } format(obj, cell) t.newRow() } return cb(t) } /** * Same as `Table.print()` but yields the result to `console.log()` */ Table.log = function(obj, format, cb) { console.log(Table.print(obj, format, cb)) } /** * Same as `.toString()` but yields the result to `console.log()` */ Table.prototype.log = function() { console.log(this.toString()) }