├── _config.yml ├── .editorconfig ├── src ├── function.php └── Formatter.php ├── LICENSE.md ├── CONTRIBUTING.md ├── composer.json ├── CHANGELOG.md ├── examples └── demo.php ├── CODE_OF_CONDUCT.md └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/function.php: -------------------------------------------------------------------------------- 1 | 4 | 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/tamtamchik/namecase). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ composer test 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tamtamchik/namecase", 3 | "type": "library", 4 | "description": "This package allows you to convert names into the correct case where possible.", 5 | "keywords": [ 6 | "tamtamchik", 7 | "namecase", 8 | "strings", 9 | "properly", 10 | "cased" 11 | ], 12 | "homepage": "https://github.com/tamtamchik/namecase", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Yuri Tkachenko", 17 | "email": "yuri.tam.tkachenko@gmail.com", 18 | "homepage": "https://tamtamchika.net" 19 | } 20 | ], 21 | "require": { 22 | "php": ">=7.3", 23 | "ext-mbstring": "*" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^9", 27 | "scrutinizer/ocular": "^1" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Tamtamchik\\NameCase\\": "src" 32 | }, 33 | "files": [ 34 | "src/function.php" 35 | ] 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Tamtamchik\\NameCase\\Test\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "tests": "vendor/bin/phpunit", 44 | "demo": "php examples/demo.php" 45 | }, 46 | "extra": { 47 | "branch-alias": { 48 | "dev-master": "3.0-dev", 49 | "2.x": "2.x-dev", 50 | "1.x": "1.x-dev" 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All Notable changes to `tamtamchik/namecase` will be documented in this file. 4 | 5 | Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles. 6 | 7 | ## 3.0.0 - 2023-01-26 8 | 9 | **Breaking Change!** Minimum PHP version is now 7.3. 10 | 11 | ### Added 12 | 13 | - Namespaced function `Tamtamchik\NameCase\str_name_case` and tests. 14 | 15 | ## 2.3.0 – 2021-02-20 16 | 17 | ### Added 18 | 19 | * Support for lowercase words The, Of, And ([#17](https://github.com/tamtamchik/namecase/issues/17)) 20 | 21 | ## 2.2.0 – 2021-01-15 22 | 23 | ### Added 24 | 25 | * PHP 8 support. 26 | 27 | ## 2.1.1 – 2020-03-05 28 | 29 | ### Added 30 | 31 | * Fix missing end word boundary on `ten`, `ter` Dutch/Flemish (#9). 32 | 33 | ## 2.1.0 – 2020-02-07 34 | 35 | ### Added 36 | 37 | * `excludePostNominals` method to add excluded values (#7). 38 | 39 | ## 2.0.1 – 2020-02-07 40 | 41 | ### Added 42 | 43 | * `MBE` post-nominal. 44 | 45 | ## 2.0.0 - 2019-07-17 46 | 47 | ### Breaking changes 48 | 49 | * Dropped support for `PHP < 7.2` 50 | * Dropped support for global function `str_name_case` 51 | * `spanish` option is now `false` by default. 52 | 53 | ### Added 54 | * Post-nominals detection. 55 | 56 | * Constructor now supports `options` parameter. 57 | * Added options: 58 | - `lazy` – Default: **true**. Do not do anything if the string is already mixed case and the lazy option is `true`. 59 | - `irish` – Default: **true**. Correct `Mac`. 60 | - `spanish` – Default: **false**. Corrects `el, la` and Spanish conjunctions. 61 | - `roman` – Default: **true**. Corrects roman numbers. 62 | - `hebrew` – Default: **true**. Corrects `ben, bat`. 63 | - `postnominal` – Default: **true**. Corrects post-nominals. 64 | 65 | ### Updated 66 | * New irish `Mac` exceptions. 67 | 68 | ## 1.0.6 - 2019-05-24 69 | 70 | ### Fixed 71 | 72 | * Fix missing end word boundary on `ten`, `ter` Dutch/Flemish (#9). 73 | 74 | ## 1.0.5 - 2019-07-16 75 | 76 | ### Added 77 | - Dutch: `ter/ten` (#6) thx, @MagicLegend 78 | 79 | ## 1.0.4 - 2019-05-24 80 | 81 | ### Fixed 82 | - Add `ext-mbstring` to composer.json 83 | 84 | ## 1.0.3 - 2018-09-12 85 | 86 | ### Fixed 87 | - "empty string" warning 88 | 89 | ## 1.0.2 - 2016-02-06 90 | 91 | ### Added 92 | - Dutch: `den` (thx, @nexxai) 93 | 94 | ## 1.0.1 - 2016-01-30 95 | 96 | ### Added 97 | - static call 98 | - instantiation 99 | - tests 100 | 101 | ## 1.0.0 - 2016-01-29 102 | 103 | Initial release. 104 | -------------------------------------------------------------------------------- /examples/demo.php: -------------------------------------------------------------------------------- 1 | ' . Formatter::nameCase('KEITH') . PHP_EOL; 11 | echo 'LEIGH-WILLIAMS => ' . Formatter::nameCase('LEIGH-WILLIAMS') . PHP_EOL; 12 | echo 'MCCARTHY => ' . Formatter::nameCase('MCCARTHY') . PHP_EOL; 13 | 14 | // As instance call 15 | $formatter = new Formatter(); 16 | echo 'O\'CALLAGHAN => ' . $formatter->nameCase('O\'CALLAGHAN') . PHP_EOL; 17 | echo 'ST. JOHN => ' . $formatter->nameCase('ST. JOHN') . PHP_EOL; 18 | echo 'VON STREIT => ' . $formatter->nameCase('VON STREIT') . PHP_EOL; 19 | echo 'AP LLWYD DAFYDD => ' . $formatter->nameCase('AP LLWYD DAFYDD') . PHP_EOL; 20 | 21 | // As function call 22 | echo 'HENRY VIII => ' . str_name_case('HENRY VIII') . PHP_EOL; 23 | echo 'VAN DYKE => ' . str_name_case('VAN DYKE') . PHP_EOL; 24 | echo 'PRINCE PHILIP, DUKE OF EDINBURGH => ' . str_name_case('PRINCE PHILIP, DUKE OF EDINBURGH') . PHP_EOL; 25 | echo 'LOUIS XIV => ' . str_name_case('LOUIS XIV') . PHP_EOL; 26 | echo 'KEITH => ' . str_name_case('KEITH') . PHP_EOL; 27 | 28 | echo PHP_EOL; 29 | echo '*** lazy = true (default) ***' . PHP_EOL; 30 | echo 'Da Vinci => ' . $formatter->nameCase('Da Vinci', ['lazy' => true]) . PHP_EOL; 31 | echo '*** lazy = false ***' . PHP_EOL; 32 | echo 'Da Vinci => ' . $formatter->nameCase('Da Vinci', ['lazy' => false]) . PHP_EOL; 33 | 34 | echo PHP_EOL; 35 | echo '*** irish = true (default) ***' . PHP_EOL; 36 | echo 'JOHN MACDONALD => ' . $formatter->nameCase('JOHN MACDONALD', ['irish' => true]) . PHP_EOL; 37 | echo '*** irish = false ***' . PHP_EOL; 38 | echo 'JOHN MACDONALD => ' . $formatter->nameCase('JOHN MACDONALD', ['irish' => false]) . PHP_EOL; 39 | 40 | echo PHP_EOL; 41 | echo '*** spanish = true ***' . PHP_EOL; 42 | echo 'EL PASO => ' . $formatter->nameCase('EL PASO', ['spanish' => true]) . PHP_EOL; 43 | echo '*** spanish = false (default) ***' . PHP_EOL; 44 | echo 'EL PASO => ' . $formatter->nameCase('EL PASO', ['spanish' => false]) . PHP_EOL; 45 | 46 | echo PHP_EOL; 47 | echo '*** roman = true (default) ***' . PHP_EOL; 48 | echo 'NA LIV => ' . $formatter->nameCase('NA LIV', ['roman' => true]) . PHP_EOL; 49 | echo '*** roman = false ***' . PHP_EOL; 50 | echo 'NA LIV => ' . $formatter->nameCase('NA LIV', ['roman' => false]) . PHP_EOL; 51 | 52 | echo PHP_EOL; 53 | echo '*** hebrew = true (default) ***' . PHP_EOL; 54 | echo 'BEN GURION => ' . $formatter->nameCase('BEN GURION', ['hebrew' => true]) . PHP_EOL; 55 | echo '*** hebrew = false ***' . PHP_EOL; 56 | echo 'BEN GURION => ' . $formatter->nameCase('BEN GURION', ['hebrew' => false]) . PHP_EOL; 57 | 58 | echo PHP_EOL; 59 | echo '*** postnominal = true (default) ***' . PHP_EOL; 60 | echo 'BRIAN MAY, CBE, PHD => ' . $formatter->nameCase('BRIAN MAY, CBE, PHD', ['postnominal' => true]) . PHP_EOL; 61 | echo '*** postnominal = false ***' . PHP_EOL; 62 | echo 'BRIAN MAY, CBE, PHD => ' . $formatter->nameCase('BRIAN MAY, CBE, PHD', ['postnominal' => false]) . PHP_EOL; 63 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at yuri.tam.tkachenko@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NameCase 2 | 3 | [![Latest Version on Packagist][ico-version]][link-packagist] 4 | ![PHP][ico-php] 5 | [![Total Downloads][ico-downloads]][link-downloads] 6 | [![Software License][ico-license]](LICENSE.md) 7 | [![Build Status][ico-scrutinizer-build]][link-scrutinizer] 8 | [![Coverage Status][ico-scrutinizer]][link-scrutinizer] 9 | [![Quality Score][ico-code-quality]][link-code-quality] 10 | 11 | [![SensioLabsInsight][ico-insight]][link-insight] 12 | 13 | Forenames and surnames are often stored either entirely in UPPERCASE or lowercase. This package allows you to convert 14 | names into the correct case where possible. Although forenames and surnames are typically stored separately if they 15 | appear in a single string, whitespace-separated, NameCase deals correctly with them. 16 | 17 | Currently, NameCase correctly name-cases names which include any of the following: 18 | 19 | ``` 20 | Mc, Mac, al, el, ap, bat, ben, bin, binti, binte, da, de, das, dos, delle, della, di, du, del, der, den, ten, ter, la, le, lo, van and von. 21 | ``` 22 | 23 | It correctly deals with names that contain apostrophes and hyphens, too. 24 | 25 | > [!NOTE] 26 | > This README.md is for version 3.x. 27 | > If you need a PHP 5 compatible version, please use 28 | > 1.0.x! [README.md](https://github.com/tamtamchik/namecase/blob/1.0.x/README.md#namecase) 29 | 30 | ## Install 31 | 32 | Via Composer 33 | 34 | ```bash 35 | $ composer require tamtamchik/namecase 36 | ``` 37 | 38 | ## Usage 39 | 40 | ```php 41 | use \Tamtamchik\NameCase\Formatter; 42 | use function \Tamtamchik\NameCase\str_name_case; 43 | 44 | // As a static call 45 | Formatter::nameCase("KEITH"); // => Keith 46 | Formatter::nameCase("LEIGH-WILLIAMS"); // => Leigh-Williams 47 | Formatter::nameCase("MCCARTHY"); // => McCarthy 48 | Formatter::nameCase("O'CALLAGHAN"); // => O'Callaghan 49 | Formatter::nameCase("ST. JOHN"); // => St. John 50 | Formatter::nameCase("VON STREIT"); // => von Streit 51 | Formatter::nameCase("AP LLWYD DAFYDD"); // => ap Llwyd Dafydd 52 | Formatter::nameCase("HENRY VIII"); // => Henry VIII 53 | Formatter::nameCase("VAN DYKE"); // => van Dyke 54 | 55 | // Or as an instance 56 | $formatter = new Formatter(); 57 | $formatter->nameCase("LOUIS XIV"); // => Louis XIV 58 | 59 | // Or as a function 60 | str_name_case("JJ ABRAMS"); // => JJ Abrams 61 | 62 | // Passing options (see below for details) 63 | Formatter::setOptions([ 64 | 'lazy' => true, 65 | 'irish' => true, 66 | 'spanish' => false, 67 | 'roman' => true, 68 | 'hebrew' => true, 69 | 'postnominal' => true, 70 | ]); 71 | 72 | // Or 73 | $formatter = new Formatter(['spanish' => true]); 74 | 75 | // Or 76 | $formatter->setOptions([ 77 | 'lazy' = false, 78 | 'postnominal' => false 79 | ]); 80 | 81 | // Or even 82 | Formatter::nameCase("VAN DYKE", ['lazy' = false]); 83 | 84 | // And for function 85 | str_name_case("VAN DYKE", ['lazy' = false]); 86 | ``` 87 | 88 | ## Options 89 | 90 | * `lazy` – Default: `true`. Do not do anything if the string is already mixed case and the lazy option is `true`. 91 | * `irish` – Default: `true`. Correct "Mac" exceptions. 92 | * `spanish` – Default: `false`. Correct `el, la` and Spanish conjunctions. 93 | * `roman` – Default: `true`. Correct Roman numbers. 94 | * `hebrew` – Default: `true`. Correct `ben, bat`. 95 | * `postnominal` – Default: `true`. Correct post-nominal e.g. `PhD`. 96 | 97 | ## Exclude Post-Nominals 98 | 99 | ```php 100 | instead of using the issue 127 | tracker. 128 | 129 | ## Acknowledgements 130 | 131 | This library is a port of the [Perl library](https://metacpan.org/release/BARBIE/Lingua-EN-NameCase-1.19) and owes most 132 | of its functionality to the Perl version by Mark Summerfield. 133 | I also used some solutions from [Ruby version](https://github.com/tenderlove/namecase) by Aaron Patterson. 134 | Any bugs in the PHP port are my fault. 135 | 136 | ## Credits 137 | 138 | Original PERL `Lingua::EN::NameCase` Version: 139 | 140 | - Copyright © Mark Summerfield 1998-2014. All Rights Reserved. 141 | - Copyright © Barbie 2014-2019. All Rights Reserved. 142 | 143 | Ruby Version: 144 | 145 | - Copyright © Aaron Patterson 2006. All Rights Reserved. 146 | 147 | PHP Version: 148 | 149 | - [Yuri Tkachenko][link-author] 150 | - [All Contributors][link-contributors] 151 | 152 | ## License 153 | 154 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 155 | 156 | --- 157 | 158 | [![Buy Me A Coffee][ico-coffee]][link-coffee] 159 | 160 | [ico-version]: https://img.shields.io/packagist/v/tamtamchik/namecase.svg?style=flat-square 161 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square 162 | [ico-scrutinizer-build]: https://img.shields.io/scrutinizer/build/g/tamtamchik/namecase.svg?style=flat-square 163 | [ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/tamtamchik/namecase.svg?style=flat-square 164 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/tamtamchik/namecase.svg?style=flat-square 165 | [ico-downloads]: https://img.shields.io/packagist/dt/tamtamchik/namecase.svg?style=flat-square 166 | [ico-coffee]: https://img.shields.io/badge/Buy%20Me%20A-Coffee-%236F4E37.svg?style=flat-square 167 | [ico-insight]: https://insight.symfony.com/projects/29bec8f4-aeb0-4a62-9c2e-2d93a0a71bcc/small.svg 168 | [ico-php]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgithub.com%2Ftamtamchik%2Fnamecase%2Fraw%2Fmaster%2Fcomposer.json&query=%24.require.php&style=flat-square&label=PHP 169 | 170 | [link-packagist]: https://packagist.org/packages/tamtamchik/namecase 171 | [link-travis]: https://app.travis-ci.com/github/tamtamchik/namecase 172 | [link-scrutinizer]: https://scrutinizer-ci.com/g/tamtamchik/namecase/code-structure 173 | [link-code-quality]: https://scrutinizer-ci.com/g/tamtamchik/namecase 174 | [link-downloads]: https://packagist.org/packages/tamtamchik/namecase 175 | [link-author]: https://github.com/tamtamchik 176 | [link-contributors]: ../../contributors 177 | [link-coffee]: https://www.buymeacoffee.com/tamtamchik 178 | [link-insight]: https://insight.symfony.com/projects/29bec8f4-aeb0-4a62-9c2e-2d93a0a71bcc 179 | -------------------------------------------------------------------------------- /src/Formatter.php: -------------------------------------------------------------------------------- 1 | 'Macedo', 11 | '\bMacEvicius' => 'Macevicius', 12 | '\bMacHado' => 'Machado', 13 | '\bMacHar' => 'Machar', 14 | '\bMacHin' => 'Machin', 15 | '\bMacHlin' => 'Machlin', 16 | '\bMacIas' => 'Macias', 17 | '\bMacIulis' => 'Maciulis', 18 | '\bMacKie' => 'Mackie', 19 | '\bMacKle' => 'Mackle', 20 | '\bMacKlin' => 'Macklin', 21 | '\bMacKmin' => 'Mackmin', 22 | '\bMacQuarie' => 'Macquarie', 23 | '\bMacOmber' => 'Macomber', 24 | '\bMacIn' => 'Macin', 25 | '\bMacKintosh' => 'Mackintosh', 26 | '\bMacKen' => 'Macken', 27 | '\bMacHen' => 'Machen', 28 | '\bMacisaac' => 'MacIsaac', 29 | '\bMacHiel' => 'Machiel', 30 | '\bMacIol' => 'Maciol', 31 | '\bMacKell' => 'Mackell', 32 | '\bMacKlem' => 'Macklem', 33 | '\bMacKrell' => 'Mackrell', 34 | '\bMacLin' => 'Maclin', 35 | '\bMacKey' => 'Mackey', 36 | '\bMacKley' => 'Mackley', 37 | '\bMacHell' => 'Machell', 38 | '\bMacHon' => 'Machon', 39 | ]; 40 | 41 | // General replacements. 42 | private const REPLACEMENTS = [ 43 | '\bAl(?=\s+\w)' => 'al', // al Arabic or forename Al. 44 | '\bAp\b' => 'ap', // ap Welsh. 45 | '\b(Bin|Binti|Binte)\b' => 'bin', // bin, binti, binte Arabic. 46 | '\bDell([ae])\b' => 'dell\1', // della and delle Italian. 47 | '\bD([aeiou])\b' => 'd\1', // da, de, di Italian; du French; do Brasil. 48 | '\bD([ao]s)\b' => 'd\1', // das, dos Brasileiros. 49 | '\bDe([lrn])\b' => 'de\1', // del Italian; der/den Dutch/Flemish. 50 | '\bL([eo])\b' => 'l\1', // lo Italian; le French. 51 | '\bTe([rn])\b' => 'te\1', // ten, ter Dutch/Flemish. 52 | '\bVan(?=\s+\w)' => 'van', // van German or forename Van. 53 | '\bVon\b' => 'von', // von Dutch/Flemish. 54 | ]; 55 | 56 | private const SPANISH = [ 57 | '\bEl\b' => 'el', // el Greek or El Spanish. 58 | '\bLa\b' => 'la', // la French or La Spanish. 59 | ]; 60 | 61 | private const HEBREW = [ 62 | '\bBen(?=\s+\w)' => 'ben', // ben Hebrew or forename Ben. 63 | '\bBat(?=\s+\w)' => 'bat', // bat Hebrew or forename Bat. 64 | ]; 65 | 66 | // Spanish conjunctions. 67 | private const CONJUNCTIONS = ['Y', 'E', 'I']; 68 | 69 | // Roman letters regexp. 70 | private const ROMAN_REGEX = '\b((?:[Xx]{1,3}|[Xx][Ll]|[Ll][Xx]{0,3})?(?:[Ii]{1,3}|[Ii][VvXx]|[Vv][Ii]{0,3})?)\b'; 71 | 72 | // Post nominal values. 73 | private const POST_NOMINALS = [ 74 | 'ACILEx', 'ACSM', 'ADC', 'AEPC', 'AFC', 'AFM', 'AICSM', 'AKC', 'AM', 'ARBRIBA', 'ARCS', 'ARRC', 'ARSM', 'AUH', 75 | 'AUS', 76 | 'BA', 'BArch', 'BCh', 'BChir', 'BCL', 'BDS', 'BEd', 'BEM', 'BEng', 'BM', 'BS', 'BSc', 'BSW', 'BVM&S', 77 | 'BVScBVetMed', 78 | 'CB', 'CBE', 'CEng', 'CertHE', 'CGC', 'CGM', 'CH', 'CIE', 'CMarEngCMarSci', 'CMarTech', 'CMG', 'CMILT', 79 | 'CML', 'CPhT', 'CPLCTP', 'CPM', 'CQSW', 'CSciTeach', 'CSI', 'CTL', 'CVO', 80 | 'DBE', 'DBEnv', 'DC', 'DCB', 'DCM', 'DCMG', 'DConstMgt', 'DCVO', 'DD', 'DEM', 'DFC', 'DFM', 'DIC', 'Dip', 81 | 'DipHE', 'DipLP', 'DipSW', 'DL', 'DLitt', 'DLP', 'DPhil', 'DProf', 'DPT', 'DREst', 'DSC', 'DSM', 'DSO', 82 | 'DSocSci', 83 | 'ED', 'EdD', 'EJLog', 'EMLog', 'EN', 'EngD', 'EngTech', 'ERD', 'ESLog', 84 | 'FADO', 'FAWM', 'FBDOFCOptom', 'FCEM', 'FCILEx', 'FCILT', 'FCSP.', 'FdAFdSc', 'FdEng', 'FFHOM', 'FFPM', 85 | 'FRCAFFPMRCA', 'FRCGP', 'FRCOG', 'FRCP', 'FRCPsych', 'FRCS', 'FRCVS', 'FSCR.', 86 | 'GBE', 'GC', 'GCB', 'GCIE', 'GCILEx', 'GCMG', 'GCSI', 'GCVO', 'GM', 87 | 'HNC', 'HNCert', 'HND', 'HNDip', 88 | 'ICTTech', 'IDSM', 'IEng', 'IMarEng', 'IOMCPM', 'ISO', 89 | 'J', 'JP', 'JrLog', 90 | 'KBE', 'KC', 'KCB', 'KCIE', 'KCMG', 'KCSI', 'KCVO', 'KG', 'KP', 'KT', 91 | 'LFHOM', 'LG', 'LJ', 'LLB', 'LLD', 'LLM', 'Log', 'LPE', /* 'LT', - excluded, see initial names */ 92 | 'LVO', 93 | 'MA', 'MAcc', 'MAnth', 'MArch', 'MarEngTech', 'MB', 'MBA', 'MBChB', 'MBE', 'MBEIOM', 'MBiochem', 'MC', 'MCEM', 94 | 'MCGI', 'MCh.', 'MChem', 'MChiro', 'MClinRes', 'MComp', 'MCOptom', 'MCSM', 'MCSP', 'MD', 'MEarthSc', 95 | 'MEng', 'MEnt', 'MEP', 'MFHOM', 'MFin', 'MFPM', 'MGeol', 'MILT', 'MJur', 'MLA', 'MLitt', 'MM', 'MMath', 96 | 'MMathStat', 'MMORSE', 'MMus', 'MOst', 'MP', 'MPAMEd', 'MPharm', 'MPhil', 'MPhys', 'MRCGP', 'MRCOG', 97 | 'MRCP', 'MRCPath', 'MRCPCHFRCPCH', 'MRCPsych', 'MRCS', 'MRCVS', 'MRes', 98 | /* 'MS', - excluded, see initial names */ 99 | 'MSc', 'MScChiro', 'MSci', 100 | 'MSCR', 'MSM', 'MSocSc', 'MSP', 'MSt', 'MSW', 'MSYP', 'MVO', 101 | 'NPQH', 102 | 'OBE', 'OBI', 'OM', 'OND', 103 | 'PgC', 'PGCAP', 'PGCE', 'PgCert', 'PGCHE', 'PgCLTHE', 'PgD', 'PGDE', 'PgDip', 'PhD', 'PLog', 'PLS', 104 | 'QAM', 'QC', 'QFSM', 'QGM', 'QHC', 'QHDS', 'QHNS', 'QHP', 'QHS', 'QPM', 'QS', 'QTSCSci', 105 | 'RD', 'RFHN', 'RGN', 'RHV', 'RIAI', 'RIAS', 'RM', 'RMN', 'RN', 'RN1RNA', 'RN2', 'RN3', 'RN4', 'RN5', 'RN6', 'RN7', 'RN8', 'RN9', 'RNC', 'RNLD', 'RNMH', 'ROH', 'RRC', 'RSAW', 'RSci', 'RSciTech', 'RSCN', 'RSN', 'RVM', 'RVN', 106 | 'SCHM', 'SCJ', 'SCLD', 'SEN', 'SGM', 'SL', 'SPANSPMH', 'SPCC', 'SPCN', 'SPDN', 'SPHP', 'SPLD', 'SrLog', 'SRN', 'SROT', 107 | 'TD', 108 | 'UD', 109 | 'V100', 'V200', 'V300', 'VC', 'VD', 'VetMB', 'VN', 'VRD' 110 | ]; 111 | 112 | // Excluded post-nominals 113 | private const INITIAL_NAME_REGEX = '\b(Aj|[bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ]{2})\s'; 114 | 115 | // Most two-letter words with no vowels should be kept in all caps as initials 116 | private const INITIAL_NAME_EXCEPTIONS = [ 117 | 'Mr', 118 | 'Ms', // Replaces Member of the Senedd post nominal. 119 | 'Dr', 120 | 'St', 121 | 'Jr', 122 | 'Sr', 123 | 'Lt', // Replaces Lady of the Order of the Thistle post nominal. 124 | ]; 125 | private const LOWER_CASE_WORDS = ['The', 'Of', 'And']; 126 | 127 | // Lowercase words 128 | private static $postNominalsExcluded = []; 129 | 130 | // Default options. 131 | private static $options = [ 132 | 'lazy' => true, 133 | 'irish' => true, 134 | 'spanish' => false, 135 | 'roman' => true, 136 | 'hebrew' => true, 137 | 'postnominal' => true, 138 | ]; 139 | 140 | /** 141 | * Formatter constructor. 142 | * 143 | * @param array $options 144 | */ 145 | public function __construct(array $options = []) 146 | { 147 | $this->setOptions($options); 148 | } 149 | 150 | /** 151 | * Global options setter. 152 | * 153 | * @param array $options 154 | */ 155 | public static function setOptions(array $options): void 156 | { 157 | self::$options = array_merge(self::$options, $options); 158 | } 159 | 160 | /** 161 | * Global post-nominals exclusions setter. 162 | * 163 | * @param array|string|null $values 164 | * @return boolean|void 165 | */ 166 | public static function excludePostNominals($values) 167 | { 168 | if (is_string($values)) { 169 | $values = [$values]; 170 | } 171 | 172 | if ( ! is_array($values)) { 173 | return false; 174 | } 175 | 176 | self::$postNominalsExcluded = array_merge(self::$postNominalsExcluded, $values); 177 | } 178 | 179 | /** 180 | * Main function for NameCase. 181 | * 182 | * @param string|null $name 183 | * @param array|null $options 184 | * 185 | * @return string 186 | */ 187 | public static function nameCase(?string $name = '', ?array $options = []): string 188 | { 189 | $name = is_null($name) ? '' : $name; 190 | 191 | self::setOptions($options); 192 | 193 | // Do not do anything if string is mixed and lazy option is true. 194 | if ( ! self::canBeProcessed($name)) { 195 | return $name; 196 | } 197 | 198 | $original = $name; 199 | 200 | // Capitalize 201 | $name = self::capitalize($name); 202 | foreach (self::getReplacements() as $pattern => $replacement) { 203 | $name = mb_ereg_replace($pattern, $replacement, $name); 204 | 205 | // Very difficult to write a test in modern environments 206 | // @codeCoverageIgnoreStart 207 | if ( ! is_string($name)) { 208 | return $original; 209 | } 210 | // @codeCoverageIgnoreEnd 211 | } 212 | 213 | $name = self::correctInitialNames($name); 214 | $name = self::correctLowerCaseWords($name); 215 | 216 | return self::processOptions($name); 217 | } 218 | 219 | /** 220 | * Check if string can be processed. 221 | * 222 | * @param string $name 223 | * 224 | * @return bool 225 | */ 226 | private static function canBeProcessed(string $name): bool 227 | { 228 | if ($name != '') { 229 | return ! (self::$options['lazy'] && self::skipMixed($name)); 230 | } 231 | 232 | return false; 233 | } 234 | 235 | /** 236 | * Skip if string is mixed case. 237 | * 238 | * @param string $name 239 | * 240 | * @return bool 241 | */ 242 | private static function skipMixed(string $name): bool 243 | { 244 | $firstLetterLower = $name[0] == mb_strtolower($name[0]); 245 | $allLowerOrUpper = (mb_strtolower($name) == $name || mb_strtoupper($name) == $name); 246 | 247 | return ! ($firstLetterLower || $allLowerOrUpper); 248 | } 249 | 250 | /** 251 | * Capitalize first letters. 252 | * 253 | * @param string $name 254 | * 255 | * @return string 256 | */ 257 | private static function capitalize(string $name): string 258 | { 259 | $name = mb_strtolower($name); 260 | 261 | $name = mb_ereg_replace_callback('\b\w', function ($matches) { 262 | return mb_strtoupper($matches[0]); 263 | }, $name); 264 | 265 | // Lowercase 's 266 | $name = mb_ereg_replace_callback('\'\w\b', function ($matches) { 267 | return mb_strtolower($matches[0]); 268 | }, $name); 269 | 270 | return self::updateIrish($name); 271 | } 272 | 273 | /** 274 | * Update for Irish names. 275 | * 276 | * @param string $name 277 | * 278 | * @return string 279 | */ 280 | private static function updateIrish(string $name): string 281 | { 282 | if ( ! self::$options['irish']) return $name; 283 | 284 | if ( 285 | mb_ereg_match('.*?\bMac[A-Za-z]{2,}[^aciozj]\b', $name) || 286 | mb_ereg_match('.*?\bMc', $name) 287 | ) { 288 | $name = self::updateMac($name); 289 | } 290 | 291 | return mb_ereg_replace('Macmurdo', 'MacMurdo', $name); 292 | } 293 | 294 | /** 295 | * Updates irish Mac & Mc. 296 | * 297 | * @param string $name 298 | * 299 | * @return string 300 | */ 301 | private static function updateMac(string $name): string 302 | { 303 | $name = mb_ereg_replace_callback('\b(Ma?c)([A-Za-z]+)', function ($matches) { 304 | return $matches[1] . mb_strtoupper(mb_substr($matches[2], 0, 1)) . mb_substr($matches[2], 1); 305 | }, $name); 306 | 307 | // Now fix "Mac" exceptions 308 | foreach (self::EXCEPTIONS as $pattern => $replacement) { 309 | $name = mb_ereg_replace($pattern, $replacement, $name); 310 | } 311 | 312 | return $name; 313 | } 314 | 315 | /** 316 | * Define required replacements. 317 | * 318 | * @return array 319 | */ 320 | private static function getReplacements(): array 321 | { 322 | // General fixes 323 | $replacements = self::REPLACEMENTS; 324 | if ( ! self::$options['spanish']) { 325 | $replacements = array_merge($replacements, self::SPANISH); 326 | } 327 | 328 | if (self::$options['hebrew']) { 329 | $replacements = array_merge($replacements, self::HEBREW); 330 | } 331 | 332 | return $replacements; 333 | } 334 | 335 | /** 336 | * Correct capitalization of initial names like JJ and TJ. 337 | * 338 | * @param string $name 339 | * 340 | * @return string 341 | */ 342 | private static function correctInitialNames(string $name): string 343 | { 344 | return mb_ereg_replace_callback(self::INITIAL_NAME_REGEX, function ($matches) { 345 | $match = $matches[0]; 346 | 347 | if (in_array($matches[1], self::INITIAL_NAME_EXCEPTIONS)) { 348 | return $match; 349 | } 350 | 351 | return mb_strtoupper($match); 352 | }, $name); 353 | } 354 | 355 | /** 356 | * Correct lower-case words of titles. 357 | * 358 | * @param string $name 359 | * 360 | * @return string 361 | */ 362 | private static function correctLowerCaseWords(string $name): string 363 | { 364 | foreach (self::LOWER_CASE_WORDS as $lowercase) { 365 | $name = mb_ereg_replace('\b' . $lowercase . '\b', mb_strtolower($lowercase), $name); 366 | } 367 | return $name; 368 | } 369 | 370 | /** 371 | * Process options with given name 372 | * 373 | * @param string $name 374 | * 375 | * @return string 376 | */ 377 | private static function processOptions(string $name): string 378 | { 379 | if (self::$options['roman']) { 380 | $name = self::updateRoman($name); 381 | } 382 | 383 | if (self::$options['spanish']) { 384 | $name = self::fixConjunction($name); 385 | } 386 | 387 | if (self::$options['postnominal']) { 388 | $name = self::fixPostNominal($name); 389 | } 390 | 391 | return $name; 392 | } 393 | 394 | /** 395 | * Fix roman numeral names. 396 | * 397 | * @param string $name 398 | * 399 | * @return string 400 | */ 401 | private static function updateRoman(string $name): string 402 | { 403 | return mb_ereg_replace_callback(self::ROMAN_REGEX, function ($matches) { 404 | return mb_strtoupper($matches[0]); 405 | }, $name); 406 | } 407 | 408 | /** 409 | * Fix Spanish conjunctions. 410 | * 411 | * @param string $name 412 | * 413 | * @return string 414 | */ 415 | private static function fixConjunction(string $name): string 416 | { 417 | foreach (self::CONJUNCTIONS as $conjunction) { 418 | $name = mb_ereg_replace('\b' . $conjunction . '\b', mb_strtolower($conjunction), $name); 419 | } 420 | return $name; 421 | } 422 | 423 | /** 424 | * Fix post-nominal letter cases. 425 | * 426 | * @param string $name 427 | * @return string 428 | */ 429 | private static function fixPostNominal(string $name): string 430 | { 431 | $postNominals = array_diff(self::POST_NOMINALS, self::$postNominalsExcluded); 432 | foreach ($postNominals as $postNominal) { 433 | $name = mb_ereg_replace('\b' . $postNominal . '\b', $postNominal, $name, 'ix'); 434 | } 435 | return $name; 436 | } 437 | } 438 | --------------------------------------------------------------------------------