Completed
Push — master ( fd68a8...6a1406 )
by Yuri
02:09 queued 24s
created

Formatter   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 318
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 29
lcom 1
cbo 0
dl 0
loc 318
ccs 64
cts 64
cp 1
rs 10
c 0
b 0
f 0

12 Methods

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