Skip to content

Commit bcb5a03

Browse files
committed
>, >=, <, <= operator
1 parent 9dd4430 commit bcb5a03

File tree

3 files changed

+221
-6
lines changed

3 files changed

+221
-6
lines changed

README.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -344,12 +344,17 @@ Show only rows where specific columns match your criteria. **Supports filtering
344344
| **OR** | `term1 OR term2` | Same cell contains either term | `error OR warning` → cell has "error" or "warning" |
345345
| **AND** | `term1 AND term2` | Same cell contains both terms | `user AND admin` → cell has both words |
346346
| **ROR** | `term1 ROR term2` | Keeps all rows matching any term | `high ROR critical` → rows with "high" + rows with "critical" |
347+
| **>** | `>value` | Numeric: greater than (number columns only) | `>30` → values greater than 30 |
348+
| **<** | `<value` | Numeric: less than (number columns only) | `<50` → values less than 50 |
349+
| **>=** | `>=value` | Numeric: greater than or equal (number columns only) | `>=100` → values 100 or more |
350+
| **<=** | `<=value` | Numeric: less than or equal (number columns only) | `<=75` → values 75 or less |
347351

348352
**Key Differences:**
349353
- **OR vs ROR**: OR checks if a single cell contains either term. ROR combines rows where any cell matches any term (row-level union).
350-
- All operators must be **UPPERCASE** and surrounded by spaces
351-
- Search terms are case-insensitive
352-
- All matching is partial (substring)
354+
- **Numeric operators** (`>`, `<`, `>=`, `<=`): Only work on numeric and date columns (automatically detected). Perform numeric comparisons instead of text matching.
355+
- All operators must be **UPPERCASE** and surrounded by spaces (except numeric operators)
356+
- Search terms are case-insensitive (except numeric comparisons)
357+
- All matching is partial (substring) for text, exact comparison for numeric operators
353358
- **Visual indicator**: Filtered column headers show 🔎 icons and orange background
354359

355360
**Examples:**
@@ -371,11 +376,28 @@ Navigate to "Description" column → f → type "user AND admin" → Enter
371376
Navigate to "Priority" column → f → type "high ROR critical" → Enter
372377
# Result: All rows with "high" + all rows with "critical" (union)
373378

379+
# Numeric comparison - greater than
380+
Navigate to "Age" column → f → type ">30" → Enter
381+
# Result: Rows where Age is greater than 30
382+
383+
# Numeric comparison - less than or equal
384+
Navigate to "Score" column → f → type "<=85" → Enter
385+
# Result: Rows where Score is 85 or less
386+
387+
# Numeric comparison - greater than or equal
388+
Navigate to "Salary" column → f → type ">=50000" → Enter
389+
# Result: Rows where Salary is 50000 or more
390+
374391
# Multi-column filtering
375392
Navigate to "City" column → f → type "New York" → Enter
376393
Navigate to "Department" column → f → type "Engineering" → Enter
377394
# Result: Rows where City="New York" AND Department contains "Engineering"
378395

396+
# Multi-column with numeric filter
397+
Navigate to "Age" column → f → type ">25" → Enter
398+
Navigate to "Score" column → f → type ">80" → Enter
399+
# Result: Rows where Age > 25 AND Score > 80
400+
379401
# Edit existing filter
380402
Navigate to filtered column → f → modify text → Enter
381403
# Or enter empty text to remove that column's filter
@@ -393,6 +415,7 @@ Navigate to each filtered column → Press r on each
393415
- Use **OR** when a single field can have alternative values
394416
- Use **AND** when a single field must meet multiple criteria
395417
- Use **ROR** when you want to combine different categories of results
418+
- Use **numeric operators** (`>`, `<`, `>=`, `<=`) to filter by numeric ranges on number or date columns
396419
- Use **multi-column filters** to narrow down data by multiple dimensions (e.g., location AND department AND status)
397420

398421
**Visual Feedback:**

buffer.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,12 @@ func (b *Buffer) filterByColumn(colIndex int, query string, caseSensitive bool)
575575
return filtered
576576
}
577577

