Handle Nothing values in Filter_Condition.to_predicate (#8600)

- Fixes #8549
- Ensures that a `Type_Error` is thrown instead of a `No_Such_Method` error on type mismatches.
- I think this is more readable.
This commit is contained in:
Radosław Waśko 2023-12-21 20:17:55 +01:00 committed by GitHub
parent d41d48e8a0
commit b3de42eb23
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 84 additions and 34 deletions

View File

@ -1,11 +1,13 @@
import project.Any.Any
import project.Data.Locale.Locale
import project.Data.Numbers.Number
import project.Data.Set.Set
import project.Data.Text.Case_Sensitivity.Case_Sensitivity
import project.Data.Text.Regex.Regex
import project.Data.Text.Text
import project.Data.Vector.Vector
import project.Error.Error
import project.Errors.Common.Incomparable_Values
import project.Errors.Illegal_Argument.Illegal_Argument
import project.Function.Function
import project.Meta
@ -184,30 +186,39 @@ type Filter_Condition
## Converts a `Filter_Condition` condition into a predicate taking an
element and returning a value indicating whether the element should be
accepted by the filter.
The predicate can handle `Nothing` values in all cases. However, the
predicate will raise an error if the value is not of the expected type.
to_predicate : (Any -> Boolean)
to_predicate self = case self of
Less value -> <value
Equal_Or_Less value -> <=value
# == does not need special handling for Nothing
Equal value -> ==value
Equal_Or_Greater value -> >=value
Greater value -> >value
Not_Equal value -> !=value
Between lower upper -> elem ->
Less value -> handle_nothing (<value)
Equal_Or_Less value -> handle_nothing (<=value)
Equal_Or_Greater value -> handle_nothing (>=value)
Greater value -> handle_nothing (>value)
Between lower upper -> handle_nothing <| elem->
(lower <= elem) && (elem <= upper)
Equal_Ignore_Case value locale -> elem-> elem.equals_ignore_case value locale
Starts_With prefix case_sensitivity -> _.starts_with prefix case_sensitivity
Ends_With suffix case_sensitivity -> _.ends_with suffix case_sensitivity
Contains substring case_sensitivity -> _.contains substring case_sensitivity
Not_Contains substring case_sensitivity -> v-> v.contains substring case_sensitivity . not
Equal_Ignore_Case value locale ->
handle_nothing <| txt-> (txt : Text).equals_ignore_case value locale
Starts_With prefix case_sensitivity ->
handle_nothing <| txt-> (txt : Text).starts_with prefix case_sensitivity
Ends_With suffix case_sensitivity ->
handle_nothing <| txt-> (txt : Text).ends_with suffix case_sensitivity
Contains substring case_sensitivity ->
handle_nothing <| txt-> (txt : Text).contains substring case_sensitivity
Not_Contains substring case_sensitivity ->
handle_nothing <| txt-> (txt : Text).contains substring case_sensitivity . not
Is_Nothing -> elem -> case elem of
Nothing -> True
_ -> False
Not_Nothing -> elem -> case elem of
Nothing -> False
_ -> True
Is_Nan -> .is_nan
Is_Infinite -> .is_infinite
Is_Finite -> .is_finite
Is_Nan -> handle_nothing x-> (x:Number).is_nan
Is_Infinite -> handle_nothing x-> (x:Number).is_infinite
Is_Finite -> handle_nothing x-> (x:Number).is_finite
Is_True -> ==True
Is_False -> ==False
Is_Empty -> elem -> case elem of
@ -220,16 +231,16 @@ type Filter_Condition
_ -> True
Like sql_pattern ->
regex = sql_like_to_regex sql_pattern
regex.matches
handle_nothing <| regex.matches
Not_Like sql_pattern ->
regex = sql_like_to_regex sql_pattern
elem -> regex.matches elem . not
handle_nothing <| elem-> regex.matches elem . not
Is_In values ->
set = Set.from_vector values
set.contains
Not_In values ->
set = Set.from_vector values
elem -> set.contains elem . not
elem-> set.contains elem . not
## PRIVATE
Convert to a display representation of this Filter_Condition.
@ -345,8 +356,23 @@ handle_constructor_missing_arguments function ~continuation =
We rely on its text representation being of the form `Filter_Condition.Between[Filter_Condition.enso:41-343]`.
_ : Meta.Primitive ->
text = function.to_text
text.starts_with "Filter_Condition." && text.contains "[Filter_Condition.enso:"
prefix = "Filter_Condition."
if (text.starts_with prefix && text.contains "[Filter_Condition.enso:") . not then False else
## The additional check for capital letter is needed, because otherwise, we get false positives:
`(Filter_Condition.Greater 1).to_predicate` evaluates to function whose text representation
may start with `Filter_Condition.handle_nothing`. Such functions are not constructors.
constructor_letter = text.get prefix.length ""
constructor_letter >= "A" && constructor_letter <= "Z"
_ -> False
if is_filter_condition_constructor.not then continuation else
message = "Got a Filter_Condition constructor without all required arguments provided. Please provide the missing arguments."
Error.throw (Illegal_Argument.Error message)
## PRIVATE
Extends the provided predicate to handle `Nothing` values without error.
The new predicate will return `False` for `Nothing`.
handle_nothing : (Any -> Boolean) -> (Any -> Boolean)
handle_nothing f = elem-> case elem of
Nothing -> False
_ -> f elem

View File

@ -372,7 +372,7 @@ type Date_Range
> Example
Checking that at least one date in the range is after 2020-10-01.
(Date.new 2020 10 01).up_to (Date.new 2020 10 31) . any (> (Date.new 2020 10 01))
(Date.new 2020 10 01).up_to (Date.new 2020 10 31) . any (Filter_Condition.Greater (Date.new 2020 10 01))
@condition date_range_default_filter_condition_widget
any : (Filter_Condition | (Date -> Boolean)) -> Boolean
any self condition = self.find condition . is_nothing . not

View File

@ -865,6 +865,7 @@ spec =
expected_vector = column_vector.filter (Filter_Condition.Is_In in_vector)
expected_neg_vector = negated_column_vector.filter (Filter_Condition.Is_In in_vector)
Test.with_clue "(Is_In "+in_vector.to_text+"): " <|
t.filter "X" (Filter_Condition.Is_In in_vector) . at "X" . to_vector . should_equal expected_vector
t.filter "X" (Filter_Condition.Is_In in_column) . at "X" . to_vector . should_equal expected_vector
t2 = t.set (t.at "X" . not) new_name="Y"

View File

@ -1,7 +1,6 @@
from Standard.Base import all
import Standard.Base.Data.List.Empty_Error
import Standard.Base.Errors.Common.Index_Out_Of_Bounds
import Standard.Base.Errors.Common.No_Such_Method
import Standard.Base.Errors.Common.Not_Found
import Standard.Base.Errors.Common.Type_Error
import Standard.Base.Errors.Common.Unsupported_Argument_Types
@ -139,7 +138,7 @@ spec = Test.group "List" <|
list.filter (Filter_Condition.Is_In [7, 3, 2]) . should_equal [2, 3].to_list
list.filter (Filter_Condition.Not_In [7, 3, 2]) . should_equal [1, 4, 5].to_list
Test.expect_panic_with (list.filter (Filter_Condition.Starts_With "a")) No_Such_Method
Test.expect_panic Type_Error (list.filter (Filter_Condition.Starts_With "a"))
list.filter Filter_Condition.Is_True . should_equal List.Nil
list.filter Filter_Condition.Is_False . should_equal List.Nil
list.filter Filter_Condition.Is_Nothing . should_equal List.Nil

View File

@ -148,8 +148,8 @@ spec = Test.group "Range" <|
range.filter (Filter_Condition.Is_In [7, 3, 2]) . should_equal [2, 3]
range.filter (Filter_Condition.Not_In [7, 3, 2]) . should_equal [1, 4, 5]
Test.expect_panic_with (range.filter (Filter_Condition.Starts_With "a")) No_Such_Method
Test.expect_panic_with (range.filter (Filter_Condition.Equal_Ignore_Case "a")) No_Such_Method
Test.expect_panic Type_Error (range.filter (Filter_Condition.Starts_With "a"))
Test.expect_panic Type_Error (range.filter (Filter_Condition.Equal_Ignore_Case "a"))
range.filter (Filter_Condition.Like "a%") . should_fail_with Type_Error
range.filter (Filter_Condition.Not_Like "a_") . should_fail_with Type_Error
range.filter Filter_Condition.Is_Nan . should_equal []

View File

@ -5,7 +5,6 @@ import Standard.Base.Data.Vector.No_Wrap
import Standard.Base.Errors.Common.Additional_Warnings
import Standard.Base.Errors.Common.Incomparable_Values
import Standard.Base.Errors.Common.Index_Out_Of_Bounds
import Standard.Base.Errors.Common.No_Such_Method
import Standard.Base.Errors.Common.Not_Found
import Standard.Base.Errors.Common.Type_Error
import Standard.Base.Errors.Common.Unsupported_Argument_Types
@ -221,7 +220,7 @@ type_spec name alter = Test.group name <|
vec.filter (Filter_Condition.Is_In []) . should_equal []
vec.filter (Filter_Condition.Not_In [7, 3, 2, 2]) . should_equal [1, 4, 5]
Test.expect_panic_with (vec.filter (Filter_Condition.Starts_With "a")) No_Such_Method
Test.expect_panic_with (vec.filter (Filter_Condition.Starts_With "a")) Type_Error
vec.filter Filter_Condition.Is_True . should_equal []
vec.filter Filter_Condition.Is_False . should_equal []
vec.filter Filter_Condition.Is_Nothing . should_equal []
@ -292,16 +291,41 @@ type_spec name alter = Test.group name <|
mixed.filter Filter_Condition.Is_Empty . should_equal [Nothing]
mixed.filter Filter_Condition.Not_Empty . should_equal [1, "b"]
boolvec = [True, False, Nothing, True]
boolvec.filter Filter_Condition.Is_True . should_equal [True, True]
boolvec.filter Filter_Condition.Is_False . should_equal [False]
numvec = [1, 2.5, Number.nan, Number.positive_infinity, Number.negative_infinity, 0]
numvec = alter [1, 2.5, Number.nan, Number.positive_infinity, Number.negative_infinity, 0]
# We need to use to_text because NaN!=NaN
numvec.filter Filter_Condition.Is_Nan . map .to_text . should_equal ["NaN"]
numvec.filter Filter_Condition.Is_Infinite . should_equal [Number.positive_infinity, Number.negative_infinity]
numvec.filter Filter_Condition.Is_Finite . should_equal [1, 2.5, 0]
Test.expect_panic Type_Error (txtvec.filter Filter_Condition.Is_Finite)
(alter [2, "a"]).filter (Filter_Condition.Greater 1) . should_fail_with Incomparable_Values
Test.specify "should allow Nothing when filtering by Filter_Condition" <|
(alter [1, 2, Nothing, 3]).filter (Filter_Condition.Greater 2) . should_equal [3]
(alter [1, 2, Nothing, 3]).filter (Filter_Condition.Equal_Or_Less 2) . should_equal [1, 2]
(alter ["a", 2, Nothing, 2]).filter (Filter_Condition.Equal 2) . should_equal [2, 2]
(alter ["a", 2, Nothing, "a", "a"]).filter (Filter_Condition.Equal "a") . should_equal ["a", "a", "a"]
(alter [1, Nothing, (1/0), (0.log 0)]).filter Filter_Condition.Is_Nan . map .to_text . should_equal ["NaN"]
(alter [1, Nothing, (1/0), (0.log 0)]).filter Filter_Condition.Is_Infinite . should_equal [Number.positive_infinity]
(alter [1, Nothing, (1/0), (0.log 0)]).filter Filter_Condition.Is_Finite . should_equal [1]
boolvec = alter [True, False, Nothing, True]
boolvec.filter Filter_Condition.Is_True . should_equal [True, True]
boolvec.filter Filter_Condition.Is_False . should_equal [False]
txtvec = alter ["abab", "baaa", Nothing, "cccc", "BAAA"]
txtvec.filter (Filter_Condition.Equal_Ignore_Case "baaA") . should_equal ["baaa", "BAAA"]
txtvec.filter (Filter_Condition.Contains "a") . should_equal ["abab", "baaa"]
txtvec.filter (Filter_Condition.Starts_With "a") . should_equal ["abab"]
txtvec.filter (Filter_Condition.Ends_With "a") . should_equal ["baaa"]
txtvec.filter (Filter_Condition.Like "b%a") . should_equal ["baaa"]
# Nothing is not included in the negation either
txtvec.filter (Filter_Condition.Not_Like "b%a") . should_equal ["abab", "cccc", "BAAA"]
(alter ["a", 2, Nothing, 3]).filter (Filter_Condition.Is_In [Nothing, 2]) . should_equal [2, Nothing]
Test.specify "should have a friendly error when missing Filter_Condition arguments" <|
v = alter [0, 1, 2]