Skip to content

Commit a5c8f62

Browse files
authored
Use casefold to compare strings case insensitively (#184)
1 parent e480b88 commit a5c8f62

File tree

3 files changed

+98
-4
lines changed

3 files changed

+98
-4
lines changed

posthog/feature_flags.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from dateutil import parser
88
from dateutil.relativedelta import relativedelta
99

10+
from posthog import utils
1011
from posthog.utils import convert_to_datetime_aware, is_valid_regex
1112

1213
__LONG_SCALE__ = float(0xFFFFFFFFFFFFFFF)
@@ -129,8 +130,8 @@ def match_property(property, property_values) -> bool:
129130

130131
def compute_exact_match(value, override_value):
131132
if isinstance(value, list):
132-
return str(override_value).lower() in [str(val).lower() for val in value]
133-
return str(value).lower() == str(override_value).lower()
133+
return str(override_value).casefold() in [str(val).casefold() for val in value]
134+
return utils.str_iequals(value, override_value)
134135

135136
if operator == "exact":
136137
return compute_exact_match(value, override_value)
@@ -141,10 +142,10 @@ def compute_exact_match(value, override_value):
141142
return key in property_values
142143

143144
if operator == "icontains":
144-
return str(value).lower() in str(override_value).lower()
145+
return utils.str_icontains(override_value, value)
145146

146147
if operator == "not_icontains":
147-
return str(value).lower() not in str(override_value).lower()
148+
return not utils.str_icontains(override_value, value)
148149

149150
if operator == "regex":
150151
return is_valid_regex(str(value)) and re.compile(str(value)).search(str(override_value)) is not None

posthog/test/test_feature_flags.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,59 @@ def test_flag_person_properties(self, patch_get):
6868
self.assertTrue(feature_flag_match)
6969
self.assertFalse(not_feature_flag_match)
7070

71+
def test_case_insensitive_matching(self):
72+
self.client.feature_flags = [
73+
{
74+
"id": 1,
75+
"name": "Beta Feature",
76+
"key": "person-flag",
77+
"is_simple_flag": True,
78+
"active": True,
79+
"filters": {
80+
"groups": [
81+
{
82+
"properties": [
83+
{
84+
"key": "location",
85+
"operator": "exact",
86+
"value": ["Straße"],
87+
"type": "person",
88+
}
89+
],
90+
"rollout_percentage": 100,
91+
},
92+
{
93+
"properties": [
94+
{
95+
"key": "star",
96+
"operator": "exact",
97+
"value": ["ſun"],
98+
"type": "person",
99+
}
100+
],
101+
"rollout_percentage": 100,
102+
},
103+
],
104+
},
105+
}
106+
]
107+
108+
self.assertTrue(
109+
self.client.get_feature_flag("person-flag", "some-distinct-id", person_properties={"location": "straße"})
110+
)
111+
112+
self.assertTrue(
113+
self.client.get_feature_flag("person-flag", "some-distinct-id", person_properties={"location": "strasse"})
114+
)
115+
116+
self.assertTrue(
117+
self.client.get_feature_flag("person-flag", "some-distinct-id", person_properties={"star": "ſun"})
118+
)
119+
120+
self.assertTrue(
121+
self.client.get_feature_flag("person-flag", "some-distinct-id", person_properties={"star": "sun"})
122+
)
123+
71124
@mock.patch("posthog.client.decide")
72125
@mock.patch("posthog.client.get")
73126
def test_flag_group_properties(self, patch_get, patch_decide):

posthog/utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,43 @@ def convert_to_datetime_aware(date_obj):
125125
if date_obj.tzinfo is None:
126126
date_obj = date_obj.replace(tzinfo=timezone.utc)
127127
return date_obj
128+
129+
130+
def str_icontains(source, search):
131+
"""
132+
Check if a string contains another string, ignoring case.
133+
134+
Args:
135+
source: The string to search within
136+
search: The substring to search for
137+
138+
Returns:
139+
bool: True if search is a substring of source (case-insensitive), False otherwise
140+
141+
Examples:
142+
>>> str_icontains("Hello World", "WORLD")
143+
True
144+
>>> str_icontains("Hello World", "python")
145+
False
146+
"""
147+
return str(search).casefold() in str(source).casefold()
148+
149+
150+
def str_iequals(value, comparand):
151+
"""
152+
Check if a string equals another string, ignoring case.
153+
154+
Args:
155+
value: The string to compare
156+
comparand: The string to compare with
157+
158+
Returns:
159+
bool: True if value and comparand are equal (case-insensitive), False otherwise
160+
161+
Examples:
162+
>>> str_iequals("Hello World", "hello world")
163+
True
164+
>>> str_iequals("Hello World", "hello")
165+
False
166+
"""
167+
return str(value).casefold() == str(comparand).casefold()

0 commit comments

Comments
 (0)