Skip to content

iliaal/nameparser

Repository files navigation

iliaal/nameparser

CI Latest Version PHP Version License: MIT Follow @iliaa

Parse a string containing a full name into its parts (salutation, first name, middle names, initials, last name with prefixes, suffix, nickname).

Fork lineage. This is a fork of theiconic/name-parser (dormant since ~2020), built on top of the modernization done by codebyzach/name-parser. It adds casing- and credential-aware parsing and a confidence/ambiguity signal, and targets PHP 8.3+.

Why this fork

The upstream parser keys every token through strtolower() before matching it against its salutation/suffix dictionaries, so it cannot tell an all-caps credential from a same-spelled name. Two failure modes follow, both common in professional and clinician name lists:

  1. A trailing credential without a comma swallows the surname: "Jane Doe DDS" parsed to last name "Dds" (the real surname lost).
  2. A short credential token that is also a real name is mis-stripped: the Vietnamese surname "Do" and given name "Vi" were consumed as the credentials DO / VI.

This fork fixes both and adds an advisory confidence pass for the genuinely ambiguous cases.

What changed

  • Casing as a signal. An ambiguous token (Do, Vi, Ma, roman numerals, two-letter credentials) is treated as a credential only when written ALL-CAPS (DO, VI); Title- or lower-case keeps it as a name part. People write credentials in caps and names in title case, so the original casing carries the signal that lowercasing discarded.
  • Terminal-token guard. A lone name-colliding token in a comma given-name segment is kept as a name rather than emptied into a credential, unless its casing reads as a credential.
  • Confidence assessor. When a token matches a credential but the casing is uninformative (uniform-case input, or a lowercase token), Confidence::assess() flags the input so you can route it to manual review instead of trusting the split.
  • Expanded English dictionary (inherited from the CodeByZach fork): DDS, DO, DVM, PsyD, LCSW, MSW, MBA, EMBA, Esq, roman numerals VI to X, Hon., and more.
  • Nursing and allied-health credentials. RN, NP, PharmD, APRN, PA-C, OTR/L, and 30+ more, mined by frequency from the NPI registry, so a trailing credential no longer leaks into the first name.
  • Unclosed nickname delimiter. An opening ( or quote with no matching close no longer swallows the surname ("John (Bob Smith" keeps Smith).
  • All-caps short names. Under uniform-uppercase input the caps cannot mark a token as initials, so a two-letter given name is kept as a name instead of being split ("JO ANDERSON" keeps Jo, not J + initial O). Mixed-case combined initials still split ("JM Walker" to J M Walker).
  • Comma middle names. Everything after the first comma is the given-name segment, so a comma-separated middle name is retained ("Smith, John, Robert" keeps Robert) while trailing credentials are still stripped.

Requirements

  • PHP 8.3+ (tested through 8.5)
  • ext-mbstring

Installation

composer require iliaal/nameparser

Usage

use Iliaal\NameParser\Parser;

$parser = new Parser();
$name = $parser->parse('Dr. Jane A. Doe DDS');

$name->getSalutation();   // "Dr."
$name->getFirstname();    // "Jane"
$name->getInitials();     // "A."
$name->getLastname();     // "Doe"
$name->getSuffix();       // "DDS"
$name->getFullName();     // "Jane A. Doe"

Beyond the example above, Name also exposes getMiddlename(), getNickname(), getLastnamePrefix(), getGivenName(), getAll(), toArray(), getConfidence(), and getSource(). getLastname(true) returns the surname without any particle prefix; the default getLastname() already includes prefixes.

Structured output

toArray() returns every part under a fixed key set, with an empty string for any part that is absent. Unlike getAll(), which omits empty parts and varies its keys, this shape is safe to consume without existence checks:

$parser->parse('Dr. Jane A. Doe DDS')->toArray();
// [
//   'salutation' => 'Dr.', 'firstname' => 'Jane', 'initials' => 'A.',
//   'middlename' => '', 'lastname_prefix' => '', 'lastname' => 'Doe',
//   'suffix' => 'DDS', 'nickname' => '', 'given_name' => 'Jane A.',
//   'full_name' => 'Jane A. Doe',
// ]

Note that lastname already includes any particle prefix (de la Torre); lastname_prefix is a convenience extract, not a component to prepend.

Confidence / ambiguity

For batch imports where a wrong split is a data-integrity problem, check whether the input was decidable from its casing. The signal is available two ways: as a standalone pre-check on a raw string, or on the parsed result itself.

use Iliaal\NameParser\Confidence;

// pre-check, before parsing (default English ambiguous-key set)
$result = Confidence::assess('NGUYEN, VI');
// ['ambiguous' => true, 'notes' => ["'VI' could be a name or a credential; input casing is uniform"]]

// or read it off the parse; uses the same input and the parser's suffix dictionaries
$result = $parser->parse('NGUYEN, VI')->getConfidence();

if ($result['ambiguous']) {
    // queue the row for manual review instead of trusting the parse
}

getConfidence() is read-only and does not change what parse() returns; it is an advisory pass you opt into. A mixed-case input like "Nguyen, Vi" stays unflagged; the title-case Vi resolves to the given name.

For a non-default language set, standalone Confidence::assess($string) still uses the full English ambiguous-key table. To match a custom parser, either call Name::getConfidence() after parse(), or pass the parser's suffix dictionary as the second argument to assess().

All-caps limitation. Disambiguation keys off casing, so uniform-case input (all-caps legacy and registry data, or all-lowercase) carries no signal: an ambiguous trailing token reads as a credential by default. The confidence pass flags these when the token plausibly collides with a real name (Do, Vi, Ma, roman numerals, MBA), so you can route them to review. Clean credentials that are not also names (RN, PT, OD) are left unflagged to keep review volume manageable on all-caps datasets.

Languages

new Parser() uses the English dictionary. Passing languages replaces that list entirely (salutations, suffixes, and surname particles), it does not merge onto English. new Parser([new German()]) gives German honorifics and ordinals only, not English professional credentials or English particles such as van.

Compose dictionaries when you need both:

use Iliaal\NameParser\Language\English;
use Iliaal\NameParser\Language\German;

$parser = new Parser([new English(), new German()]);

Dictionary keys merge in constructor order, and the first language wins on collisions. With English first, Fr. resolves to Fr.. With German first, it resolves to Frau.

Configuration

Fluent setters on Parser:

  • setSurnameFirst(true) reads comma-less space-separated names in CJK order (Mao Zedong → last Mao). Opt-in; romanized order cannot be auto-detected.
  • setNicknameDelimiters(['<<' => '>>']) replaces the default pairs (()[]{}<> and quotes). An empty array restores the defaults; it does not disable nicknames.
  • setWhitespace, setMaxCombinedInitials, setMaxSalutationIndex tune collapse and mapper gates.
  • setMappers([...]) replaces the single-segment (Western, no-comma) pipeline only. Comma forms and setSurnameFirst(true) use dedicated sub-parsers that always build their own mapper lists from the language dictionaries. Pass [] to restore the default pipeline.

Parsing limits

Some inputs have no structural signal. A comma followed only by credentials can mean full name plus credentials (Jane Doe, MD) or surname plus credentials (Hidalgo Castillo, MD). The parser keeps the left side in Western order in that case. Use an explicit given-name segment, for example Hidalgo Castillo, Maria, MD, or post-process feeds where the left side is a surname-only field.

Two-token surnames without particles are also ambiguous in space-separated names. Jennifer Chen Wu and Mary Jo Li share the same token structure, but one wants Chen Wu as a surname while the other wants Jo as a middle name. The parser keeps the existing compound-surname heuristic for two-character terminal surnames.

Unknown trailing credentials follow the same casing rule as the ambiguous tokens. When a known credential anchors the tail and the input is mixed-case, an adjacent unknown all-caps token is kept as a credential too: John Smith MD FACS and Smith, John, MD, FACS keep both in the suffix. A pure all-caps segment with no prior dictionary anchor is kept as a name (Smith, JOHN, MD → first John, suffix MD), because it is indistinguishable from an all-caps given name. Prefer the known credential first when the unknown stands alone (Smith, MD, FACS). Uniform all-caps rows cannot recover unknown credentials; with no case signal an unknown token could equally be a surname, so it stays in the name.

getFullName() and toArray()['full_name'] are the given name plus surname only (no salutation, nickname, or suffix). __toString() is the richer display line from getAll(true) (salutation through suffix, nickname wrapped). Both drop comma structure and are not guaranteed to re-parse to the same fields, so treat them as output, not as a round-trippable serialization.

Performance

Reuse one Parser across a batch rather than constructing a new one per row. The parser memoizes its merged dictionaries, its mapper pipeline, and the comma-segment sub-parsers on first use, so a shared instance amortizes that setup across every parse() call.

Development

composer install
composer test     # phpunit
composer analyse  # phpstan (level 9)
composer lint     # php-cs-fixer (dry run)

Credits

Original library by The Iconic. Modernization to PHP 8.3+ by Zachary Miller. Casing/credential parsing and confidence signal in this fork by Ilia Alshanetsky.

License

MIT. See LICENSE. Upstream copyright notices are retained.

About

Casing- and credential-aware PHP full-name parser. Fork of theiconic/name-parser, tuned for professional/clinician names.

Topics

Resources

License

Stars

8 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors

Languages