Skip to content

Commit afe12b2

Browse files
authored
Merge pull request #295 from olekukonko/counter
Counter Implementation
2 parents e520aed + fe53e02 commit afe12b2

File tree

6 files changed

+245
-20
lines changed

6 files changed

+245
-20
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ go get github.com/olekukonko/[email protected]
2828
#### Latest Version
2929
The latest stable version
3030
```bash
31-
go get github.com/olekukonko/tablewriter@v1.0.9
31+
go get github.com/olekukonko/tablewriter@v1.1.0
3232
```
3333

3434
**Warning:** Version `v1.0.0` contains missing functionality and should not be used.
@@ -62,7 +62,7 @@ func main() {
6262
data := [][]string{
6363
{"Package", "Version", "Status"},
6464
{"tablewriter", "v0.0.5", "legacy"},
65-
{"tablewriter", "v1.0.9", "latest"},
65+
{"tablewriter", "v1.1.0", "latest"},
6666
}
6767

6868
table := tablewriter.NewWriter(os.Stdout)
@@ -77,7 +77,7 @@ func main() {
7777
│ PACKAGE │ VERSION │ STATUS │
7878
├─────────────┼─────────┼────────┤
7979
│ tablewriter │ v0.0.5 │ legacy │
80-
│ tablewriter │ v1.0.9 │ latest │
80+
│ tablewriter │ v1.1.0 │ latest │
8181
└─────────────┴─────────┴────────┘
8282
```
8383

@@ -1081,6 +1081,8 @@ func (t Time) Format() string {
10811081

10821082
- `AutoFormat` changes See [#261](https://github.com/olekukonko/tablewriter/issues/261)
10831083

1084+
## What is new
1085+
- `Counting` changes See [#294](https://github.com/olekukonko/tablewriter/issues/294)
10841086

10851087
## Command-Line Tool
10861088

config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type Config struct {
1414
Stream tw.StreamConfig
1515
Behavior tw.Behavior
1616
Widths tw.CellWidth
17+
Counter tw.Counter
1718
}
1819

1920
// ConfigBuilder provides a fluent interface for building Config

option.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,31 @@ func WithSymbols(symbols tw.Symbols) Option {
671671
}
672672
}
673673

674+
// WithCounters enables line counting by wrapping the table's writer.
675+
// If a custom counter (that implements tw.Counter) is provided, it will be used.
676+
// If the provided counter is nil, a default tw.LineCounter will be used.
677+
// The final count can be retrieved via the table.Lines() method after Render() is called.
678+
func WithCounters(counters ...tw.Counter) Option {
679+
return func(target *Table) {
680+
// Iterate through the provided counters and add any non-nil ones.
681+
for _, c := range counters {
682+
if c != nil {
683+
target.counters = append(target.counters, c)
684+
}
685+
}
686+
}
687+
}
688+
689+
// WithLineCounter enables the default line counter.
690+
// A new instance of tw.LineCounter is added to the table's list of counters.
691+
// The total count can be retrieved via the table.Lines() method after Render() is called.
692+
func WithLineCounter() Option {
693+
return func(target *Table) {
694+
// Important: Create a new instance so tables don't share counters.
695+
target.counters = append(target.counters, &tw.LineCounter{})
696+
}
697+
}
698+
674699
// defaultConfig returns a default Config with sensible settings for headers, rows, footers, and behavior.
675700
func defaultConfig() Config {
676701
return Config{

tablewriter.go

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
// Table represents a table instance with content and rendering capabilities.
2323
type Table struct {
2424
writer io.Writer // Destination for table output
25+
counters []tw.Counter // Counters for indices
2526
rows [][][]string // Row data, supporting multi-line cells
2627
headers [][]string // Header content
2728
footers [][]string // Footer content
@@ -510,6 +511,28 @@ func (t *Table) Render() error {
510511
return t.render()
511512
}
512513

514+
// Lines returns the total number of lines rendered.
515+
// This method is only effective if the WithLineCounter() option was used during
516+
// table initialization and must be called *after* Render().
517+
// It actively searches for the default tw.LineCounter among all active counters.
518+
// It returns -1 if the line counter was not enabled.
519+
func (t *Table) Lines() int {
520+
for _, counter := range t.counters {
521+
if lc, ok := counter.(*tw.LineCounter); ok {
522+
return lc.Total()
523+
}
524+
}
525+
// use -1 to indicate no line counter is attached
526+
return -1
527+
}
528+
529+
// Counters returns the slice of all active counter instances.
530+
// This is useful when multiple counters are enabled.
531+
// It must be called *after* Render().
532+
func (t *Table) Counters() []tw.Counter {
533+
return t.counters
534+
}
535+
513536
// Trimmer trims whitespace from a string based on the Table’s configuration.
514537
// It conditionally applies strings.TrimSpace to the input string if the TrimSpace behavior
515538
// is enabled in t.config.Behavior, otherwise returning the string unchanged. This method
@@ -1361,20 +1384,38 @@ func (t *Table) prepareWithMerges(content [][]string, config tw.CellConfig, posi
13611384
func (t *Table) render() error {
13621385
t.ensureInitialized()
13631386

1387+
// Save the original writer and schedule its restoration upon function exit.
1388+
// This guarantees the table's writer is restored even if errors occur.
1389+
originalWriter := t.writer
1390+
defer func() {
1391+
t.writer = originalWriter
1392+
}()
1393+
1394+
// If a counter is active, wrap the writer in a MultiWriter.
1395+
if len(t.counters) > 0 {
1396+
// The slice must be of type io.Writer.
1397+
// Start it with the original destination writer.
1398+
allWriters := []io.Writer{originalWriter}
1399+
1400+
// Append each counter to the slice of writers.
1401+
for _, c := range t.counters {
1402+
allWriters = append(allWriters, c)
1403+
}
1404+
1405+
// Create a MultiWriter that broadcasts to the original writer AND all counters.
1406+
t.writer = io.MultiWriter(allWriters...)
1407+
}
1408+
13641409
if t.config.Stream.Enable {
13651410
t.logger.Warn("Render() called in streaming mode. Use Start/Append/Close methods instead.")
13661411
return errors.New("render called in streaming mode; use Start/Append/Close")
13671412
}
13681413

1369-
// Calculate and cache numCols for THIS batch render pass
1370-
t.batchRenderNumCols = t.maxColumns() // Calculate ONCE
1371-
t.isBatchRenderNumColsSet = true // Mark the cache as active for this render pass
1372-
t.logger.Debugf("Render(): Set batchRenderNumCols to %d and isBatchRenderNumColsSet to true.", t.batchRenderNumCols)
1373-
1414+
// Calculate and cache the column count for this specific batch render pass.
1415+
t.batchRenderNumCols = t.maxColumns()
1416+
t.isBatchRenderNumColsSet = true
13741417
defer func() {
13751418
t.isBatchRenderNumColsSet = false
1376-
// t.batchRenderNumCols = 0; // Optional: reset to 0, or leave as is.
1377-
// Since isBatchRenderNumColsSet is false, its value won't be used by getNumColsToUse.
13781419
t.logger.Debugf("Render(): Cleared isBatchRenderNumColsSet to false (batchRenderNumCols was %d).", t.batchRenderNumCols)
13791420
}()
13801421

@@ -1383,9 +1424,10 @@ func (t *Table) render() error {
13831424
(t.caption.Spot >= tw.SpotTopLeft && t.caption.Spot <= tw.SpotBottomRight)
13841425

13851426
var tableStringBuffer *strings.Builder
1386-
targetWriter := t.writer
1387-
originalWriter := t.writer // Save original writer for restoration if needed
1427+
targetWriter := t.writer // Can be the original writer or the MultiWriter.
13881428

1429+
// If a caption is present, the main table content must be rendered to an
1430+
// in-memory buffer first to calculate its final width.
13891431
if isTopOrBottomCaption {
13901432
tableStringBuffer = &strings.Builder{}
13911433
targetWriter = tableStringBuffer
@@ -1394,17 +1436,15 @@ func (t *Table) render() error {
13941436
t.logger.Debugf("No caption detected. Rendering table core directly to writer.")
13951437
}
13961438

1397-
// Render Table Core
1439+
// Point the table's writer to the target (either the final destination or the buffer).
13981440
t.writer = targetWriter
13991441
ctx, mctx, err := t.prepareContexts()
14001442
if err != nil {
1401-
t.writer = originalWriter
14021443
t.logger.Errorf("prepareContexts failed: %v", err)
14031444
return errors.Newf("failed to prepare table contexts").Wrap(err)
14041445
}
14051446

14061447
if err := ctx.renderer.Start(t.writer); err != nil {
1407-
t.writer = originalWriter
14081448
t.logger.Errorf("Renderer Start() error: %v", err)
14091449
return errors.Newf("renderer start failed").Wrap(err)
14101450
}
@@ -1436,18 +1476,21 @@ func (t *Table) render() error {
14361476
renderError = true
14371477
}
14381478

1439-
t.writer = originalWriter // Restore original writer
1479+
// Restore the writer to the original for the caption-handling logic.
1480+
// This is necessary because the caption must be written to the final
1481+
// destination, not the temporary buffer used for the table body.
1482+
t.writer = originalWriter
14401483

14411484
if renderError {
1442-
return firstRenderErr // Return error from core rendering if any
1485+
return firstRenderErr
14431486
}
14441487

1445-
// Caption Handling & Final Output ---
1488+
// Caption Handling & Final Output
14461489
if isTopOrBottomCaption {
14471490
renderedTableContent := tableStringBuffer.String()
14481491
t.logger.Debugf("[Render] Table core buffer length: %d", len(renderedTableContent))
14491492

1450-
// Check if the buffer is empty AND borders are enabled
1493+
// Handle edge case where table is empty but should have borders.
14511494
shouldHaveBorders := t.renderer != nil && (t.renderer.Config().Borders.Top.Enabled() || t.renderer.Config().Borders.Bottom.Enabled())
14521495
if len(renderedTableContent) == 0 && shouldHaveBorders {
14531496
var sb strings.Builder
@@ -1499,7 +1542,7 @@ func (t *Table) render() error {
14991542

15001543
t.hasPrinted = true
15011544
t.logger.Info("Render() completed.")
1502-
return nil // Success
1545+
return nil
15031546
}
15041547

15051548
// renderFooter renders the table's footer section with borders and padding.

tests/feature_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package tests
22

33
import (
44
"bytes"
5+
"io"
56
"testing"
67

78
"github.com/olekukonko/tablewriter"
@@ -253,3 +254,129 @@ func TestTrimLine(t *testing.T) {
253254
t.Error(table.Debug())
254255
}
255256
}
257+
258+
// A simple ByteCounter to demonstrate a custom counter implementation.
259+
type ByteCounter struct {
260+
count int
261+
}
262+
263+
func (bc *ByteCounter) Write(p []byte) (n int, err error) {
264+
bc.count += len(p)
265+
return len(p), nil
266+
}
267+
func (bc *ByteCounter) Total() int {
268+
return bc.count
269+
}
270+
271+
// TestLinesCounter verifies the functionality of the WithLineCounter and WithCounters options.
272+
func TestLinesCounter(t *testing.T) {
273+
274+
data := [][]string{
275+
{"A", "The Good", "500"},
276+
{"B", "The Very Very Bad Man", "288"},
277+
{"C", "The Ugly", "120"},
278+
{"D", "The Gopher", "800"},
279+
}
280+
281+
// Test Case 1: Default line counting on a standard table using the new API.
282+
t.Run("WithLineCounter", func(t *testing.T) {
283+
table := tablewriter.NewTable(io.Discard,
284+
tablewriter.WithLineCounter(), // Use the new, explicit function.
285+
)
286+
table.Header("Name", "Sign", "Rating")
287+
table.Bulk(data)
288+
table.Render()
289+
290+
// Expected: 1 Top border + 1 Header + 1 Separator + 4 Rows + 1 Bottom border = 8
291+
expectedLines := 8
292+
if got := table.Lines(); got != expectedLines {
293+
t.Errorf("expected %d lines, but got %d", expectedLines, got)
294+
}
295+
})
296+
297+
// Test Case 2: Line counting with auto-wrapping enabled.
298+
t.Run("LineCounterWithWrapping", func(t *testing.T) {
299+
table := tablewriter.NewTable(io.Discard,
300+
tablewriter.WithLineCounter(), // Use the new, explicit function.
301+
tablewriter.WithRowAutoWrap(tw.WrapNormal),
302+
tablewriter.WithMaxWidth(40),
303+
)
304+
table.Header("Name", "Sign", "Rating")
305+
table.Bulk(data)
306+
table.Render()
307+
308+
// Expected: 1 Top border + 1 Header + 1 Separator + 1+3+1+1 Rows + 1 Bottom border = 10
309+
expectedLines := 10
310+
if got := table.Lines(); got != expectedLines {
311+
t.Errorf("expected %d lines with wrapping, but got %d", expectedLines, got)
312+
}
313+
})
314+
315+
// Test Case 3: Ensure Lines() returns -1 when no counter is enabled at all.
316+
t.Run("NoCounters", func(t *testing.T) {
317+
table := tablewriter.NewTable(io.Discard) // No counter options
318+
table.Header("Name", "Sign")
319+
table.Append("A", "B")
320+
table.Render()
321+
322+
expected := -1
323+
if got := table.Lines(); got != expected {
324+
t.Errorf("expected %d when no counter is used, but got %d", expected, got)
325+
}
326+
})
327+
328+
// Test Case 4: Use a custom counter and verify it's retrieved via Counters().
329+
t.Run("WithCustomCounter", func(t *testing.T) {
330+
byteCounter := &ByteCounter{}
331+
var buf bytes.Buffer
332+
333+
table := tablewriter.NewTable(&buf,
334+
tablewriter.WithCounters(byteCounter), // Use the new plural function for custom counters.
335+
)
336+
table.Header("A", "B")
337+
table.Append("1", "2")
338+
table.Render()
339+
340+
// Crucial Test: Lines() should return -1 because no *LineCounter* was added.
341+
if got := table.Lines(); got != -1 {
342+
t.Errorf("expected Lines() to return -1 when only a custom counter is used, but got %d", got)
343+
}
344+
345+
// Verify the custom counter via the Counters() method.
346+
allCounters := table.Counters()
347+
if len(allCounters) != 1 {
348+
t.Fatalf("expected 1 counter, but found %d", len(allCounters))
349+
}
350+
351+
if custom, ok := allCounters[0].(*ByteCounter); ok {
352+
if custom.Total() <= 0 {
353+
t.Errorf("expected a positive byte count from custom counter, but got %d", custom.Total())
354+
}
355+
if custom.Total() != buf.Len() {
356+
t.Errorf("byte counter total (%d) does not match buffer length (%d)", custom.Total(), buf.Len())
357+
}
358+
} else {
359+
t.Error("expected the first counter to be of type *ByteCounter")
360+
}
361+
})
362+
363+
// Test Case 5: Ensure Lines() finds the line counter even when mixed with others.
364+
t.Run("LinesWithMixedCounters", func(t *testing.T) {
365+
byteCounter := &ByteCounter{}
366+
367+
// Add counters in a specific order: custom first, then default.
368+
table := tablewriter.NewTable(io.Discard,
369+
tablewriter.WithCounters(byteCounter),
370+
tablewriter.WithLineCounter(),
371+
)
372+
table.Header("Name", "Sign", "Rating")
373+
table.Bulk(data)
374+
table.Render()
375+
376+
// Lines() should still find the line count correctly, regardless of order.
377+
expectedLines := 8
378+
if got := table.Lines(); got != expectedLines {
379+
t.Errorf("expected %d lines even with mixed counters, but got %d", expectedLines, got)
380+
}
381+
})
382+
}

0 commit comments

Comments
 (0)