@@ -16,52 +16,52 @@ func TestIsRowMatched(t *testing.T) {
1616 NewColumn ("title" , "title" , 10 ).WithFiltered (true ),
1717 NewColumn ("description" , "description" , 10 )}
1818
19- assert .True (t , isRowMatched (columns ,
19+ assert .True (t , filterFuncContains (columns ,
2020 NewRow (RowData {
2121 "title" : "AAA" ,
2222 "description" : "" ,
2323 }), "" ))
2424
25- assert .True (t , isRowMatched (columns ,
25+ assert .True (t , filterFuncContains (columns ,
2626 NewRow (RowData {
2727 "title" : "AAA" ,
2828 "description" : "" ,
2929 }), "AA" ))
3030
31- assert .True (t , isRowMatched (columns ,
31+ assert .True (t , filterFuncContains (columns ,
3232 NewRow (RowData {
3333 "title" : "AAA" ,
3434 "description" : "" ,
3535 }), "A" ))
3636
37- assert .True (t , isRowMatched (columns ,
37+ assert .True (t , filterFuncContains (columns ,
3838 NewRow (RowData {
3939 "title" : "AAA" ,
4040 "description" : "" ,
4141 }), "a" ))
4242
43- assert .False (t , isRowMatched (columns ,
43+ assert .False (t , filterFuncContains (columns ,
4444 NewRow (RowData {
4545 "title" : "AAA" ,
4646 "description" : "" ,
4747 }), "B" ))
4848
49- assert .False (t , isRowMatched (columns ,
49+ assert .False (t , filterFuncContains (columns ,
5050 NewRow (RowData {
5151 "title" : "AAA" ,
5252 "description" : "BBB" ,
5353 }), "BBB" ))
5454
5555 timeFrom2020 := time .Date (2020 , time .July , 1 , 1 , 1 , 1 , 1 , time .UTC )
5656
57- assert .True (t , isRowMatched (columns ,
57+ assert .True (t , filterFuncContains (columns ,
5858 NewRow (RowData {
5959 "title" : timeFrom2020 ,
6060 }),
6161 "2020" ,
6262 ))
6363
64- assert .False (t , isRowMatched (columns ,
64+ assert .False (t , filterFuncContains (columns ,
6565 NewRow (RowData {
6666 "title" : timeFrom2020 ,
6767 }),
@@ -75,13 +75,13 @@ func TestIsRowMatchedForStyled(t *testing.T) {
7575 NewColumn ("description" , "description" , 10 ),
7676 }
7777
78- assert .True (t , isRowMatched (columns ,
78+ assert .True (t , filterFuncContains (columns ,
7979 NewRow (RowData {
8080 "title" : "AAA" ,
8181 "description" : "" ,
8282 }), "AA" ))
8383
84- assert .True (t , isRowMatched (columns ,
84+ assert .True (t , filterFuncContains (columns ,
8585 NewRow (RowData {
8686 "title" : NewStyledCell ("AAA" , lipgloss .NewStyle ()),
8787 "description" : "" ,
@@ -93,22 +93,22 @@ func TestIsRowMatchedForNonStringer(t *testing.T) {
9393 NewColumn ("val" , "val" , 10 ).WithFiltered (true ),
9494 }
9595
96- assert .True (t , isRowMatched (columns ,
96+ assert .True (t , filterFuncContains (columns ,
9797 NewRow (RowData {
9898 "val" : 12 ,
9999 }), "12" ))
100100
101- assert .True (t , isRowMatched (columns ,
101+ assert .True (t , filterFuncContains (columns ,
102102 NewRow (RowData {
103103 "val" : 12 ,
104104 }), "1" ))
105105
106- assert .True (t , isRowMatched (columns ,
106+ assert .True (t , filterFuncContains (columns ,
107107 NewRow (RowData {
108108 "val" : 12 ,
109109 }), "2" ))
110110
111- assert .False (t , isRowMatched (columns ,
111+ assert .False (t , filterFuncContains (columns ,
112112 NewRow (RowData {
113113 "val" : 12 ,
114114 }), "3" ))
@@ -314,7 +314,7 @@ func TestFilterFunc(t *testing.T) {
314314 NewRow (RowData {}),
315315 }
316316
317- filterFunc := func (r Row , s string ) bool {
317+ filterFunc := func (_ [] Column , r Row , s string ) bool {
318318 // Completely arbitrary check for testing purposes
319319 title := fmt .Sprintf ("%v" , r .Data ["title" ])
320320
@@ -420,3 +420,141 @@ func BenchmarkFilteredRenders(b *testing.B) {
420420 _ = model .View ()
421421 }
422422}
423+
424+ func TestFuzzyFilter_EmptyFilterMatchesAll (t * testing.T ) {
425+ cols := []Column {
426+ NewColumn ("name" , "Name" , 10 ).WithFiltered (true ),
427+ }
428+ rows := []Row {
429+ NewRow (RowData {"name" : "Acme Steel" }),
430+ NewRow (RowData {"name" : "Globex" }),
431+ }
432+
433+ for i , r := range rows {
434+ if ! filterFuncFuzzy (cols , r , "" ) {
435+ t .Fatalf ("row %d should match empty filter" , i )
436+ }
437+ }
438+ }
439+
440+ func TestFuzzyFilter_SubsequenceAcrossColumns (t * testing.T ) {
441+ cols := []Column {
442+ NewColumn ("name" , "Name" , 10 ).WithFiltered (true ),
443+ NewColumn ("city" , "City" , 10 ).WithFiltered (true ),
444+ }
445+ row := NewRow (RowData {
446+ "name" : "Acme" ,
447+ "city" : "Stuttgart" ,
448+ })
449+
450+ // subsequence match: "agt" appears in order inside "stuttgart"
451+ if ! filterFuncFuzzy (cols , row , "agt" ) {
452+ t .Fatalf ("expected subsequence 'agt' to match 'Stuttgart'" )
453+ }
454+ // case-insensitive
455+ if ! filterFuncFuzzy (cols , row , "ACM" ) {
456+ t .Fatalf ("expected case-insensitive subsequence to match 'Acme'" )
457+ }
458+ // not a subsequence
459+ if filterFuncFuzzy (cols , row , "zzt" ) {
460+ t .Fatalf ("did not expect 'zzt' to match" )
461+ }
462+ }
463+
464+ func TestFuzzyFilter_ColumnNotInRow (t * testing.T ) {
465+ cols := []Column {
466+ NewColumn ("column_name_doesnt_match" , "Name" , 10 ).WithFiltered (true ),
467+ }
468+ row := NewRow (RowData {
469+ "name" : "Acme Steel" ,
470+ })
471+
472+ if filterFuncFuzzy (cols , row , "steel" ) {
473+ t .Fatalf ("did not expect 'steel' to match" )
474+ }
475+ }
476+
477+ func TestFuzzyFilter_RowHasEmptyHaystack (t * testing.T ) {
478+ cols := []Column {
479+ NewColumn ("name" , "Name" , 10 ).WithFiltered (true ),
480+ }
481+ row := NewRow (RowData {"name" : "" })
482+
483+ // literally any value other than an empty string
484+ // should not match
485+ if filterFuncFuzzy (cols , row , "a" ) {
486+ t .Fatalf ("did not expect 'a' to match" )
487+ }
488+ }
489+
490+ func TestFuzzyFilter_MultiToken_AND (t * testing.T ) {
491+ cols := []Column {
492+ NewColumn ("name" , "Name" , 10 ).WithFiltered (true ),
493+ NewColumn ("dept" , "Dept" , 10 ).WithFiltered (true ),
494+ }
495+ row := NewRow (RowData {
496+ "name" : "Wayne Enterprises" ,
497+ "dept" : "R&D" ,
498+ })
499+
500+ // Both tokens must match as subsequences somewhere in the concatenated haystack
501+ if ! filterFuncFuzzy (cols , row , "wy ent" ) { // "wy" in Wayne, "ent" in Enterprises
502+ t .Fatalf ("expected multi-token AND to match" )
503+ }
504+ if filterFuncFuzzy (cols , row , "wy zzz" ) {
505+ t .Fatalf ("expected multi-token AND to fail when a token doesn't match" )
506+ }
507+ }
508+
509+ func TestFuzzyFilter_IgnoresNonFilterableColumns (t * testing.T ) {
510+ cols := []Column {
511+ NewColumn ("name" , "Name" , 10 ).WithFiltered (true ),
512+ NewColumn ("secret" , "Secret" , 10 ).WithFiltered (false ), // should be ignored
513+ }
514+ row := NewRow (RowData {
515+ "name" : "Acme" ,
516+ "secret" : "topsecretpattern" ,
517+ })
518+
519+ if filterFuncFuzzy (cols , row , "topsecret" ) {
520+ t .Fatalf ("should not match on non-filterable column content" )
521+ }
522+ }
523+
524+ func TestFuzzyFilter_UnwrapsStyledCell (t * testing.T ) {
525+ cols := []Column {
526+ NewColumn ("name" , "Name" , 10 ).WithFiltered (true ),
527+ }
528+ row := NewRow (RowData {
529+ "name" : NewStyledCell ("Nakatomi Plaza" , lipgloss .NewStyle ()),
530+ })
531+
532+ if ! filterFuncFuzzy (cols , row , "nak plz" ) {
533+ t .Fatalf ("expected fuzzy subsequence to match within StyledCell data" )
534+ }
535+ }
536+
537+ func TestFuzzyFilter_NonStringValuesFormatted (t * testing.T ) {
538+ cols := []Column {
539+ NewColumn ("id" , "ID" , 6 ).WithFiltered (true ),
540+ }
541+ row := NewRow (RowData {
542+ "id" : 12345 , // should be formatted via fmt.Sprintf("%v", v)
543+ })
544+
545+ if ! filterFuncFuzzy (cols , row , "245" ) { // subsequence of "12345"
546+ t .Fatalf ("expected matcher to format non-strings and match subsequence" )
547+ }
548+ }
549+
550+ func TestFuzzySubSequenceMatch_EmptyString (t * testing.T ) {
551+ if ! fuzzySubsequenceMatch ("anything" , "" ) {
552+ t .Fatalf ("empty needle should match anything" )
553+ }
554+ if fuzzySubsequenceMatch ("" , "a" ) {
555+ t .Fatalf ("non-empty needle should not match empty haystack" )
556+ }
557+ if ! fuzzySubsequenceMatch ("" , "" ) {
558+ t .Fatalf ("empty needle should match empty haystack" )
559+ }
560+ }
0 commit comments