|
| 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