Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Change string rules to be case insensitive. #55

Merged
merged 2 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions actual/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import datetime
import enum
import re
import typing
import unicodedata

import pydantic

Expand All @@ -12,6 +14,14 @@
from actual.schedules import Schedule


def get_normalized_string(value: str) -> str:
"""Normalization of string for comparison. Uses lowercase and Canonical Decomposition.

See https://github.com/actualbudget/actual/blob/a22160579d6e1f7a17213561cec79c321a14525b/packages/loot-core/src/shared/normalisation.ts
"""
return unicodedata.normalize("NFD", value.lower())


class ConditionType(enum.Enum):
IS = "is"
IS_APPROX = "isapprox"
Expand All @@ -25,6 +35,7 @@ class ConditionType(enum.Enum):
DOES_NOT_CONTAIN = "doesNotContain"
NOT_ONE_OF = "notOneOf"
IS_BETWEEN = "isbetween"
MATCHES = "matches"


class ActionType(enum.Enum):
Expand Down Expand Up @@ -67,7 +78,7 @@ def is_valid(self, operation: ConditionType) -> bool:
if self == ValueType.DATE:
return operation.value in ("is", "isapprox", "gt", "gte", "lt", "lte")
elif self == ValueType.STRING:
return operation.value in ("is", "contains", "oneOf", "isNot", "doesNotContain", "notOneOf")
return operation.value in ("is", "contains", "oneOf", "isNot", "doesNotContain", "notOneOf", "matches")
elif self == ValueType.ID:
return operation.value in ("is", "isNot", "oneOf", "notOneOf")
elif self == ValueType.NUMBER:
Expand Down Expand Up @@ -130,6 +141,11 @@ def get_value(
return datetime.datetime.strptime(str(value), "%Y%m%d").date()
elif value_type is ValueType.BOOLEAN:
return int(value) # database accepts 0 or 1
elif value_type is ValueType.STRING:
if isinstance(value, list):
return [get_value(v, value_type) for v in value]
else:
return get_normalized_string(value)
return value


Expand Down Expand Up @@ -173,7 +189,9 @@ def condition_evaluation(
elif op == ConditionType.ONE_OF:
return true_value in self_value
elif op == ConditionType.CONTAINS:
return self_value in true_value
return get_normalized_string(self_value) in get_normalized_string(true_value)
elif op == ConditionType.MATCHES:
return bool(re.match(self_value, true_value, re.IGNORECASE))
elif op == ConditionType.NOT_ONE_OF:
return true_value not in self_value
elif op == ConditionType.DOES_NOT_CONTAIN:
Expand Down
11 changes: 11 additions & 0 deletions tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,19 @@ def test_string_condition():
assert Condition(field="notes", op="notOneOf", value=["foo", "bar"]).run(t) is False
assert Condition(field="notes", op="contains", value="fo").run(t) is True
assert Condition(field="notes", op="contains", value="foobar").run(t) is False
assert Condition(field="notes", op="matches", value="f.*").run(t) is True
assert Condition(field="notes", op="matches", value="g.*").run(t) is False
assert Condition(field="notes", op="doesNotContain", value="foo").run(t) is False
assert Condition(field="notes", op="doesNotContain", value="foobar").run(t) is True
# test the cases where the case do not match
assert Condition(field="notes", op="oneOf", value=["FOO", "BAR"]).run(t) is True
assert Condition(field="notes", op="notOneOf", value=["FOO", "BAR"]).run(t) is False
assert Condition(field="notes", op="contains", value="FO").run(t) is True
assert Condition(field="notes", op="contains", value="FOOBAR").run(t) is False
assert Condition(field="notes", op="matches", value="F.*").run(t) is True
assert Condition(field="notes", op="matches", value="G.*").run(t) is False
assert Condition(field="notes", op="doesNotContain", value="FOO").run(t) is False
assert Condition(field="notes", op="doesNotContain", value="FOOBAR").run(t) is True


def test_numeric_condition():
Expand Down
Loading