Completed
Push — develop ( b33d79...ec48d8 )
by Greg
43:47
created

Relationship::fixed()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2021 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Closure;
23
24
use function abs;
25
use function array_slice;
26
use function count;
27
use function in_array;
28
use function intdiv;
29
use function min;
30
31
/**
32
 * Class Relationship - define a relationship for a language.
33
 */
34
class Relationship
35
{
36
    // The basic components of a relationship.
37
    // These strings are needed for compatibility with the legacy algorithm.
38
    // Once that has been replaced, it may be more efficient to use integers here.
39
    public const SISTER   = 'sis';
40
    public const BROTHER  = 'bro';
41
    public const SIBLING  = 'sib';
42
    public const MOTHER   = 'mot';
43
    public const FATHER   = 'fat';
44
    public const PARENT   = 'par';
45
    public const DAUGHTER = 'dau';
46
    public const SON      = 'son';
47
    public const CHILD    = 'chi';
48
    public const WIFE     = 'wif';
49
    public const HUSBAND  = 'hus';
50
    public const SPOUSE   = 'spo';
51
52
    public const SIBLINGS = ['F' => self::SISTER, 'M' => self::BROTHER, 'U' => self::SIBLING];
53
    public const PARENTS  = ['F' => self::MOTHER, 'M' => self::FATHER, 'U' => self::PARENT];
54
    public const CHILDREN = ['F' => self::DAUGHTER, 'M' => self::SON, 'U' => self::CHILD];
55
    public const SPOUSES  = ['F' => self::WIFE, 'M' => self::HUSBAND, 'U' => self::SPOUSE];
56
57
    // Generates a name from the matched relationship.
58
    private Closure $callback;
59
60
    // List of rules that need to match.
61
    private array $matchers;
62
63
    /**
64
     * Relationship constructor.
65
     *
66
     * @param Closure $callback
67
     */
68
    private function __construct(Closure $callback)
69
    {
70
        $this->callback = $callback;
71
        $this->matchers = [];
72
    }
73
74
    /**
75
     * Allow fluent constructor.
76
     *
77
     * @param string $nominative
78
     * @param string $genitive
79
     *
80
     * @return Relationship
81
     */
82
    public static function fixed(string $nominative, string $genitive): Relationship
83
    {
84
        return new static(fn () => [$nominative, $genitive]);
85
    }
86
87
    /**
88
     * Allow fluent constructor.
89
     *
90
     * @param Closure $callback
91
     *
92
     * @return Relationship
93
     */
94
    public static function dynamic(Closure $callback): Relationship
95
    {
96
        return new static($callback);
97
    }
98
99
    /**
100
     * Does this relationship match the pattern?
101
     *
102
     * @param array<Individual|Family> $nodes
103
     * @param array<string>            $patterns
104
     *
105
     * @return array [nominative, genitive] or null
106
     */
107
    public function match(array $nodes, array $patterns): ?array
108
    {
109
        $captures = [];
110
111
        foreach ($this->matchers as $matcher) {
112
            if (!$matcher($nodes, $patterns, $captures)) {
113
                return null;
114
            }
115
        }
116
117
        if ($patterns === []) {
118
            return ($this->callback)(...$captures);
119
        }
120
121
        return null;
122
    }
123
124
    /**
125
     * @return Relationship
126
     */
127
    public function adopted(): Relationship
128
    {
129
        $this->matchers[] = fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
130
                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
131
                ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'adopted');
132
133
        return $this;
134
    }
135
136
    /**
137
     * @return Relationship
138
     */
139
    public function adoptive(): Relationship
140
    {
141
        $this->matchers[] = fn (array $nodes): bool => $nodes[0]
142
            ->facts(['FAMC'], false, Auth::PRIV_HIDE)
143
            ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'adopted');
144
145
        return $this;
146
    }
147
148
    /**
149
     * @return Relationship
150
     */
151
    public function brother(): Relationship
152
    {
153
        return $this->relation([self::BROTHER]);
154
    }
155
156
    /**
157
     * Match the next relationship in the path.
158
     *
159
     * @param array<string> $relationships
160
     *
161
     * @return Relationship
162
     */
163
    protected function relation(array $relationships): Relationship
164
    {
165
        $this->matchers[] = static function (array &$nodes, array &$patterns) use ($relationships): bool {
166
            if (in_array($patterns[0] ?? '', $relationships, true)) {
167
                $nodes    = array_slice($nodes, 2);
168
                $patterns = array_slice($patterns, 1);
169
170
                return true;
171
            }
172
173
            return false;
174
        };
175
176
        return $this;
177
    }
