From e041366758b0da3753f07d61a2b23c8261f59c16 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Fri, 7 Aug 2026 02:53:28 -0700 Subject: [PATCH 1/2] Demonstrate FAMILY_FIRST on a name the constant actually decides The example used "Nguyen Van Minh", which could not distinguish FAMILY_FIRST from FAMILY_FIRST_GIVEN_LAST: "Van" collides with the Dutch particle "van", so the vocabulary layer joins it forward before the positional layer sees three tokens, and both orders return family='Nguyen' given='Van Minh' with an empty middle. The block was demonstrating particle-joining rather than name_order, and reported the Vietnamese given name as "Van Minh" rather than "Minh". It also contradicted modules.rst, which names Vietnamese as the exemplar for FAMILY_FIRST_GIVEN_LAST rather than for FAMILY_FIRST. Switch to a Hungarian name, which is what modules.rst claims for this constant and which has no ambiguous-particle collision, and show the middle field so the assignment order is visible. Co-Authored-By: Claude Opus 5 --- docs/customize.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/customize.rst b/docs/customize.rst index a34341b..942c4b1 100644 --- a/docs/customize.rst +++ b/docs/customize.rst @@ -296,20 +296,20 @@ without going through a locale pack -- call :meth:`Policy.patched() Family-first name order ~~~~~~~~~~~~~~~~~~~~~~~~ -``name_order`` is the one most likely to matter for non-Western data. -Positional input is assigned in the order you declare, so a -family-first name parses as written instead of needing to be -rearranged afterwards: +``name_order`` is the one most likely to matter for data that is not +in Western order. Positional input is assigned in the order you +declare, so a name written family-first — Hungarian, here — parses as +written instead of needing to be rearranged afterwards: .. doctest:: >>> from nameparser import Parser, Policy, FAMILY_FIRST, parse - >>> parse("Nguyen Van Minh").family # default GIVEN_FIRST - 'Van Minh' + >>> parse("Nagy Laszlo Peter").family # default GIVEN_FIRST + 'Peter' >>> family_first = Parser(policy=Policy(name_order=FAMILY_FIRST)) - >>> name = family_first.parse("Nguyen Van Minh") - >>> name.family, name.given - ('Nguyen', 'Van Minh') + >>> name = family_first.parse("Nagy Laszlo Peter") + >>> name.family, name.given, name.middle + ('Nagy', 'Laszlo', 'Peter') An explicit comma still wins, on the reasoning that someone who wrote one meant it — so the same parser reads ``"Thomas, John"`` as From 7050f1e0e27e252b3035900c20c433ba947c0931 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Fri, 7 Aug 2026 02:54:48 -0700 Subject: [PATCH 2/2] Document FAMILY_FIRST_GIVEN_LAST in the customize guide The constant was exported and reference-documented in modules.rst, but nothing in the guide showed what it does to a name -- the only worked examples lived in the test suite. Since it is keyed to no script and has no locale pack, an explicit Policy is the only way to reach it, which makes the guide the one place a reader could have found it. Extend the existing name-order section rather than adding a sibling: the constant only means anything in contrast with FAMILY_FIRST, and a separate section would duplicate the comma-precedence paragraph. Also record the particle collision that made the old example inert, as prose plus a unit test rather than a second doctest block: "Nguyen Van Minh" parses identically under both family-first orders because "Van" is the ambiguous Dutch particle. Co-Authored-By: Claude Opus 5 --- docs/customize.rst | 31 +++++++++++++++++++++++++++++++ tests/v2/test_parser.py | 10 ++++++++++ 2 files changed, 41 insertions(+) diff --git a/docs/customize.rst b/docs/customize.rst index 942c4b1..115f2b0 100644 --- a/docs/customize.rst +++ b/docs/customize.rst @@ -320,6 +320,37 @@ family-then-given regardless of the configured order: >>> family_first.parse("Thomas, John").family 'Thomas' +A Vietnamese full name needs a third order. It is written family, then +middle, then given — the name a person is actually called by is the +*last* word, not the second. Family-first order gets the family name +right and then reverses the remaining two, so +``FAMILY_FIRST_GIVEN_LAST`` exists for the names that read this way: + +.. doctest:: + + >>> from nameparser import FAMILY_FIRST_GIVEN_LAST + >>> family_first.parse("Tran Quoc Toan").given # FAMILY_FIRST + 'Quoc' + >>> given_last = Parser(policy=Policy(name_order=FAMILY_FIRST_GIVEN_LAST)) + >>> viet = given_last.parse("Tran Quoc Toan") + >>> viet.family, viet.middle, viet.given + ('Tran', 'Quoc', 'Toan') + +Nothing keys this order to a script the way the East Asian defaults +below do — Vietnamese is written in the Latin alphabet, which carries +no order of its own — so it applies only where you set it, and there +is no ``vn`` locale pack yet (issue `#146 +`_). + +One caution, which is why the example above is not the more obvious +``"Nguyen Van Minh"``: a middle word that is also a shipped particle +is claimed by the vocabulary layer before ``name_order`` is consulted +at all. ``Van`` is the Dutch particle ``van``, so that name reads +family ``Nguyen`` with ``Van Minh`` given under *both* family-first +orders, and the choice between them makes no difference. `Words that +are also ordinary names`_ covers dropping such a word from the +vocabulary. + East Asian defaults, and turning them off ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/v2/test_parser.py b/tests/v2/test_parser.py index 7bfd5e5..6cfb7cf 100644 --- a/tests/v2/test_parser.py +++ b/tests/v2/test_parser.py @@ -147,6 +147,16 @@ def test_family_first_given_last_places_middle_between() -> None: assert (pn.family, pn.middle, pn.given) == ("Zeng", "Xiao", "Long") +def test_ambiguous_particle_middle_defeats_both_family_first_orders() -> None: + # the vocabulary layer joins the ambiguous particle "van" forward + # before the positional layer runs, so a name whose middle word + # collides with it parses identically under either family-first + # order -- the caution in customize.rst rests on this + for order in (FAMILY_FIRST, FAMILY_FIRST_GIVEN_LAST): + pn = Parser(policy=Policy(name_order=order)).parse("Nguyen Van Minh") + assert (pn.family, pn.middle, pn.given) == ("Nguyen", "", "Van Minh") + + def test_multiple_unbalanced_delimiters_each_reported() -> None: # T4: the extract scan continues past the first unmatched opener; # each one is reported and treated as literal text