# TableMaker

[custom](..) ⟩ [class]() ⟩ TableMaker

{% hint style="success" %}
print a table for 2D array of strings (with column settings, optional icons, separators).
{% endhint %}

{% tabs %}
{% tab title="💾 程式" %}
* replit： [TableMaker (with separators)](https://replit.com/@pegasusroe/TableMaker-with-separators#helpers/TableMaker.js)

{% code lineNumbers="true" %}
```javascript
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 };
```
{% endcode %}
{% endtab %}

{% tab title="💈範例" %}
replit： [TableMaker (with separators)](https://replit.com/@pegasusroe/TableMaker-with-separators#index.js)

```javascript
const {log} = console;
const { testCases } = require('./testCases.js');
const { isPrimitive } = require('./helpers/isObject.js');
const { TableMaker } = require('./helpers/TableMaker.js');

// ⭐ data (with extra `value`)
const data = testCases.map(testcase => {
    
    if (testcase === '---') return '---';
    
    // test cases: [value, expr, desc]
    const value = testcase[0];
    const expr = testcase[1] || String(value);
    const desc = testcase[2] || '';
    const toNum = String(+value);
    const toStr = `"${String(value)}"`;

    // [expr, desc, num, str, value] (extra `value` is for icon chooser)
    return [ expr, desc, toNum, toStr, value];
})

// ⭐ column settings (4 columns)
const columns = [
    ['value', ],
    ['desc', ],
    ['-> number', TableMaker.align.right],
    ['-> string', ],
];

// ⭐ icon chooser
const icon = (row) => isPrimitive(row[4]) ? '✅' : '❌';

// ⭐ TableMaker
const table = new TableMaker(data, columns, {
    icon: icon,
    iconDescription: '(primitive?)'
});

table.print();
```

📃 結果：

```javascript
┌── (primitive?)
│
│  value       desc          -> number       -> string              
────────────────────────────────────────────────────────────────────
✅ null                                  0   "null"                 
────────────────────────────────────────────────────────────────────
✅ undefined                           NaN   "undefined"            
────────────────────────────────────────────────────────────────────
✅ 0                                     0   "0"                    
✅ Infinity                       Infinity   "Infinity"             
✅ NaN                                 NaN   "NaN"                  
────────────────────────────────────────────────────────────────────
✅ ""          empty str                 0   ""                     
✅ "1.2"       numeric                 1.2   "1.2"                  
✅ "one"       non-numeric             NaN   "one"                  
────────────────────────────────────────────────────────────────────
✅ true                                  1   "true"                 
✅ false                                 0   "false"                
────────────────────────────────────────────────────────────────────
❌ {a:1}                               NaN   "[object Object]"      
❌ []          empty arr                 0   ""                     
❌ [6]         one numeric               6   "6"                    
❌ ['a']       any other               NaN   "a"                    
❌ new Date                  1667313821772   "Tue Nov 01 2022 14:...
❌ /regex/                             NaN   "/regex/"              
────────────────────────────────────────────────────────────────────
❌ () => {}                            NaN   "() => {}"             
❌ class {}                            NaN   "class {}"             
────────────────────────────────────────────────────────────────────
```
{% endtab %}

{% tab title="⬇️ 應用" %}
* [Broken link](broken-reference "mention")
{% endtab %}
{% endtabs %}
