Skip to content

Commit ebb61dc

Browse files
derek73claude
andauthored
Read Никитич and Саввич as patronymics under locales.RU (#994) (#996)
The masculine patronymics of Никита and Савва fit neither -ович nor -евич, so `Петров Иван Никитич` under locales.RU read family 'Никитич'. Both join the irregular forms listed whole beside Ильич and Кузьмич, in Cyrillic and Latin, in all three copies of the pattern (_vocab, the config regexes and the RU pack's declared-deviation predicate). Their feminines already matched -ична. Closes #994 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b2b7594 commit ebb61dc

8 files changed

Lines changed: 27 additions & 6 deletions

File tree

‎docs/design/decisions/O1-east-slavic-rotation.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Open: none in this file.
2121
Measured 2026-09-07 with `parser_for(locales.RU, base=Parser(policy=Policy(name_order=FAMILY_FIRST)))`. The family-first listing reads the same either way: `Сидоров Иван Петрович` gives family `Сидоров`, given `Иван`, middle `Петрович` under the default order by rotation, and the identical parse under FAMILY_FIRST by position. The divergence is on natural-order input — `Иван Петрович Сидоров` gives family `Сидоров` by rotation under the default order, and family `Иван`, given `Петрович`, middle `Сидоров` under FAMILY_FIRST. That row is ACCEPTED, not a defect: the caller said this source is written family-first and is being taken at their word. rules/O1 carries it as an Accepted consequence.
2222
O2 is the same decision on the Turkic marker and its 2026-09-07 entry says so. The shapes differ, which is why the two rules state their own consequences rather than sharing one: under FAMILY_FIRST `Ali Ahmad Vali oglu` reads family `Ali` exactly as the rotation would, and it is under FAMILY_FIRST_GIVEN_LAST that it reads given `oglu`, middle `Ahmad Vali` — the marker in a name field, which is O2's existing Accepted consequence reached by a second route. Both readings are pinned in tests/v2/test_locales.py beside O1's row (measured 2026-09-07).
2323
Where the row is pinned, and why not in rules.md. The example grammar admits ONE annotation, and `tests/v2/rules_doc.py`'s `resolve_annotation` resolves it to a policy, a locale or a gated locale — never a pair. Composite keys exist for other pairings (`family-first+middle_as_family`, `[ja+segmenter]`), but no registered annotation combines a pack with an order, so the two cannot be written on the same example line. Registering a combined policy would be a test change made inside a docs decision. #470 asked whether an example line should declare its format and was closed not-planned on 2026-09-02 — "no notation is needed. The Accepted clause is already the right home for this" — so prose in the rule is the decided home here, not a workaround. The parse is a `tests/v2/test_locales.py` row beside the pack's other rows, and rules/O1 states it in prose.
24+
- 2026-11-02 #994 — `Никитич` and `Саввич` (Latin `Nikitich`, `Savvich`) join the irregular forms the patronymic ending lists whole beside `Ильич` and `Кузьмич`: the masculine patronymics of Никита and Савва, which fit neither `-ович` nor `-евич`, so `Петров Иван Никитич` under `locales.RU` read family 'Никитич' and now reads family 'Петров', given 'Иван'. Their feminines already matched `-ична`. Found by scoring the tree against datacoon/russiannames' 61,466 labelled Cyrillic names (`parser_for(locales.RU)` matched 58,011 of them on surname, given name and patronymic, 2026-11-02 at db7e8fc4); that project's patronymic list gives Никитич a count of 1,225 and Саввич 160. Excluded: the spelling `Кузмич` (44 in the same list); the rest of that list's unmatched tail is typos (`Николавна`, `Владимирвич`).
2425

2526
Declined:
2627

‎docs/release_log.rst‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Release Log
3838
- **Fix British post-nominals read as the family name.** ``parse("Jane Doe KC")`` gives family ``Doe``, suffix ``KC``, where 1.4.0 through 2.8.0 gave middle ``Doe``, family ``KC``, and ``parse("Jane Doe, CChem")`` no longer reverses the name around the credential. ``KC``, ``PC``, ``DL``, ``FRS``, ``LLB``, ``CChem``, ``FRAeS`` and 16 more join the vocabulary. ``KC`` is also a Nepali surname, so ``Ram KC`` keeps it as the family name, as do ``K.C.`` and ``Kc``, while ``Gita Bahadur KC`` reads it as the suffix, and ``PC`` and ``DL`` keep a family name the same way (``Anna DL``); ``FIET`` and ``FREng`` keep the surnames Fiet and Freng the same way, so a bare ``Jane Doe FREng`` still gives family ``FREng``. :issue:`965`
3939
- **Fix Policy(name_order=...) accepting a set.** ``Policy(name_order=frozenset(FAMILY_FIRST))`` raises ``TypeError``, where 2.0.0 through 2.8.0 accepted it as an order under some hash seeds and raised ``ValueError`` under others; pass the tuple. :issue:`754`
4040
- **Divide five more mainland Chinese surnames under locales.ZH.** ``付晓明`` under ``locales.ZH`` gives family ``付``, given ``晓明``, where 2.1.0 through 2.8.0 gave family ``付晓明``; ``代``, ``候``, ``芦`` (``蘆``) and ``农`` (``農``) divide too. A Japanese name opening with ``代`` or ``芦`` now splits after it (``芦川誠`` gives family ``芦``). :issue:`952`
41+
- **Read Никитич and Саввич as patronymics under locales.RU.** ``parser_for(locales.RU).parse("Петров Иван Никитич")`` gives family ``Петров``, given ``Иван``, where 2.0.0 through 2.8.0 gave family ``Никитич``, given ``Петров``; ``Саввич`` and the Latin ``Nikitich`` and ``Savvich`` read the same way. :issue:`994`
4142

4243
* 2.8.0 - October 28, 2026
4344

‎nameparser/_pipeline/_vocab.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,10 @@ def is_initial_shaped(text: str) -> bool:
282282
# the standalone Turkic patronymic markers, which rules/O1's and O2's
283283
# rotations and rules/R9's reading ask through the two predicates below.
284284
_EAST_SLAVIC = re.compile(
285-
r"(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich)$",
285+
r"(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich|nikitich|savvich)$",
286286
re.I)
287287
_EAST_SLAVIC_CYR = re.compile(
288-
r"(ович|овна|евич|евна|ична|ильич|кузьмич|лукич|фомич|фокич)$",
288+
r"(ович|овна|евич|евна|ична|ильич|кузьмич|лукич|фомич|фокич|никитич|саввич)$",
289289
re.I)
290290
_TURKIC = re.compile(
291291
r"^(oglu|oğlu|ogly|ogli|o['’ʻ]g['’ʻ]li"

‎nameparser/config/regexes.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@
4444
# trailing comma before parsing (#265).
4545
"commas": re.compile(r'[,،,]'),
4646
"east_slavic_patronymic": re.compile(
47-
r'(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich)$',
47+
r'(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich|nikitich|savvich)$',
4848
re.I,
4949
),
5050
"east_slavic_patronymic_cyrillic": re.compile(
51-
r'(ович|овна|евич|евна|ична|ильич|кузьмич|лукич|фомич|фокич)$',
51+
r'(ович|овна|евич|евна|ична|ильич|кузьмич|лукич|фомич|фокич|никитич|саввич)$',
5252
re.I,
5353
),
5454
"turkic_patronymic_marker": re.compile(

‎nameparser/locales/ru.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@
4141
# gate -- layering forbids importing the pipeline from a pack). Keep
4242
# both alternations byte-identical to those two patterns.
4343
_EAST_SLAVIC = re.compile(
44-
r"(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich)$",
44+
r"(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich|nikitich|savvich)$",
4545
re.I)
4646
_EAST_SLAVIC_CYR = re.compile(
47-
r"(ович|овна|евич|евна|ична|ильич|кузьмич|лукич|фомич|фокич)$", re.I)
47+
r"(ович|овна|евич|евна|ична|ильич|кузьмич|лукич|фомич|фокич|никитич|саввич)$", re.I)
4848

4949

5050
def DEVIATES(name: str) -> bool:

‎tests/test_east_slavic_patronymic_order.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,19 @@ def test_cyrillic_capitalized_irregular_form(self) -> None:
9292
assert n.middle == "Ильич"
9393
assert n.family == "Иванов"
9494

95+
def test_irregular_forms_of_nikita_and_savva(self) -> None:
96+
# Никитич and Саввич fit neither -ович nor -евич, so they are
97+
# listed whole like Ильич (#994); their feminines end in -ична.
98+
for text, patronymic in [
99+
("Петров Иван Никитич", "Никитич"),
100+
("Петров Иван Саввич", "Саввич"),
101+
("Petrov Ivan Nikitich", "Nikitich"),
102+
("Petrov Ivan Savvich", "Savvich"),
103+
]:
104+
n = self.hn(text)
105+
assert (n.given, n.middle, n.family) == (
106+
text.split()[1], patronymic, text.split()[0]), text
107+
95108
def test_title_preserved(self) -> None:
96109
n = self.hn("Dr. Ivanov Ivan Ivanovich")
97110
assert n.title == "Dr."

‎tests/test_turkic_patronymic_order.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ def test_no_regex_collision_latin(self) -> None:
274274
east_slavic_examples = [
275275
"Ivanovich", "Ivanovna", "Sergeevich", "Sergeevna",
276276
"Nikitichna", "Ilyich", "Kuzmich", "Lukich", "Fomich", "Fokich",
277+
"Nikitich", "Savvich",
277278
]
278279
turkic_examples = [
279280
"oglu", "oğlu", "ogly", "ogli", "o'g'li",
@@ -292,6 +293,7 @@ def test_no_regex_collision_cyrillic(self) -> None:
292293
east_slavic_examples = [
293294
"Иванович", "Ивановна", "Сергеевич", "Сергеевна",
294295
"Никитична", "Ильич", "Кузьмич", "Лукич", "Фомич", "Фокич",
296+
"Никитич", "Саввич",
295297
]
296298
turkic_examples = [
297299
"оглу", "оглы", "оғлу", "ўғли", "угли",

‎tests/v2/test_locales.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,8 @@ def _default_corpus() -> list[str]:
10631063
"Titov Pyotr Lukich", # lukich
10641064
"Orlov Semyon Fomich", # fomich
10651065
"Zaitsev Andrei Fokich", # fokich
1066+
"Pronin Ivan Nikitich", # nikitich
1067+
"Mamontov Ivan Savvich", # savvich
10661068
"Сидоров Иван Петрович", # ович
10671069
"Сидорова Анна Петровна", # овна
10681070
"Карпов Олег Сергеевич", # евич
@@ -1073,6 +1075,8 @@ def _default_corpus() -> list[str]:
10731075
"Титов Пётр Лукич", # лукич
10741076
"Орлов Семён Фомич", # фомич
10751077
"Зайцев Андрей Фокич", # фокич
1078+
"Пронин Иван Никитич", # никитич
1079+
"Мамонтов Иван Саввич", # саввич
10761080
]
10771081
_ROTATORS["tr_az"] = [
10781082
"Aliyev Ali Vali oglu", # oglu

0 commit comments

Comments
 (0)