From 63855aee0da731063cc6b5e2034ecdbe252d5935 Mon Sep 17 00:00:00 2001 From: av-virlan Date: Sat, 12 Jun 2021 11:40:01 +0300 Subject: [PATCH] feat(row separator): Added row separator option (#372) --- src/internalTable/internal-table-printer.ts | 16 ++ src/internalTable/internal-table.ts | 29 ++-- src/models/common.ts | 1 + src/models/external-table.ts | 1 + src/models/internal-table.ts | 1 + src/utils/table-constants.ts | 8 + src/utils/table-helpers.ts | 20 ++- test/internalTable/borderStyle.test.ts | 12 ++ test/internalTable/rowSeparator.test.ts | 165 ++++++++++++++++++++ 9 files changed, 240 insertions(+), 13 deletions(-) create mode 100644 test/internalTable/rowSeparator.test.ts diff --git a/src/internalTable/internal-table-printer.ts b/src/internalTable/internal-table-printer.ts index 09a42850..eda1b56e 100644 --- a/src/internalTable/internal-table-printer.ts +++ b/src/internalTable/internal-table-printer.ts @@ -175,6 +175,21 @@ const renderTableEnding = (table: TableInternal): string[] => { return ret; }; +const renderRowSeparator = (table: TableInternal, row: Row): string[] => { + const ret: string[] = []; + let lastRowIndex = table.rows.length - 1; + let rowIndex = table.rows.indexOf(row); + let addSeparator = row.separator !== undefined ? row.separator : table.rowSeparator; + + if (rowIndex > -1 && rowIndex < lastRowIndex && addSeparator) { + ret.push(renderTableHorizontalBorders( + table.tableStyle.rowSeparator, + table.columns.map((m) => m.length || DEFAULT_COLUMN_LEN) + )); + } + return ret; +} + export const renderTable = (table: TableInternal): string => { preProcessColumns(table); // enable / disable cols, find maxLn of each col/ computed Columns preProcessRows(table); // sort and filter @@ -186,6 +201,7 @@ export const renderTable = (table: TableInternal): string => { table.rows.forEach((row) => { renderRow(table, row).forEach((row_) => ret.push(row_)); + renderRowSeparator(table, row).forEach((row_) => ret.push(row_)); }); renderTableEnding(table).forEach((row) => ret.push(row)); return ret.join('\n'); diff --git a/src/internalTable/internal-table.ts b/src/internalTable/internal-table.ts index fa8a567a..f9569248 100644 --- a/src/internalTable/internal-table.ts +++ b/src/internalTable/internal-table.ts @@ -10,6 +10,7 @@ import { DEFAULT_TABLE_STYLE, DEFAULT_ROW_ALIGNMENT, DEFAULT_ROW_FONT_COLOR, + DEFAULT_ROW_SEPARATOR } from '../utils/table-constants'; import { createColumFromComputedColumn, @@ -43,6 +44,8 @@ class TableInternal { computedColumns: any[]; + rowSeparator: boolean; + initSimple(columns: string[]) { this.columns = columns.map((column) => ({ name: column, @@ -52,14 +55,15 @@ class TableInternal { } initDetailed(options: ComplexOptions) { - this.title = options.title || undefined; - this.tableStyle = options?.style || DEFAULT_TABLE_STYLE; - this.sortFunction = options?.sort || DEFAULT_ROW_SORT_FUNC; - this.filterFunction = options?.filter || DEFAULT_ROW_FILTER_FUNC; - this.enabledColumns = options?.enabledColumns || []; - this.disabledColumns = options?.disabledColumns || []; - this.computedColumns = options?.computedColumns || []; - this.columns = options.columns?.map(rawColumnToInternalColumn) || []; + this.title = options?.title || this.title; + this.tableStyle = options?.style || this.tableStyle; + this.sortFunction = options?.sort || this.sortFunction; + this.filterFunction = options?.filter || this.filterFunction; + this.enabledColumns = options?.enabledColumns || this.enabledColumns; + this.disabledColumns = options?.disabledColumns || this.disabledColumns; + this.computedColumns = options?.computedColumns || this.computedColumns; + this.columns = options?.columns?.map(rawColumnToInternalColumn) || this.columns; + this.rowSeparator = options?.rowSeparator || this.rowSeparator; } constructor(options?: ComplexOptions | string[]) { @@ -73,6 +77,7 @@ class TableInternal { this.enabledColumns = []; this.disabledColumns = []; this.computedColumns = []; + this.rowSeparator = DEFAULT_ROW_SEPARATOR; if (options instanceof Array) { this.initSimple(options); @@ -106,7 +111,13 @@ class TableInternal { addRow(text: Dictionary, options?: RowOptions) { this.createColumnFromRow(text); - this.rows.push(createRow(options?.color || DEFAULT_ROW_FONT_COLOR, text)); + this.rows.push( + createRow( + options?.color || DEFAULT_ROW_FONT_COLOR, + text, + options?.separator + ) + ); } addRows(toBeInsertedRows: Dictionary[], options?: RowOptions) { diff --git a/src/models/common.ts b/src/models/common.ts index b1b7ce6d..2186e661 100644 --- a/src/models/common.ts +++ b/src/models/common.ts @@ -8,5 +8,6 @@ export interface Dictionary { } export interface Row { color: COLOR; + separator?: boolean; text: Dictionary; } diff --git a/src/models/external-table.ts b/src/models/external-table.ts index dcbeb867..01014052 100644 --- a/src/models/external-table.ts +++ b/src/models/external-table.ts @@ -29,4 +29,5 @@ export interface ComplexOptions { enabledColumns?: string[]; disabledColumns?: string[]; computedColumns?: ComputedColumn[]; + rowSeparator?: boolean; } diff --git a/src/models/internal-table.ts b/src/models/internal-table.ts index 2e965090..5f525597 100644 --- a/src/models/internal-table.ts +++ b/src/models/internal-table.ts @@ -27,4 +27,5 @@ export type TableStyleDetails = { headerBottom: TableLineDetails; tableBottom: TableLineDetails; vertical: string; + rowSeparator: TableLineDetails; }; diff --git a/src/utils/table-constants.ts b/src/utils/table-constants.ts index 60ff6bc9..90d7bf92 100644 --- a/src/utils/table-constants.ts +++ b/src/utils/table-constants.ts @@ -3,6 +3,8 @@ import { TableStyleDetails } from '../models/internal-table'; export const DEFAULT_COLUMN_LEN = 20; +export const DEFAULT_ROW_SEPARATOR = false; + export const DEFAULT_TABLE_STYLE: TableStyleDetails = { /* Default Style @@ -30,6 +32,12 @@ export const DEFAULT_TABLE_STYLE: TableStyleDetails = { other: '─', }, vertical: '│', + rowSeparator: { + left: '├', + mid: '┼', + right: '┤', + other: '─', + }, }; export const ALIGNMENTS = ['right', 'left', 'center']; diff --git a/src/utils/table-helpers.ts b/src/utils/table-helpers.ts index 74cf0493..80ec2380 100644 --- a/src/utils/table-helpers.ts +++ b/src/utils/table-helpers.ts @@ -4,7 +4,11 @@ import { ComputedColumn } from '../models/external-table'; import { Column } from '../models/internal-table'; import findWidthInConsole from './console-utils'; import { biggestWordInSentence, limitWidth } from './string-utils'; -import { DEFAULT_COLUMN_LEN, DEFAULT_ROW_ALIGNMENT } from './table-constants'; +import { + DEFAULT_COLUMN_LEN, + DEFAULT_ROW_ALIGNMENT, + DEFAULT_ROW_SEPARATOR, +} from './table-constants'; const max = (a: number, b: number) => Math.max(a, b); @@ -13,11 +17,13 @@ export const cellText = (text: string | number): string => text === undefined || text === null ? '' : `${text}`; export interface RowOptionsRaw { - color: string; + color?: string; + separator?: boolean; } export interface RowOptions { color: COLOR; + separator: boolean; } export const convertRawRowOptionsToStandard = ( @@ -26,6 +32,7 @@ export const convertRawRowOptionsToStandard = ( if (options) { return { color: options.color as COLOR, + separator: options.separator || DEFAULT_ROW_SEPARATOR, }; } return undefined; @@ -72,8 +79,13 @@ export const createColumFromComputedColumn = ( alignment: column.alignment || DEFAULT_ROW_ALIGNMENT, }); -export const createRow = (color: COLOR, text: Dictionary): Row => ({ +export const createRow = ( + color: COLOR, + text: Dictionary, + separator?: boolean +): Row => ({ color, + separator, text, }); @@ -116,7 +128,7 @@ export const renderTableHorizontalBorders = ( export const createHeaderAsRow = (createRowFn: any, columns: Column[]): Row => { const headerColor: COLOR = 'white_bold'; - const row: Row = createRowFn(headerColor, {}); + const row: Row = createRowFn(headerColor, {}, false); columns.forEach((column) => { row.text[column.name] = column.title; }); diff --git a/test/internalTable/borderStyle.test.ts b/test/internalTable/borderStyle.test.ts index 177430b1..502997b1 100644 --- a/test/internalTable/borderStyle.test.ts +++ b/test/internalTable/borderStyle.test.ts @@ -33,6 +33,12 @@ describe('Example: Check if borders are styled properly', () => { other: '═', }, vertical: '║', + rowSeparator: { + left: '╟', + mid: '╬', + right: '╢', + other: '═', + }, }, columns: [ { name: 'index', alignment: 'left' }, @@ -101,6 +107,12 @@ describe('Example: Check if borders are styled properly', () => { other: '\x1b[31m═\x1b[0m', }, vertical: '\x1b[31m║\x1b[0m', + rowSeparator: { + left: '\x1b[31m╟\x1b[0m', + mid: '\x1b[31m╬\x1b[0m', + right: '\x1b[31m╢\x1b[0m', + other: '\x1b[31m═\x1b[0m', + }, }, columns: [ { name: 'index', alignment: 'left' }, diff --git a/test/internalTable/rowSeparator.test.ts b/test/internalTable/rowSeparator.test.ts new file mode 100644 index 00000000..0faef4da --- /dev/null +++ b/test/internalTable/rowSeparator.test.ts @@ -0,0 +1,165 @@ +import { renderTable } from '../../src/internalTable/internal-table-printer'; +import { Table } from '../../index'; + +describe('Testing Row separator', () => { + it('Batch Row separator by each row', () => { + // Create a table + const p = new Table(); + + p.addRow({ index: 3, text: 'row without separator', value: 100 }); + + p.addRow( + { index: 4, text: 'row with separator', value: 300 }, + { separator: true } + ); + + p.addRow({ index: 5, text: 'row without separator', value: 100 }); + + // print + const returned = renderTable(p.table); + + const expected = [ + '┌───────┬───────────────────────┬───────┐', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[01mindex\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01m text\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01mvalue\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼───────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 3\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mrow without separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 4\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m row with separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 300\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼───────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 5\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mrow without separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '└───────┴───────────────────────┴───────┘', + ]; + expect(returned).toBe(expected.join('\n')); + }); + + it('Batch Row default separator is working', () => { + // Create a table + const p = new Table(); + + p.addRows([ + // adding multiple rows are possible + { index: 3, text: 'row default separator', value: 100 }, + { index: 4, text: 'row default separator', value: 300 }, + ]); + + // print + const returned = renderTable(p.table); + + const expected = [ + '┌───────┬───────────────────────┬───────┐', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[01mindex\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01m text\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01mvalue\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼───────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 3\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mrow default separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 4\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mrow default separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 300\u001b[0m\u001b[37m │\u001b[0m', + '└───────┴───────────────────────┴───────┘', + ]; + expect(returned).toBe(expected.join('\n')); + }); + + it('Batch Row table separator option is working', () => { + // Create a table + const p = new Table({ rowSeparator: true }); + + p.addRows([ + // adding multiple rows are possible + { index: 3, text: 'table row separator', value: 100 }, + { index: 4, text: 'table row separator', value: 300 }, + ]); + + // print + const returned = renderTable(p.table); + + const expected = [ + '┌───────┬─────────────────────┬───────┐', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[01mindex\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01m text\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01mvalue\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼─────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 3\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mtable row separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼─────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 4\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mtable row separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 300\u001b[0m\u001b[37m │\u001b[0m', + '└───────┴─────────────────────┴───────┘', + ]; + expect(returned).toBe(expected.join('\n')); + }); + + it('Batch Row table separator override is working', () => { + // Create a table + const p = new Table({ rowSeparator: true }); + + p.addRows([ + // adding multiple rows are possible + { index: 3, text: 'override table row separator', value: 100 }, + { index: 4, text: 'override table row separator', value: 300 }, + ], { separator: false }); + + // print + const returned = renderTable(p.table); + + const expected = [ + '┌───────┬──────────────────────────────┬───────┐', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[01mindex\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01m text\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01mvalue\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼──────────────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 3\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37moverride table row separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 4\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37moverride table row separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 300\u001b[0m\u001b[37m │\u001b[0m', + '└───────┴──────────────────────────────┴───────┘', + ]; + expect(returned).toBe(expected.join('\n')); + }); + + it('Batch Row separator is working', () => { + // Create a table + const p = new Table(); + + p.addRows( + [ + // adding multiple rows are possible + { index: 3, text: 'row with separator', value: 100 }, + { index: 4, text: 'row with separator', value: 300 }, + ], + { separator: true } + ); + + // print + const returned = renderTable(p.table); + + const expected = [ + '┌───────┬────────────────────┬───────┐', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[01mindex\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01m text\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01mvalue\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 3\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mrow with separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 4\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mrow with separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 300\u001b[0m\u001b[37m │\u001b[0m', + '└───────┴────────────────────┴───────┘', + ]; + expect(returned).toBe(expected.join('\n')); + }); + + it('Batch Row separator combined with sorting', () => { + // Create a table and sort by index + const p = new Table({ sort: (row1, row2) => row1.index - row2.index, rowSeparator: true }); + + //Row with index 1 will have separator because it inherits from Table options + p.addRow({ index: 1, text: 'row inherit separator', value: 100 }); + p.addRow({ index: 4, text: 'row without separator', value: 100 }, { separator: false }); + //Row with index 5 will be last row so separator will be ignored anyway + p.addRow({ index: 5, text: 'row with separator', value: 100 }, { separator: true }); + p.addRow({ index: 2, text: 'row with separator', value: 100 }, { separator: true }); + p.addRow({ index: 3, text: 'row without separator', value: 100 }, { separator: false }); + + // print + const returned = renderTable(p.table); + + const expected = [ + '┌───────┬───────────────────────┬───────┐', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[01mindex\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01m text\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[01mvalue\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼───────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 1\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mrow inherit separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼───────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 2\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m row with separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '├───────┼───────────────────────┼───────┤', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 3\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mrow without separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 4\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37mrow without separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '\u001b[37m│\u001b[0m\u001b[37m \u001b[0m\u001b[37m 5\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m row with separator\u001b[0m\u001b[37m │\u001b[0m\u001b[37m \u001b[0m\u001b[37m 100\u001b[0m\u001b[37m │\u001b[0m', + '└───────┴───────────────────────┴───────┘', + ]; + expect(returned).toBe(expected.join('\n')); + }); +});