Skip to content

Commit

Permalink
Handle Nothing values in Filter_Condition.to_predicate (#8600)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
radeusgd authored Dec 21, 2023
1 parent d41d48e commit b3de42e
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions test/Table_Tests/src/In_Memory/Table_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -865,11 +865,12 @@ 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)

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"
t2.filter "Y" (Filter_Condition.Is_In in_vector) . at "Y" . to_vector . should_equal expected_neg_vector
t2.filter "Y" (Filter_Condition.Is_In in_column) . at "Y" . to_vector . should_equal expected_neg_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"
t2.filter "Y" (Filter_Condition.Is_In in_vector) . at "Y" . to_vector . should_equal expected_neg_vector
t2.filter "Y" (Filter_Condition.Is_In in_column) . at "Y" . to_vector . should_equal expected_neg_vector

Test.group "[In-Memory-specific] Table.join" <|
Test.specify "should correctly report unsupported cross-backend joins" <|
Expand Down
3 changes: 1 addition & 2 deletions test/Tests/src/Data/List_Spec.enso
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions test/Tests/src/Data/Range_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -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 []
Expand Down
38 changes: 31 additions & 7 deletions test/Tests/src/Data/Vector_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 []
Expand Down Expand Up @@ -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]

Expand Down

0 comments on commit b3de42e

Please sign in to comment.