UNPKG

wmata-cli

Version:
417 lines (349 loc) 10.3 kB
import { isNumber } from "../utils/helpers.js"; export default class Table { /** * The table width. */ tableWidth = 0; /** * The column names. * Including the dynamic and origin column names */ columnNames = []; /** * The padding around cell data or "cellpadding" */ paddingSize = 2; /** * The character to use for padding */ paddingChar = ' '; constructor(data) { this.dataset = data; this.build(); } /** * Gets the value of the given cell in the dataset. * * @param row the cell's row * @param col the cell's col * @returns the cell's value */ getDataCell(row, col) { return this.dataset[row][col]; } /** * Gets the character padding of the given `size`. * * @param size the padding size * @returns the character padding */ getPadding(size) { return this.paddingChar.repeat(size); } /** * Get the width of the console window. * Padding is substracted from the width. * * @returns the console width */ getConsoleWidth() { const numberOfCols = this.columnNames.length; return process.stderr.columns - numberOfCols * 2 * this.paddingSize; } /** * Gets the display name of the given column. * * @param col the column * @param cropped whether the column name should be cropped or not. * @returns the column's display name `string` */ getColumnDisplayName(col, cropped) { let name = col.toString(); if (cropped) name = name.substring(0, this.getColumnWidth(col)); return name; } /** * Gets the width of the given column. * * @param col the column * @returns the column's text width */ getColumnWidth(col) { return this.columnWidths.get(col); } /** * Builds a cell content array. * * @param padLeft the cell's left padding * @param text the cell's text * @param padRight the cell's right padding * @returns the cell content */ buildCellContent(padLeft, text, padRight) { return [this.getPadding(padLeft), text, this.getPadding(padRight)]; } /** * Builds an empty cell content. * * @param col the cell's column * @returns the empty cell content */ buildEmptyCellContent(col) { return this.buildCellContent( this.paddingSize, this.getPadding(this.getColumnWidth(col)), this.paddingSize ); } /** * Parses the given cell text to `String`. * * @param text the text to parse * @returns the parsed cell text */ parseCellText(text) { if (isNumber(text) && !Number.isInteger(text)) return text.toFixed(3); return text.toString(); } /** * Calculates the width of all columns. * The result is stored in {@link Table.columnWidths} */ calculateColumnWidths() { const widths = new Map; const data = this.dataset.slice(); const colNames = this.columnNames; const maxWidth = Number.MAX_SAFE_INTEGER; // Initalize with maxWidth / column text length for (const name of colNames) { widths.set(name, Math.min(this.getColumnDisplayName(name).length, maxWidth)); } // Search longest string / value for (const col of colNames) { for (let iRow = 0; iRow < data.length; iRow++) { const text = this.getDataCell(iRow, col); const textLen = Math.min(this.parseCellText(text).length, maxWidth); widths.set(col, Math.max(widths.get(col), textLen)); } } this.columnWidths = widths; } /** * Builds the row separator. * * @param separator the separator character * @returns the row separator string */ buildRowSeparator(separator) { const terminalWidth = this.tableWidth < process.stderr.columns ? this.tableWidth : process.stderr.columns; return separator.repeat(terminalWidth); } /** * Builds the subsequent lines (overflow) of the given row. * Set `row=0` for the header. * * @param row the initial row * @param overflow the text overflow for each solumn * @returns the subsequent lines */ buildRowOverflow(row, overflow) { let content = '\n'; // A overflowed row might have overflow as well (makes sense, right?) let hasOverflow = false; for (let i = 0; i < overflow.length; i++) { const colName = this.columnNames[i]; const colWidth = this.getColumnWidth(colName); const text = overflow[i].substring(0, colWidth); if (!text.length) { content += buildEmptyCellContent(colName); } else { const cellContentLeft = this.buildCellContent( this.paddingSize, text, colWidth - text.length + this.paddingSize ); content += cellContentLeft; } // Cut overflow and check if there's more left overflow[i] = overflow[i].substring(colWidth); if (overflow[i].length) hasOverflow = true; } if (hasOverflow) content += this.buildRowOverflow(row, overflow); return content; } /** * Builds the column names from the dataset in the right order. * The result is stored in {@link Table.columnNames}. * * All column names are converted to `string` in order to avoid * complications when using arrays and numbers and indices. */ buildColumnNames() { if (!this.dataset.length) return; const names = new Set; Object.keys(this.dataset[0]).forEach((col) => { names.add(col); }); this.columnNames = Array.from(names); } /** * Calculates the header cell padding. * The padding is based on the column's width and its display name. * * @param col the cell's column * @returns the cell padding */ calculateHeaderCellPadding(col) { return this.getColumnWidth(col) - this.getColumnDisplayName(col, true).length + this.paddingSize; } /** * Calculates the body cell padding. * * @param row the cell's row * @param col the cell's column * @returns the cell padding */ calculateBodyCellPadding(row, col) { return this.getColumnWidth(col) - this.getCellText(row, col).length + this.paddingSize; } /** * Builds the given header cell content. * * @param col the cell's column * @returns the built cell content */ buildHeaderCell(col) { let content; const displayName = this.getColumnDisplayName(col, true); const overflow = this.getColumnDisplayName(col).substring(this.getColumnWidth(col)).trim(); const paddingR = this.calculateHeaderCellPadding(col); content = this.buildCellContent(this.paddingSize, displayName, paddingR); return [content, overflow]; } /** * Builds the header. * * @returns the built header */ buildHeader() { let rowContent = ''; let hasOverflow = false; // Overflowed text that did not fit in 1 single row const txtOverflow = []; for (const col of this.columnNames) { const [cell, overflow] = this.buildHeaderCell(col); rowContent += cell.toString().replace(/,/g, ''); txtOverflow.push(overflow); if (overflow.length) hasOverflow = true; } if (hasOverflow) rowContent += this.buildRowOverflow(0, txtOverflow); rowContent += '\n' + this.buildRowSeparator('='); return rowContent; } /** * Gets the text of given the given cell. * * @param row the cell's row * @param col the cell's column * @param cropped whether the text should be cropped or not. * @returns the cell text */ getCellText(row, col, cropped = true) { let text = ''; text = this.parseCellText(this.getDataCell(row, col)); text = text.trim(); if (cropped) text = text.substring(0, this.getColumnWidth(col)); return text; } /** * Builds the given body cell content. * * @param row the cell's row * @param col the cell's column * @returns the built cell content */ buildBodyCell(row, col) { let content; const cellText = this.getCellText(row, col); const overflow = this.getCellText(row, col, false).substring(this.getColumnWidth(col)).trim(); console.log(this.getCellText(row, col, false).length) const paddingR = this.calculateBodyCellPadding(row, col); content = this.buildCellContent(this.paddingSize, cellText, paddingR); return [content, overflow]; } /** * Builds the given body row. * * @param row the row * @returns the built row content */ buildBodyRow(row) { let rowContent = ''; let hasOverflow = false; // Overflowed text that did not fit in 1 single row const txtOverflow = []; for (const col of this.columnNames) { const [cell, overflow] = this.buildBodyCell(row, col); rowContent += cell; txtOverflow.push(overflow); if (overflow.length) hasOverflow = true; } if (hasOverflow) rowContent += this.buildRowOverflow(row, txtOverflow); return rowContent; } /** * Builds the body. * * @returns the build body */ buildBody() { let rows = []; rows = this.dataset.map((_, i) => this.buildBodyRow(i)); return rows.filter((row) => row.length).join('\n').replace(/,/g, ''); } /** * Calculates the width of the complete table. * The table width is based on the width of the columns, the padding and the border. * * @returns the table width */ calculateTableWidth() { const numberOfCols = this.columnNames.length; const borderLen = 0; return ( Array.from(this.columnWidths.values()).reduce((prev, val) => prev + val, 0) + numberOfCols * this.paddingSize * 2 + borderLen ); } /** * Gets the table as string. * Can be used to print the table on the console. * * @returns the table string */ toString() { this.build(); return [this.buildHeader(), this.buildBody()].join('\n'); } /** * Prints the table to the console. * * @param clear clear the console before printing */ print(clear = false) { if (clear) console.clear(); console.log(this.toString()); } /** * Builds the table. * For performance reasons the table is only built if {@link Table.touched} is `true`. * * @param force force the build */ build() { this.buildColumnNames(); this.calculateColumnWidths(); this.tableWidth = this.calculateTableWidth(); } }