Completed
Pull Request — main (#3858)
by Jonathan
08:42
created

Relationship::linkingByType()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 6
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 9
rs 9.6111
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
    /** @var array<Closure> 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 self(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 self($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<string>|null [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
        return $this->linkedByType('adopted');
130
    }
131
132
    /**
133
     * @return Relationship
134
     */
135
    public function adoptive(): Relationship
136
    {
137
        return $this->linkingByType('adopted');
138
    }
139
140
    /**
141
     * @return Relationship
142
     */
143
    public function biologicallyBorn(): Relationship
144
    {
145
        return $this->linkedByType('birth', true);
146
    }
147
148
    /**
149
     * @return Relationship
150
     */
151
    public function biological(): Relationship
152
    {
153
        return $this->linkingByType('birth', true);
154
    }
155
156
    /**
157
     * Matches a pedigree linkage type based on the target node's child family status.
158
     *
159
     * @param string $pedigree_type
160
     * @param bool $is_default
161
     * @return Relationship
162
     */
163
    protected function linkedByType(string $pedigree_type, bool $is_default = false): Relationship
164
    {
165
        $this->matchers[] = fn (array $nodes): bool => count($nodes) > 2 && $nodes[2]
166
                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
167
                ->map(fn (Fact $fact): array => [$fact->value(), $fact->attribute('PEDI')])
168
                ->contains(fn (array $fact): bool => $fact[0] === '@' . $nodes[1]->xref() . '@'
169
                    && ($fact[1] === $pedigree_type || ($fact[1] === '' && $is_default)));
170
171
        return $this;
172
    }
173
174
    /**
175
     * Matches a pedigree linkage type based on the initial node's child family status.
176
     *
177
     * @param string $pedigree_type
178
     * @return Relationship
179
     */
180
    protected function linkingByType(string $pedigree_type, bool $is_default = false): Relationship
181
    {
182
        $this->matchers[] = fn (array $nodes): bool => count($nodes) > 1 && $nodes[0]
183
                ->facts(['FAMC'], false, Auth::PRIV_HIDE)
184
                ->map(fn (Fact $fact): array => [$fact->value(), $fact->attribute('PEDI')])
185
                ->contains(fn (array $fact): bool => $fact[0] === '@' . $nodes[1]->xref() . '@'
186
                    && ($fact[1] === $pedigree_type || ($fact[1] === '' && $is_default)));
187
188
        return $this;
189
    }
190
191
    /**
192
     * @return Relationship
193
     */
194
    public function brother(): Relationship
195
    {
196
        return $this->relation([self::BROTHER]);
197
    }
198
199
    /**
200
     * Match the next relationship in the path.
201
     *
202
     * @param array<string> $relationships
203
     *
204
     * @return Relationship
205
     */
206
    protected function relation(array $relationships): Relationship
207
    {
208
        $this->matchers[] = static function (array &$nodes, array &$patterns) use ($relationships): bool {
209
            if (in_array($patterns[0] ?? '', $relationships, true)) {
210
                $nodes    = array_slice($nodes, 2);
211
                $patterns = array_slice($patterns, 1);
212
213
                return true;
214
            }
215
216
            return false;
217
        };
218
219
        return $this;
220
    }
221
222
    /**
223
     * The number of ancestors may be different to the number of descendants
224
     *
225
     * @return Relationship
226
     */
227
    public function cousin(): Relationship
228
    {
229
        return $this->ancestor()->sibling()->descendant();
230
    }
231
232
    /**
233
     * @return Relationship
234
     */
235
    public function descendant(): Relationship
236
    {
237
        return $this->repeatedRelationship(self::CHILDREN);
238
    }
239
240
    /**
241
     * Match a repeated number of the same type of component
242
     *
243
     * @param array<string> $relationships
244
     *
245
     * @return Relationship
246
     */
247
    protected function repeatedRelationship(array $relationships): Relationship
248
    {
249
        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures) use ($relationships): bool {
250
            $limit = min(intdiv(count($nodes), 2), count($patterns));
251
252
            for ($generations = 0; $generations < $limit; ++$generations) {
253
                if (!in_array($patterns[$generations], $relationships, true)) {
254
                    break;
255
                }
256
            }
257
258
            if ($generations > 0) {
259
                $nodes      = array_slice($nodes, 2 * $generations);
260
                $patterns   = array_slice($patterns, $generations);
261
                $captures[] = $generations;
262
263
                return true;
264
            }
265
266
            return false;
267
        };
268
269
        return $this;
270
    }
