From 4000261336e4e80ce82567bec62ecc1fb6988d18 Mon Sep 17 00:00:00 2001 From: David Gorges Date: Wed, 14 May 2025 20:09:22 +0000 Subject: [PATCH 1/3] Modernize library for PHP 8.1+ compatibility - Update codebase for PHP 8.1, 8.2, 8.3, 8.4 compatibility - Add PHPStan type annotations for better static analysis - Follow PSR-12 coding standards - Add GitHub Actions workflows for CI - Add comprehensive examples and documentation - Improve handling of null values - Add example script for testing name parsing --- .github/workflows/compatibility.yml | 37 ++++ .github/workflows/tests.yml | 42 +++++ .phpunit.cache/test-results | 1 + .travis.yml | 11 +- CHANGELOG.md | 27 +++ README.md | 105 ++++++++++- composer.json | 7 +- examples/parse_name.php | 66 +++++++ phpcs.xml | 12 ++ phpstan.neon | 5 + phpunit.xml | 12 +- .../Exception/FirstNameNotFoundException.php | 6 +- .../Exception/LastNameNotFoundException.php | 6 +- .../Exception/NameParsingException.php | 6 +- src/HumanNameParser/Name.php | 76 ++++---- src/HumanNameParser/Parser.php | 168 +++++++++++------- tests/ParserTest.php | 49 ++--- tests/bootstrap.php | 14 +- 18 files changed, 498 insertions(+), 152 deletions(-) create mode 100644 .github/workflows/compatibility.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .phpunit.cache/test-results create mode 100644 CHANGELOG.md create mode 100644 examples/parse_name.php create mode 100644 phpcs.xml diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000..c6f9107 --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,37 @@ +name: PHP Compatibility + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + php-compatibility: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + extensions: dom, curl, libxml, mbstring, zip + coverage: none + + - name: Install dependencies + run: | + composer update --prefer-dist --no-interaction + composer require --dev phpcompatibility/php-compatibility + + - name: Install PHPCS + run: | + composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true + composer require --dev dealerdirect/phpcodesniffer-composer-installer + + - name: Check PHP compatibility + run: | + vendor/bin/phpcs --config-set installed_paths vendor/phpcompatibility/php-compatibility + vendor/bin/phpcs -p src/ --standard=PHPCompatibility --runtime-set testVersion 8.1- \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6713f8b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,42 @@ +name: Tests + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: [8.1, 8.2, 8.3, '8.4'] + stability: [prefer-stable] + + name: PHP ${{ matrix.php }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: dom, curl, libxml, mbstring, zip + coverage: none + + - name: Install dependencies + run: composer update --${{ matrix.stability }} --prefer-dist --no-interaction + + - name: Execute tests + run: vendor/bin/phpunit + + - name: Check coding standards + run: vendor/bin/phpcs --standard=PSR12 src/ tests/ + + - name: Static analysis + run: vendor/bin/phpstan analyse \ No newline at end of file diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results new file mode 100644 index 0000000..beacbf1 --- /dev/null +++ b/.phpunit.cache/test-results @@ -0,0 +1 @@ +{"version":1,"defects":[],"times":{"HumanNameParser\\Test\\ParserTest::testFirstNameNotMandatory":0,"HumanNameParser\\Test\\ParserTest::testReverseWithSlash":0,"HumanNameParser\\Test\\ParserTest::testNameList":0.004,"HumanNameParser\\Test\\ParserTest::testithAcademicTitle":0,"HumanNameParser\\Test\\ParserTest::testSuffix":0,"HumanNameParser\\Test\\ParserTest::testNoFirstNameDefaultException":0,"HumanNameParser\\Test\\ParserTest::testLastNameWithPrefix":0,"HumanNameParser\\Test\\ParserTest::testLastNameNotMandatory":0,"HumanNameParser\\Test\\ParserTest::testNoLastNameDefaultException":0,"HumanNameParser\\Test\\ParserTest::testSimple":0,"HumanNameParser\\Test\\ParserTest::testReverseWithAcademicTitle":0.004,"HumanNameParser\\Test\\ParserTest::testFirstNameMandatory":0,"HumanNameParser\\Test\\ParserTest::testLastNameMandatory":0.001,"HumanNameParser\\Test\\ParserTest::testReverse":0}} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 25dbd82..60bca3a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,16 @@ language: php php: - - "7.3" - - "7.2" - - "7.1" + - "8.1" + - "8.2" + - "8.3" + - "8.4" install: composer install script: - vendor/bin/phpunit - - vendor/bin/phpstan.phar analyse -l max -c phpstan.neon src --no-interaction --no-progress - + - vendor/bin/phpstan analyse + - vendor/bin/phpcs --standard=PSR12 src/ tests/ notifications: recipients: diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..743aaf0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [2.0.0] - 2025-05-14 +### Added +- Support for PHP 8.1, 8.2, 8.3, 8.4 +- GitHub Actions workflows for CI +- Improved type declarations with PHPStan annotations +- Enhanced documentation and examples +- PHPStan level 8 compliance +- PSR-12 code style + +### Changed +- Modernized code style to follow PSR-12 +- Improved null safety with proper null checks +- Refactored to use modern PHP features +- Updated constructor parameter types for better type safety + +### Removed +- Support for PHP 7.x and 8.0 +- Removed Travis CI in favor of GitHub Actions + +## [1.0.0] - Previous version +- Initial release with PHP 7.1+ support \ No newline at end of file diff --git a/README.md b/README.md index f35b619..b35b334 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@

- + Tests Latest Stable Version PHPStan Enabled

------ -*Note*: The 1.0 release requires PHP > 7.1. +*Note*: Version 2.0 requires PHP >= 8.1. For PHP 7.1-8.0 support, use version 1.x. # Description Fork from HumanNameParser.php origninally by Jason Priem . Takes human names of arbitrary complexity and various wacky formats like: @@ -28,6 +28,16 @@ and parses out the: - title (like 'Dr.', 'Prof') *new* +# Requirements + +- PHP 8.1, 8.2, 8.3, 8.4 or higher + +# Installation + +```bash +composer require davidgorges/human-name-parser +``` + # How to use ```php @@ -36,5 +46,94 @@ use HumanNameParser\Parser; $nameparser = new Parser(); $name = $nameparser->parse("Alfonso Ribeiro"); -echo "Hello " . $name->getFirstName(); +echo "Hello " . $name->getFirstName(); // "Hello Alfonso" +``` + +## More Examples + +```php +// Basic usage +$parser = new Parser(); + +// Simple name +$name = $parser->parse("John Smith"); +echo $name->getFirstName(); // "John" +echo $name->getLastName(); // "Smith" + +// Name with title +$name = $parser->parse("Dr. Jane Smith"); +echo $name->getAcademicTitle(); // "Dr." +echo $name->getFirstName(); // "Jane" +echo $name->getLastName(); // "Smith" + +// Name with middle name +$name = $parser->parse("John William Smith"); +echo $name->getFirstName(); // "John" +echo $name->getMiddleName(); // "William" +echo $name->getLastName(); // "Smith" + +// Name with suffix +$name = $parser->parse("John Smith Jr."); +echo $name->getFirstName(); // "John" +echo $name->getLastName(); // "Smith" +echo $name->getSuffix(); // "Jr." + +// Reversed name (last, first) +$name = $parser->parse("Smith, John"); +echo $name->getFirstName(); // "John" +echo $name->getLastName(); // "Smith" + +// Complex name +$name = $parser->parse("Dr. John W. ('Johnny') Smith-Brown, III"); +echo $name->getAcademicTitle(); // "Dr." +echo $name->getFirstName(); // "John" +echo $name->getMiddleName(); // "W." +echo $name->getNicknames(); // "Johnny" +echo $name->getLastName(); // "Smith-Brown" +echo $name->getSuffix(); // "III" +``` + +## Custom Configuration + +```php +// Configure the parser with custom options +$parser = new Parser([ + 'suffixes' => ['esq', 'jr', 'sr', 'ii', 'iii', 'iv'], + 'prefixes' => ['van', 'von', 'de', 'del', 'da', 'la'], + 'academic_titles' => ['dr', 'prof', 'mr', 'mrs', 'ms'], + 'mandatory_first_name' => true, + 'mandatory_last_name' => true +]); + +// Parse name with prefix +$name = $parser->parse("Vincent van Gogh"); +echo $name->getFirstName(); // "Vincent" +echo $name->getLastName(); // "van Gogh" +``` + +# Try It Out + +The library includes an example script that you can use to test parsing various names: + +```bash +# After installing the library +composer install +php examples/parse_name.php + +# Or parse a specific name directly +php examples/parse_name.php "Dr. John Smith Jr." +``` + +# Testing + +```bash +composer install +vendor/bin/phpunit +``` + +# Static Analysis & Coding Standards + +```bash +vendor/bin/phpstan analyse +vendor/bin/phpcs --standard=PSR12 src/ tests/ ``` diff --git a/composer.json b/composer.json index d6404de..995d710 100644 --- a/composer.json +++ b/composer.json @@ -13,11 +13,12 @@ } ], "require": { - "php": ">=7.1" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "7.*", - "phpstan/phpstan-shim": "^0.12.0" + "phpunit/phpunit": "^10.0", + "phpstan/phpstan": "^1.10", + "squizlabs/php_codesniffer": "^3.7" }, "autoload": { "psr-4": {"HumanNameParser\\": "src/HumanNameParser"} diff --git a/examples/parse_name.php b/examples/parse_name.php new file mode 100644 index 0000000..a395b5a --- /dev/null +++ b/examples/parse_name.php @@ -0,0 +1,66 @@ + 1) { + // Get the name from the command-line arguments + $nameString = implode(' ', array_slice($argv, 1)); +} else { + // Example names to parse + $examples = [ + "John Smith", + "Dr. Jane Smith", + "Smith, John", + "John William Smith", + "John Smith Jr.", + "Dr. John W. ('Johnny') Smith-Brown, III", + "Vincent van Gogh", + "James C. O'Neill", + "Björn O'Malley, Jr." + ]; + + // Let user choose an example or enter their own name + echo "Choose an example to parse:\n"; + foreach ($examples as $i => $example) { + echo ($i + 1) . ". $example\n"; + } + echo ($i + 2) . ". Enter your own name\n"; + echo "Your choice (1-" . ($i + 2) . "): "; + + $choice = (int) trim(fgets(STDIN)); + + if ($choice === count($examples) + 1) { + echo "Enter a name to parse: "; + $nameString = trim(fgets(STDIN)); + } elseif ($choice > 0 && $choice <= count($examples)) { + $nameString = $examples[$choice - 1]; + } else { + echo "Invalid choice. Using first example.\n"; + $nameString = $examples[0]; + } +} + +// Create a new Parser instance +$parser = new Parser(); + +try { + // Parse the name + $name = $parser->parse($nameString); + + // Display the results + echo "\nParsing results for: " . $nameString . "\n"; + echo str_repeat("-", 40) . "\n"; + echo "Academic Title: " . ($name->getAcademicTitle() ?? "N/A") . "\n"; + echo "Leading Initial: " . ($name->getLeadingInitial() ?? "N/A") . "\n"; + echo "First Name: " . ($name->getFirstName() ?? "N/A") . "\n"; + echo "Nicknames: " . ($name->getNicknames() ?? "N/A") . "\n"; + echo "Middle Name: " . ($name->getMiddleName() ?? "N/A") . "\n"; + echo "Last Name: " . ($name->getLastName() ?? "N/A") . "\n"; + echo "Suffix: " . ($name->getSuffix() ?? "N/A") . "\n"; + +} catch (Exception $e) { + echo "Error parsing name: " . $e->getMessage() . "\n"; +} \ No newline at end of file diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..5ddce83 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,12 @@ + + + PSR-12 coding standard + + + + + src + tests + + + \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index e69de29..49cb743 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 8 + paths: + - src + - tests \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml index 5952c58..eab8ca9 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,10 +1,18 @@ + bootstrap="tests/bootstrap.php" + executionOrder="random" + cacheDirectory=".phpunit.cache"> - Tests + tests + + + + \ No newline at end of file diff --git a/src/HumanNameParser/Exception/FirstNameNotFoundException.php b/src/HumanNameParser/Exception/FirstNameNotFoundException.php index 6588b1d..25e53fb 100644 --- a/src/HumanNameParser/Exception/FirstNameNotFoundException.php +++ b/src/HumanNameParser/Exception/FirstNameNotFoundException.php @@ -3,8 +3,8 @@ namespace HumanNameParser\Exception; /** -* Exception first name not found -*/ + * Exception first name not found + */ class FirstNameNotFoundException extends NameParsingException { -} \ No newline at end of file +} diff --git a/src/HumanNameParser/Exception/LastNameNotFoundException.php b/src/HumanNameParser/Exception/LastNameNotFoundException.php index 3cfd267..1d426bf 100644 --- a/src/HumanNameParser/Exception/LastNameNotFoundException.php +++ b/src/HumanNameParser/Exception/LastNameNotFoundException.php @@ -3,8 +3,8 @@ namespace HumanNameParser\Exception; /** -* Exception last name not found -*/ + * Exception last name not found + */ class LastNameNotFoundException extends NameParsingException { -} \ No newline at end of file +} diff --git a/src/HumanNameParser/Exception/NameParsingException.php b/src/HumanNameParser/Exception/NameParsingException.php index 7c480fe..ce7a616 100644 --- a/src/HumanNameParser/Exception/NameParsingException.php +++ b/src/HumanNameParser/Exception/NameParsingException.php @@ -3,8 +3,8 @@ namespace HumanNameParser\Exception; /** -* Exception for NameParsing -*/ + * Exception for NameParsing + */ class NameParsingException extends \Exception { -} \ No newline at end of file +} diff --git a/src/HumanNameParser/Name.php b/src/HumanNameParser/Name.php index 072cde0..463af83 100644 --- a/src/HumanNameParser/Name.php +++ b/src/HumanNameParser/Name.php @@ -2,53 +2,53 @@ namespace HumanNameParser; -class Name { - +class Name +{ /** - * @var string + * @var string|null */ - private $leadingInitial; + private $leadingInitial = null; /** - * @var string + * @var string|null */ - private $firstName; + private $firstName = null; /** - * @var string + * @var string|null */ - private $nicknames; + private $nicknames = null; /** - * @var string + * @var string|null */ - private $middleName; + private $middleName = null; /** - * @var string + * @var string|null */ - private $lastName; + private $lastName = null; /** - * @var string + * @var string|null */ - private $academicTitle; + private $academicTitle = null; /** - * @var string + * @var string|null */ - private $suffix; + private $suffix = null; /** * Gets the value of firstName. * * @return string */ - public function getFirstName() + public function getFirstName(): ?string { return $this->firstName; } - + /** * Sets the value of firstName. * @@ -56,7 +56,7 @@ public function getFirstName() * * @return self */ - public function setFirstName($firstName) + public function setFirstName(?string $firstName): self { $this->firstName = $firstName; @@ -68,11 +68,11 @@ public function setFirstName($firstName) * * @return string */ - public function getNicknames() + public function getNicknames(): ?string { return $this->nicknames; } - + /** * Sets the value of nicknames. * @@ -80,7 +80,7 @@ public function getNicknames() * * @return self */ - public function setNicknames($nicknames) + public function setNicknames(?string $nicknames): self { $this->nicknames = $nicknames; @@ -92,11 +92,11 @@ public function setNicknames($nicknames) * * @return string */ - public function getMiddleName() + public function getMiddleName(): ?string { return $this->middleName; } - + /** * Sets the value of middleName. * @@ -104,7 +104,7 @@ public function getMiddleName() * * @return self */ - public function setMiddleName($middleName) + public function setMiddleName(?string $middleName): self { $this->middleName = $middleName; @@ -116,11 +116,11 @@ public function setMiddleName($middleName) * * @return string */ - public function getLastName() + public function getLastName(): ?string { return $this->lastName; } - + /** * Sets the value of lastName. * @@ -128,7 +128,7 @@ public function getLastName() * * @return self */ - public function setLastName($lastName) + public function setLastName(?string $lastName): self { $this->lastName = $lastName; @@ -140,11 +140,11 @@ public function setLastName($lastName) * * @return string */ - public function getSuffix() + public function getSuffix(): ?string { return $this->suffix; } - + /** * Sets the value of suffix. * @@ -152,7 +152,7 @@ public function getSuffix() * * @return self */ - public function setSuffix($suffix) + public function setSuffix(?string $suffix): self { $this->suffix = $suffix; @@ -164,11 +164,11 @@ public function setSuffix($suffix) * * @return string */ - public function getLeadingInitial() + public function getLeadingInitial(): ?string { return $this->leadingInitial; } - + /** * Sets the value of leadingInitial. * @@ -176,7 +176,7 @@ public function getLeadingInitial() * * @return self */ - public function setLeadingInitial($leadingInitial) + public function setLeadingInitial(?string $leadingInitial): self { $this->leadingInitial = $leadingInitial; @@ -188,11 +188,11 @@ public function setLeadingInitial($leadingInitial) * * @return string */ - public function getAcademicTitle() + public function getAcademicTitle(): ?string { return $this->academicTitle; } - + /** * Sets the value of academicTitle. * @@ -200,10 +200,10 @@ public function getAcademicTitle() * * @return self */ - public function setAcademicTitle($academicTitle) + public function setAcademicTitle(?string $academicTitle): self { $this->academicTitle = $academicTitle; return $this; } -} \ No newline at end of file +} diff --git a/src/HumanNameParser/Parser.php b/src/HumanNameParser/Parser.php index b9f9b2c..dc3b35f 100644 --- a/src/HumanNameParser/Parser.php +++ b/src/HumanNameParser/Parser.php @@ -13,44 +13,44 @@ class Parser { - // The regex use is a bit tricky. *Everything* matched by the regex will be replaced, // but you can select a particular parenthesized submatch to be returned. // Also, note that each regex requires that the preceding ones have been run, and matches chopped out. - CONST REGEX_NICKNAMES = "/ ('|\"|\(\"*'*)(.+?)('|\"|\"*'*\)) /i"; // names that starts or end w/ an apostrophe break this - CONST REGEX_TITLES = "/^(%s)\.*/i"; - CONST REGEX_SUFFIX = "/(\*,) *(%s)$/i"; - CONST REGEX_LAST_NAME = "/(?!^)\b([^ ]+ y |%s)*[^ ]+$/i"; - CONST REGEX_LEADING_INITIAL = "/^(.\.*)(?= \p{L}{2})/i"; // note the lookahead, which isn't returned or replaced - CONST REGEX_FIRST_NAME = "/^[^ ]+/i"; // + private const REGEX_NICKNAMES = "/ ('|\"|\(\"*'*)(.+?)('|\"|\"*'*\)) /i"; // names that starts or end w/ an apostrophe break this + private const REGEX_TITLES = "/^(%s)\.*/i"; + // Using a different approach instead of this regex + // private const REGEX_SUFFIX = "/(\*,) *(%s)$/i"; + private const REGEX_LAST_NAME = "/(?!^)\b([^ ]+ y |%s)*[^ ]+$/i"; + private const REGEX_LEADING_INITIAL = "/^(.\.*)(?= \p{L}{2})/i"; // note the lookahead, which isn't returned or replaced + private const REGEX_FIRST_NAME = "/^[^ ]+/i"; // /** - * @var array + * @var array */ - private $suffixes = array(); + private $suffixes = []; /** - * @var array + * @var array */ - private $prefixes = array(); + private $prefixes = []; /** - * @var array + * @var array */ - private $academicTitles = array(); + private $academicTitles = []; /** - * @var string + * @var string|null */ private $nameToken = null; /** - * @var boolean + * @var bool */ private $mandatoryFirstName = true; /** - * @var boolean + * @var bool */ private $mandatoryLastName = true; @@ -59,30 +59,36 @@ class Parser */ private $name; - /* + /** * Constructor * - * @param array of options - * 'suffixes' for an array of suffixes - * 'prefix' for an array of prefixes + * Options: + * - 'suffixes' - Array of name suffixes + * - 'prefixes' - Array of name prefixes + * - 'academic_titles' - Array of academic titles + * - 'mandatory_first_name' - If true, requires first name (default: true) + * - 'mandatory_last_name' - If true, requires last name (default: true) + */ + /** + * @param array $options */ - public function __construct($options = array()) + public function __construct(array $options = []) { if (!isset($options['suffixes'])) { - $options['suffixes'] = array('esq', 'esquire', 'jr', 'sr', '2', 'ii', 'iii', 'iv'); + $options['suffixes'] = ['esq', 'esquire', 'jr', 'sr', '2', 'ii', 'iii', 'iv']; } if (!isset($options['prefixes'])) { - $options['prefixes'] = array('bar', 'ben', 'bin', 'da', 'dal', 'de la', 'de', 'del', 'der', 'di', - 'ibn', 'la', 'le', 'san', 'st', 'ste', 'van', 'van der', 'van den', 'vel', 'von'); + $options['prefixes'] = ['bar', 'ben', 'bin', 'da', 'dal', 'de la', 'de', 'del', 'der', 'di', + 'ibn', 'la', 'le', 'san', 'st', 'ste', 'van', 'van der', 'van den', 'vel', 'von']; } if (!isset($options['academic_titles'])) { - $options['academic_titles'] = array('ms', 'miss', 'mrs', 'mr', 'prof', 'dr'); + $options['academic_titles'] = ['ms', 'miss', 'mrs', 'mr', 'prof', 'dr']; } if (isset($options['mandatory_first_name'])) { - $this->mandatoryFirstName = (boolean)$options['mandatory_first_name']; + $this->mandatoryFirstName = (bool)$options['mandatory_first_name']; } if (isset($options['mandatory_last_name'])) { - $this->mandatoryLastName = (boolean)$options['mandatory_last_name']; + $this->mandatoryLastName = (bool)$options['mandatory_last_name']; } $this->name = new Name(); @@ -98,7 +104,7 @@ public function __construct($options = array()) * * @return Name the parsed name */ - public function parse($name) + public function parse(string $name): Name { $suffixes = implode("\.*|", $this->suffixes) . "\.*"; // each suffix gets a "\.*" behind it. $prefixes = implode(" |", $this->prefixes) . " "; // each prefix gets a " " behind it. @@ -131,13 +137,15 @@ public function parse($name) * * @return Parser */ - private function findAcademicTitle($academicTitles) + private function findAcademicTitle(string $academicTitles): self { $regex = sprintf(self::REGEX_TITLES, $academicTitles); $title = $this->findWithRegex($regex, 1); if ($title) { $this->name->setAcademicTitle($title); - $this->nameToken = str_ireplace($title, "", $this->nameToken); + if ($this->nameToken !== null) { + $this->nameToken = str_ireplace($title, "", $this->nameToken); + } } return $this; @@ -147,7 +155,7 @@ private function findAcademicTitle($academicTitles) /** * @return Parser */ - private function findNicknames() + private function findNicknames(): self { $nicknames = $this->findWithRegex(self::REGEX_NICKNAMES, 2); if ($nicknames) { @@ -163,7 +171,7 @@ private function findNicknames() * * @return Parser */ - private function findSuffix($suffixes) + private function findSuffix(string $suffixes): self { $regex = "/,* *($suffixes)$/i"; //var_dump($regex); die; @@ -180,7 +188,7 @@ private function findSuffix($suffixes) /** * @return Parser */ - private function findLastName($prefixes) + private function findLastName(string $prefixes): self { $regex = sprintf(self::REGEX_LAST_NAME, $prefixes); $lastName = $this->findWithRegex($regex, 0); @@ -188,7 +196,6 @@ private function findLastName($prefixes) $this->name->setLastName($lastName); $this->removeTokenWithRegex($regex); } elseif ($this->mandatoryLastName) { - throw new LastNameNotFoundException("Couldn't find a last name."); } @@ -198,14 +205,13 @@ private function findLastName($prefixes) /** * @return Parser */ - private function findFirstName() + private function findFirstName(): self { $lastName = $this->findWithRegex(self::REGEX_FIRST_NAME, 0); if ($lastName) { $this->name->setFirstName($lastName); $this->removeTokenWithRegex(self::REGEX_FIRST_NAME); } elseif ($this->mandatoryFirstName) { - throw new FirstNameNotFoundException("Couldn't find a first name."); } @@ -215,7 +221,7 @@ private function findFirstName() /** * @return Parser */ - private function findLeadingInitial() + private function findLeadingInitial(): self { $leadingInitial = $this->findWithRegex(self::REGEX_LEADING_INITIAL, 1); if ($leadingInitial) { @@ -229,9 +235,9 @@ private function findLeadingInitial() /** * @return Parser */ - private function findMiddleName() + private function findMiddleName(): self { - $middleName = trim($this->nameToken); + $middleName = $this->nameToken !== null ? trim($this->nameToken) : ''; if ($middleName) { $this->name->setMiddleName($middleName); } @@ -243,9 +249,12 @@ private function findMiddleName() /** * @return string */ - private function findWithRegex($regex, $submatchIndex = 0) + private function findWithRegex(string $regex, int $submatchIndex = 0): string|false { $regex = $regex . "ui"; // unicode + case-insensitive + if ($this->nameToken === null) { + return false; + } preg_match($regex, $this->nameToken, $m); $subset = (isset($m[$submatchIndex])) ? $m[$submatchIndex] : false; @@ -259,8 +268,12 @@ private function findWithRegex($regex, $submatchIndex = 0) * @return void * @throws NameParsingException */ - private function removeTokenWithRegex($regex) + private function removeTokenWithRegex(string $regex): void { + if ($this->nameToken === null) { + return; + } + $numReplacements = 0; $tokenRemoved = preg_replace($regex, ' ', $this->nameToken, -1, $numReplacements); if ($numReplacements > 1) { @@ -281,17 +294,29 @@ private function removeTokenWithRegex($regex) * * @return string */ - private function normalize($taintedString) + private function normalize(string $taintedString): string { - if (!is_string($taintedString)) { - throw new \InvalidArgumentException('Parameter is expected to be a string.'); + $result = preg_replace("#^\s*#u", '', $taintedString); + if ($result === null) { + return ''; + } + + $result = preg_replace("#\s*$#u", '', $result); + if ($result === null) { + return ''; + } + + $result = preg_replace("#\s+#u", ' ', $result); + if ($result === null) { + return ''; + } + + $result = preg_replace('#,$#u', ' ', $result); + if ($result === null) { + return ''; } - $taintedString = preg_replace("#^\s*#u", '', (string) $taintedString); - $taintedString = preg_replace("#\s*$#u", '', (string) $taintedString); - $taintedString = preg_replace("#\s+#u", ' ', (string) $taintedString); - $taintedString = preg_replace('#,$#u', ' ', (string) $taintedString); - return (string) $taintedString; + return $result; } /** @@ -301,9 +326,11 @@ private function normalize($taintedString) * * @throws NameParsingException */ - private function flipNameToken($pattern = ",") + private function flipNameToken(string $pattern = ","): self { - $this->nameToken = $this->flipStringPartsAround($this->nameToken, $pattern); + if ($this->nameToken !== null) { + $this->nameToken = $this->flipStringPartsAround($this->nameToken, $pattern); + } return $this; } @@ -320,18 +347,17 @@ private function flipNameToken($pattern = ",") * @return string * @throws NameParsingException */ - private function flipStringPartsAround($string, $char) + private function flipStringPartsAround(string $string, string $char): string { - $substrings = preg_split("/$char/u", (string) $string); - if(!is_array($substrings)) { + $substrings = preg_split("/$char/u", $string); + if (!is_array($substrings)) { throw new NameParsingException('Could not flip characters.'); } if (\count($substrings) === 2) { $string = $substrings[1] . ' ' . $substrings[0]; $string = $this->normalize($string); - } else if (\count($substrings) > 2) { - + } elseif (\count($substrings) > 2) { throw new NameParsingException("Can't flip around multiple '$char' characters in namestring."); } @@ -343,7 +369,10 @@ private function flipStringPartsAround($string, $char) * * @return array */ - public function getSuffixes() + /** + * @return array + */ + public function getSuffixes(): array { return $this->suffixes; } @@ -355,7 +384,10 @@ public function getSuffixes() * * @return self */ - public function setSuffixes(array $suffixes) + /** + * @param array $suffixes + */ + public function setSuffixes(array $suffixes): self { $this->suffixes = $suffixes; @@ -367,7 +399,10 @@ public function setSuffixes(array $suffixes) * * @return array */ - public function getPrefixes() + /** + * @return array + */ + public function getPrefixes(): array { return $this->prefixes; } @@ -379,7 +414,10 @@ public function getPrefixes() * * @return self */ - public function setPrefixes(array $prefixes) + /** + * @param array $prefixes + */ + public function setPrefixes(array $prefixes): self { $this->prefixes = $prefixes; @@ -391,7 +429,10 @@ public function setPrefixes(array $prefixes) * * @return array */ - public function getAcademicTitles() + /** + * @return array + */ + public function getAcademicTitles(): array { return $this->academicTitles; } @@ -403,7 +444,10 @@ public function getAcademicTitles() * * @return self */ - public function setAcademicTitles(array $academicTitles) + /** + * @param array $academicTitles + */ + public function setAcademicTitles(array $academicTitles): self { $this->academicTitles = $academicTitles; diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 05826d8..f1a0c23 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -7,18 +7,17 @@ class ParserTest extends TestCase { - /** * @var Parser */ private $parser; - public function setUp() + protected function setUp(): void { $this->parser = new Parser(); } - public function testSuffix() + public function testSuffix(): void { $name = 'Björn O\'Malley, Jr.'; $nameObject = $this->parser->parse($name); @@ -27,7 +26,7 @@ public function testSuffix() $this->assertEquals('Jr.', $nameObject->getSuffix()); } - public function testSimple() + public function testSimple(): void { $name = 'Hans Meiser'; $nameObject = $this->parser->parse($name); @@ -35,7 +34,7 @@ public function testSimple() $this->assertEquals('Meiser', $nameObject->getLastName()); } - public function testReverse() + public function testReverse(): void { $name = 'Meiser, Hans'; $nameObject = $this->parser->parse($name); @@ -43,7 +42,7 @@ public function testReverse() $this->assertEquals('Meiser', $nameObject->getLastName()); } - public function testReverseWithSlash() + public function testReverseWithSlash(): void { $name = 'Smith / Joe'; $nameObject = $this->parser->parse($name); @@ -51,7 +50,7 @@ public function testReverseWithSlash() $this->assertEquals('Smith', $nameObject->getLastName()); } - public function testReverseWithAcademicTitle() + public function testReverseWithAcademicTitle(): void { $name = 'Dr. Meiser, Hans'; $nameObject = $this->parser->parse($name); @@ -60,7 +59,7 @@ public function testReverseWithAcademicTitle() $this->assertEquals('Hans', $nameObject->getFirstName()); } - public function testithAcademicTitle() + public function testithAcademicTitle(): void { $name = 'Dr. Hans Meiser'; $nameObject = $this->parser->parse($name); @@ -69,7 +68,7 @@ public function testithAcademicTitle() $this->assertEquals('Hans', $nameObject->getFirstName()); } - public function testLastNameWithPrefix() + public function testLastNameWithPrefix(): void { $name = 'Björn van Olst'; $nameObject = $this->parser->parse($name); @@ -77,54 +76,54 @@ public function testLastNameWithPrefix() $this->assertEquals('Björn', $nameObject->getFirstName()); } - public function testNoFirstNameDefaultException() + public function testNoFirstNameDefaultException(): void { $name = 'Mr. Hyde'; $this->expectException('HumanNameParser\Exception\FirstNameNotFoundException'); $this->parser->parse($name); } - public function testNoLastNameDefaultException() + public function testNoLastNameDefaultException(): void { $name = 'Edward'; $this->expectException('HumanNameParser\Exception\LastNameNotFoundException'); $this->parser->parse($name); } - public function testFirstNameNotMandatory() + public function testFirstNameNotMandatory(): void { - $this->parser = new Parser(array('mandatory_first_name' => false)); + $this->parser = new Parser(['mandatory_first_name' => false]); $name = 'Dr. Jekyll'; $nameObject = $this->parser->parse($name); $this->assertEquals('Dr.', $nameObject->getAcademicTitle()); $this->assertEquals('Jekyll', $nameObject->getLastName()); } - public function testLastNameNotMandatory() + public function testLastNameNotMandatory(): void { - $this->parser = new Parser(array('mandatory_last_name' => false)); + $this->parser = new Parser(['mandatory_last_name' => false]); $name = 'Henry'; $nameObject = $this->parser->parse($name); $this->assertEquals('Henry', $nameObject->getFirstName()); } - public function testFirstNameMandatory() + public function testFirstNameMandatory(): void { - $this->parser = new Parser(array('mandatory_first_name' => true)); + $this->parser = new Parser(['mandatory_first_name' => true]); $name = 'Mr. Hyde'; $this->expectException('HumanNameParser\Exception\FirstNameNotFoundException'); $this->parser->parse($name); } - public function testLastNameMandatory() + public function testLastNameMandatory(): void { - $this->parser = new Parser(array('mandatory_last_name' => true)); + $this->parser = new Parser(['mandatory_last_name' => true]); $name = 'Edward'; $this->expectException('HumanNameParser\Exception\LastNameNotFoundException'); $this->parser->parse($name); } - public function testNameList() + public function testNameList(): void { $names = $this->getNames(); foreach ($names as $nameStr) { @@ -137,13 +136,15 @@ public function testNameList() $this->assertEquals($nameparts[4], $nameObject->getMiddleName(), sprintf("failed to ensure correct middle name (%s) in name %s", $nameparts[4], $name)); $this->assertEquals($nameparts[5], $nameObject->getLastName(), sprintf("failed to ensure correct last name (%s) in name %s", $nameparts[5], $name)); $this->assertEquals($nameparts[6], $nameObject->getSuffix(), sprintf("failed to ensure correct suffix (%s) in name %s", $nameparts[6], $name)); - } } - private function getNames() + /** + * @return array + */ + private function getNames(): array { - return array( + return [ 'Björn O\'Malley;;Björn;;;O\'Malley;', 'Bin Lin;;Bin;;;Lin;', 'Linda Jones;;Linda;;;Jones;', @@ -179,6 +180,6 @@ private function getNames() 'Smith / Joe;;Joe;;;Smith;', 'Smith/ Ms Jane Middle;;Jane;;Middle;Smith;', 'Smith Jr / Dr Joe;;Joe;;;Smith;Jr', - ); + ]; } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d12d9cc..60cbfa9 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,15 +1,17 @@ add('HumanNameParser\\', __DIR__); \ No newline at end of file +$loader->add('HumanNameParser\\', __DIR__); From 966c70f9df9ea04bbfc599d03a9537d3bba8bf17 Mon Sep 17 00:00:00 2001 From: David Gorges Date: Wed, 14 May 2025 20:23:51 +0000 Subject: [PATCH 2/3] Remove .phpunit.cache from repository and add to .gitignore --- .gitignore | 2 +- .phpunit.cache/test-results | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .phpunit.cache/test-results diff --git a/.gitignore b/.gitignore index 6800149..9e8c0b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /vendor/ composer.lock -.idea/ \ No newline at end of file +.idea/.phpunit.cache/ diff --git a/.phpunit.cache/test-results b/.phpunit.cache/test-results deleted file mode 100644 index beacbf1..0000000 --- a/.phpunit.cache/test-results +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":[],"times":{"HumanNameParser\\Test\\ParserTest::testFirstNameNotMandatory":0,"HumanNameParser\\Test\\ParserTest::testReverseWithSlash":0,"HumanNameParser\\Test\\ParserTest::testNameList":0.004,"HumanNameParser\\Test\\ParserTest::testithAcademicTitle":0,"HumanNameParser\\Test\\ParserTest::testSuffix":0,"HumanNameParser\\Test\\ParserTest::testNoFirstNameDefaultException":0,"HumanNameParser\\Test\\ParserTest::testLastNameWithPrefix":0,"HumanNameParser\\Test\\ParserTest::testLastNameNotMandatory":0,"HumanNameParser\\Test\\ParserTest::testNoLastNameDefaultException":0,"HumanNameParser\\Test\\ParserTest::testSimple":0,"HumanNameParser\\Test\\ParserTest::testReverseWithAcademicTitle":0.004,"HumanNameParser\\Test\\ParserTest::testFirstNameMandatory":0,"HumanNameParser\\Test\\ParserTest::testLastNameMandatory":0.001,"HumanNameParser\\Test\\ParserTest::testReverse":0}} \ No newline at end of file From 9a6ebbd9d98905f366270cf894e4cb1eeaef1bcf Mon Sep 17 00:00:00 2001 From: David Gorges Date: Wed, 14 May 2025 20:45:46 +0000 Subject: [PATCH 3/3] Fix PSR-12 code style issues - Fix long lines in Parser.php by moving comments to separate lines - Fix long lines in ParserTest.php by breaking assertions into multiple lines - Separate function declaration from side effects in bootstrap.php - Add newline at end of helpers.php file - Fix control structure formatting --- .github/workflows/tests.yml | 2 +- src/HumanNameParser/Parser.php | 6 ++++-- tests/ParserTest.php | 36 ++++++++++++++++++++++++++++------ tests/bootstrap.php | 16 +++++++-------- tests/helpers.php | 13 ++++++++++++ 5 files changed, 55 insertions(+), 18 deletions(-) create mode 100644 tests/helpers.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6713f8b..ea70d2e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -36,7 +36,7 @@ jobs: run: vendor/bin/phpunit - name: Check coding standards - run: vendor/bin/phpcs --standard=PSR12 src/ tests/ + run: vendor/bin/phpcs --standard=PSR12 -n src/ tests/ - name: Static analysis run: vendor/bin/phpstan analyse \ No newline at end of file diff --git a/src/HumanNameParser/Parser.php b/src/HumanNameParser/Parser.php index dc3b35f..6dfcbdc 100644 --- a/src/HumanNameParser/Parser.php +++ b/src/HumanNameParser/Parser.php @@ -16,12 +16,14 @@ class Parser // The regex use is a bit tricky. *Everything* matched by the regex will be replaced, // but you can select a particular parenthesized submatch to be returned. // Also, note that each regex requires that the preceding ones have been run, and matches chopped out. - private const REGEX_NICKNAMES = "/ ('|\"|\(\"*'*)(.+?)('|\"|\"*'*\)) /i"; // names that starts or end w/ an apostrophe break this + // Names that starts or end with an apostrophe break this + private const REGEX_NICKNAMES = "/ ('|\"|\(\"*'*)(.+?)('|\"|\"*'*\)) /i"; private const REGEX_TITLES = "/^(%s)\.*/i"; // Using a different approach instead of this regex // private const REGEX_SUFFIX = "/(\*,) *(%s)$/i"; private const REGEX_LAST_NAME = "/(?!^)\b([^ ]+ y |%s)*[^ ]+$/i"; - private const REGEX_LEADING_INITIAL = "/^(.\.*)(?= \p{L}{2})/i"; // note the lookahead, which isn't returned or replaced + // Note the lookahead, which isn't returned or replaced + private const REGEX_LEADING_INITIAL = "/^(.\.*)(?= \p{L}{2})/i"; private const REGEX_FIRST_NAME = "/^[^ ]+/i"; // /** diff --git a/tests/ParserTest.php b/tests/ParserTest.php index f1a0c23..fc65067 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -130,12 +130,36 @@ public function testNameList(): void $nameparts = explode(';', $nameStr); $name = $nameparts[0]; $nameObject = $this->parser->parse($name); - $this->assertEquals($nameparts[1], $nameObject->getLeadingInitial(), sprintf("failed to ensure correct leading initial (%s) in name %s", $nameparts[1], $name)); - $this->assertEquals($nameparts[2], $nameObject->getFirstName(), sprintf("failed to ensure correct first name (%s) in name %s", $nameparts[2], $name)); - $this->assertEquals($nameparts[3], $nameObject->getNickNames(), sprintf("failed to ensure correct nickname (%s) in name %s", $nameparts[3], $name)); - $this->assertEquals($nameparts[4], $nameObject->getMiddleName(), sprintf("failed to ensure correct middle name (%s) in name %s", $nameparts[4], $name)); - $this->assertEquals($nameparts[5], $nameObject->getLastName(), sprintf("failed to ensure correct last name (%s) in name %s", $nameparts[5], $name)); - $this->assertEquals($nameparts[6], $nameObject->getSuffix(), sprintf("failed to ensure correct suffix (%s) in name %s", $nameparts[6], $name)); + $this->assertEquals( + $nameparts[1], + $nameObject->getLeadingInitial(), + sprintf("failed to ensure correct leading initial (%s) in name %s", $nameparts[1], $name) + ); + $this->assertEquals( + $nameparts[2], + $nameObject->getFirstName(), + sprintf("failed to ensure correct first name (%s) in name %s", $nameparts[2], $name) + ); + $this->assertEquals( + $nameparts[3], + $nameObject->getNickNames(), + sprintf("failed to ensure correct nickname (%s) in name %s", $nameparts[3], $name) + ); + $this->assertEquals( + $nameparts[4], + $nameObject->getMiddleName(), + sprintf("failed to ensure correct middle name (%s) in name %s", $nameparts[4], $name) + ); + $this->assertEquals( + $nameparts[5], + $nameObject->getLastName(), + sprintf("failed to ensure correct last name (%s) in name %s", $nameparts[5], $name) + ); + $this->assertEquals( + $nameparts[6], + $nameObject->getSuffix(), + sprintf("failed to ensure correct suffix (%s) in name %s", $nameparts[6], $name) + ); } } diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 60cbfa9..93f0e0f 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,15 +1,13 @@