Completed
Push — master ( 625a12...112896 )
by Yuri
10:41
created

Formatter::excludePostNominals()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 4
nop 1
crap 3
1
<?php namespace Tamtamchik\NameCase;
2
3
/**
4
 * Class Formatter.
5
 */
6
class Formatter
7
{
8
    // Irish exceptions.
9
    private const EXCEPTIONS = [
10
        '\bMacEdo'     => '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])'            => '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', 'AUS',
75
        'BA', 'BArch', 'BCh', 'BChir', 'BCL', 'BDS', 'BEd', 'BEM', 'BEng', 'BM', 'BS', 'BSc', 'BSW', 'BVM&S', 'BVScBVetMed',
76
        'CB', 'CBE', 'CEng', 'CertHE', 'CGC', 'CGM', 'CH', 'CIE', 'CMarEngCMarSci', 'CMarTech', 'CMG', 'CMILT', 'CML', 'CPhT', 'CPLCTP', 'CPM', 'CQSW', 'CSciTeach', 'CSI', 'CTL', 'CVO',
77
        'DBE', 'DBEnv', 'DC', 'DCB', 'DCM', 'DCMG', 'DConstMgt', 'DCVO', 'DD', 'DEM', 'DFC', 'DFM', 'DIC', 'Dip', 'DipHE', 'DipLP', 'DipSW', 'DL', 'DLitt', 'DLP', 'DPhil', 'DProf', 'DPT', 'DREst', 'DSC', 'DSM', 'DSO', 'DSocSci',
78
        'ED', 'EdD', 'EJLog', 'EMLog', 'EN', 'EngD', 'EngTech', 'ERD', 'ESLog',
79
        'FADO', 'FAWM', 'FBDOFCOptom', 'FCEM', 'FCILEx', 'FCILT', 'FCSP.', 'FdAFdSc', 'FdEng', 'FFHOM', 'FFPM', 'FRCAFFPMRCA', 'FRCGP', 'FRCOG', 'FRCP', 'FRCPsych', 'FRCS', 'FRCVS', 'FSCR.',
80
        'GBE', 'GC', 'GCB', 'GCIE', 'GCILEx', 'GCMG', 'GCSI', 'GCVO', 'GM',
81
        'HNC', 'HNCert', 'HND', 'HNDip',
82
        'ICTTech', 'IDSM', 'IEng', 'IMarEng', 'IOMCPM', 'ISO',
83
        'J', 'JP', 'JrLog',
84
        'KBE', 'KC', 'KCB', 'KCIE', 'KCMG', 'KCSI', 'KCVO', 'KG', 'KP', 'KT',
85
        'LFHOM', 'LG', 'LJ', 'LLB', 'LLD', 'LLM', 'Log', 'LPE', 'LT', 'LVO',
86
        'MA', 'MAcc', 'MAnth', 'MArch', 'MarEngTech', 'MB', 'MBA', 'MBChB', 'MBE', 'MBEIOM', 'MBiochem', 'MC', 'MCEM', 'MCGI', 'MCh.', 'MChem', 'MChiro', 'MClinRes', 'MComp', 'MCOptom', 'MCSM', 'MCSP', 'MD', 'MEarthSc', 'MEng', 'MEnt', 'MEP', 'MFHOM', 'MFin', 'MFPM', 'MGeol', 'MILT', 'MJur', 'MLA', 'MLitt', 'MM', 'MMath', 'MMathStat', 'MMORSE', 'MMus', 'MOst', 'MP', 'MPAMEd', 'MPharm', 'MPhil', 'MPhys', 'MRCGP', 'MRCOG', 'MRCP', 'MRCPath', 'MRCPCHFRCPCH', 'MRCPsych', 'MRCS', 'MRCVS', 'MRes', 'MS', 'MSc', 'MScChiro', 'MSci', 'MSCR', 'MSM', 'MSocSc', 'MSP', 'MSt', 'MSW', 'MSYP', 'MVO',
87
        'NPQH',
88
        'OBE', 'OBI', 'OM', 'OND',
89
        'PgC', 'PGCAP', 'PGCE', 'PgCert', 'PGCHE', 'PgCLTHE', 'PgD', 'PGDE', 'PgDip', 'PhD', 'PLog', 'PLS',
90
        'QAM', 'QC', 'QFSM', 'QGM', 'QHC', 'QHDS', 'QHNS', 'QHP', 'QHS', 'QPM', 'QS', 'QTSCSci',
91
        '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',
92
        'SCHM', 'SCJ', 'SCLD', 'SEN', 'SGM', 'SL', 'SPANSPMH', 'SPCC', 'SPCN', 'SPDN', 'SPHP', 'SPLD', 'SrLog', 'SRN', 'SROT',
93
        'TD',
94
        'UD',
95
        'V100', 'V200', 'V300', 'VC', 'VD', 'VetMB', 'VN', 'VRD'
96
    ];
97
98
    // Excluded post-nominals
99
    private static $postNominalsExcluded = [];
100
101
    // Default options.
102
    private static $options = [
103
        'lazy'        => true,
104
        'irish'       => true,
105
        'spanish'     => false,
106
        'roman'       => true,
107
        'hebrew'      => true,
108
        'postnominal' => true,
109
    ];
110
111
    /**
112
     * Formatter constructor.
113 4
     *
114
     * @param array $options
115 4
     */
116 4
    public function __construct($options = [])
117
    {
118
        $this->setOptions($options);
119
    }
120
121
    /**
122
     * Global options setter.
123 18
     *
124
     * @param array $options
125 18
     */
126 18
    public static function setOptions($options)
127
    {
128
        self::$options = array_merge(self::$options, $options);
129
    }
130
131
    /**
132
     * Global post-nominals exclusions setter.
133
     *
134
     * @param array|string $values
135
     * @return boolean
136 20
     */
137
    public static function excludePostNominals($values)
138 20
    {
139
        if (is_string($values)) $values = [$values];
140 18
        if (!is_array($values)) return false;
141
142
        self::$postNominalsExcluded = array_merge(self::$postNominalsExcluded, $values);
143 18
    }
144
145
    /**
146 16
     * Main function for NameCase.
147
     *
148 16
     * @param string $name
149 16
     * @param array $options
150
     *
151
     * @return string
152 16
     */
153
    public static function nameCase($name = '', array $options = []): string
154
    {
155 16
        if ($name == '') return $name;
156
157 16
        self::setOptions($options);
158 16
159
        // Do not do anything if string is mixed and lazy option is true.
160
        if (self::$options['lazy'] && self::skipMixed($name)) return $name;
161 16
162 2
        // Capitalize
163
        $name = self::capitalize($name);
164
165 16
        foreach (self::getReplacements() as $pattern => $replacement) {
166 16
            $name = mb_ereg_replace($pattern, $replacement, $name);
167
        }
168
169 16
        return self::processOptions($name);
170
    }
171
172
    private static function processOptions(string $name): string
173
    {
174
        if (self::$options['roman']) {
175
            $name = self::updateRoman($name);
176
        }
177
178
        if (self::$options['spanish']) {
179 16
            $name = self::fixConjunction($name);
180
        }
181 16
182
        if (self::$options['postnominal']) {
183
            $name = self::fixPostNominal($name);
184 16
        }
185 16
186
        return $name;
187
    }
188
189 4
    /**
190 16
     * Capitalize first letters.
191
     *
192 16
     * @param string $name
193
     *
194 16
     * @return string
195
     */
196
    private static function capitalize(string $name): string
197
    {
198
        $name = mb_strtolower($name);
199
200
        $name = mb_ereg_replace_callback('\b\w', function ($matches) {
201
            return mb_strtoupper($matches[0]);
202 16
        }, $name);
203
204
        // Lowercase 's
205 16
        $name = mb_ereg_replace_callback('\'\w\b', function ($matches) {
206 16
            return mb_strtolower($matches[0]);
207 16
        }, $name);
208
209
        $name = self::updateIrish($name);
210 16
211 16
        return $name;
212
    }
213
214 16
    /**
215
     * Define required replacements.
216
     *
217
     * @return array
218
     */
219
    private static function getReplacements(): array
220
    {
221
        // General fixes
222
        $replacements = self::REPLACEMENTS;
223
        if ( ! self::$options['spanish']) {
224 18
            $replacements = array_merge($replacements, self::SPANISH);
225
        }
226 18
227 18
        if (self::$options['hebrew']) {
228
            $replacements = array_merge($replacements, self::HEBREW);
229 18
        }
230
231
        return $replacements;
232
    }
233
234
    /**
235
     * Skip if string is mixed case.
236
     *
237
     * @param string $name
238
     *
239 16
     * @return bool
240
     */
241 16
    private static function skipMixed(string $name): bool
242
    {
243
        $firstLetterLower = $name[0] == mb_strtolower($name[0]);
244 16
        $allLowerOrUpper = (mb_strtolower($name) == $name || mb_strtoupper($name) == $name);
245 16
246
        return ! ($firstLetterLower || $allLowerOrUpper);
247 6
    }
248
249
    /**
250 16
     * Update for Irish names.
251
     *
252
     * @param string $name
253
     *
254
     * @return string
255
     */
256
    private static function updateIrish(string $name): string
257
    {
258
        if ( ! self::$options['irish']) return $name;
259
260 16
        if (
261
            mb_ereg_match('.*?\bMac[A-Za-z]{2,}[^aciozj]\b', $name) ||
262
            mb_ereg_match('.*?\bMc', $name)
263 16
        ) {
264 16
            $name = self::updateMac($name);
265
        }
266
267
        return mb_ereg_replace('Macmurdo', 'MacMurdo', $name);
268
    }
269
270
    /**
271
     * Fix roman numeral names.
272
     *
273
     * @param string $name
274 6
     *
275
     * @return string
276
     */
277 6
    private static function updateRoman(string $name): string
278 6
    {
279
        return mb_ereg_replace_callback(self::ROMAN_REGEX, function ($matches) {
280
            return mb_strtoupper($matches[0]);
281 6
        }, $name);
282 6
    }
283
284
    /**
285 6
     * Updates irish Mac & Mc.
286
     *
287
     * @param string $name
288
     *
289
     * @return string
290
     */
291
    private static function updateMac(string $name): string
292
    {
293
        $name = mb_ereg_replace_callback('\b(Ma?c)([A-Za-z]+)', function ($matches) {
294
            return $matches[1] . mb_strtoupper(mb_substr($matches[2], 0, 1)) . mb_substr($matches[2], 1);
295 2
        }, $name);
296
297 2
        // Now fix "Mac" exceptions
298 2
        foreach (self::EXCEPTIONS as $pattern => $replacement) {
299
            $name = mb_ereg_replace($pattern, $replacement, $name);
300 2
        }
301
302
        return $name;
303
    }
304
305
    /**
306
     * Fix Spanish conjunctions.
307
     *
308
     * @param string $name
309 16
     *
310
     * @return string
311 16
     */
312 16
    private static function fixConjunction(string $name): string
313
    {
314 16
        foreach (self::CONJUNCTIONS as $conjunction) {
315
            $name = mb_ereg_replace('\b' . $conjunction . '\b', mb_strtolower($conjunction), $name);
316
        }
317
        return $name;
318
    }
319
320
    /**
321
     * Fix post-nominal letter cases.
322
     *
323
     * @param string $name
324
     * @return string
325
     */
326
    private static function fixPostNominal(string $name): string
327
    {
328
        $postNominals = array_diff(self::POST_NOMINALS, self::$postNominalsExcluded);
329
        foreach ($postNominals as $postNominal) {
330
            $name = mb_ereg_replace('\b' . $postNominal . '\b', $postNominal, $name, 'ix');
331
        }
332
        return $name;
333
    }
334
}
335