# TableMaker

[custom](/web/appendix/custom.md) ⟩ [class](/web/appendix/custom/class.md) ⟩ 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 mention](broken://pages/l1v0bwCArjuB3OOG3NfE)
  {% endtab %}
  {% endtabs %}


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lochiwei.gitbook.io/web/appendix/custom/class/tablemaker.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
