diff --git a/lib/generate.js b/lib/generate.js new file mode 100644 index 0000000..96f5c8c --- /dev/null +++ b/lib/generate.js @@ -0,0 +1,80 @@ +const Table = require('..'); + +const cellContent = ({ x, y, colSpan = 1, rowSpan = 1 }) => { + return `${y}-${x} (${rowSpan}x${colSpan})`; +}; + +const generateBasicTable = (rows, cols, options = {}) => { + const table = new Table(options); + for (let y = 0; y < rows; y++) { + let row = []; + for (let x = 0; x < cols; x++) { + row.push(cellContent({ y, x })); + } + table.push(row); + } + return table; +}; + +const randomNumber = (min, max, op = 'round') => { + return Math[op](Math.random() * (max - min) + min); +}; + +const next = (alloc, idx, dir = 1) => { + if (alloc[idx]) { + return next(alloc, idx + 1 * dir); + } + return idx; +}; + +const generateComplexRow = (y, spanX, cols, alloc, options = {}) => { + let x = next(alloc, 0); + const row = []; + while (x < cols) { + const { colSpans = {} } = options; + const opt = { + colSpan: colSpans[x] || next(alloc, randomNumber(x + 1, options.maxCols || cols, 'ceil'), -1) - x, + rowSpan: randomNumber(1, spanX), + }; + row.push({ content: cellContent({ y, x, ...opt }), ...opt }); + if (opt.rowSpan > 1) { + for (let i = 0; i < opt.colSpan; i++) { + alloc[x + i] = opt.rowSpan; + } + } + + x = next(alloc, x + opt.colSpan); + } + return row; +}; + +const generateComplexRows = (y, rows, cols, alloc = {}, options = {}) => { + const remaining = rows - y; + let spanX = remaining > 1 ? randomNumber(1, remaining) : 1; + let lines = []; + while (spanX > 0) { + lines.push(generateComplexRow(y, spanX, cols, alloc, options)); + y++; + spanX--; + Object.keys(alloc).forEach((idx) => { + alloc[idx]--; + if (alloc[idx] <= 0) delete alloc[idx]; + }); + } + return lines; +}; + +const generateComplexTable = (rows, cols, options = {}) => { + const table = new Table(options.tableOptions); + while (table.length < rows) { + let y = table.length || (table.options.head && 1) || 0; + generateComplexRows(y, rows, cols, options).forEach((row) => table.push(row)); + } + return table; +}; + +module.exports = { + generateBasicTable, + generateComplexTable, + generateComplexRow, +}; diff --git a/scripts/generate.js b/scripts/generate.js new file mode 100644 index 0000000..b9705a9 --- /dev/null +++ b/scripts/generate.js @@ -0,0 +1,160 @@ +/** + * See generate.md + */ +const { performance } = require('perf_hooks'); +const { generateBasicTable, generateComplexTable, generateComplexRow } = require('../lib/generate'); + +const argv = process.argv; + +const timeScales = [ + ['millisecond', 1000], + ['second', 60], + ['minute', 60], +]; +const duration = (v, scales = [...timeScales]) => { + const [unit, min] = scales.shift(); + if (v > min && scales.length) { + return duration(v / min, scales); + } + let locale = undefined; + if (process.env.LANG) { + const userLocale = process.env.LANG.match(/[a-z]{2}_[A-Z]{2}/).shift(); + if (userLocale.match(/^[a-z]{2}[-_][A-Z]{2}$/)) { + locale = userLocale.replace(/_/, '-'); + } + } + return v.toLocaleString(locale, { style: 'unit', unit }); +}; + +const argVal = (idx, def = 10) => { + if (argv[idx] && argv[idx].match(/^[0-9]+$/)) { + return parseInt(argv[idx], 10); + } + return def; +}; +const optEnabled = (opt) => argv.indexOf(opt) > -1; +const optValue = (opt) => { + const idx = argv.indexOf(opt); + return idx > -1 ? argv[idx + 1] : 0; +}; + +const logMemory = (text = '') => { + let suffix = 'kb'; + let used = process.memoryUsage().heapUsed / 1024; + if (used % 1024 > 1) { + used = used / 1024; + suffix = 'mb'; + } + return `Memory usage ${text}: ${used}${suffix}`; +}; + +const printHelp = () => { + console.log(`node scripts/generate [ROWS = 10] [COLS = 10]`); + [ + ['--print', 'Print the generated table to the screen.'], + ['--dump', 'Print the generated table code to the screen.'], + ['--complex', 'Generate a complex table (basic tables are generated by default)'], + ['--debug', 'Print table debugging output (warnings only).'], + ].forEach(([opt, desc]) => console.log(` ${opt} ${desc}`)); +}; + +const dumpTable = (t) => { + const lines = []; + lines.push(`const table = new Table();`); + lines.push(`table.push(`); + t.forEach((row) => { + if (row.length) { + let prefix = ' '; + let suffix = ''; + const multiLine = row.length > 1 && row.some((v) => v.content !== undefined); + if (multiLine) { + lines.push(' ['); + } + const cellLines = []; + row.forEach((cell) => { + if (cell.content) { + const attrib = []; + Object.entries(cell).forEach(([k, v]) => { + if (!['style'].includes(k)) { + attrib.push(`${k}: ${typeof v === 'string' ? `'${v}'` : v}`); + } + }); + cellLines.push(`{ ${attrib.join(', ')} },`); + } else { + cellLines.push(`${typeof cell === 'string' ? `'${cell}'` : cell}`); + } + }); + if (multiLine) { + cellLines.forEach((cl) => lines.push([prefix, cl, suffix].join(''))); + lines.push(' ],'); + } else { + lines.push(` [${cellLines.join(',')}]`); + } + } else { + lines.push(' [],'); + } + }); + lines.push(');'); + lines.push('console.log(table.toString());'); + return lines.forEach((line) => console.log(line)); +}; + +if (optEnabled('--help')) { + printHelp(); + process.exit(0); +} + +const results = []; +results.push(logMemory('at startup')); + +const rows = argVal(2); +const cols = argVal(3); + +const maxRowSpan = rows > 10 ? Math.ceil(Math.round(rows * 0.1)) : Math.ceil(rows / 2); +const maxColSpan = cols; + +const complex = optEnabled('--complex'); + +console.log(`Generating ${complex ? 'complex' : 'basic'} table with ${rows} rows and ${cols} columns:`); + +if (complex) { + console.log(`Max rowSpan: ${maxRowSpan}`, `Max colSpan ${maxColSpan}`); +} + +const options = { + tableOptions: {}, +}; + +if (optEnabled('--compact')) { + options.tableOptions.style = { compact: true }; +} + +if (optEnabled('--head')) { + const head = generateComplexRow(0, 1, cols, {}, { maxCols: cols - 1 }); + options.tableOptions.head = head; +} + +const colWidth = optValue('--col-width'); +if (colWidth) { + options.tableOptions.colWidths = []; + for (let i = 0; i < cols; i++) { + options.tableOptions.colWidths.push(parseInt(colWidth, 10)); + } +} + +// console.log(`table: ${rows} rows X ${cols} columns; ${rows * cols} total cells`); +// console.time('build table'); +const buildStart = performance.now(); +const table = complex ? generateComplexTable(rows, cols, options) : generateBasicTable(rows, cols, options); +// console.timeEnd('build table'); +results.push(logMemory('after table build')); + +results.push(`table built in ${duration(performance.now() - buildStart)}`); + +const start = performance.now(); +const output = table.toString(); +results.push(logMemory('after table rendered')); +results.push(`table rendered in ${duration(performance.now() - start)}`); +if (optEnabled('--print')) console.log(output); +if (optEnabled('--dump')) dumpTable(table); +results.forEach((result) => console.log(result)); diff --git a/scripts/generate.md b/scripts/generate.md new file mode 100644 index 0000000..a7dca04 --- /dev/null +++ b/scripts/generate.md @@ -0,0 +1,185 @@ +# cli-table3 generation script + +This experimental [generate script](./generate.js) is a utility primarily suited for testing, +debugging, experimenting, and having fun with cli-table3! + +While this script is mainly purposed for developers/contributors of cli-table3, +some might find it useful for getting started. + +## Basic usage + +``` +➭ node scripts/generate [ROWS = 10] [COLS = 10] [options] + +When installing this package: +➭ node node_modules/cli-table3/scripts/generate 10 5 --print + +When cloning this repository: +➭ node scripts/generate 10 5 --print +``` + +Note: Options can be specified without specifying rows and cols but must come +last if rows and cols are specified. + +_See available options._ + +## Features + +* Generate basic tables +* Generate complex tables +* Performance testing +* Optionally output usable code for generated tables + +### Generate basic tables + +Basic table generation produces only simple 1x1 cells. Eg. + +``` +➭ node script/generate 5 4 --print +``` + +Outputs: + +``` +┌───────────┬───────────┬───────────┬───────────┬───────────┬───────────┐ +│ 0-0 (1x1) │ 0-1 (1x1) │ 0-2 (1x1) │ 0-3 (1x1) │ 0-4 (1x1) │ 0-5 (1x1) │ +├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ +│ 1-0 (1x1) │ 1-1 (1x1) │ 1-2 (1x1) │ 1-3 (1x1) │ 1-4 (1x1) │ 1-5 (1x1) │ +├───────────┼───────────┼───────────┼───────────┼───────────┼───────────┤ +│ 2-0 (1x1) │ 2-1 (1x1) │ 2-2 (1x1) │ 2-3 (1x1) │ 2-4 (1x1) │ 2-5 (1x1) │ +└───────────┴───────────┴───────────┴───────────┴───────────┴───────────┘ +Memory usage at startup: 2.6648712158203125mb +Memory usage after table build: 2.6932601928710938mb +table built in 0.305 ms +Memory usage after table rendered: 3.2284088134765625mb +table rendered in 16.506 ms +``` + +### Generate complex tables + +Complex tables can include cells that span multiple rows and/or columns. + +``` +➭ node script/generate --complex --print +``` + +**Randomly** outputs something like: + +``` +┌─────────────────────────┬───────────┬───────────┐ +│ 0-0 (6x8) │ 0-8 (6x1) │ 0-9 (4x1) │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ ├───────────┤ +│ │ │ 4-9 (4x1) │ +│ │ │ │ +│ │ │ │ +├─────────────────────────┼───────────┤ │ +│ 6-0 (2x8) │ 6-8 (1x1) │ │ +│ ├───────────┤ │ +│ │ 7-8 (2x1) │ │ +├─────────────────────────┤ ├───────────┤ +│ 8-0 (1x8) │ │ 8-9 (1x1) │ +├─────────┬───────────┬───┴───────────┼───────────┤ +│ 9-0 (1… │ 9-5 (1x1) │ 9-6 (1x3) │ 9-9 (1x1) │ +└─────────┴───────────┴───────────────┴───────────┘ +``` + +Limits for rowSpans and colSpans are determined based on the number rows & cols. + +_Complex table generation is prone to using up the rowSpans toward the top of +the table._ + + +### Performance testing + +This script was primarily developed for testing performance (manually). By +default, that's all it does. Observe memory usage and rendering time when +generating tables with this script. Eg. + +``` +➭ node script/generate +``` + +Outputs: + +``` +Memory usage at startup: 2.6579513549804688mb +Memory usage after table build: 2.7062149047851562mb +table built in 0.325 ms +Memory usage after table rendered: 4.779487609863281mb +table rendered in 23.820 ms +``` + +## Generate table code + +Use the `--dump` option to output code for the table that is generated. Eg. + +``` +➭ node script/generate 5 6 --dump +``` + +Outputs: + +``` +const table = new Table(); +table.push( + ['0-0 (1x1)','0-1 (1x1)','0-2 (1x1)','0-3 (1x1)','0-4 (1x1)','0-5 (1x1)'] + ['1-0 (1x1)','1-1 (1x1)','1-2 (1x1)','1-3 (1x1)','1-4 (1x1)','1-5 (1x1)'] + ['2-0 (1x1)','2-1 (1x1)','2-2 (1x1)','2-3 (1x1)','2-4 (1x1)','2-5 (1x1)'] + ['3-0 (1x1)','3-1 (1x1)','3-2 (1x1)','3-3 (1x1)','3-4 (1x1)','3-5 (1x1)'] + ['4-0 (1x1)','4-1 (1x1)','4-2 (1x1)','4-3 (1x1)','4-4 (1x1)','4-5 (1x1)'] +); +console.log(table.toString()); +Memory usage at startup: 2.689239501953125mb +Memory usage after table build: 2.721099853515625mb +table built in 0.300 ms +Memory usage after table rendered: 3.4740676879882812mb +table rendered in 18.327 ms +``` + +#### Generate complex table code: + +``` +➭ node script/generate --dump --complex +``` + +**Randomly** generates something like: + +``` +const table = new Table(); +table.push( + [ + { content: '0-0 (1x3)', colSpan: 3, rowSpan: 1 }, + { content: '0-3 (1x1)', colSpan: 1, rowSpan: 1 }, + { content: '0-4 (1x1)', colSpan: 1, rowSpan: 1 }, + ], + [ + { content: '1-0 (2x4)', colSpan: 4, rowSpan: 2 }, + { content: '1-4 (2x1)', colSpan: 1, rowSpan: 2 }, + ], + [], + [ + { content: '3-0 (1x1)', colSpan: 1, rowSpan: 1 }, + { content: '3-1 (1x3)', colSpan: 3, rowSpan: 1 }, + { content: '3-4 (1x1)', colSpan: 1, rowSpan: 1 }, + ], +); +console.log(table.toString()); +Memory usage at startup: 3.0095062255859375mb +Memory usage after table build: 3.0915908813476562mb +table built in 1.477 ms +Memory usage after table rendered: 4.018150329589844mb +table rendered in 11.284 ms +``` + +## Generation Options + +## Warranty + +This script is provided as is and it's functionality is not guranteed to match +what has been described above.