Skip to content

Commit 37cf758

Browse files
derek73claude
andcommitted
Document the Japanese support (#272)
usage.rst gains the Japanese story in the East Asian section, told background first: how a Japanese name is written across kanji and the two kana syllabaries, why hiragana identifies a native name and katakana does not, then the two behaviors that follow without configuration (kana-licensed family-first order, the nakaguro as a token separator), then the segmenter opt-in, then the boundaries -- pure katakana, romanized text, the two-character rule, the SEGMENTATION report's own wording, silent declines, and the CLI's inability to attach a segmenter. locales.rst gains a Segmenters section carrying the decline contract (segment_scripts unions, so a segmenter is offered every activated script's tokens and must decline what it cannot read) and the empty-lexicon pack recipe ja is the shipped example of; customize.rst states that the kana behaviors ride the two existing switches and that the middle dot rides neither; concepts.rst qualifies "parsing never raises" with the one exception a user-supplied callable creates. migrate.rst's CJK section absorbs the new shapes rather than growing a parallel one, every claim checked shape by shape against the pinned 1.4 worker: the spaced flips, the lone-token moves, both nakaguro outcomes, the nickname render, and the katakana non-change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d3e7596 commit 37cf758

7 files changed

Lines changed: 297 additions & 47 deletions

File tree

docs/concepts.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,11 @@ Honest ambiguity
146146
Parsing never raises. Pass in a string that doesn't look like a name
147147
at all, and you get back a :class:`~nameparser.ParsedName` with empty
148148
fields, not an exception. The parser's job is to make a reasonable
149-
call on real-world text, not to reject it.
149+
call on real-world text, not to reject it. The single exception is
150+
code you supplied yourself: a :data:`~nameparser.Segmenter` passed to
151+
``Parser(segmenter=...)`` runs inside the parse, and its own
152+
exceptions propagate rather than being swallowed — a failure there is
153+
a bug in your callable, not a fact about the name.
150154

151155
Some calls are irreducibly ambiguous — both readings are legitimate,
152156
and no amount of rule-tuning resolves them without breaking some other

docs/customize.rst

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,12 @@ East Asian defaults, and turning them off
307307
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
308308

309309
Two defaults key on the *script* a name is written in rather than on
310-
anything you set: a name written wholly in Han or Hangul is assigned
311-
family-first (``script_orders``), and an unspaced hangul name is split
312-
into surname and given name against the shipped Korean census list
313-
(``segment_scripts``). :ref:`east-asian-names` explains the naming
314-
conventions both rest on — this section is how to switch them off,
315-
which you can do separately:
310+
anything you set: a name written wholly in Han or Hangul — or one
311+
mixing kanji with kana — is assigned family-first (``script_orders``),
312+
and an unspaced hangul name is split into surname and given name
313+
against the shipped Korean census list (``segment_scripts``).
314+
:ref:`east-asian-names` explains the naming conventions both rest on —
315+
this section is how to switch them off, which you can do separately:
316316

317317
.. doctest::
318318

@@ -345,6 +345,18 @@ Chinese surnames are deliberately absent from that default set,
345345
because splitting Han text requires knowing Chinese from Japanese;
346346
:doc:`locales` covers the opt-in ``zh`` pack that supplies them.
347347

348+
The Japanese behaviors ride these same two fields, so they need no
349+
switches of their own: ``script_orders={}`` clears the kana-licensed
350+
entry along with the Han and Hangul ones, and ``segment_scripts=()``
351+
deactivates every script at once, which also stops a parser consulting
352+
whatever segmenter it was given. The segmenter has an off-switch as
353+
well — ``Parser(segmenter=None)``, which is the default; see
354+
:ref:`segmenter-contract` for what one is expected to do with text it
355+
does not handle. One Japanese behavior is not a policy field at all:
356+
the katakana middle dot ・ separates tokens the way a space does,
357+
decided in tokenization, so it applies however these two fields are
358+
set.
359+
348360
.. note::
349361

350362
Both fields are annotated with their canonical *storage* type

docs/locales.rst

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,18 @@ background (covered fully under :ref:`east-asian-names` in
6161
:doc:`usage`): Chinese, Japanese, and Korean names put the family name
6262
first in native script and are usually written with no space between
6363
the parts. Both defaults follow from facts the script alone
64-
establishes. A name written wholly in Han or Hangul is assigned
64+
establishes. A name written wholly in Han or Hangul — or one mixing
65+
kanji with kana, a combination only Japanese produces — is assigned
6566
family-first, because every language written in those scripts orders
66-
names that way no language guess is involved. An unspaced hangul
67+
names that way; no language guess is involved. An unspaced hangul
6768
name is additionally split into surname and given name, because hangul
6869
is written by nothing but Korean and Korean surnames are a closed
6970
census set that ships as default vocabulary. Splitting an unspaced
7071
*Han* name is the one behavior the script cannot license — the same
7172
characters could be a Chinese or a Japanese name, and a Chinese
7273
surname list splits Japanese names in the wrong place — so it is not a
73-
default: the ``zh`` pack below turns it on when you can declare the
74-
data Chinese.
74+
default: the ``zh`` and ``ja`` packs below turn it on for data whose
75+
language you can declare.
7576

7677
A pack is for something different: a *structural* rule, like reordering
7778
a patronymic, that vocabulary alone can't express.
@@ -175,6 +176,47 @@ new naming rule belongs in.
175176
mixes traditions, parse the subsets separately with different
176177
parsers rather than enabling a pack over all of it.
177178

179+
.. _segmenter-contract:
180+
181+
Segmenters
182+
-----------
183+
184+
``ja`` is policy-only in a second sense: it turns division on for
185+
Japanese text without supplying anything to divide with. That job goes
186+
to a **segmenter**, which is passed to
187+
:func:`~nameparser.parser_for` rather than carried by the pack — a
188+
:class:`~nameparser.Locale` is pure data, and a third-party callable is
189+
neither pure nor data. :func:`~nameparser.locales.ja_segmenter` is the
190+
shipped one; writing your own is worth it for any script whose
191+
divisions you know better than a surname list does.
192+
193+
A :data:`~nameparser.Segmenter` is any callable taking a token's text
194+
and returning a :class:`~nameparser.Segmentation` — the interior
195+
offsets to cut at, plus how confident you are — or ``None`` to decline,
196+
leaving the token whole. Declining is the load-bearing half of the
197+
contract, because ``segment_scripts`` unions across packs: your
198+
segmenter is offered every token of every activated script, not only
199+
the ones its own pack turned on. Recognize the text you can actually
200+
read and return ``None`` for the rest, rather than answering for a
201+
script you never meant to handle. Exceptions are the one thing that
202+
does not stay inside the parse — a segmenter is your code, so its
203+
errors propagate out of ``parse()`` instead of being absorbed as
204+
content errors.
205+
206+
The pack half of that arrangement carries no words at all, which makes
207+
it the shortest kind of pack there is:
208+
209+
.. doctest::
210+
211+
>>> from nameparser import Lexicon, Locale, PolicyPatch, Script
212+
>>> mine = Locale(code="myscript", lexicon=Lexicon.empty(),
213+
... policy=PolicyPatch(
214+
... segment_scripts=frozenset({Script.HAN})))
215+
216+
``nameparser/locales/ja.py`` is the shipped example of exactly that
217+
shape: activation is the pack's entire contribution, and a pack
218+
applied without a segmenter simply divides nothing.
219+
178220
Creating your own Locale
179221
-------------------------
180222

@@ -245,11 +287,14 @@ by ``tests/v2/test_locales.py``:
245287
needs at least one name exercising every alternation branch of every
246288
regex it defines — ``test_rotators_cover_every_marker_branch`` fails
247289
until each branch is hit. A pack declaring by *codepoint range*
248-
(``zh``) has no branches to sweep and drops out of that test, so its
249-
rotators have to carry the same weight by hand: the unspaced names
250-
the pack must split, one per shape of the vocabulary it ships —
251-
single surname, compound surname, and any spelling variant it means
252-
to cover.
290+
(``zh``, ``ja``) has no branches to sweep and drops out of that
291+
test, so its rotators have to carry the same weight by hand: the
292+
unspaced names the pack must split, one per shape of the vocabulary
293+
it ships — single surname, compound surname, and any spelling
294+
variant it means to cover. A pack that ships no vocabulary lists
295+
the shapes its *segmenter* must divide instead, and marks the
296+
rotator tests to skip when the optional dependency is absent, so
297+
the contract tests still run everywhere.
253298
#. Keep the non-interference gate green over the shared corpus plus
254299
your rotators: every name the packed parser parses differently from
255300
the default must be one your ``DEVIATES`` predicate flags — no
@@ -264,16 +309,17 @@ by ``tests/v2/test_locales.py``:
264309
language its script does not* — a Chinese surname list, which
265310
silently mangles the Japanese names written in the same characters
266311
— belongs in the pack, where asking for it is the declaration.
267-
``ru`` and ``tr_az`` need no vocabulary at all and ship an empty
268-
:class:`~nameparser.Lexicon`; ``nameparser/locales/zh.py`` is the
269-
template for one that does.
312+
``ja``, ``ru`` and ``tr_az`` need no vocabulary at all and ship an
313+
empty :class:`~nameparser.Lexicon`; ``nameparser/locales/zh.py`` is
314+
the template for one that does.
270315
#. Curate vocabulary conservatively, the same rule as
271316
:doc:`customize`: when you're unsure whether a word or a marker
272317
belongs, leave it out.
273318

274319
``nameparser/locales/ru.py`` is the reference implementation for a
275320
policy-only pack, ``nameparser/locales/zh.py`` for one that carries
276-
vocabulary. Packs still in progress are tracked in issues `#272
277-
<https://github.com/derek73/python-nameparser/issues/272>`_ (Japanese)
278-
and `#146 <https://github.com/derek73/python-nameparser/issues/146>`_
321+
vocabulary, and ``nameparser/locales/ja.py`` for one whose whole
322+
contribution is turning a stage on. Packs still in progress are
323+
tracked in issue `#146
324+
<https://github.com/derek73/python-nameparser/issues/146>`_
279325
(Vietnamese).

docs/migrate.rst

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,10 +340,11 @@ custom suffix delimiter configured, a no-space delimiter group renders
340340
whole (``"RN/CRNA"``) where 1.x split it (``"RN, CRNA"``) — the role
341341
assignment is identical, only the rendered string differs.
342342

343-
2.1 adds two more, and unlike most of the 2.0 API these do reach
344-
``HumanName``: a name written wholly in Han or Hangul is read
345-
family-first, and an unspaced Korean name is split into surname and
346-
given name.
343+
2.1 adds three more, and unlike most of the 2.0 API these do reach
344+
``HumanName``: a name written in East Asian script is read
345+
family-first, an unspaced Korean name is split into surname and given
346+
name, and the katakana middle dot separates tokens the way a space
347+
does.
347348

348349
.. doctest::
349350

@@ -377,7 +378,58 @@ Both were ``first`` under 1.4. If you feed unspaced CJK through
377378
reach you, and it is silent — the string is intact, just in the other
378379
field.
379380

381+
Japanese kana carries the same order rule, which widens both shapes
382+
past the wholly-Han text described above. A name that mixes kanji with
383+
hiragana or katakana is read family-first, and a lone kana-bearing
384+
token moves from ``first`` to ``last`` exactly as ``毛泽东`` does:
385+
386+
.. doctest::
387+
388+
>>> HumanName("高橋 みなみ").last, HumanName("高橋 みなみ").first
389+
('高橋', 'みなみ')
390+
>>> HumanName("山田 エミ").last
391+
'山田'
392+
>>> HumanName("高橋みなみ").last, HumanName("高橋みなみ").first
393+
('高橋みなみ', '')
394+
395+
1.4 read the spaced ones as ``first="高橋"``/``last="みなみ"`` and
396+
``first="山田"``/``last="エミ"``, and put the unspaced one whole in
397+
``first``. The spaced shapes change what the name renders as too:
398+
``str(HumanName("高橋 みなみ"))`` was ``"高橋 みなみ"`` and is now
399+
``"みなみ 高橋"``. A name written *wholly* in katakana is deliberately
400+
left alone — it is usually a transcribed foreign name already in
401+
given-first order — so ``HumanName("マイケル ジャクソン")`` reads
402+
``first="マイケル"``/``last="ジャクソン"`` on both versions.
403+
404+
One more shape changes for a different reason: the katakana middle dot
405+
````, which divides the parts of such a transcription, is now a token
406+
separator rather than an ordinary character. 1.4 saw one token and put
407+
it in ``first``; 2.1 sees two:
408+
409+
.. doctest::
410+
411+
>>> HumanName("マイケル・ジャクソン").first
412+
'マイケル'
413+
>>> HumanName("マイケル・ジャクソン").last
414+
'ジャクソン'
415+
>>> HumanName("高橋・一郎").last, HumanName("高橋・一郎").first
416+
('高橋', '一郎')
417+
418+
The two divide the same way and land in opposite fields, because the
419+
katakana pair keeps its source order while the kanji pair takes the
420+
family-first rule. Separating also changes the rendered string, the
421+
same way the Korean split does: the dot comes back as a space, so
422+
``str(HumanName("マイケル・ジャクソン"))`` was ``"マイケル・ジャクソン"``
423+
and is now ``"マイケル ジャクソン"``. That reaches delimited content
424+
too — the nickname in ``"山田 太郎 (マイケル・ジャクソン)"`` was
425+
``"マイケル・ジャクソン"`` under 1.4 and is ``"マイケル ジャクソン"``
426+
now.
427+
380428
``Constants`` has no switch for any of this — the v1 configuration
381429
surface is frozen for 2.x — so the way out is the 2.0 API:
382430
``Parser(policy=Policy(script_orders={}, segment_scripts=()))``
383-
restores 1.4's reading of all three shapes.
431+
restores 1.4's reading of every shape above that turns on order or
432+
splitting. The middle dot is the one exception: it is decided in
433+
tokenization rather than by policy, so a name written with one still
434+
divides at the dot, and still renders with a space, whatever those two
435+
fields are set to.

docs/modules.rst

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,25 @@ Parsing
1414

1515
.. autofunction:: nameparser.parser_for
1616

17+
.. autoclass:: nameparser.Segmentation
18+
:members:
19+
20+
.. py:data:: nameparser.Segmenter
21+
:value: Callable[[str], Segmentation | None]
22+
23+
The type of the optional ``Parser(segmenter=...)`` hook: a callable
24+
given one token's text, returning a
25+
:class:`~nameparser.Segmentation` that divides it, or ``None`` to
26+
decline and leave it whole. An alias, not a class — any callable of
27+
that shape qualifies, and nothing needs to be subclassed or
28+
registered. It is consulted only for tokens whose script is listed
29+
in :attr:`Policy.segment_scripts
30+
<nameparser.Policy.segment_scripts>`, and only where the surname
31+
vocabulary declined first; see :ref:`segmenter-contract` for what a
32+
segmenter owes its caller.
33+
:func:`~nameparser.locales.ja_segmenter` is the shipped
34+
implementation.
35+
1736
Results
1837
~~~~~~~
1938

@@ -106,10 +125,15 @@ because only these three orders have defined assignment semantics.
106125
e.g. Vietnamese full-name order.
107126

108127
.. py:data:: nameparser.DEFAULT_SCRIPT_ORDERS
109-
:value: ((Script.HAN, FAMILY_FIRST), (Script.HANGUL, FAMILY_FIRST))
128+
:value: ((Script.HAN, FAMILY_FIRST), (Script.HANGUL, FAMILY_FIRST), (Script.HIRAGANA, FAMILY_FIRST))
110129

111130
The default :attr:`~nameparser.Policy.script_orders` table: a name
112-
written wholly in Han or Hangul reads family-first. A matching
131+
written wholly in Han or Hangul reads family-first, and so does a
132+
Japanese name mixing kanji with kana, which resolves to the
133+
``HIRAGANA`` entry whichever of the two syllabaries it actually
134+
uses — that member is the license's carrier key, not a claim about
135+
the characters present. A name written wholly in katakana has no
136+
entry on purpose and stays positional. A matching
113137
entry in this table takes precedence over ``name_order``, including
114138
a ``name_order`` you set explicitly — ``name_order`` governs only
115139
the names no entry matches. The values are drawn from the same
@@ -147,6 +171,8 @@ Locales
147171
.. automodule:: nameparser.locales
148172
:members: get, available
149173

174+
.. autofunction:: nameparser.locales.ja_segmenter
175+
150176
1.x compatibility layer
151177
------------------------
152178

0 commit comments

Comments
 (0)