Skip to content

Commit c4c7a07

Browse files
authored
Add ignore_accents option to by_name() (#103)
* Add ignore_accents option to by_name() by_name() gains an ignore_accents parameter (default False). When True, accents and other diacritical marks are stripped before matching, The generated modules import unicodedata for this. * Rename by_name() test to match the test_pr convention * Move unicodedata import into remove_accents() * Handle special letters in by_name() ignore_accents remove_accents() only stripped accents that Unicode can split off from their base letter (NFD normalization), so letters with no such decomposition (ß, œ, æ, ø, ł, đ, etc.) passed through unchanged. Add an explicit translation map, applied after the NFD strip so composed forms (e.g. "ǿ") are also handled correctly. Updated test.
1 parent 1c7d100 commit c4c7a07

2 files changed

Lines changed: 66 additions & 3 deletions

File tree

pipeline/src/additional_methods/by_name.py.txt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
match: str = "equals",
1010
all: bool = False,
1111
case_sensitive: bool = True,
12+
ignore_accents: bool = False,
1213
):
1314
"""
1415
Search for instances in the openMINDS instance library based on their name.
@@ -24,6 +25,10 @@
2425
(the given string contains the name-like property).
2526
all (bool, optional): Whether to return all objects that match the name, or only the first. Defaults to False.
2627
case_sensitive (bool, optional): Whether the search should be case-sensitive. Defaults to True.
28+
ignore_accents (bool, optional): Whether to ignore accents (acute, grave, circumflex) and
29+
other diacritical marks (cedilla, tilde, ring, etc.) when matching. Also treat
30+
special letters (ß, œ, æ, ø, ł, etc.) as their closest plain-letter equivalents
31+
(e.g. "ß" as "ss"). Defaults to False.
2732
"""
2833
namelike_properties = ("name", "lookup_label", "family_name", "full_name", "short_name", "abbreviation")
2934
if cls._instance_lookup is None:
@@ -43,16 +48,39 @@
4348
else:
4449
cls._instance_lookup[key] = [instance]
4550

51+
def remove_accents(s):
52+
import unicodedata
53+
54+
special = str.maketrans({
55+
"Ł": "L", "ł": "l",
56+
"Ø": "O", "ø": "o",
57+
"Đ": "D", "đ": "d",
58+
"Ð": "D", "ð": "d",
59+
"Þ": "Th", "þ": "th",
60+
"Æ": "AE", "æ": "ae",
61+
"Œ": "OE", "œ": "oe",
62+
"ß": "ss", "ẞ": "SS",
63+
"Ə": "E", "ə": "e",
64+
"ı": "i",
65+
})
66+
nfd_form = unicodedata.normalize("NFD", s)
67+
stripped = "".join(c for c in nfd_form if not unicodedata.combining(c))
68+
return stripped.translate(special)
69+
4670
def normalize(s):
47-
return s if case_sensitive else s.casefold()
71+
if not case_sensitive:
72+
s = s.casefold()
73+
if ignore_accents:
74+
s = remove_accents(s)
75+
return s
4876

4977
if match == "equals":
50-
if case_sensitive:
78+
if case_sensitive and not ignore_accents:
5179
matches = cls._instance_lookup.get(name, [])
5280
else:
5381
matches = []
5482
for key, instances in cls._instance_lookup.items():
55-
if key.casefold() == name.casefold():
83+
if normalize(key) == normalize(name):
5684
matches.extend(instances)
5785
elif match == "contains":
5886
matches = []

pipeline/tests/test_regressions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,3 +728,38 @@ def test_pr0100_by_name_match_within(om):
728728
# but none of those full names is itself a substring of "Macaca".
729729
assert Species.by_name("Macaca", match="contains", all=True) is not None
730730
assert Species.by_name("Macaca", match="within", all=True) is None
731+
732+
733+
@pytest.mark.parametrize("om", [openminds.latest])
734+
def test_pr0103_by_name_ignore_accents(om):
735+
# https://github.com/openMetadataInitiative/openMINDS_Python/pull/103
736+
# by_name(..., ignore_accents=True) strips accents/diacritics (Unicode NFD) before matching
737+
SovereignState = om.controlled_terms.SovereignState
738+
739+
# (query, case_sensitive, ignore_accents, should match France)
740+
cases = [
741+
("République française", True, False, True), # exact
742+
("Republique francaise", True, True, True), # accents differ
743+
("république française", False, False, True), # case differs
744+
("republique francaise", False, True, True), # case and accents differ
745+
("republique francaise", True, False, False), # defaults: neither absorbed
746+
("Republique francaise", True, False, False), # accents still matter
747+
("république française", True, True, False), # case still matters
748+
]
749+
for query, case_sensitive, ignore_accents, should_match in cases:
750+
match = SovereignState.by_name(query, case_sensitive=case_sensitive, ignore_accents=ignore_accents)
751+
assert (match is not None and match.name == "France") == should_match
752+
753+
# ignore_accents also has to map special letters
754+
special_letter_cases = [
755+
# (query, ignore_accents, expected_country_or_None)
756+
("Azərbaycan Respublikası", False, "Azerbaijan"), # exact
757+
("Azerbaycan Respublikasi", True, "Azerbaijan"),
758+
("Azerbaycan Respublikasi", False, None),
759+
("Wááshindoon Bikéyah Ałhidadiidzooígíí", False, "United States"), # exact
760+
("Waashindoon Bikeyah Alhidadiidzooigii", True, "United States"),
761+
("Waashindoon Bikeyah Alhidadiidzooigii", False, None),
762+
]
763+
for query, ignore_accents, expected_name in special_letter_cases:
764+
match = SovereignState.by_name(query, ignore_accents=ignore_accents)
765+
assert (match.name if match else None) == expected_name

0 commit comments

Comments
 (0)