578+
// Get column type for numeric comparisons
579+
colType := colTypeStr
580+
if colIndex < len(b.colType) {
581+
colType = b.colType[colIndex]
582+
}
583+
578584
// Filter data rows for OR/AND/simple filters
579585
startRow := b.rowFreeze
580586
for i := startRow; i < b.rowLen; i++ {
@@ -585,7 +591,7 @@ func (b *Buffer) filterByColumn(colIndex int, query string, caseSensitive bool)
585591
cellValue := b.cont[i][colIndex]
586592

587593
// Evaluate filter condition
588-
if evaluateFilter(cellValue, query, caseSensitive, hasOR, hasAND) {
594+
if evaluateFilter(cellValue, query, caseSensitive, hasOR, hasAND, colType) {
589595
filtered.cont = append(filtered.cont, b.cont[i])
590596
filtered.rowLen++
591597
}
@@ -595,8 +601,45 @@ func (b *Buffer) filterByColumn(colIndex int, query string, caseSensitive bool)
595601
}
596602

597603
// evaluateFilter checks if a cell value matches the filter query
598-
// Supports simple matching, OR logic, and AND logic
599-
func evaluateFilter(cellValue, query string, caseSensitive, hasOR, hasAND bool) bool {
604+
// Supports simple matching, OR logic, AND logic, and numeric comparisons (>, <, >=, <=)
605+
func evaluateFilter(cellValue, query string, caseSensitive, hasOR, hasAND bool, colType int) bool {
606+
// Check for numeric comparison operators (>, <, >=, <=) for numeric columns
607+
if colType == colTypeFloat || colType == colTypeDate {
608+
query = trimSpace(query)
609+
610+
// Check for >= operator
611+
if len(query) >= 2 && query[0] == '>' && query[1] == '=' {
612+
threshold := trimSpace(query[2:])
613+
cellVal := parseNumericValueFast(cellValue)
614+
thresholdVal := parseNumericValueFast(threshold)
615+
return cellVal >= thresholdVal
616+
}
617+
618+
// Check for <= operator
619+
if len(query) >= 2 && query[0] == '<' && query[1] == '=' {
620+
threshold := trimSpace(query[2:])
621+
cellVal := parseNumericValueFast(cellValue)
622+
thresholdVal := parseNumericValueFast(threshold)
623+
return cellVal <= thresholdVal
624+
}
625+
626+
// Check for > operator
627+
if len(query) >= 1 && query[0] == '>' {
628+
threshold := trimSpace(query[1:])
629+
cellVal := parseNumericValueFast(cellValue)
630+
thresholdVal := parseNumericValueFast(threshold)
631+
return cellVal > thresholdVal
632+
}
633+
634+
// Check for < operator
635+
if len(query) >= 1 && query[0] == '<' {
636+
threshold := trimSpace(query[1:])
637+
cellVal := parseNumericValueFast(cellValue)
638+
thresholdVal := parseNumericValueFast(threshold)
639+
return cellVal < thresholdVal
640+
}
641+
}
642+
600643
// Handle OR operator (takes precedence)
601644
if hasOR {
602645
// Split by OR (uppercase only)

filter_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,152 @@ func TestBuffer_filterByColumn_EdgeCases(t *testing.T) {
285285
}
286286
})
287287
}
288+
289+
func TestBuffer_filterByColumn_NumericComparison(t *testing.T) {
290+
tests := []struct {
291+
name string
292+
data [][]string
293+
colIndex int
294+
query string
295+
expectedRows int // including header
296+
}{
297+
{
298+
name: "Filter > operator",
299+
data: [][]string{
300+
{"Name", "Age", "Score"},
301+
{"Alice", "30", "85"},
302+
{"Bob", "25", "90"},
303+
{"Charlie", "35", "78"},
304+
{"David", "28", "92"},
305+
},
306+
colIndex: 1, // Age column
307+
query: ">28",
308+
expectedRows: 3, // header + Alice (30), Charlie (35)
309+
},
310+
{
311+
name: "Filter < operator",
312+
data: [][]string{
313+
{"Name", "Age", "Score"},
314+
{"Alice", "30", "85"},
315+
{"Bob", "25", "90"},
316+
{"Charlie", "35", "78"},
317+
{"David", "28", "92"},
318+
},
319+
colIndex: 1, // Age column
320+
query: "<30",
321+
expectedRows: 3, // header + Bob (25), David (28)
322+
},
323+
{
324+
name: "Filter >= operator",
325+
data: [][]string{
326+
{"Name", "Age", "Score"},
327+
{"Alice", "30", "85"},
328+
{"Bob", "25", "90"},
329+
{"Charlie", "35", "78"},
330+
{"David", "28", "92"},
331+
},
332+
colIndex: 1, // Age column
333+
query: ">=30",
334+
expectedRows: 3, // header + Alice (30), Charlie (35)
335+
},
336+
{
337+
name: "Filter <= operator",
338+
data: [][]string{
339+
{"Name", "Age", "Score"},
340+
{"Alice", "30", "85"},
341+
{"Bob", "25", "90"},
342+
{"Charlie", "35", "78"},
343+
{"David", "28", "92"},
344+
},
345+
colIndex: 1, // Age column
346+
query: "<=28",
347+
expectedRows: 3, // header + Bob (25), David (28)
348+
},
349+
{
350+
name: "Filter > with decimals",
351+
data: [][]string{
352+
{"Name", "Score"},
353+
{"Alice", "85.5"},
354+
{"Bob", "90.2"},
355+
{"Charlie", "78.8"},
356+
{"David", "92.1"},
357+
},
358+
colIndex: 1, // Score column
359+
query: ">85",
360+
expectedRows: 4, // header + Alice (85.5), Bob (90.2), David (92.1)
361+
},
362+
{
363+
name: "Filter < with decimals",
364+
data: [][]string{
365+
{"Name", "Score"},
366+
{"Alice", "85.5"},
367+
{"Bob", "90.2"},
368+
{"Charlie", "78.8"},
369+
{"David", "92.1"},
370+
},
371+
colIndex: 1, // Score column
372+
query: "<85",
373+
expectedRows: 2, // header + Charlie (78.8)
374+
},
375+
{
376+
name: "Filter > on string column (should not match)",
377+
data: [][]string{
378+
{"Name", "Status"},
379+
{"Alice", "Active"},
380+
{"Bob", "Inactive"},
381+
{"Charlie", "Pending"},
382+
},
383+
colIndex: 1, // Status column (string type)
384+
query: ">5",
385+
expectedRows: 1, // header only (string column, no numeric comparison)
386+
},
387+
{
388+
name: "Filter > with spaces",
389+
data: [][]string{
390+
{"Name", "Age"},
391+
{"Alice", "30"},
392+
{"Bob", "25"},
393+
{"Charlie", "35"},
394+
},
395+
colIndex: 1, // Age column
396+
query: "> 28",
397+
expectedRows: 3, // header + Alice (30), Charlie (35)
398+
},
399+
}
400+
401+
for _, tt := range tests {
402+
t.Run(tt.name, func(t *testing.T) {
403+
// Create buffer with test data
404+
b, err := createNewBufferWithData(tt.data, false)
405+
if err != nil {
406+
t.Fatalf("Failed to create buffer: %v", err)
407+
}
408+
b.rowFreeze = 1 // Set header row
409+
410+
// Detect column types
411+
b.detectAllColumnTypes()
412+
413+
// Apply filter
414+
filtered := b.filterByColumn(tt.colIndex, tt.query, false)
415+
416+
// Check result
417+
if filtered.rowLen != tt.expectedRows {
418+
t.Errorf("Expected %d rows (including header), got %d", tt.expectedRows, filtered.rowLen)
419+
// Print actual data for debugging
420+
t.Logf("Actual rows:")
421+
for i := 0; i < filtered.rowLen; i++ {
422+
t.Logf(" Row %d: %v", i, filtered.cont[i])
423+
}
424+
}
425+
426+
// Verify header is preserved
427+
if filtered.rowLen > 0 {
428+
for col := 0; col < filtered.colLen && col < len(tt.data[0]); col++ {
429+
if filtered.cont[0][col] != tt.data[0][col] {
430+
t.Errorf("Header not preserved: expected %s, got %s", tt.data[0][col], filtered.cont[0][col])
431+
}
432+
}
433+
}
434+
})
435+
}
436+
}

0 commit comments

Comments
 (0)