From c7805ed9146004f0fe449476fb40b264aa394706 Mon Sep 17 00:00:00 2001 From: Naveen Mahalingam Date: Fri, 20 Aug 2021 11:15:01 -0700 Subject: [PATCH] fix issues/smells identified by sonar (#182) --- cmd/demo-table/demo.go | 64 ++++++++------ list/render.go | 38 +++++---- progress/render.go | 38 +++++---- progress/units.go | 64 +++++++------- table/render.go | 111 +++++++++--------------- table/render_html.go | 47 +++++----- table/render_markdown.go | 23 ++--- table/sort.go | 40 +++++---- table/sort_test.go | 18 +++- table/table.go | 180 ++++++++++++++++++++++++++------------- text/transformer.go | 53 ++++++------ text/wrap.go | 13 ++- 12 files changed, 391 insertions(+), 298 deletions(-) diff --git a/cmd/demo-table/demo.go b/cmd/demo-table/demo.go index 4aebea1..a79908d 100644 --- a/cmd/demo-table/demo.go +++ b/cmd/demo-table/demo.go @@ -9,15 +9,23 @@ import ( "github.com/jedib0t/go-pretty/v6/text" ) +var ( + colTitleIndex = "#" + colTitleFirstName = "First Name" + colTitleLastName = "Last Name" + colTitleSalary = "Salary" + rowHeader = table.Row{colTitleIndex, colTitleFirstName, colTitleLastName, colTitleSalary} + row1 = table.Row{1, "Arya", "Stark", 3000} + row2 = table.Row{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"} + row3 = table.Row{300, "Tyrion", "Lannister", 5000} + rowFooter = table.Row{"", "", "Total", 10000} +) + func demoTableColors() { tw := table.NewWriter() - tw.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) - tw.AppendRows([]table.Row{ - {1, "Arya", "Stark", 3000}, - {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, - {300, "Tyrion", "Lannister", 5000}, - }) - tw.AppendFooter(table.Row{"", "", "Total", 10000}) + tw.AppendHeader(rowHeader) + tw.AppendRows([]table.Row{row1, row2, row3}) + tw.AppendFooter(rowFooter) tw.SetIndexColumn(1) tw.SetTitle("Game Of Thrones") @@ -99,7 +107,7 @@ func demoTableFeatures() { //+---+-----+--------+-----------+------+-----------------------------+ //Table with Auto-Indexing. // - t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"}) + t.AppendHeader(rowHeader) t.SetCaption("Table with Auto-Indexing (columns-only).\n") fmt.Println(t.Render()) //+---+-----+------------+-----------+--------+-----------------------------+ @@ -151,7 +159,7 @@ func demoTableFeatures() { // go right and everything else left. but what if you want the first name to // go right too? and the last column to be "justified"? t.SetColumnConfigs([]table.ColumnConfig{ - {Name: "First Name", Align: text.AlignRight}, + {Name: colTitleFirstName, Align: text.AlignRight}, // the 5th column does not have a title, so use the column number as the // identifier for the column {Number: 5, Align: text.AlignJustify}, @@ -202,9 +210,9 @@ func demoTableFeatures() { // // time to Align/VAlign the columns... t.SetColumnConfigs([]table.ColumnConfig{ - {Name: "First Name", Align: text.AlignRight, VAlign: text.VAlignMiddle}, - {Name: "Last Name", VAlign: text.VAlignBottom}, - {Name: "Salary", Align: text.AlignRight, VAlign: text.VAlignMiddle}, + {Name: colTitleFirstName, Align: text.AlignRight, VAlign: text.VAlignMiddle}, + {Name: colTitleLastName, VAlign: text.VAlignBottom}, + {Name: colTitleSalary, Align: text.AlignRight, VAlign: text.VAlignMiddle}, // the 5th column does not have a title, so use the column number {Number: 5, Align: text.AlignJustify}, }) @@ -229,9 +237,9 @@ func demoTableFeatures() { // // changed your mind about AlignJustify? t.SetColumnConfigs([]table.ColumnConfig{ - {Name: "First Name", Align: text.AlignRight, VAlign: text.VAlignMiddle}, - {Name: "Last Name", VAlign: text.VAlignBottom}, - {Name: "Salary", Align: text.AlignRight, VAlign: text.VAlignMiddle}, + {Name: colTitleFirstName, Align: text.AlignRight, VAlign: text.VAlignMiddle}, + {Name: colTitleLastName, VAlign: text.VAlignBottom}, + {Name: colTitleSalary, Align: text.AlignRight, VAlign: text.VAlignMiddle}, {Number: 5, Align: text.AlignCenter}, }) t.SetCaption("Table with a Multi-line Row with VAlign and changed Align.\n") @@ -259,10 +267,10 @@ func demoTableFeatures() { // custom separators? //========================================================================== t.ResetRows() - t.AppendRow(table.Row{1, "Arya", "Stark", 3000}) - t.AppendRow(table.Row{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}) + t.AppendRow(row1) + t.AppendRow(row2) t.AppendSeparator() - t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) + t.AppendRow(row3) t.SetCaption("Simple Table with 3 Rows and a Separator in-between.\n") fmt.Println(t.Render()) //+-----+--------+-----------+------+-----------------------------+ @@ -279,9 +287,9 @@ func demoTableFeatures() { //========================================================================== t.ResetRows() t.SetColumnConfigs(nil) - t.AppendRow(table.Row{1, "Arya", "Stark", 3000}) - t.AppendRow(table.Row{20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}) - t.AppendRow([]interface{}{300, "Tyrion", "Lannister", 5000}) + t.AppendRow(row1) + t.AppendRow(row2) + t.AppendRow(row3) t.SetCaption("Starting afresh with a Simple Table again.\n") fmt.Println(t.Render()) //+-----+------------+-----------+--------+-----------------------------+ @@ -364,9 +372,9 @@ func demoTableFeatures() { // But I want to see all the data! //========================================================================== t.SetColumnConfigs([]table.ColumnConfig{ - {Name: "First Name", WidthMax: 6}, - {Name: "Last Name", WidthMax: 9}, - {Name: "Salary", WidthMax: 6}, + {Name: colTitleFirstName, WidthMax: 6}, + {Name: colTitleLastName, WidthMax: 9}, + {Name: colTitleSalary, WidthMax: 6}, {Number: 5, WidthMax: 10}, }) t.SetCaption("Table on a diet.\n") @@ -475,10 +483,10 @@ func demoTableFeatures() { colorBOnW := text.Colors{text.BgWhite, text.FgBlack} // set colors using Colors/ColorsHeader/ColorsFooter t.SetColumnConfigs([]table.ColumnConfig{ - {Name: "#", Colors: text.Colors{text.FgYellow}, ColorsHeader: colorBOnW}, - {Name: "First Name", Colors: text.Colors{text.FgHiRed}, ColorsHeader: colorBOnW}, - {Name: "Last Name", Colors: text.Colors{text.FgHiRed}, ColorsHeader: colorBOnW, ColorsFooter: colorBOnW}, - {Name: "Salary", Colors: text.Colors{text.FgGreen}, ColorsHeader: colorBOnW, ColorsFooter: colorBOnW}, + {Name: colTitleIndex, Colors: text.Colors{text.FgYellow}, ColorsHeader: colorBOnW}, + {Name: colTitleFirstName, Colors: text.Colors{text.FgHiRed}, ColorsHeader: colorBOnW}, + {Name: colTitleLastName, Colors: text.Colors{text.FgHiRed}, ColorsHeader: colorBOnW, ColorsFooter: colorBOnW}, + {Name: colTitleSalary, Colors: text.Colors{text.FgGreen}, ColorsHeader: colorBOnW, ColorsFooter: colorBOnW}, {Number: 5, Colors: text.Colors{text.FgCyan}, ColorsHeader: colorBOnW}, }) t.SetCaption("Table with Colors.\n") diff --git a/list/render.go b/list/render.go index 54d4388..5e9bce7 100644 --- a/list/render.go +++ b/list/render.go @@ -57,14 +57,14 @@ func (l *List) renderItem(out *strings.Builder, idx int, item *listItem, hint re // render the prefix or the leading text before the actual item l.renderItemBulletPrefix(out, idx, item.Level, lineIdx, hint) - l.renderItemBullet(out, idx, item.Level, lineIdx, hint) + l.renderItemBullet(out, lineIdx, hint) // render the actual item out.WriteString(lineStr) } } -func (l *List) renderItemBullet(out *strings.Builder, itemIdx int, itemLevel int, lineIdx int, hint renderHint) { +func (l *List) renderItemBullet(out *strings.Builder, lineIdx int, hint renderHint) { if lineIdx > 0 { // multi-line item.Text if hint.isLastItem { @@ -73,24 +73,28 @@ func (l *List) renderItemBullet(out *strings.Builder, itemIdx int, itemLevel int out.WriteString(l.style.CharItemVertical) } } else { - // single-line item.Text (or first line of a multi-line item.Text) - if hint.isOnlyItem { - if hint.isTopItem { - out.WriteString(l.style.CharItemSingle) - } else { - out.WriteString(l.style.CharItemBottom) - } - } else if hint.isTopItem { - out.WriteString(l.style.CharItemTop) - } else if hint.isFirstItem { - out.WriteString(l.style.CharItemFirst) - } else if hint.isBottomItem || hint.isLastItem { - out.WriteString(l.style.CharItemBottom) + l.renderItemBulletSingleLine(out, lineIdx, hint) + } +} + +func (l *List) renderItemBulletSingleLine(out *strings.Builder, lineIdx int, hint renderHint) { + // single-line item.Text (or first line of a multi-line item.Text) + if hint.isOnlyItem { + if hint.isTopItem { + out.WriteString(l.style.CharItemSingle) } else { - out.WriteString(l.style.CharItemMiddle) + out.WriteString(l.style.CharItemBottom) } - out.WriteRune(' ') + } else if hint.isTopItem { + out.WriteString(l.style.CharItemTop) + } else if hint.isFirstItem { + out.WriteString(l.style.CharItemFirst) + } else if hint.isBottomItem || hint.isLastItem { + out.WriteString(l.style.CharItemBottom) + } else { + out.WriteString(l.style.CharItemMiddle) } + out.WriteRune(' ') } func (l *List) renderItemBulletPrefix(out *strings.Builder, itemIdx int, itemLevel int, lineIdx int, hint renderHint) { diff --git a/progress/render.go b/progress/render.go index 80d6f2c..a778622 100644 --- a/progress/render.go +++ b/progress/render.go @@ -318,23 +318,7 @@ func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint ren outStats.WriteString(" in ") } if !hint.hideTime { - var td, tp time.Duration - if t.IsDone() { - td = t.timeStop.Sub(t.timeStart) - } else { - td = time.Since(t.timeStart) - } - if hint.isOverallTracker { - tp = p.style.Options.TimeOverallPrecision - } else if t.IsDone() { - tp = p.style.Options.TimeDonePrecision - } else { - tp = p.style.Options.TimeInProgressPrecision - } - outStats.WriteString(p.style.Colors.Time.Sprint(td.Round(tp))) - if p.showETA || hint.isOverallTracker { - p.renderTrackerStatsETA(&outStats, t, hint) - } + p.renderTrackerStatsTime(&outStats, t, hint) } outStats.WriteRune(']') @@ -342,6 +326,26 @@ func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint ren } } +func (p *Progress) renderTrackerStatsTime(outStats *strings.Builder, t *Tracker, hint renderHint) { + var td, tp time.Duration + if t.IsDone() { + td = t.timeStop.Sub(t.timeStart) + } else { + td = time.Since(t.timeStart) + } + if hint.isOverallTracker { + tp = p.style.Options.TimeOverallPrecision + } else if t.IsDone() { + tp = p.style.Options.TimeDonePrecision + } else { + tp = p.style.Options.TimeInProgressPrecision + } + outStats.WriteString(p.style.Colors.Time.Sprint(td.Round(tp))) + if p.showETA || hint.isOverallTracker { + p.renderTrackerStatsETA(outStats, t, hint) + } +} + func (p *Progress) renderTrackerStatsETA(out *strings.Builder, t *Tracker, hint renderHint) { tpETA := p.style.Options.ETAPrecision if eta := t.ETA().Round(tpETA); hint.isOverallTracker || eta > tpETA { diff --git a/progress/units.go b/progress/units.go index 96c93dd..5839fac 100644 --- a/progress/units.go +++ b/progress/units.go @@ -4,7 +4,7 @@ import ( "fmt" ) -// UnitsNotationPosition determines units position relative of tracker value. +// UnitsNotationPosition determines notation position relative to unit value. type UnitsNotationPosition int // Supported unit positions relative to tracker value; @@ -16,68 +16,72 @@ const ( // Units defines the "type" of the value being tracked by the Tracker. type Units struct { + Formatter func(value int64) string // default: FormatNumber Notation string - NotationPosition UnitsNotationPosition - Formatter func(value int64) string + NotationPosition UnitsNotationPosition // default: UnitsNotationPositionBefore +} + +// Sprint prints the value as defined by the Units. +func (tu Units) Sprint(value int64) string { + formatter := tu.Formatter + if formatter == nil { + formatter = FormatNumber + } + + formattedValue := formatter(value) + switch tu.NotationPosition { + case UnitsNotationPositionAfter: + return formattedValue + tu.Notation + default: // UnitsNotationPositionBefore + return tu.Notation + formattedValue + } } var ( // UnitsDefault doesn't define any units. The value will be treated as any // other number. UnitsDefault = Units{ - Notation: "", - Formatter: FormatNumber, + Notation: "", + NotationPosition: UnitsNotationPositionBefore, + Formatter: FormatNumber, } // UnitsBytes defines the value as a storage unit. Values will be converted // and printed in one of these forms: B, KB, MB, GB, TB, PB UnitsBytes = Units{ - Notation: "", - Formatter: FormatBytes, + Notation: "", + NotationPosition: UnitsNotationPositionBefore, + Formatter: FormatBytes, } // UnitsCurrencyDollar defines the value as a Dollar amount. Values will be // converted and printed in one of these forms: $x.yz, $x.yzK, $x.yzM, // $x.yzB, $x.yzT UnitsCurrencyDollar = Units{ - Notation: "$", - Formatter: FormatNumber, + Notation: "$", + NotationPosition: UnitsNotationPositionBefore, + Formatter: FormatNumber, } // UnitsCurrencyEuro defines the value as a Euro amount. Values will be // converted and printed in one of these forms: ₠x.yz, ₠x.yzK, ₠x.yzM, // ₠x.yzB, ₠x.yzT UnitsCurrencyEuro = Units{ - Notation: "₠", - Formatter: FormatNumber, + Notation: "₠", + NotationPosition: UnitsNotationPositionBefore, + Formatter: FormatNumber, } // UnitsCurrencyPound defines the value as a Pound amount. Values will be // converted and printed in one of these forms: £x.yz, £x.yzK, £x.yzM, // £x.yzB, £x.yzT UnitsCurrencyPound = Units{ - Notation: "£", - Formatter: FormatNumber, + Notation: "£", + NotationPosition: UnitsNotationPositionBefore, + Formatter: FormatNumber, } ) -// Sprint prints the value as defined by the Units. -func (tu Units) Sprint(value int64) string { - formatter := tu.Formatter - if formatter == nil { - formatter = FormatNumber - } - - formattedValue := formatter(value) - - switch tu.NotationPosition { - case UnitsNotationPositionAfter: - return formattedValue + tu.Notation - default: // UnitsNotationPositionBefore - return tu.Notation + formattedValue - } -} - // FormatBytes formats the given value as a "Byte". func FormatBytes(value int64) string { if value < 1000 { diff --git a/table/render.go b/table/render.go index 2fa1772..0537115 100644 --- a/table/render.go +++ b/table/render.go @@ -180,7 +180,7 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) { out.WriteRune('\n') } - // use a brand new strings.Builder if a row length limit has been set + // use a brand-new strings.Builder if a row length limit has been set var outLine *strings.Builder if t.allowedRowLength > 0 { outLine = &strings.Builder{} @@ -202,16 +202,7 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) { // merge the strings.Builder objects if a new one was created earlier if outLine != out { - outLineStr := outLine.String() - if text.RuneCount(outLineStr) > t.allowedRowLength { - trimLength := t.allowedRowLength - utf8.RuneCountInString(t.style.Box.UnfinishedRow) - if trimLength > 0 { - out.WriteString(text.Trim(outLineStr, trimLength)) - out.WriteString(t.style.Box.UnfinishedRow) - } - } else { - out.WriteString(outLineStr) - } + t.renderLineMergeOutputs(out, outLine) } // if a page size has been set, and said number of lines has already @@ -229,27 +220,22 @@ func (t *Table) renderLine(out *strings.Builder, row rowStr, hint renderHint) { } } -func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) { - if t.style.Options.DrawBorder { - border := t.style.Box.Left - if hint.isBorderTop { - if t.title != "" { - border = t.style.Box.LeftSeparator - } else { - border = t.style.Box.TopLeft - } - } else if hint.isBorderBottom { - border = t.style.Box.BottomLeft - } else if hint.isSeparatorRow { - if t.autoIndex && hint.isHeaderOrFooterSeparator() { - border = t.style.Box.Left - } else if !t.autoIndex && t.shouldMergeCellsVertically(0, hint) { - border = t.style.Box.Left - } else { - border = t.style.Box.LeftSeparator - } +func (t *Table) renderLineMergeOutputs(out *strings.Builder, outLine *strings.Builder) { + outLineStr := outLine.String() + if text.RuneCount(outLineStr) > t.allowedRowLength { + trimLength := t.allowedRowLength - utf8.RuneCountInString(t.style.Box.UnfinishedRow) + if trimLength > 0 { + out.WriteString(text.Trim(outLineStr, trimLength)) + out.WriteString(t.style.Box.UnfinishedRow) } + } else { + out.WriteString(outLineStr) + } +} +func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) { + if t.style.Options.DrawBorder { + border := t.getBorderLeft(hint) colors := t.getBorderColors(hint) if colors.EscapeSeq() != "" { out.WriteString(colors.Sprint(border)) @@ -261,23 +247,7 @@ func (t *Table) renderMarginLeft(out *strings.Builder, hint renderHint) { func (t *Table) renderMarginRight(out *strings.Builder, hint renderHint) { if t.style.Options.DrawBorder { - border := t.style.Box.Right - if hint.isBorderTop { - if t.title != "" { - border = t.style.Box.RightSeparator - } else { - border = t.style.Box.TopRight - } - } else if hint.isBorderBottom { - border = t.style.Box.BottomRight - } else if hint.isSeparatorRow { - if t.shouldMergeCellsVertically(t.numColumns-1, hint) { - border = t.style.Box.Right - } else { - border = t.style.Box.RightSeparator - } - } - + border := t.getBorderRight(hint) colors := t.getBorderColors(hint) if colors.EscapeSeq() != "" { out.WriteString(colors.Sprint(border)) @@ -292,16 +262,7 @@ func (t *Table) renderRow(out *strings.Builder, row rowStr, hint renderHint) { // fit every column into the allowedColumnLength/maxColumnLength limit // and in the process find the max. number of lines in any column in // this row - colMaxLines := 0 - rowWrapped := make(rowStr, len(row)) - for colIdx, colStr := range row { - widthEnforcer := t.columnConfigMap[colIdx].getWidthMaxEnforcer() - rowWrapped[colIdx] = widthEnforcer(colStr, t.maxColumnLengths[colIdx]) - colNumLines := strings.Count(rowWrapped[colIdx], "\n") + 1 - if colNumLines > colMaxLines { - colMaxLines = colNumLines - } - } + colMaxLines, rowWrapped := t.wrapRow(row) // if there is just 1 line in all columns, add the row as such; else // split each column into individual lines and render them one-by-one @@ -410,22 +371,26 @@ func (t *Table) renderTitle(out *strings.Builder) { } titleText := text.WrapText(t.title, lenText) for _, titleLine := range strings.Split(titleText, "\n") { - titleLine = strings.TrimSpace(titleLine) - titleLine = t.style.Title.Format.Apply(titleLine) - titleLine = t.style.Title.Align.Apply(titleLine, lenText) - titleLine = t.style.Box.PaddingLeft + titleLine + t.style.Box.PaddingRight - titleLine = t.style.Title.Colors.Sprint(titleLine) - - if out.Len() > 0 { - out.WriteRune('\n') - } - if t.style.Options.DrawBorder { - out.WriteString(t.style.Box.Left) - } - out.WriteString(titleLine) - if t.style.Options.DrawBorder { - out.WriteString(t.style.Box.Right) - } + t.renderTitleLine(out, lenText, titleLine) } } } + +func (t *Table) renderTitleLine(out *strings.Builder, lenText int, titleLine string) { + titleLine = strings.TrimSpace(titleLine) + titleLine = t.style.Title.Format.Apply(titleLine) + titleLine = t.style.Title.Align.Apply(titleLine, lenText) + titleLine = t.style.Box.PaddingLeft + titleLine + t.style.Box.PaddingRight + titleLine = t.style.Title.Colors.Sprint(titleLine) + + if out.Len() > 0 { + out.WriteRune('\n') + } + if t.style.Options.DrawBorder { + out.WriteString(t.style.Box.Left) + } + out.WriteString(titleLine) + if t.style.Options.DrawBorder { + out.WriteString(t.style.Box.Right) + } +} diff --git a/table/render_html.go b/table/render_html.go index 3f550dd..10a7325 100644 --- a/table/render_html.go +++ b/table/render_html.go @@ -78,6 +78,22 @@ func (t *Table) RenderHTML() string { return t.render(&out) } +func (t *Table) htmlGetColStrAndTag(row rowStr, colIdx int, hint renderHint) (string, string) { + // get the column contents + var colStr string + if colIdx < len(row) { + colStr = row[colIdx] + } + + // header uses "th" instead of "td" + colTagName := "td" + if hint.isHeaderRow { + colTagName = "th" + } + + return colStr, colTagName +} + func (t *Table) htmlRenderCaption(out *strings.Builder) { if t.caption != "" { out.WriteString(" ") @@ -86,6 +102,16 @@ func (t *Table) htmlRenderCaption(out *strings.Builder) { } } +func (t *Table) htmlRenderColumn(out *strings.Builder, colStr string) { + if t.style.HTML.EscapeText { + colStr = html.EscapeString(colStr) + } + if t.style.HTML.Newline != "\n" { + colStr = strings.Replace(colStr, "\n", t.style.HTML.Newline, -1) + } + out.WriteString(colStr) +} + func (t *Table) htmlRenderColumnAttributes(out *strings.Builder, row rowStr, colIdx int, hint renderHint) { // determine the HTML "align"/"valign" property values align := t.getAlign(colIdx, hint).HTMLProperty() @@ -131,18 +157,7 @@ func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint) t.htmlRenderColumnAutoIndex(out, hint) } - // get the column contents - var colStr string - if colIdx < len(row) { - colStr = row[colIdx] - } - - // header uses "th" instead of "td" - colTagName := "td" - if hint.isHeaderRow { - colTagName = "th" - } - + colStr, colTagName := t.htmlGetColStrAndTag(row, colIdx, hint) // write the row out.WriteString(" <") out.WriteString(colTagName) @@ -151,13 +166,7 @@ func (t *Table) htmlRenderRow(out *strings.Builder, row rowStr, hint renderHint) if len(colStr) == 0 { out.WriteString(t.style.HTML.EmptyColumn) } else { - if t.style.HTML.EscapeText { - colStr = html.EscapeString(colStr) - } - if t.style.HTML.Newline != "\n" { - colStr = strings.Replace(colStr, "\n", t.style.HTML.Newline, -1) - } - out.WriteString(colStr) + t.htmlRenderColumn(out, colStr) } out.WriteString(" 0 { for idx, row := range rows { diff --git a/table/sort.go b/table/sort.go index 621bc1c..ae55f30 100644 --- a/table/sort.go +++ b/table/sort.go @@ -96,24 +96,32 @@ func (rs rowsSorter) Less(i, j int) bool { for _, col := range rs.sortBy { rowI, rowJ, colIdx := rs.rows[realI], rs.rows[realJ], col.Number-1 if colIdx < len(rowI) && colIdx < len(rowJ) { - if rowI[colIdx] == rowJ[colIdx] { - continue - } else if col.Mode == Asc { - return rowI[colIdx] < rowJ[colIdx] - } else if col.Mode == Dsc { - return rowI[colIdx] > rowJ[colIdx] - } - - iVal, iErr := strconv.ParseFloat(rowI[colIdx], 64) - jVal, jErr := strconv.ParseFloat(rowJ[colIdx], 64) - if iErr == nil && jErr == nil { - if col.Mode == AscNumeric { - return iVal < jVal - } else if col.Mode == DscNumeric { - return jVal < iVal - } + shouldContinue, returnValue := rs.lessColumns(rowI, rowJ, colIdx, col) + if !shouldContinue { + return returnValue } } } return false } + +func (rs rowsSorter) lessColumns(rowI rowStr, rowJ rowStr, colIdx int, col SortBy) (bool, bool) { + if rowI[colIdx] == rowJ[colIdx] { + return true, false + } else if col.Mode == Asc { + return false, rowI[colIdx] < rowJ[colIdx] + } else if col.Mode == Dsc { + return false, rowI[colIdx] > rowJ[colIdx] + } + + iVal, iErr := strconv.ParseFloat(rowI[colIdx], 64) + jVal, jErr := strconv.ParseFloat(rowJ[colIdx], 64) + if iErr == nil && jErr == nil { + if col.Mode == AscNumeric { + return false, iVal < jVal + } else if col.Mode == DscNumeric { + return false, jVal < iVal + } + } + return true, false +} diff --git a/table/sort_test.go b/table/sort_test.go index 6ff487f..bd8f9a4 100644 --- a/table/sort_test.go +++ b/table/sort_test.go @@ -1,8 +1,9 @@ package table import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestTable_sortRows_WithName(t *testing.T) { @@ -127,3 +128,18 @@ func TestTable_sortRows_WithoutName(t *testing.T) { table.SortBy(nil) assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) } + +func TestTable_sortRows_InvalidMode(t *testing.T) { + table := Table{} + table.AppendRows([]Row{ + {1, "Arya", "Stark", 3000}, + {11, "Sansa", "Stark", 3000}, + {20, "Jon", "Snow", 2000, "You know nothing, Jon Snow!"}, + {300, "Tyrion", "Lannister", 5000}, + }) + table.initForRenderRows() + + // sort by "First Name" + table.SortBy([]SortBy{{Number: 2, Mode: AscNumeric}}) + assert.Equal(t, []int{0, 1, 2, 3}, table.getSortedRowIndices()) +} diff --git a/table/table.go b/table/table.go index ae49725..77d4dcc 100644 --- a/table/table.go +++ b/table/table.go @@ -11,6 +11,15 @@ import ( // Row defines a single row in the Table. type Row []interface{} +func (r Row) findColumnNumber(colName string) int { + for colIdx, col := range r { + if fmt.Sprint(col) == colName { + return colIdx + 1 + } + } + return 0 +} + // RowPainter is a custom function that takes a Row as input and returns the // text.Colors{} to use on the entire row type RowPainter func(row Row) text.Colors @@ -308,26 +317,30 @@ func (t *Table) analyzeAndStringify(row Row, hint renderHint) rowStr { t.columnIsNonNumeric[colIdx] = true } - // convert to a string and store it in the row - var colStr string - if transformer := t.getColumnTransformer(colIdx, hint); transformer != nil { - colStr = transformer(col) - } else if colStrVal, ok := col.(string); ok { - colStr = colStrVal - } else { - colStr = fmt.Sprint(col) - } - if strings.Contains(colStr, "\t") { - colStr = strings.Replace(colStr, "\t", " ", -1) - } - if strings.Contains(colStr, "\r") { - colStr = strings.Replace(colStr, "\r", "", -1) - } - rowOut[colIdx] = colStr + rowOut[colIdx] = t.analyzeAndStringifyColumn(colIdx, col, hint) } return rowOut } +func (t *Table) analyzeAndStringifyColumn(colIdx int, col interface{}, hint renderHint) string { + // convert to a string and store it in the row + var colStr string + if transformer := t.getColumnTransformer(colIdx, hint); transformer != nil { + colStr = transformer(col) + } else if colStrVal, ok := col.(string); ok { + colStr = colStrVal + } else { + colStr = fmt.Sprint(col) + } + if strings.Contains(colStr, "\t") { + colStr = strings.Replace(colStr, "\t", " ", -1) + } + if strings.Contains(colStr, "\r") { + colStr = strings.Replace(colStr, "\r", "", -1) + } + return colStr +} + func (t *Table) getAlign(colIdx int, hint renderHint) text.Align { align := text.AlignDefault if cfg, ok := t.columnConfigMap[colIdx]; ok { @@ -366,6 +379,48 @@ func (t *Table) getBorderColors(hint renderHint) text.Colors { return t.style.Color.Header } +func (t *Table) getBorderLeft(hint renderHint) string { + border := t.style.Box.Left + if hint.isBorderTop { + if t.title != "" { + border = t.style.Box.LeftSeparator + } else { + border = t.style.Box.TopLeft + } + } else if hint.isBorderBottom { + border = t.style.Box.BottomLeft + } else if hint.isSeparatorRow { + if t.autoIndex && hint.isHeaderOrFooterSeparator() { + border = t.style.Box.Left + } else if !t.autoIndex && t.shouldMergeCellsVertically(0, hint) { + border = t.style.Box.Left + } else { + border = t.style.Box.LeftSeparator + } + } + return border +} + +func (t *Table) getBorderRight(hint renderHint) string { + border := t.style.Box.Right + if hint.isBorderTop { + if t.title != "" { + border = t.style.Box.RightSeparator + } else { + border = t.style.Box.TopRight + } + } else if hint.isBorderBottom { + border = t.style.Box.BottomRight + } else if hint.isSeparatorRow { + if t.shouldMergeCellsVertically(t.numColumns-1, hint) { + border = t.style.Box.Right + } else { + border = t.style.Box.RightSeparator + } + } + return border +} + func (t *Table) getColumnColors(colIdx int, hint renderHint) text.Colors { if t.rowPainter != nil && hint.isRegularRow() && !t.isIndexColumn(colIdx, hint) { colors := t.rowsColors[hint.rowNumber-1] @@ -552,6 +607,15 @@ func (t *Table) getVAlign(colIdx int, hint renderHint) text.VAlign { return vAlign } +func (t *Table) hasHiddenColumns() bool { + for _, cc := range t.columnConfigMap { + if cc.Hidden { + return true + } + } + return false +} + func (t *Table) initForRender() { // pick a default style if none was set until now t.Style() @@ -573,22 +637,13 @@ func (t *Table) initForRender() { } func (t *Table) initForRenderColumnConfigs() { - findColumnNumber := func(row Row, colName string) int { - for colIdx, col := range row { - if fmt.Sprint(col) == colName { - return colIdx + 1 - } - } - return 0 - } - t.columnConfigMap = map[int]ColumnConfig{} for _, colCfg := range t.columnConfigs { // find the column number if none provided; this logic can work only if // a header row is present and has a column with the given name if colCfg.Number == 0 { for _, row := range t.rowsHeaderRaw { - colCfg.Number = findColumnNumber(row, colCfg.Name) + colCfg.Number = row.findColumnNumber(colCfg.Name) if colCfg.Number > 0 { break } @@ -601,21 +656,10 @@ func (t *Table) initForRenderColumnConfigs() { } func (t *Table) initForRenderColumnLengths() { - var findMaxColumnLengths = func(rows []rowStr) { - for _, row := range rows { - for colIdx, colStr := range row { - longestLineLen := text.LongestLineLen(colStr) - if longestLineLen > t.maxColumnLengths[colIdx] { - t.maxColumnLengths[colIdx] = longestLineLen - } - } - } - } - t.maxColumnLengths = make([]int, t.numColumns) - findMaxColumnLengths(t.rowsHeader) - findMaxColumnLengths(t.rows) - findMaxColumnLengths(t.rowsFooter) + t.parseRowForMaxColumnLengths(t.rowsHeader) + t.parseRowForMaxColumnLengths(t.rows) + t.parseRowForMaxColumnLengths(t.rowsFooter) // restrict the column lengths if any are over or under the limits for colIdx := range t.maxColumnLengths { @@ -630,22 +674,10 @@ func (t *Table) initForRenderColumnLengths() { } } -func (t *Table) initForRenderHideColumns() { - // if there is nothing to hide, return fast - hasHiddenColumns := false - for _, cc := range t.columnConfigMap { - if cc.Hidden { - hasHiddenColumns = true - break - } - } - if !hasHiddenColumns { - return - } - +func (t *Table) hideColumns() map[int]int { colIdxMap := make(map[int]int) numColumns := 0 - _hideColumns := func(rows []rowStr) []rowStr { + hideColumnsInRows := func(rows []rowStr) []rowStr { var rsp []rowStr for _, row := range rows { var rowNew rowStr @@ -665,13 +697,22 @@ func (t *Table) initForRenderHideColumns() { } // hide columns as directed - t.rows = _hideColumns(t.rows) - t.rowsFooter = _hideColumns(t.rowsFooter) - t.rowsHeader = _hideColumns(t.rowsHeader) + t.rows = hideColumnsInRows(t.rows) + t.rowsFooter = hideColumnsInRows(t.rowsFooter) + t.rowsHeader = hideColumnsInRows(t.rowsHeader) // reset numColumns to the new number of columns t.numColumns = numColumns + return colIdxMap +} + +func (t *Table) initForRenderHideColumns() { + if !t.hasHiddenColumns() { + return + } + colIdxMap := t.hideColumns() + // re-create columnIsNonNumeric with new column indices columnIsNonNumeric := make([]bool, t.numColumns) for oldColIdx, nonNumeric := range t.columnIsNonNumeric { @@ -798,6 +839,17 @@ func (t *Table) isIndexColumn(colIdx int, hint renderHint) bool { return t.indexColumn == colIdx+1 || hint.isAutoIndexColumn } +func (t *Table) parseRowForMaxColumnLengths(rows []rowStr) { + for _, row := range rows { + for colIdx, colStr := range row { + longestLineLen := text.LongestLineLen(colStr) + if longestLineLen > t.maxColumnLengths[colIdx] { + t.maxColumnLengths[colIdx] = longestLineLen + } + } + } +} + func (t *Table) render(out *strings.Builder) string { outStr := out.String() if t.outputMirror != nil && len(outStr) > 0 { @@ -893,6 +945,20 @@ func (t *Table) shouldMergeCellsVertically(colIdx int, hint renderHint) bool { return false } +func (t *Table) wrapRow(row rowStr) (int, rowStr) { + colMaxLines := 0 + rowWrapped := make(rowStr, len(row)) + for colIdx, colStr := range row { + widthEnforcer := t.columnConfigMap[colIdx].getWidthMaxEnforcer() + rowWrapped[colIdx] = widthEnforcer(colStr, t.maxColumnLengths[colIdx]) + colNumLines := strings.Count(rowWrapped[colIdx], "\n") + 1 + if colNumLines > colMaxLines { + colMaxLines = colNumLines + } + } + return colMaxLines, rowWrapped +} + // renderHint has hints for the Render*() logic type renderHint struct { isAutoIndexColumn bool // auto-index column? diff --git a/text/transformer.go b/text/transformer.go index d01c0eb..6538668 100644 --- a/text/transformer.go +++ b/text/transformer.go @@ -133,26 +133,15 @@ func NewJSONTransformer(prefix string, indent string) Transformer { // location (use time.Local to get localized timestamps). func NewTimeTransformer(layout string, location *time.Location) Transformer { return func(val interface{}) string { - formatTime := func(t time.Time) string { - rsp := "" - if t.Unix() > 0 { - if location != nil { - t = t.In(location) - } - rsp = t.Format(layout) - } - return rsp - } - rsp := fmt.Sprint(val) if valTime, ok := val.(time.Time); ok { - rsp = formatTime(valTime) + rsp = formatTime(valTime, layout, location) } else { // cycle through some supported layouts to see if the string form // of the object matches any of these layouts for _, possibleTimeLayout := range possibleTimeLayouts { if valTime, err := time.Parse(possibleTimeLayout, rsp); err == nil { - rsp = formatTime(valTime) + rsp = formatTime(valTime, layout, location) break } } @@ -168,24 +157,14 @@ func NewTimeTransformer(layout string, location *time.Location) Transformer { // If a non-nil location value is provided, the time will be localized to that // location (use time.Local to get localized timestamps). func NewUnixTimeTransformer(layout string, location *time.Location) Transformer { - timeTransformer := NewTimeTransformer(layout, location) - formatUnixTime := func(unixTime int64) string { - if unixTime >= unixTimeMinNanoSeconds { - unixTime = unixTime / time.Second.Nanoseconds() - } else if unixTime >= unixTimeMinMicroseconds { - unixTime = unixTime / (time.Second.Nanoseconds() / 1000) - } else if unixTime >= unixTimeMinMilliseconds { - unixTime = unixTime / (time.Second.Nanoseconds() / 1000000) - } - return timeTransformer(time.Unix(unixTime, 0)) - } + transformer := NewTimeTransformer(layout, location) return func(val interface{}) string { if unixTime, ok := val.(int64); ok { - return formatUnixTime(unixTime) + return formatTimeUnix(unixTime, transformer) } else if unixTimeStr, ok := val.(string); ok { if unixTime, err := strconv.ParseInt(unixTimeStr, 10, 64); err == nil { - return formatUnixTime(unixTime) + return formatTimeUnix(unixTime, transformer) } } return fmt.Sprint(val) @@ -199,3 +178,25 @@ func NewURLTransformer() Transformer { return colorsURL.Sprint(val) } } + +func formatTime(t time.Time, layout string, location *time.Location) string { + rsp := "" + if t.Unix() > 0 { + if location != nil { + t = t.In(location) + } + rsp = t.Format(layout) + } + return rsp +} + +func formatTimeUnix(unixTime int64, timeTransformer Transformer) string { + if unixTime >= unixTimeMinNanoSeconds { + unixTime = unixTime / time.Second.Nanoseconds() + } else if unixTime >= unixTimeMinMicroseconds { + unixTime = unixTime / (time.Second.Nanoseconds() / 1000) + } else if unixTime >= unixTimeMinMilliseconds { + unixTime = unixTime / (time.Second.Nanoseconds() / 1000000) + } + return timeTransformer(time.Unix(unixTime, 0)) +} diff --git a/text/wrap.go b/text/wrap.go index 1cf61e2..7617bb1 100644 --- a/text/wrap.go +++ b/text/wrap.go @@ -225,11 +225,8 @@ func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) { if escSeq != "" { lastSeenEscSeq = escSeq } - spacing, spacingLen := "", 0 - if lineLen > 0 { - spacing, spacingLen = " ", 1 - } + spacing, spacingLen := wrapSoftSpacing(lineLen) wordLen := RuneCount(word) if lineLen+spacingLen+wordLen <= wrapLen { // word fits within the line out.WriteString(spacing) @@ -254,3 +251,11 @@ func wrapSoft(paragraph string, wrapLen int, out *strings.Builder) { } terminateOutput(lastSeenEscSeq, out) } + +func wrapSoftSpacing(lineLen int) (string, int) { + spacing, spacingLen := "", 0 + if lineLen > 0 { + spacing, spacingLen = " ", 1 + } + return spacing, spacingLen +}