271
272
    /**
273
     * @return Relationship
274
     */
275
    public function sibling(): Relationship
276
    {
277
        return $this->relation(self::SIBLINGS);
278
    }
279
280
    /**
281
     * @return Relationship
282
     */
283
    public function ancestor(): Relationship
284
    {
285
        return $this->repeatedRelationship(self::PARENTS);
286
    }
287
288
    /**
289
     * @return Relationship
290
     */
291
    public function child(): Relationship
292
    {
293
        return $this->relation(self::CHILDREN);
294
    }
295
296
    /**
297
     * @return Relationship
298
     */
299
    public function daughter(): Relationship
300
    {
301
        return $this->relation([self::DAUGHTER]);
302
    }
303
304
    /**
305
     * @return Relationship
306
     */
307
    public function divorced(): Relationship
308
    {
309
        return $this->marriageStatus('DIV');
310
    }
311
312
    /**
313
     * Match a marriage status
314
     *
315
     * @param string $status
316
     *
317
     * @return Relationship
318
     */
319
    protected function marriageStatus(string $status): Relationship
320
    {
321
        $this->matchers[] = static function (array $nodes) use ($status): bool {
322
            $family = $nodes[1] ?? null;
323
324
            if ($family instanceof Family) {
325
                $fact = $family->facts(['ENGA', 'MARR', 'DIV', 'ANUL'], true, Auth::PRIV_HIDE)->last();
326
327
                if ($fact instanceof Fact) {
328
                    switch ($status) {
329
                        case 'MARR':
330
                            return $fact->tag() === 'FAM:MARR';
331
332
                        case 'DIV':
333
                            return $fact->tag() === 'FAM:DIV' || $fact->tag() === 'FAM:ANUL';
334
335
                        case 'ENGA':
336
                            return $fact->tag() === 'FAM:ENGA';
337
                    }
338
                }
339
            }
340
341
            return false;
342
        };
343
344
        return $this;
345
    }
346
347
    /**
348
     * @return Relationship
349
     */
350
    public function engaged(): Relationship
351
    {
352
        return $this->marriageStatus('ENGA');
353
    }
354
355
    /**
356
     * @return Relationship
357
     */
358
    public function father(): Relationship
359
    {
360
        return $this->relation([self::FATHER]);
361
    }
362
363
    /**
364
     * @return Relationship
365
     */
366
    public function female(): Relationship
367
    {
368
        return $this->sex('F');
369
    }
370
371
    /**
372
     * Match the sex of the current individual
373
     *
374
     * @param string $sex
375
     *
376
     * @return Relationship
377
     */
378
    protected function sex(string $sex): Relationship
379
    {
380
        $this->matchers[] = static function (array $nodes) use ($sex): bool {
381
            return $nodes[0]->sex() === $sex;
382
        };
383
384
        return $this;
385
    }
386
387
    /**
388
     * @return Relationship
389
     */
390
    public function fostered(): Relationship
391
    {
392
        return $this->linkedByType('foster');
393
    }
394
395
    /**
396
     * @return Relationship
397
     */
398
    public function fostering(): Relationship
399
    {
400
        return $this->linkingByType('foster');
401
    }
402
403
    /**
404
     * @return Relationship
405
     */
406
    public function husband(): Relationship
407
    {
408
        return $this->married()->relation([self::HUSBAND]);
409
    }
410
411
    /**
412
     * @return Relationship
413
     */
414
    public function married(): Relationship
415
    {
416
        return $this->marriageStatus('MARR');
417
    }
418
419
    /**
420
     * @return Relationship
421
     */
422
    public function male(): Relationship
423
    {
424
        return $this->sex('M');
425
    }
426
427
    /**
428
     * @return Relationship
429
     */
