From 54f0831e44c79c8555f1ef239d86611f386ad964 Mon Sep 17 00:00:00 2001 From: Naveen Mahalingam Date: Wed, 13 Mar 2024 22:01:28 -0700 Subject: [PATCH] text: AlignAuto to align numbers Right and rest Left --- table/render_test.go | 28 ++++++++++++++++++ text/align.go | 67 +++++++++++++++++++++++++++----------------- text/align_test.go | 15 ++++++++++ 3 files changed, 85 insertions(+), 25 deletions(-) diff --git a/table/render_test.go b/table/render_test.go index d5969eb..4dc2e26 100644 --- a/table/render_test.go +++ b/table/render_test.go @@ -111,6 +111,34 @@ func TestTable_Render(t *testing.T) { A Song of Ice and Fire`) } +func TestTable_Render_Align(t *testing.T) { + tw := NewWriter() + tw.AppendHeader(testHeader) + tw.AppendRows(testRows) + tw.AppendRow(Row{500, "Jamie", "Lannister", "Kingslayer", "The things I do for love."}) + tw.AppendRow(Row{1000, "Tywin", "Lannister", nil}) + tw.AppendFooter(testFooter) + tw.SetColumnConfigs([]ColumnConfig{ + {Name: "First Name", Align: text.AlignLeft, AlignHeader: text.AlignLeft, AlignFooter: text.AlignLeft}, + {Name: "Last Name", Align: text.AlignRight, AlignHeader: text.AlignRight, AlignFooter: text.AlignRight}, + {Name: "Salary", Align: text.AlignAuto, AlignHeader: text.AlignRight, AlignFooter: text.AlignAuto}, + {Number: 5, Align: text.AlignJustify, AlignHeader: text.AlignJustify, AlignFooter: text.AlignJustify}, + }) + + compareOutput(t, tw.Render(), ` ++------+------------+-----------+------------+-----------------------------+ +| # | FIRST NAME | LAST NAME | SALARY | | ++------+------------+-----------+------------+-----------------------------+ +| 1 | Arya | Stark | 3000 | | +| 20 | Jon | Snow | 2000 | You know nothing, Jon Snow! | +| 300 | Tyrion | Lannister | 5000 | | +| 500 | Jamie | Lannister | Kingslayer | The things I do for love. | +| 1000 | Tywin | Lannister | | | ++------+------------+-----------+------------+-----------------------------+ +| | | TOTAL | 10000 | | ++------+------------+-----------+------------+-----------------------------+`) +} + func TestTable_Render_AutoIndex(t *testing.T) { tw := NewWriter() for rowIdx := 0; rowIdx < 10; rowIdx++ { diff --git a/text/align.go b/text/align.go index 7a6d92c..1cce7ec 100644 --- a/text/align.go +++ b/text/align.go @@ -2,6 +2,7 @@ package text import ( "fmt" + "regexp" "strconv" "strings" "unicode/utf8" @@ -17,6 +18,12 @@ const ( AlignCenter // " center " AlignJustify // "justify it" AlignRight // " right" + AlignAuto // AlignRight for numbers, AlignLeft for the rest +) + +var ( + // reNumericText - Regular Expression to match numbers. + reNumericText = regexp.MustCompile(`^\s*[+\-]?\d*[.]?\d+\s*$`) ) // Apply aligns the text as directed. For ex.: @@ -25,14 +32,24 @@ const ( // - AlignCenter.Apply("Jon Snow", 12) returns " Jon Snow " // - AlignJustify.Apply("Jon Snow", 12) returns "Jon Snow" // - AlignRight.Apply("Jon Snow", 12) returns " Jon Snow" +// - AlignAuto.Apply("Jon Snow", 12) returns "Jon Snow " func (a Align) Apply(text string, maxLength int) string { - text = a.trimString(text) + aComputed := a + if aComputed == AlignAuto { + if reNumericText.MatchString(text) { + aComputed = AlignRight + } else { + aComputed = AlignLeft + } + } + + text = aComputed.trimString(text) sLen := utf8.RuneCountInString(text) sLenWoE := RuneWidthWithoutEscSequences(text) numEscChars := sLen - sLenWoE // now, align the text - switch a { + switch aComputed { case AlignDefault, AlignLeft: return fmt.Sprintf("%-"+strconv.Itoa(maxLength+numEscChars)+"s", text) case AlignCenter: @@ -42,7 +59,7 @@ func (a Align) Apply(text string, maxLength int) string { text+strings.Repeat(" ", (maxLength-sLenWoE)/2)) } case AlignJustify: - return a.justifyText(text, sLenWoE, maxLength) + return justifyText(text, sLenWoE, maxLength) } return fmt.Sprintf("%"+strconv.Itoa(maxLength+numEscChars)+"s", text) } @@ -77,16 +94,34 @@ func (a Align) MarkdownProperty() string { } } -func (a Align) justifyText(text string, textLength int, maxLength int) string { +func (a Align) trimString(text string) string { + switch a { + case AlignDefault, AlignLeft: + if strings.HasSuffix(text, " ") { + return strings.TrimRight(text, " ") + } + case AlignRight: + if strings.HasPrefix(text, " ") { + return strings.TrimLeft(text, " ") + } + default: + if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") { + return strings.Trim(text, " ") + } + } + return text +} + +func justifyText(text string, textLength int, maxLength int) string { // split the text into individual words - wordsUnfiltered := strings.Split(text, " ") - words := Filter(wordsUnfiltered, func(item string) bool { + words := Filter(strings.Split(text, " "), func(item string) bool { return item != "" }) - // empty string implies spaces for maxLength + // empty string implies result is just spaces for maxLength if len(words) == 0 { return strings.Repeat(" ", maxLength) } + // get the number of spaces to insert into the text numSpacesNeeded := maxLength - textLength + strings.Count(text, " ") numSpacesNeededBetweenWords := 0 @@ -117,21 +152,3 @@ func (a Align) justifyText(text string, textLength int, maxLength int) string { } return outText.String() } - -func (a Align) trimString(text string) string { - switch a { - case AlignDefault, AlignLeft: - if strings.HasSuffix(text, " ") { - return strings.TrimRight(text, " ") - } - case AlignRight: - if strings.HasPrefix(text, " ") { - return strings.TrimLeft(text, " ") - } - default: - if strings.HasPrefix(text, " ") || strings.HasSuffix(text, " ") { - return strings.Trim(text, " ") - } - } - return text -} diff --git a/text/align_test.go b/text/align_test.go index b3250d1..a148845 100644 --- a/text/align_test.go +++ b/text/align_test.go @@ -13,12 +13,16 @@ func ExampleAlign_Apply() { fmt.Printf("AlignCenter : '%s'\n", AlignCenter.Apply("Jon Snow", 12)) fmt.Printf("AlignJustify: '%s'\n", AlignJustify.Apply("Jon Snow", 12)) fmt.Printf("AlignRight : '%s'\n", AlignRight.Apply("Jon Snow", 12)) + fmt.Printf("AlignAuto : '%s'\n", AlignAuto.Apply("Jon Snow", 12)) + fmt.Printf("AlignAuto : '%s'\n", AlignAuto.Apply("-5.43", 12)) // Output: AlignDefault: 'Jon Snow ' // AlignLeft : 'Jon Snow ' // AlignCenter : ' Jon Snow ' // AlignJustify: 'Jon Snow' // AlignRight : ' Jon Snow' + // AlignAuto : 'Jon Snow ' + // AlignAuto : ' -5.43' } func TestAlign_Apply(t *testing.T) { @@ -50,6 +54,17 @@ func TestAlign_Apply(t *testing.T) { assert.Equal(t, " Jon Snow ", AlignRight.Apply("Jon Snow ", 12)) assert.Equal(t, " Jon Snow ", AlignRight.Apply(" Jon Snow ", 12)) assert.Equal(t, " ", AlignRight.Apply("", 12)) + + // Align Auto + assert.Equal(t, "Jon Snow ", AlignAuto.Apply("Jon Snow", 12)) + assert.Equal(t, "Jon Snow ", AlignAuto.Apply("Jon Snow ", 12)) + assert.Equal(t, " Jon Snow ", AlignAuto.Apply(" Jon Snow ", 12)) + assert.Equal(t, " ", AlignAuto.Apply("", 12)) + assert.Equal(t, " 13", AlignAuto.Apply("13", 12)) + assert.Equal(t, " -5.43", AlignAuto.Apply("-5.43", 12)) + assert.Equal(t, " +.43", AlignAuto.Apply("+.43", 12)) + assert.Equal(t, " +5.43", AlignAuto.Apply("+5.43", 12)) + assert.Equal(t, "+5.43x ", AlignAuto.Apply("+5.43x", 12)) } func TestAlign_Apply_ColoredText(t *testing.T) {