Completed
Pull Request — master (#31)
by Andre
01:32
created

LastnameMapper::mapParts()   B

Complexity

Conditions 8
Paths 6

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 38
rs 8.0675
c 0
b 0
f 0
cc 8
nc 6
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 ($this->isApplicablePrefix($parts, $k)) {
61
                    $parts[$k] = new LastnamePrefix($part, $this->prefixes[$this->getKey($part)]);
62
                    continue;
63
                }
64
65
                if ($this->isCombinedWithPrefix($part)) {
66
                    $parts[$k] = new Lastname($part);
67
                    continue;
68
                }
69
70
                if ($this->shouldStopMapping($parts, $k)) {
71
                    break;
72
                }
73
            }
74
75
            $parts[$k] = new Lastname($part);
76
            $remapIgnored = false;
77
        }
78
79
        if ($remapIgnored) {
80
            $parts = $this->remapIgnored($parts);
81
        }
82
83
        return $parts;
84
    }
85
86
    private function isCombinedWithPrefix(string $part): bool
87
    {
88
        $pos = strpos($part, '-');
89
90
        if (false === $pos) {
91
            return false;
92
        }
93
94
        return $this->isPrefix(substr($part, $pos + 1));
95
    }
96
97
    /**
98
     * skip through the parts we want to ignore and return the start index
99
     *
100
     * @param array $parts
101
     * @return int
102
     */
103
    protected function skipIgnoredParts(array $parts): int
104
    {
105
        $k = count($parts);
106
107
        while (--$k >= 0) {
108
            if (!$this->isIgnoredPart($parts[$k])) {
109
                break;
110
            }
111
        }
112
113
        return $k;
114
    }
115
116
    /**
117
     * indicates if we should stop mapping at the given index $k
118
     *
119
     * the assumption is that lastname parts have already been found
120
     * but we want to see if we should add more parts
121
     *
122
     * @param array $parts
123
     * @param int $k
124
     * @return bool
125
     */
126
    protected function shouldStopMapping(array $parts, int $k): bool
127
    {
128
        if ($k < 1) {
129
            return true;
130
        }
131
132
        $lastPart = $parts[$k + 1];
133
134
        if ($lastPart instanceof LastnamePrefix) {
135
            return true;
136
        }
137
138
139
140
        return strlen($lastPart->getValue()) >= 3;
141
    }
142
143
    /**
144
     * indicates if the given part should be ignored (skipped) during mapping
145
     *
146
     * @param $part
147
     * @return bool
148
     */
149
    protected function isIgnoredPart($part) {
150
        return $part instanceof Suffix || $part instanceof Nickname || $part instanceof Salutation;
151
    }
152
153
    /**
154
     * remap ignored parts as lastname
155
     *
156
     * if the mapping did not derive any lastname this is called to transform
157
     * any previously ignored parts into lastname parts
158
     *
159
     * @param array $parts
160
     * @return array
161
     */
162
    protected function remapIgnored(array $parts): array
163
    {
164
        $k = count($parts);
165
166
        while (--$k >= 0) {
167
            $part = $parts[$k];
168
169
            if (!$this->isIgnoredPart($part)) {
170
                break;
171
            }
172
173
            $parts[$k] = new Lastname($part);
174
        }
175
176
        return $parts;
177
    }
178
179
    /**
180
     * @param array $parts
181
     * @param int $index
182
     * @return bool
183
     */
184
    protected function isFollowedByLastnamePart(array $parts, int $index): bool
185
    {
186
        $next = $this->skipNicknameParts($parts, $index + 1);
187
188
        return (isset($parts[$next]) && $parts[$next] instanceof Lastname);
189
    }
190
191
    /**
192
     * Assuming that the part at the given index is matched as a prefix,
193
     * determines if the prefix should be applied to the lastname.
194
     *
195
     * We only apply it to the lastname if we already have at least one
196
     * lastname part and there are other parts left in
197
     * the name (this effectively prioritises firstname over prefix matching).
198
     *
199
     * This expects the parts array and index to be in the original order.
200
     *
201
     * @param array $parts
202
     * @param int $index
203
     * @return bool
204
     */
205
    protected function isApplicablePrefix(array $parts, int $index): bool
206
    {
207
        if (!$this->isPrefix($parts[$index])) {
208
            return false;
209
        }
210
211
        return $this->hasUnmappedPartsBefore($parts, $index);
212
    }
213
214
    /**
215
     * check if the given word is a lastname prefix
216
     *
217
     * @param string $word the word to check
218
     * @return bool
219
     */
220
    protected function isPrefix($word): bool
221
    {
222
        return (array_key_exists($this->getKey($word), $this->prefixes));
223
    }
224
225
    /**
226
     * find the next non-nickname index in parts
227
     *
228
     * @param $parts
229
     * @param $startIndex
230
     * @return int|void
231
     */
232 View Code Duplication
    protected function skipNicknameParts($parts, $startIndex)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
233
    {
234
        $total = count($parts);
235
236
        for ($i = $startIndex; $i < $total; $i++) {
237
            if (!($parts[$i] instanceof Nickname)) {
238
                return $i;
239
            }
240
        }
241
242
        return $total - 1;
243
    }
244
}
245