diff --git a/pipeline/src/additional_methods/by_name.py.txt b/pipeline/src/additional_methods/by_name.py.txt index a7515ee3..5bbfef33 100644 --- a/pipeline/src/additional_methods/by_name.py.txt +++ b/pipeline/src/additional_methods/by_name.py.txt @@ -9,6 +9,7 @@ match: str = "equals", all: bool = False, case_sensitive: bool = True, + ignore_accents: bool = False, ): """ Search for instances in the openMINDS instance library based on their name. @@ -24,6 +25,10 @@ (the given string contains the name-like property). all (bool, optional): Whether to return all objects that match the name, or only the first. Defaults to False. case_sensitive (bool, optional): Whether the search should be case-sensitive. Defaults to True. + ignore_accents (bool, optional): Whether to ignore accents (acute, grave, circumflex) and + other diacritical marks (cedilla, tilde, ring, etc.) when matching. Also treat + special letters (ß, œ, æ, ø, ł, etc.) as their closest plain-letter equivalents + (e.g. "ß" as "ss"). Defaults to False. """ namelike_properties = ("name", "lookup_label", "family_name", "full_name", "short_name", "abbreviation") if cls._instance_lookup is None: @@ -43,16 +48,39 @@ else: cls._instance_lookup[key] = [instance] + def remove_accents(s): + import unicodedata + + special = str.maketrans({ + "Ł": "L", "ł": "l", + "Ø": "O", "ø": "o", + "Đ": "D", "đ": "d", + "Ð": "D", "ð": "d", + "Þ": "Th", "þ": "th", + "Æ": "AE", "æ": "ae", + "Œ": "OE", "œ": "oe", + "ß": "ss", "ẞ": "SS", + "Ə": "E", "ə": "e", + "ı": "i", + }) + nfd_form = unicodedata.normalize("NFD", s) + stripped = "".join(c for c in nfd_form if not unicodedata.combining(c)) + return stripped.translate(special) + def normalize(s): - return s if case_sensitive else s.casefold() + if not case_sensitive: + s = s.casefold() + if ignore_accents: + s = remove_accents(s) + return s if match == "equals": - if case_sensitive: + if case_sensitive and not ignore_accents: matches = cls._instance_lookup.get(name, []) else: matches = [] for key, instances in cls._instance_lookup.items(): - if key.casefold() == name.casefold(): + if normalize(key) == normalize(name): matches.extend(instances) elif match == "contains": matches = [] diff --git a/pipeline/tests/test_regressions.py b/pipeline/tests/test_regressions.py index a2d34ca8..b0481cf2 100644 --- a/pipeline/tests/test_regressions.py +++ b/pipeline/tests/test_regressions.py @@ -728,3 +728,38 @@ def test_pr0100_by_name_match_within(om): # but none of those full names is itself a substring of "Macaca". assert Species.by_name("Macaca", match="contains", all=True) is not None assert Species.by_name("Macaca", match="within", all=True) is None + + +@pytest.mark.parametrize("om", [openminds.latest]) +def test_pr0103_by_name_ignore_accents(om): + # https://github.com/openMetadataInitiative/openMINDS_Python/pull/103 + # by_name(..., ignore_accents=True) strips accents/diacritics (Unicode NFD) before matching + SovereignState = om.controlled_terms.SovereignState + + # (query, case_sensitive, ignore_accents, should match France) + cases = [ + ("République française", True, False, True), # exact + ("Republique francaise", True, True, True), # accents differ + ("république française", False, False, True), # case differs + ("republique francaise", False, True, True), # case and accents differ + ("republique francaise", True, False, False), # defaults: neither absorbed + ("Republique francaise", True, False, False), # accents still matter + ("république française", True, True, False), # case still matters + ] + for query, case_sensitive, ignore_accents, should_match in cases: + match = SovereignState.by_name(query, case_sensitive=case_sensitive, ignore_accents=ignore_accents) + assert (match is not None and match.name == "France") == should_match + + # ignore_accents also has to map special letters + special_letter_cases = [ + # (query, ignore_accents, expected_country_or_None) + ("Azərbaycan Respublikası", False, "Azerbaijan"), # exact + ("Azerbaycan Respublikasi", True, "Azerbaijan"), + ("Azerbaycan Respublikasi", False, None), + ("Wááshindoon Bikéyah Ałhidadiidzooígíí", False, "United States"), # exact + ("Waashindoon Bikeyah Alhidadiidzooigii", True, "United States"), + ("Waashindoon Bikeyah Alhidadiidzooigii", False, None), + ] + for query, ignore_accents, expected_name in special_letter_cases: + match = SovereignState.by_name(query, ignore_accents=ignore_accents) + assert (match.name if match else None) == expected_name