178
179
    /**
180
     * The number of ancestors may be different to the number of descendants
181
     *
182
     * @return Relationship
183
     */
184
    public function cousin(): Relationship
185
    {
186
        return $this->ancestor()->sibling()->descendant();
187
    }
188
189
    /**
190
     * @return Relationship
191
     */
192
    public function descendant(): Relationship
193
    {
194
        return $this->repeatedRelationship(self::CHILDREN);
195
    }
196
197
    /**
198
     * Match a repeated number of the same type of component
199
     *
200
     * @param array<string> $relationships
201
     *
202
     * @return Relationship
203
     */
204
    protected function repeatedRelationship(array $relationships): Relationship
205
    {
206
        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures) use ($relationships): bool {
207
            $limit = min(intdiv(count($nodes), 2), count($patterns));
208
209
            for ($generations = 0; $generations < $limit; ++$generations) {
210
                if (!in_array($patterns[$generations], $relationships, true)) {
211
                    break;
212
                }
213
            }
214
215
            if ($generations > 0) {
216
                $nodes      = array_slice($nodes, 2 * $generations);
217
                $patterns   = array_slice($patterns, $generations);
218
                $captures[] = $generations;
219
220
                return true;
221
            }
222
223
            return false;
224
        };
225
226
        return $this;
227
    }
228
229
    /**
230
     * @return Relationship
231
     */
232
    public function sibling(): Relationship
233
    {
234
        return $this->relation(self::SIBLINGS);
235
    }
236
237
    /**
238
     * @return Relationship
239
     */
240
    public function ancestor(): Relationship
241
    {
242
        return $this->repeatedRelationship(self::PARENTS);
243
    }
244
245
    /**
246
     * @return Relationship
247
     */
248
    public function child(): Relationship
249
    {
250
        return $this->relation(self::CHILDREN);
251
    }
252
253
    /**
254
     * @return Relationship
255
     */
256
    public function daughter(): Relationship
257
    {
258
        return $this->relation([self::DAUGHTER]);
259
    }
260
261
    /**
262
     * @return Relationship
263
     */
264
    public function divorced(): Relationship
265
    {
266
        return $this->marriageStatus('DIV');
267
    }
268
269
    /**
270
     * Match a marriage status
271
     *
272
     * @param string $status
273
     *
274
     * @return Relationship
275
     */
276
    protected function marriageStatus(string $status): Relationship
277
    {
278
        $this->matchers[] = static function (array $nodes) use ($status): bool {
279
            $family = $nodes[1] ?? null;
280
281
            if ($family instanceof Family) {
282
                $fact = $family->facts(['ENGA', 'MARR', 'DIV', 'ANUL'], true, Auth::PRIV_HIDE)->last();
283
284
                if ($fact instanceof Fact) {
285
                    switch ($status) {
286
                        case 'MARR':
287
                            return $fact->tag() === 'FAM:MARR';
288
289
                        case 'DIV':
290
                            return $fact->tag() === 'FAM:DIV' || $fact->tag() === 'FAM:ANUL';
291
292
                        case 'ENGA':
293
                            return $fact->tag() === 'FAM:ENGA';
294
                    }
295
                }
296
            }
297
298
            return false;
299
        };
300
301
        return $this;
302
    }
303
304
    /**
305
     * @return Relationship
306
     */
307
    public function engaged(): Relationship
308
    {
309
        return $this->marriageStatus('ENGA');
310
    }
311
312
    /**
313
     * @return Relationship
314
     */
315
    public function father(): Relationship
316
    {
317
        return $this->relation([self::FATHER]);
318
    }
319
320
    /**
321
     * @return Relationship
322
     */
323
    public function female(): Relationship
324
    {
325
        return $this->sex('F');
326
    }
327
328
    /**
329
     * Match the sex of the current individual
330
     *
331
     * @param string $sex
332
     *
333
     * @return Relationship
334
     */
335
    protected function sex(string $sex): Relationship
336
    {
337
        $this->matchers[] = static function (array $nodes) use ($sex): bool {
338
            return $nodes[0]->sex() === $sex;
339
        };
340
341
        return $this;
342
    }
343
344
    /**
345
     * @return Relationship
346
     */
347
    public function fostered(): Relationship
348
    {
349
        $this->matchers[] = fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
350
                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
351
                ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'foster');
352
353
        return $this;
354
    }
355
356
    /**
357
     * @return Relationship
358
     */
