Skip to content

Commit ed62d35

Browse files
committed
table: FilterBy: support filtering rows
1 parent 66db774 commit ed62d35

File tree

10 files changed

+1235
-36
lines changed

10 files changed

+1235
-36
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
[![Coverage Status](https://coveralls.io/repos/github/jedib0t/go-pretty/badge.svg?branch=main)](https://coveralls.io/github/jedib0t/go-pretty?branch=main)
66
[![Go Report Card](https://goreportcard.com/badge/github.com/jedib0t/go-pretty/v6)](https://goreportcard.com/report/github.com/jedib0t/go-pretty/v6)
77
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=jedib0t_go-pretty&metric=alert_status)](https://sonarcloud.io/dashboard?id=jedib0t_go-pretty)
8-
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
98

109
Utilities to prettify console output of tables, lists, progress bars, text, and more
1110
with a heavy emphasis on customization and flexibility.

table/EXAMPLES.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,77 @@ to get:
310310

311311
---
312312

313+
<details>
314+
<summary><strong>Filtering</strong></summary>
315+
316+
Filtering can be done on one or more columns. All filters are applied with AND logic (all must match).
317+
Filters are applied before sorting.
318+
319+
```golang
320+
t.FilterBy([]table.FilterBy{
321+
{Name: "Salary", Operator: table.GreaterThan, Value: 2000},
322+
{Name: "First Name", Operator: table.Contains, Value: "on"},
323+
})
324+
```
325+
326+
The `Operator` field in `FilterBy` supports various filtering operators:
327+
- `Equal` / `NotEqual` - Exact match
328+
- `GreaterThan` / `GreaterThanOrEqual` - Numeric comparisons
329+
- `LessThan` / `LessThanOrEqual` - Numeric comparisons
330+
- `Contains` / `NotContains` - String search
331+
- `StartsWith` / `EndsWith` - String prefix/suffix matching
332+
- `RegexMatch` / `RegexNotMatch` - Regular expression matching
333+
334+
You can make string comparisons case-insensitive by setting `IgnoreCase: true`:
335+
```golang
336+
t.FilterBy([]table.FilterBy{
337+
{Name: "First Name", Operator: table.Equal, Value: "JON", IgnoreCase: true},
338+
})
339+
```
340+
341+
For advanced filtering requirements, you can provide a custom filter function:
342+
```golang
343+
t.FilterBy([]table.FilterBy{
344+
{
345+
Number: 2,
346+
CustomFilter: func(cellValue string) bool {
347+
// Custom logic: include rows where first name length > 3
348+
return len(cellValue) > 3
349+
},
350+
},
351+
})
352+
```
353+
354+
Example: Filter by salary and name
355+
```golang
356+
t := table.NewWriter()
357+
t.AppendHeader(table.Row{"#", "First Name", "Last Name", "Salary"})
358+
t.AppendRows([]table.Row{
359+
{1, "Arya", "Stark", 3000},
360+
{20, "Jon", "Snow", 2000},
361+
{300, "Tyrion", "Lannister", 5000},
362+
{400, "Sansa", "Stark", 2500},
363+
})
364+
t.FilterBy([]table.FilterBy{
365+
{Number: 4, Operator: table.GreaterThan, Value: 2000},
366+
{Number: 3, Operator: table.Contains, Value: "Stark"},
367+
})
368+
t.Render()
369+
```
370+
to get:
371+
```
372+
+-----+------------+-----------+--------+
373+
| # | FIRST NAME | LAST NAME | SALARY |
374+
+-----+------------+-----------+--------+
375+
| 1 | Arya | Stark | 3000 |
376+
| 400 | Sansa | Stark | 2500 |
377+
+-----+------------+-----------+--------+
378+
```
379+
380+
</details>
381+
382+
---
383+
313384
<details>
314385
<summary><strong>Sorting</strong></summary>
315386

table/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,19 @@ If you want very specific examples, look at the [EXAMPLES.md](EXAMPLES.md) file.
7676

7777
### Sorting & Filtering
7878

79-
- Sort by one or more Columns (`SortBy`)
79+
- **Sorting**
80+
- Sort by one or more Columns (`SortBy`)
8081
- Multiple column sorting support
81-
- Various sort modes: alphabetical, numeric, alphanumeric, numeric-alpha
82+
- Various sort modes: Alphabetical, Numeric, Alpha-numeric, Numeric-alpha
8283
- Case-insensitive sorting option (`IgnoreCase`)
8384
- Custom sorting functions (`CustomLess`) for advanced sorting logic
85+
- **Filtering**
86+
- Filter by one or more Columns (`FilterBy`)
87+
- Multiple filters with AND logic (all must match)
88+
- Various filter operators: Equal, NotEqual, GreaterThan, LessThan, Contains, StartsWith, EndsWith, RegexMatch
89+
- Case-insensitive filtering option (`IgnoreCase`)
90+
- Custom filter functions (`CustomFilter`) for advanced filtering logic
91+
- Filters are applied before sorting
8492
- Suppress/hide columns with no content (`SuppressEmptyColumns`)
8593
- Hide specific columns (`ColumnConfig.Hidden`)
8694
- Suppress trailing spaces in the last column (`SuppressTrailingSpaces`)

table/filter.go

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
package table
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strconv"
7+
"strings"
8+
)
9+
10+
// FilterBy defines what to filter (Column Name or Number), how to filter (Operator),
11+
// and the value to compare against.
12+
type FilterBy struct {
13+
// Name is the name of the Column as it appears in the first Header row.
14+
// If a Header is not provided, or the name is not found in the header, this
15+
// will not work.
16+
Name string
17+
// Number is the Column # from left. When specified, it overrides the Name
18+
// property. If you know the exact Column number, use this instead of Name.
19+
Number int
20+
21+
// Operator defines how to compare the column value against the Value.
22+
Operator FilterOperator
23+
24+
// Value is the value to compare against. The type should match the expected
25+
// comparison type (string for string operations, numeric for numeric operations).
26+
// For Contains, StartsWith, EndsWith, and RegexMatch, Value should be a string.
27+
// For numeric comparisons (Equal, NotEqual, GreaterThan, etc.), Value can be
28+
// a number (int, float64) or a string representation of a number.
29+
Value interface{}
30+
31+
// IgnoreCase makes string comparisons case-insensitive (only applies to
32+
// string-based operators).
33+
IgnoreCase bool
34+
35+
// CustomFilter is a function that can be used to filter rows in a custom
36+
// manner. Note that:
37+
// * This overrides and ignores the Operator, Value, and IgnoreCase settings
38+
// * This is called after the column contents are converted to string form
39+
// * This function is expected to return:
40+
// * true => include the row
41+
// * false => exclude the row
42+
//
43+
// Use this when the default filtering logic is not sufficient.
44+
CustomFilter func(cellValue string) bool
45+
}
46+
47+
// FilterOperator defines how to filter.
48+
type FilterOperator int
49+
50+
const (
51+
// Equal filters rows where the column value equals the Value.
52+
Equal FilterOperator = iota
53+
// NotEqual filters rows where the column value does not equal the Value.
54+
NotEqual
55+
// GreaterThan filters rows where the column value is greater than the Value.
56+
GreaterThan
57+
// GreaterThanOrEqual filters rows where the column value is greater than or equal to the Value.
58+
GreaterThanOrEqual
59+
// LessThan filters rows where the column value is less than the Value.
60+
LessThan
61+
// LessThanOrEqual filters rows where the column value is less than or equal to the Value.
62+
LessThanOrEqual
63+
// Contains filters rows where the column value contains the Value (string search).
64+
Contains
65+
// NotContains filters rows where the column value does not contain the Value (string search).
66+
NotContains
67+
// StartsWith filters rows where the column value starts with the Value.
68+
StartsWith
69+
// EndsWith filters rows where the column value ends with the Value.
70+
EndsWith
71+
// RegexMatch filters rows where the column value matches the Value as a regular expression.
72+
RegexMatch
73+
// RegexNotMatch filters rows where the column value does not match the Value as a regular expression.
74+
RegexNotMatch
75+
)
76+
77+
func (t *Table) parseFilterBy(filterBy []FilterBy) []FilterBy {
78+
var resFilterBy []FilterBy
79+
for _, filter := range filterBy {
80+
colNum := 0
81+
if filter.Number > 0 && filter.Number <= t.numColumns {
82+
colNum = filter.Number
83+
} else if filter.Name != "" && len(t.rowsHeaderRaw) > 0 {
84+
// Parse from raw header rows
85+
for idx, colName := range t.rowsHeaderRaw[0] {
86+
if fmt.Sprint(colName) == filter.Name {
87+
colNum = idx + 1
88+
break
89+
}
90+
}
91+
}
92+
if colNum > 0 {
93+
resFilterBy = append(resFilterBy, FilterBy{
94+
Name: filter.Name,
95+
Number: colNum,
96+
Operator: filter.Operator,
97+
Value: filter.Value,
98+
IgnoreCase: filter.IgnoreCase,
99+
CustomFilter: filter.CustomFilter,
100+
})
101+
}
102+
}
103+
return resFilterBy
104+
}
105+
106+
func (t *Table) matchesFiltersRaw(row Row, filters []FilterBy) bool {
107+
// All filters must match (AND logic)
108+
for _, filter := range filters {
109+
if !t.matchesFilterRaw(row, filter) {
110+
return false
111+
}
112+
}
113+
return true
114+
}
115+
116+
func (t *Table) matchesFilterRaw(row Row, filter FilterBy) bool {
117+
colIdx := filter.Number - 1
118+
if colIdx < 0 || colIdx >= len(row) {
119+
return false
120+
}
121+
122+
cellValue := row[colIdx]
123+
cellValueStr := fmt.Sprint(cellValue)
124+
125+
// Use custom filter if provided
126+
if filter.CustomFilter != nil {
127+
return filter.CustomFilter(cellValueStr)
128+
}
129+
130+
// Use operator-based filtering
131+
return t.matchesOperator(cellValueStr, filter)
132+
}
133+
134+
func (t *Table) matchesOperator(cellValue string, filter FilterBy) bool {
135+
switch filter.Operator {
136+
case Equal:
137+
return t.compareEqual(cellValue, filter.Value, filter.IgnoreCase)
138+
case NotEqual:
139+
return !t.compareEqual(cellValue, filter.Value, filter.IgnoreCase)
140+
case GreaterThan:
141+
return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a > b })
142+
case GreaterThanOrEqual:
143+
return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a >= b })
144+
case LessThan:
145+
return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a < b })
146+
case LessThanOrEqual:
147+
return t.compareNumeric(cellValue, filter.Value, func(a, b float64) bool { return a <= b })
148+
case Contains:
149+
return t.compareContains(cellValue, filter.Value, filter.IgnoreCase)
150+
case NotContains:
151+
return !t.compareContains(cellValue, filter.Value, filter.IgnoreCase)
152+
case StartsWith:
153+
return t.compareStartsWith(cellValue, filter.Value, filter.IgnoreCase)
154+
case EndsWith:
155+
return t.compareEndsWith(cellValue, filter.Value, filter.IgnoreCase)
156+
case RegexMatch:
157+
return t.compareRegexMatch(cellValue, filter.Value, filter.IgnoreCase)
158+
case RegexNotMatch:
159+
return !t.compareRegexMatch(cellValue, filter.Value, filter.IgnoreCase)
160+
default:
161+
return false
162+
}
163+
}
164+
165+
func (t *Table) compareEqual(cellValue string, filterValue interface{}, ignoreCase bool) bool {
166+
filterStr := fmt.Sprint(filterValue)
167+
if ignoreCase {
168+
return strings.EqualFold(cellValue, filterStr)
169+
}
170+
return cellValue == filterStr
171+
}
172+
173+
func (t *Table) compareNumeric(cellValue string, filterValue interface{}, compareFunc func(float64, float64) bool) bool {
174+
cellNum, cellErr := strconv.ParseFloat(cellValue, 64)
175+
if cellErr != nil {
176+
return false
177+
}
178+
179+
var filterNum float64
180+
switch v := filterValue.(type) {
181+
case int:
182+
filterNum = float64(v)
183+
case int64:
184+
filterNum = float64(v)
185+
case float64:
186+
filterNum = v
187+
case float32:
188+
filterNum = float64(v)
189+
case string:
190+
var err error
191+
filterNum, err = strconv.ParseFloat(v, 64)
192+
if err != nil {
193+
return false
194+
}
195+
default:
196+
// Try to convert to string and parse
197+
filterStr := fmt.Sprint(filterValue)
198+
var err error
199+
filterNum, err = strconv.ParseFloat(filterStr, 64)
200+
if err != nil {
201+
return false
202+
}
203+
}
204+
205+
return compareFunc(cellNum, filterNum)
206+
}
207+
208+
func (t *Table) compareContains(cellValue string, filterValue interface{}, ignoreCase bool) bool {
209+
filterStr := fmt.Sprint(filterValue)
210+
if ignoreCase {
211+
return strings.Contains(strings.ToLower(cellValue), strings.ToLower(filterStr))
212+
}
213+
return strings.Contains(cellValue, filterStr)
214+
}
215+
216+
func (t *Table) compareStartsWith(cellValue string, filterValue interface{}, ignoreCase bool) bool {
217+
filterStr := fmt.Sprint(filterValue)
218+
if ignoreCase {
219+
return strings.HasPrefix(strings.ToLower(cellValue), strings.ToLower(filterStr))
220+
}
221+
return strings.HasPrefix(cellValue, filterStr)
222+
}
223+
224+
func (t *Table) compareEndsWith(cellValue string, filterValue interface{}, ignoreCase bool) bool {
225+
filterStr := fmt.Sprint(filterValue)
226+
if ignoreCase {
227+
return strings.HasSuffix(strings.ToLower(cellValue), strings.ToLower(filterStr))
228+
}
229+
return strings.HasSuffix(cellValue, filterStr)
230+
}
231+
232+
func (t *Table) compareRegexMatch(cellValue string, filterValue interface{}, ignoreCase bool) bool {
233+
filterStr := fmt.Sprint(filterValue)
234+
235+
// Compile the regex pattern
236+
var pattern *regexp.Regexp
237+
var err error
238+
if ignoreCase {
239+
pattern, err = regexp.Compile("(?i)" + filterStr)
240+
} else {
241+
pattern, err = regexp.Compile(filterStr)
242+
}
243+
244+
if err != nil {
245+
// If regex compilation fails, fall back to simple string matching
246+
return t.compareEqual(cellValue, filterValue, ignoreCase)
247+
}
248+
249+
return pattern.MatchString(cellValue)
250+
}

0 commit comments

Comments
 (0)