Completed
Push — master ( 87b10e...1961d1 )
by Andre
16s queued 12s
created

LastnameMapper::isCombinedWithPrefix()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace TheIconic\NameParser\Mapper;
4
5
use TheIconic\NameParser\LanguageInterface;
6
use TheIconic\NameParser\Part\AbstractPart;
7
use TheIconic\NameParser\Part\Lastname;
8
use TheIconic\NameParser\Part\LastnamePrefix;
9
use TheIconic\NameParser\Part\Nickname;
10
use TheIconic\NameParser\Part\Salutation;
11
use TheIconic\NameParser\Part\Suffix;
12
13
class LastnameMapper extends AbstractMapper
14
{
15
    protected $prefixes = [];
16
17
    protected $matchSinglePart = false;
18
19
    public function __construct(array $prefixes, bool $matchSinglePart = false)
20
    {
21
        $this->prefixes = $prefixes;
22
        $this->matchSinglePart = $matchSinglePart;
23
    }
24
25
    /**
26
     * map lastnames in the parts array
27
     *
28
     * @param array $parts the name parts
29
     * @return array the mapped parts
30
     */
31
    public function map(array $parts): array
32
    {
33
        if (!$this->matchSinglePart && count($parts) < 2) {
34
            return $parts;
35
        }
36
37
        return $this->mapParts($parts);
38
    }
39
40
    /**
41
     * we map the parts in reverse order because it makes more
42
     * sense to parse for the lastname starting from the end
43
     *
44
     * @param array $parts
45
     * @return array
46
     */
47
    protected function mapParts(array $parts): array
48
    {
49
        $k = $this->skipIgnoredParts($parts) + 1;
50
        $remapIgnored = true;
51
52
        while (--$k >= 0) {
53
            $part = $parts[$k];
54
55
            if ($part instanceof AbstractPart) {
56
                break;
57
            }
58
59
            if ($this->isFollowedByLastnamePart($parts, $k)) {
60
                if ($mapped = $this->mapAsPrefixIfPossible($parts, $k)) {
61
                    $parts[$k] = $mapped;
62
                    continue;
63
                }
64
65
                if ($this->shouldStopMapping($parts, $k)) {
66
                    break;
67
                }
68
            }
69
70
            $parts[$k] = new Lastname($part);
71
            $remapIgnored = false;
72
        }
73
74
        if ($remapIgnored) {
75
            $parts = $this->remapIgnored($parts);
76
        }
77
78
        return $parts;
79
    }
80
81
    /**
82
     * try to map this part as a lastname prefix or as a combined
83
     * lastname part containing a prefix
84
     *
85
     * @param array $parts
86
     * @param int $k
87
     * @return Lastname|null
88
     */
89
    private function mapAsPrefixIfPossible(array $parts, int $k): ?Lastname
90
    {
91
        if ($this->isApplicablePrefix($parts, $k)) {
92
            return new LastnamePrefix($parts[$k], $this->prefixes[$this->getKey($parts[$k])]);
93
        }
94
95
        if ($this->isCombinedWithPrefix($parts[$k])) {
96
            return new Lastname($parts[$k]);
97
        }
98
99
        return null;
100
    }
101
102
    /**
103
     * check if the given part is a combined lastname part
104
     * that ends in a lastname prefix
105
     *
106
     * @param string $part
107
     * @return bool
108
     */
109
    private function isCombinedWithPrefix(string $part): bool
110
    {
111
        $pos = strpos($part, '-');
112
113
        if (false === $pos) {
114
            return false;
115
        }
116
117
        return $this->isPrefix(substr($part, $pos + 1));
118
    }
119
120
    /**
121
     * skip through the parts we want to ignore and return the start index
122
     *
123
     * @param array $parts
124
     * @return int
125
     */
126
    protected function skipIgnoredParts(array $parts): int
127
    {
128
        $k = count($parts);
129
130
        while (--$k >= 0) {
131
            if (!$this->isIgnoredPart($parts[$k])) {
132
                break;
133
            }
134
        }
135
136
        return $k;
137
    }
138
139
    /**
140
     * indicates if we should stop mapping at the given index $k
141
     *
142
     * the assumption is that lastname parts have already been found
143
     * but we want to see if we should add more parts
144
     *
145
     * @param array $parts
146
     * @param int $k
147
     * @return bool
148
     */
149
    protected function shouldStopMapping(array $parts, int $k): bool
150
    {
151
        if ($k < 1) {
152
            return true;
153
        }
154
155
        $lastPart = $parts[$k + 1];
156
157
        if ($lastPart instanceof LastnamePrefix) {
158
            return true;
159
        }
160
161
162
163
        return strlen($lastPart->getValue()) >= 3;
164
    }
165
166
    /**
167
     * indicates if the given part should be ignored (skipped) during mapping
168
     *
169
     * @param $part
170
     * @return bool
171
     */
172
    protected function isIgnoredPart($part) {
173
        return $part instanceof Suffix || $part instanceof Nickname || $part instanceof Salutation;
174
    }
175
176
    /**
177
     * remap ignored parts as lastname
178
     *
179
     * if the mapping did not derive any lastname this is called to transform
180
     * any previously ignored parts into lastname parts
181
     *
182
     * @param array $parts
183
     * @return array
184
     */
185
    protected function remapIgnored(array $parts): array
186
    {
187
        $k = count($parts);
188
189
        while (--$k >= 0) {
190
            $part = $parts[$k];
191
192
            if (!$this->isIgnoredPart($part)) {
193
                break;
194
            }
195
196
            $parts[$k] = new Lastname($part);
197
        }
198
199
        return $parts;
200
    }
201
202
    /**
203
     * @param array $parts
204
     * @param int $index
205
     * @return bool
206
     */
207
    protected function isFollowedByLastnamePart(array $parts, int $index): bool
208
    {
209
        $next = $this->skipNicknameParts($parts, $index + 1);
210
211
        return (isset($parts[$next]) && $parts[$next] instanceof Lastname);
212
    }
213
214
    /**
215
     * Assuming that the part at the given index is matched as a prefix,
216
     * determines if the prefix should be applied to the lastname.
217
     *
218
     * We only apply it to the lastname if we already have at least one
219
     * lastname part and there are other parts left in
220
     * the name (this effectively prioritises firstname over prefix matching).
221
     *
222
     * This expects the parts array and index to be in the original order.
223
     *
224
     * @param array $parts
225
     * @param int $index
226
     * @return bool
227
     */
228
    protected function isApplicablePrefix(array $parts, int $index): bool
229
    {
230
        if (!$this->isPrefix($parts[$index])) {
231
            return false;
232
        }
233
234
        return $this->hasUnmappedPartsBefore($parts, $index);
235
    }
236
237
    /**
238
     * check if the given word is a lastname prefix
239
     *
240
     * @param string $word the word to check
241
     * @return bool
242
     */
243
    protected function isPrefix($word): bool
244
    {
245
        return (array_key_exists($this->getKey($word), $this->prefixes));
246
    }
247
248
    /**
249
     * find the next non-nickname index in parts
250
     *
251
     * @param $parts
252
     * @param $startIndex
253
     * @return int|void
254
     */
255
    protected function skipNicknameParts($parts, $startIndex)
256
    {
257
        $total = count($parts);
258
259
        for ($i = $startIndex; $i < $total; $i++) {
260
            if (!($parts[$i] instanceof Nickname)) {
261
                return $i;
262
            }
263
        }
264
265
        return $total - 1;
266
    }
267
}
268