359
    public function fostering(): Relationship
360
    {
361
        $this->matchers[] = fn (array $nodes): bool => $nodes[0]
362
            ->facts(['FAMC'], false, Auth::PRIV_HIDE)
363
            ->contains(fn (Fact $fact): bool => $fact->value() === '@' . $nodes[1]->xref() . '@' && $fact->attribute('PEDI') === 'foster');
364
365
        return $this;
366
    }
367
368
    /**
369
     * @return Relationship
370
     */
371
    public function husband(): Relationship
372
    {
373
        return $this->married()->relation([self::HUSBAND]);
374
    }
375
376
    /**
377
     * @return Relationship
378
     */
379
    public function married(): Relationship
380
    {
381
        return $this->marriageStatus('MARR');
382
    }
383
384
    /**
385
     * @return Relationship
386
     */
387
    public function male(): Relationship
388
    {
389
        return $this->sex('M');
390
    }
391
392
    /**
393
     * @return Relationship
394
     */
395
    public function mother(): Relationship
396
    {
397
        return $this->relation([self::MOTHER]);
398
    }
399
400
    /**
401
     * @return Relationship
402
     */
403
    public function older(): Relationship
404
    {
405
        $this->matchers[] = static function (array $nodes): bool {
406
            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
407
            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
408
409
            return Date::compare($date1, $date2) > 0;
410
        };
411
412
        return $this;
413
    }
414
415
    /**
416
     * @return Relationship
417
     */
418
    public function parent(): Relationship
419
    {
420
        return $this->relation(self::PARENTS);
421
    }
422
423
    /**
424
     * @return Relationship
425
     */
426
    public function sister(): Relationship
427
    {
428
        return $this->relation([self::SISTER]);
429
    }
430
431
    /**
432
     * @return Relationship
433
     */
434
    public function son(): Relationship
435
    {
436
        return $this->relation([self::SON]);
437
    }
438
439
    /**
440
     * @return Relationship
441
     */
442
    public function spouse(): Relationship
443
    {
444
        return $this->married()->partner();
445
    }
446
447
    /**
448
     * @return Relationship
449
     */
450
    public function partner(): Relationship
451
    {
452
        return $this->relation(self::SPOUSES);
453
    }
454
455
    /**
456
     * The number of ancestors must be the same as the number of descendants
457
     *
458
     * @return Relationship
459
     */
460
    public function symmetricCousin(): Relationship
461
    {
462
        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures): bool {
463
            $count = count($patterns);
464
465
            $n = 0;
466
467
            // Ancestors
468
            while ($n < $count && in_array($patterns[$n], Relationship::PARENTS, true)) {
469
                $n++;
470
            }
471
472
            // No ancestors?  Not enough path left for descendants?
473
            if ($n === 0 || $n * 2 + 1 !== $count) {
474
                return false;
475
            }
476
477
            // Siblings
478
            if (!in_array($patterns[$n], Relationship::SIBLINGS, true)) {
479
                return false;
480
            }
481
482
            // Descendants
483
            for ($descendants = $n + 1; $descendants < $count; ++$descendants) {
484
                if (!in_array($patterns[$descendants], Relationship::CHILDREN, true)) {
485
                    return false;
486
                }
487
            }
488
489
490
            $nodes      = array_slice($nodes, 2 * (2 * $n + 1));
491
            $patterns   = [];
492
            $captures[] = $n;
493
494
            return true;
495
        };
496
497
        return $this;
498
    }
499
500
    /**
501
     * @return Relationship
502
     */
503
    public function twin(): Relationship
504
    {
505
        $this->matchers[] = static function (array $nodes): bool {
506
            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
507
            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
508
509
            return
510
                $date1->isOK() &&
511
                $date2->isOK() &&
512
                abs($date1->julianDay() - $date2->julianDay()) < 2 &&
513
                $date1->minimumDate()->day > 0 &&
514
                $date2->minimumDate()->day > 0;
515
        };
516
517
        return $this;
518
    }
519
520
    /**
521
     * @return Relationship
522
     */
523
    public function wife(): Relationship
524
    {
525
        return $this->married()->relation([self::WIFE]);
526
    }
527
528
    /**
529
     * @return Relationship
530
     */
531
    public function younger(): Relationship
532
    {
533
        $this->matchers[] = static function (array $nodes): bool {
534
            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
535
            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
536
537
            return Date::compare($date1, $date2) < 0;
538
        };
539
540
        return $this;
541
    }
542
}
543