430
    public function mother(): Relationship
431
    {
432
        return $this->relation([self::MOTHER]);
433
    }
434
435
    /**
436
     * @return Relationship
437
     */
438
    public function older(): Relationship
439
    {
440
        $this->matchers[] = static function (array $nodes): bool {
441
            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
442
            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
443
444
            return Date::compare($date1, $date2) > 0;
445
        };
446
447
        return $this;
448
    }
449
450
    /**
451
     * @return Relationship
452
     */
453
    public function parent(): Relationship
454
    {
455
        return $this->relation(self::PARENTS);
456
    }
457
458
    /**
459
     * @return Relationship
460
     */
461
    public function sister(): Relationship
462
    {
463
        return $this->relation([self::SISTER]);
464
    }
465
466
    /**
467
     * @return Relationship
468
     */
469
    public function son(): Relationship
470
    {
471
        return $this->relation([self::SON]);
472
    }
473
474
    /**
475
     * @return Relationship
476
     */
477
    public function spouse(): Relationship
478
    {
479
        return $this->married()->partner();
480
    }
481
482
    /**
483
     * @return Relationship
484
     */
485
    public function partner(): Relationship
486
    {
487
        return $this->relation(self::SPOUSES);
488
    }
489
490
    /**
491
     * The number of ancestors must be the same as the number of descendants
492
     *
493
     * @return Relationship
494
     */
495
    public function symmetricCousin(): Relationship
496
    {
497
        $this->matchers[] = static function (array &$nodes, array &$patterns, array &$captures): bool {
498
            $count = count($patterns);
499
500
            $n = 0;
501
502
            // Ancestors
503
            while ($n < $count && in_array($patterns[$n], Relationship::PARENTS, true)) {
504
                $n++;
505
            }
506
507
            // No ancestors?  Not enough path left for descendants?
508
            if ($n === 0 || $n * 2 + 1 !== $count) {
509
                return false;
510
            }
511
512
            // Siblings
513
            if (!in_array($patterns[$n], Relationship::SIBLINGS, true)) {
514
                return false;
515
            }
516
517
            // Descendants
518
            for ($descendants = $n + 1; $descendants < $count; ++$descendants) {
519
                if (!in_array($patterns[$descendants], Relationship::CHILDREN, true)) {
520
                    return false;
521
                }
522
            }
523
524
525
            $nodes      = array_slice($nodes, 2 * (2 * $n + 1));
526
            $patterns   = [];
527
            $captures[] = $n;
528
529
            return true;
530
        };
531
532
        return $this;
533
    }
534
535
    /**
536
     * @return Relationship
537
     */
538
    public function twin(): Relationship
539
    {
540
        $this->matchers[] = static function (array $nodes): bool {
541
            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
542
            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
543
544
            return
545
                $date1->isOK() &&
546
                $date2->isOK() &&
547
                abs($date1->julianDay() - $date2->julianDay()) < 2 &&
548
                $date1->minimumDate()->day > 0 &&
549
                $date2->minimumDate()->day > 0;
550
        };
551
552
        return $this;
553
    }
554
555
    /**
556
     * @return Relationship
557
     */
558
    public function wetNursed(): Relationship
559
    {
560
        return $this->linkedByType('rada');
561
    }
562
563
    /**
564
     * @return Relationship
565
     */
566
    public function wetNursing(): Relationship
567
    {
568
        return $this->linkingByType('rada');
569
    }
570
571
    /**
572
     * @return Relationship
573
     */
574
    public function wife(): Relationship
575
    {
576
        return $this->married()->relation([self::WIFE]);
577
    }
578
579
    /**
580
     * @return Relationship
581
     */
582
    public function younger(): Relationship
583
    {
584
        $this->matchers[] = static function (array $nodes): bool {
585
            $date1 = $nodes[0]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
586
            $date2 = $nodes[2]->facts(['BIRT'], false, Auth::PRIV_HIDE)->map(fn (Fact $fact): Date => $fact->date())->first() ?? new Date('');
587
588
            return Date::compare($date1, $date2) < 0;
589
        };
590
591
        return $this;
592
    }
593
}
594