💾TableMaker

customclass ⟩ TableMaker

print a table for 2D array of strings (with column settings, optional icons, separators).

const {log} = console;

// ⭐ TableMaker
class TableMaker {

    // • ⭐ data   : 2D array of strings
    // • ⭐ columns: column settings (array of [header, align])
    // • table settings:  ({spacing: 3, icon: null})
    constructor(data, columns, { 
        spacing = 3,     // ⭐ between columns
        padding = 0,     // ⭐ inside column
        maxStringLength = 24,
        icon = null,     // ⭐ choose icon for each row (function)
        iconDescription = `<icon description here>`,
    } = {}) {

        this.data = data;
        this.columnSettings = columns;
        this.spacing = spacing;
        this.padding = padding;
        this.maxStringLength = maxStringLength;
        this.icon = icon;
        this.iconDescription = iconDescription;

        // number of rows/cols (header not included)
        this.rows = this.data.length;    // ❗ may include separators
        this.cols = this.columnSettings.length;

        // truncate strings if needed
        this._truncateData();

        // init column widths
        this._calculateColumnWidths();    // this._widths

        // convert data rows to lines 
        // ⭐ (1st pass) this.divider still undefined
        this._convertDataToLines();        // this._lines

        // setup divider
        const w = this._lines[0].length;
        this.divider = '─'.repeat(w+1);

        // convert data rows to lines 
        // ⭐ (2nd pass) this.divider is defined!
        this._convertDataToLines();        // this._lines
    }

    // truncate data string if needed
    _truncateData() {
        for (let row of this.data) {
            if (row === '---') continue;    // skip separator
            const max = this.maxStringLength;
            for(let [j, s] of row.entries()) {
                if (j >= this.cols) break;    // skip extra values
                if (s.length > max) row[j] = s.slice(0, max-4) + `...`;
            }
        }
    }

    // calculate width of every column
    _calculateColumnWidths() {
        
        // header widths first
        let widths = this.columnSettings.map(s => s[0].length);
        
        // iterate over rows
        for (const row of this.data) {
            if (row === '---') continue;        // ❗ skip separators
            for (const [j, s] of row.entries()) {
                if (j >= this.cols) break;      // ❗ extra values ignored
                const len = s.length;
                if (len > widths[j]) widths[j] = len;
            }
        }
        
        this._widths = widths;
    }

    // align text with `width`, `align`
    _alignText(text, width, align = TableMaker.align.left) {
        
        let result;

        // truncate text if needed
        const max = this.maxStringLength;
        if (text.length > max) text = text.slice(0, max-3) + '...';
        
        switch (align) {
                
            case TableMaker.align.left:
                result = text.padEnd(width, ' '); 
                break;
                
            case TableMaker.align.right:
                result = text.padStart(width, ' '); 
                break;
                
            case TableMaker.align.center:
                const len = text.length;
                const padstart = Math.floor((width - len) / 2);
                const padend = width - padstart - len;
                result = ' '.repeat(padstart) + text + ' '.repeat(padend); 
                break;
                
            default:
                result = text.padEnd(width, ' '); 
        }

        return result;
    }

    // convert data rows to lines
    _convertDataToLines() {
        this._lines = this.data.map(row => this._rowToString(row));
    }

    // data row -> string (with/out icon)
    _rowToString(row) {

        if (row === '---') return this.divider;    // ❗ separator

        const pad = ' '.repeat(this.padding);
        const spaces = ' '.repeat(this.spacing);
        const rowIcon = this.icon ?. (row);

        return (rowIcon ? rowIcon + ' ' : '') + row
            .slice(0, this.cols)                  // ❗ extra values ignored
            .map((v, j) => pad 
                + this._alignText(v, this._widths[j], this.columnSettings[j][1]) 
                + pad
            )
            .join(spaces);
    }

    // header row -> string
    _header() {
        
        const pad = ' '.repeat(this.padding);
        const spaces = ' '.repeat(this.spacing);
        
        const iconPad = this.icon ? `┌── ${this.iconDescription}\n│\n│  ` : '';
        
        const row = this.columnSettings.map(s => s[0]);

        return iconPad + row
            .map((v, j) => pad 
                + this._alignText(v, this._widths[j]) 
                + pad)
            .join(spaces);
    }

    // table -> string
    toString() {

        const divider = this.divider;
        
        log(`table width: ${divider.length}`);

        return this._header() + '\n'
            + divider + '\n' 
            + this._lines.join('\n') + '\n'
            + divider + '\n';
    }

    // print to console
    print() {
        console.log(this.toString());
    }

}

// column alignment
TableMaker.align = {
    left: 0, center: 1, right: 2, 
    dot: 3,    // not implemented (may have bugs)
};

// icons
TableMaker.icon = {
    diamond: '🔸',
    star   : '⭐️',
    check  : '✅',
    cross  : '❌',
};

// export
module.exports = { TableMaker };

Last updated