@@ -347,7 +347,41 @@ def remove_duplicate_products(data_products, uri_key):
347
347
return unique_products
348
348
349
349
350
- def parse_numeric_product_filter (val ):
350
+ def _combine_positive_negative_masks (mask_funcs ):
351
+ """
352
+ Combines a list of mask functions into a single mask according to:
353
+ - OR logic among positive masks
354
+ - AND logic among negative masks applied after positives
355
+
356
+ Parameters
357
+ ----------
358
+ mask_funcs : list of tuples (func, is_negated)
359
+ Each element is a tuple where:
360
+ - func: a callable that takes an array and returns a boolean mask
361
+ - is_negated: boolean, True if the mask should be negated before combining
362
+
363
+ Returns
364
+ -------
365
+ combined_mask : np.ndarray
366
+ Combined boolean mask.
367
+ """
368
+ def combined (col ):
369
+ positive_masks = [f (col ) for f , neg in mask_funcs if not neg ]
370
+ negative_masks = [~ f (col ) for f , neg in mask_funcs if neg ]
371
+
372
+ # Use OR logic between positive masks
373
+ pos_mask = np .logical_or .reduce (positive_masks ) if positive_masks else np .ones (len (col ), dtype = bool )
374
+
375
+ # Use AND logic between negative masks
376
+ neg_mask = np .logical_and .reduce (negative_masks ) if negative_masks else np .ones (len (col ), dtype = bool )
377
+
378
+ # Use AND logic to combine positive and negative masks
379
+ return pos_mask & neg_mask
380
+
381
+ return combined
382
+
383
+
384
+ def parse_numeric_product_filter (vals ):
351
385
"""
352
386
Parses a numeric product filter value and returns a function that can be used to filter
353
387
a column of a product table.
@@ -369,30 +403,35 @@ def parse_numeric_product_filter(val):
369
403
# Regular expression to match range patterns
370
404
range_pattern = re .compile (r'[+-]?(\d+(\.\d*)?|\.\d+)\.\.[+-]?(\d+(\.\d*)?|\.\d+)' )
371
405
372
- def single_condition ( cond ):
373
- """Helper function to create a condition function for a single value ."""
374
- if isinstance (cond , (int , float )):
375
- return lambda col : col == float (cond )
376
- if cond .startswith ('>=' ):
377
- return lambda col : col >= float (cond [2 :])
378
- elif cond .startswith ('<=' ):
379
- return lambda col : col <= float (cond [2 :])
380
- elif cond .startswith ('>' ):
381
- return lambda col : col > float (cond [1 :])
382
- elif cond .startswith ('<' ):
383
- return lambda col : col < float (cond [1 :])
384
- elif range_pattern .fullmatch (cond ):
385
- start , end = map (float , cond .split ('..' ))
406
+ def base_condition ( cond_str ):
407
+ """Create a mask function for a numeric condition string (no negation handling here) ."""
408
+ if isinstance (cond_str , (int , float )):
409
+ return lambda col : col == float (cond_str )
410
+ elif cond_str .startswith ('>=' ):
411
+ return lambda col : col >= float (cond_str [2 :])
412
+ elif cond_str .startswith ('<=' ):
413
+ return lambda col : col <= float (cond_str [2 :])
414
+ elif cond_str .startswith ('>' ):
415
+ return lambda col : col > float (cond_str [1 :])
416
+ elif cond_str .startswith ('<' ):
417
+ return lambda col : col < float (cond_str [1 :])
418
+ elif range_pattern .fullmatch (cond_str ):
419
+ start , end = map (float , cond_str .split ('..' ))
386
420
return lambda col : (col >= start ) & (col <= end )
387
421
else :
388
- return lambda col : col == float (cond )
422
+ return lambda col : col == float (cond_str )
389
423
390
- if isinstance (val , list ):
391
- # If val is a list, create a condition for each value and combine them with logical OR
392
- conditions = [single_condition (v ) for v in val ]
393
- return lambda col : np .logical_or .reduce ([cond (col ) for cond in conditions ])
394
- else :
395
- return single_condition (val )
424
+ vals = [vals ] if not isinstance (vals , list ) else vals
425
+ mask_funcs = []
426
+ for v in vals :
427
+ # Check if the value is negated and strip the negation if present
428
+ is_negated = isinstance (v , str ) and v .startswith ('!' )
429
+ v = v [1 :] if is_negated else v
430
+
431
+ func = base_condition (v )
432
+ mask_funcs .append ((func , is_negated ))
433
+
434
+ return _combine_positive_negative_masks (mask_funcs )
396
435
397
436
398
437
def apply_column_filters (products , filters ):
@@ -426,11 +465,20 @@ def apply_column_filters(products, filters):
426
465
except ValueError :
427
466
raise InvalidQueryError (f"Could not parse numeric filter '{ vals } ' for column '{ colname } '." )
428
467
else : # Assume string or list filter
429
- if isinstance (vals , str ):
430
- vals = [vals ]
431
- this_mask = np .isin (col_data , vals )
468
+ vals = [vals ] if isinstance (vals , str ) else vals
469
+ mask_funcs = []
470
+ for val in vals :
471
+ # Check if the value is negated and strip the negation if present
472
+ is_negated = isinstance (val , str ) and val .startswith ('!' )
473
+ v = val [1 :] if is_negated else val
474
+
475
+ def func (col , v = v ):
476
+ return np .isin (col , [v ])
477
+ mask_funcs .append ((func , is_negated ))
478
+
479
+ this_mask = _combine_positive_negative_masks (mask_funcs )(col_data )
432
480
433
- # Combine the current column mask with the overall mask
481
+ # AND logic across different columns
434
482
col_mask &= this_mask
435
483
436
484
return col_mask
0 commit comments