Passed
Push — master ( 63f369...ac5ee7 )
by Greg
05:52
created

Fact   F

Complexity

Total Complexity 115

Size/Duplication

Total Lines 761
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 330
c 0
b 0
f 0
dl 0
loc 761
rs 2
wmc 115

29 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
A value() 0 7 2
A sortFactTags() 0 9 1
A canShow() 0 28 6
A isPendingAddition() 0 3 1
A getCitations() 0 12 4
A canEdit() 0 12 6
A __toString() 0 3 1
B sortFacts() 0 48 7
A getMedia() 0 12 4
D typeComparator() 0 63 20
A setTag() 0 3 1
A getNotes() 0 19 5
A dateComparator() 0 22 5
D target() 0 38 18
A date() 0 7 2
A longitude() 0 9 2
A record() 0 3 1
A latitude() 0 9 2
A setPendingAddition() 0 4 1
A setPendingDeletion() 0 4 1
A attribute() 0 7 2
A place() 0 7 2
A getTag() 0 3 1
A id() 0 3 1
A isPendingDeletion() 0 3 1
A label() 0 8 4
A gedcom() 0 3 1
B summary() 0 39 11

How to fix   Complexity   

Complex Class

Complex classes like Fact often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Fact, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees;
21
22
use Closure;
23
use Fisharebest\Webtrees\Functions\FunctionsPrint;
24
use Fisharebest\Webtrees\Services\GedcomService;
25
use Illuminate\Support\Collection;
26
use InvalidArgumentException;
27
28
use function array_flip;
29
use function array_key_exists;
30
use function count;
31
use function e;
32
use function implode;
33
use function in_array;
34
use function preg_match;
35
use function preg_match_all;
36
use function preg_replace;
37
use function strpos;
38
use function trim;
39
use function usort;
40
41
use const PREG_SET_ORDER;
42
43
/**
44
 * A GEDCOM fact or event object.
45
 */
46
class Fact
47
{
48
    private const FACT_ORDER = [
49
        'BIRT',
50
        '_HNM',
51
        'ALIA',
52
        '_AKA',
53
        '_AKAN',
54
        'ADOP',
55
        '_ADPF',
56
        '_ADPF',
57
        '_BRTM',
58
        'CHR',
59
        'BAPM',
60
        'FCOM',
61
        'CONF',
62
        'BARM',
63
        'BASM',
64
        'EDUC',
65
        'GRAD',
66
        '_DEG',
67
        'EMIG',
68
        'IMMI',
69
        'NATU',
70
        '_MILI',
71
        '_MILT',
72
        'ENGA',
73
        'MARB',
74
        'MARC',
75
        'MARL',
76
        '_MARI',
77
        '_MBON',
78
        'MARR',
79
        'MARR_CIVIL',
80
        'MARR_RELIGIOUS',
81
        'MARR_PARTNERS',
82
        'MARR_UNKNOWN',
83
        '_COML',
84
        '_STAT',
85
        '_SEPR',
86
        'DIVF',
87
        'MARS',
88
        '_BIRT_CHIL',
89
        'DIV',
90
        'ANUL',
91
        '_BIRT_',
92
        '_MARR_',
93
        '_DEAT_',
94
        '_BURI_',
95
        'CENS',
96
        'OCCU',
97
        'RESI',
98
        'PROP',
99
        'CHRA',
100
        'RETI',
101
        'FACT',
102
        'EVEN',
103
        '_NMR',
104
        '_NMAR',
105
        'NMR',
106
        'NCHI',
107
        'WILL',
108
        '_HOL',
109
        '_????_',
110
        'DEAT',
111
        '_FNRL',
112
        'CREM',
113
        'BURI',
114
        '_INTE',
115
        '_YART',
116
        '_NLIV',
117
        'PROB',
118
        'TITL',
119
        'COMM',
120
        'NATI',
121
        'CITN',
122
        'CAST',
123
        'RELI',
124
        'SSN',
125
        'IDNO',
126
        'TEMP',
127
        'SLGC',
128
        'BAPL',
129
        'CONL',
130
        'ENDL',
131
        'SLGS',
132
        'ADDR',
133
        'PHON',
134
        'EMAIL',
135
        '_EMAIL',
136
        'EMAL',
137
        'FAX',
138
        'WWW',
139
        'URL',
140
        '_URL',
141
        'AFN',
142
        'REFN',
143
        '_PRMN',
144
        'REF',
145
        'RIN',
146
        '_UID',
147
        'OBJE',
148
        'NOTE',
149
        'SOUR',
150
        'CHAN',
151
        '_TODO',
152
    ];
153
154
    /** @var string Unique identifier for this fact (currently implemented as a hash of the raw data). */
155
    private $id;
156
157
    /** @var GedcomRecord The GEDCOM record from which this fact is taken */
158
    private $record;
159
160
    /** @var string The raw GEDCOM data for this fact */
161
    private $gedcom;
162
163
    /** @var string The GEDCOM tag for this record */
164
    private $tag;
165
166
    /** @var bool Is this a recently deleted fact, pending approval? */
167
    private $pending_deletion = false;
168
169
    /** @var bool Is this a recently added fact, pending approval? */
170
    private $pending_addition = false;
171
172
    /** @var Date The date of this fact, from the “2 DATE …” attribute */
173
    private $date;
174
175
    /** @var Place The place of this fact, from the “2 PLAC …” attribute */
176
    private $place;
177
178
    /** @var int Used by Functions::sortFacts() */
179
    private $sortOrder;
180
181
    /**
182
     * Create an event object from a gedcom fragment.
183
     * We need the parent object (to check privacy) and a (pseudo) fact ID to
184
     * identify the fact within the record.
185
     *
186
     * @param string       $gedcom
187
     * @param GedcomRecord $parent
188
     * @param string       $id
189
     *
190
     * @throws InvalidArgumentException
191
     */
192
    public function __construct($gedcom, GedcomRecord $parent, $id)
193
    {
194
        if (preg_match('/^1 (' . Gedcom::REGEX_TAG . ')/', $gedcom, $match)) {
195
            $this->gedcom = $gedcom;
196
            $this->record = $parent;
197
            $this->id     = $id;
198
            $this->tag    = $match[1];
199
        } else {
200
            throw new InvalidArgumentException('Invalid GEDCOM data passed to Fact::_construct(' . $gedcom . ')');
201
        }
202
    }
203
204
    /**
205
     * Get the value of level 1 data in the fact
206
     * Allow for multi-line values
207
     *
208
     * @return string
209
     */
210
    public function value(): string
211
    {
212
        if (preg_match('/^1 (?:' . $this->tag . ') ?(.*(?:(?:\n2 CONT ?.*)*))/', $this->gedcom, $match)) {
213
            return preg_replace("/\n2 CONT ?/", "\n", $match[1]);
214
        }
215
216
        return '';
217
    }
218
219
    /**
220
     * Get the record to which this fact links
221
     *
222
     * @return Individual|Family|Source|Submitter|Submission|Repository|Media|Note|GedcomRecord|null
223
     */
224
    public function target()
225
    {
226
        if (!preg_match('/^@(' . Gedcom::REGEX_XREF . ')@$/', $this->value(), $match)) {
227
            return null;
228
        }
229
230
        $xref = $match[1];
231
232
        switch ($this->tag) {
233
            case 'FAMC':
234
            case 'FAMS':
235
                return Family::getInstance($xref, $this->record()->tree());
236
            case 'HUSB':
237
            case 'WIFE':
238
            case 'ALIA':
239
            case 'CHIL':
240
            case '_ASSO':
241
                return Individual::getInstance($xref, $this->record()->tree());
242
            case 'ASSO':
243
                return
244
                    Individual::getInstance($xref, $this->record()->tree()) ??
245
                    Submitter::getInstance($xref, $this->record()->tree());
246
            case 'SOUR':
247
                return Source::getInstance($xref, $this->record()->tree());
248
            case 'OBJE':
249
                return Media::getInstance($xref, $this->record()->tree());
250
            case 'REPO':
251
                return Repository::getInstance($xref, $this->record()->tree());
252
            case 'NOTE':
253
                return Note::getInstance($xref, $this->record()->tree());
254
            case 'ANCI':
255
            case 'DESI':
256
            case 'SUBM':
257
                return Submitter::getInstance($xref, $this->record()->tree());
258
            case 'SUBN':
259
                return Submission::getInstance($xref, $this->record()->tree());
260
            default:
261
                return GedcomRecord::getInstance($xref, $this->record()->tree());
262
        }
263
    }
264
265
    /**
266
     * Get the value of level 2 data in the fact
267
     *
268
     * @param string $tag
269
     *
270
     * @return string
271
     */
272
    public function attribute($tag): string
273
    {
274
        if (preg_match('/\n2 (?:' . $tag . ') ?(.*(?:(?:\n3 CONT ?.*)*)*)/', $this->gedcom, $match)) {
275
            return preg_replace("/\n3 CONT ?/", "\n", $match[1]);
276
        }
277
278
        return '';
279
    }
280
281
    /**
282
     * Get the PLAC:MAP:LATI for the fact.
283
     *
284
     * @return float
285
     */
286
    public function latitude(): float
287
    {
288
        if (preg_match('/\n4 LATI (.+)/', $this->gedcom, $match)) {
289
            $gedcom_service = new GedcomService();
290
291
            return $gedcom_service->readLatitude($match[1]);
292
        }
293
294
        return 0.0;
295
    }
296
297
    /**
298
     * Get the PLAC:MAP:LONG for the fact.
299
     *
300
     * @return float
301
     */
302
    public function longitude(): float
303
    {
304
        if (preg_match('/\n4 LONG (.+)/', $this->gedcom, $match)) {
305
            $gedcom_service = new GedcomService();
306
307
            return $gedcom_service->readLongitude($match[1]);
308
        }
309
310
        return 0.0;
311
    }
312
313
    /**
314
     * Do the privacy rules allow us to display this fact to the current user
315
     *
316
     * @param int|null $access_level
317
     *
318
     * @return bool
319
     */
320
    public function canShow(int $access_level = null): bool
321
    {
322
        $access_level = $access_level ?? Auth::accessLevel($this->record->tree());
323
324
        // Does this record have an explicit RESN?
325
        if (strpos($this->gedcom, "\n2 RESN confidential") !== false) {
326
            return Auth::PRIV_NONE >= $access_level;
327
        }
328
        if (strpos($this->gedcom, "\n2 RESN privacy") !== false) {
329
            return Auth::PRIV_USER >= $access_level;
330
        }
331
        if (strpos($this->gedcom, "\n2 RESN none") !== false) {
332
            return true;
333
        }
334
335
        // Does this record have a default RESN?
336
        $xref                    = $this->record->xref();
337
        $fact_privacy            = $this->record->tree()->getFactPrivacy();
338
        $individual_fact_privacy = $this->record->tree()->getIndividualFactPrivacy();
339
        if (isset($individual_fact_privacy[$xref][$this->tag])) {
340
            return $individual_fact_privacy[$xref][$this->tag] >= $access_level;
341
        }
342
        if (isset($fact_privacy[$this->tag])) {
343
            return $fact_privacy[$this->tag] >= $access_level;
344
        }
345
346
        // No restrictions - it must be public
347
        return true;
348
    }
349
350
    /**
351
     * Check whether this fact is protected against edit
352
     *
353
     * @return bool
354
     */
355
    public function canEdit(): bool
356
    {
357
        if ($this->isPendingDeletion()) {
358
            return false;
359
        }
360
361
        if (Auth::isManager($this->record->tree())) {
362
            return true;
363
        }
364
365
        // Members cannot edit RESN, CHAN and locked records
366
        return Auth::isEditor($this->record->tree()) && strpos($this->gedcom, "\n2 RESN locked") === false && $this->getTag() !== 'RESN' && $this->getTag() !== 'CHAN';
367
    }
368
369
    /**
370
     * The place where the event occured.
371
     *
372
     * @return Place
373
     */
374
    public function place(): Place
375
    {
376
        if ($this->place === null) {
377
            $this->place = new Place($this->attribute('PLAC'), $this->record()->tree());
378
        }
379
380
        return $this->place;
381
    }
382
383
    /**
384
     * Get the date for this fact.
385
     * We can call this function many times, especially when sorting,
386
     * so keep a copy of the date.
387
     *
388
     * @return Date
389
     */
390
    public function date(): Date
391
    {
392
        if ($this->date === null) {
393
            $this->date = new Date($this->attribute('DATE'));
394
        }
395
396
        return $this->date;
397
    }
398
399
    /**
400
     * The raw GEDCOM data for this fact
401
     *
402
     * @return string
403
     */
404
    public function gedcom(): string
405
    {
406
        return $this->gedcom;
407
    }
408
409
    /**
410
     * Get a (pseudo) primary key for this fact.
411
     *
412
     * @return string
413
     */
414
    public function id(): string
415
    {
416
        return $this->id;
417
    }
418
419
    /**
420
     * What is the tag (type) of this fact, such as BIRT, MARR or DEAT.
421
     *
422
     * @return string
423
     */
424
    public function getTag(): string
425
    {
426
        return $this->tag;
427
    }
428
429
    /**
430
     * Used to convert a real fact (e.g. BIRT) into a close-relative’s fact (e.g. _BIRT_CHIL)
431
     *
432
     * @param string $tag
433
     *
434
     * @return void
435
     */
436
    public function setTag($tag): void
437
    {
438
        $this->tag = $tag;
439
    }
440
441
    /**
442
     * The Person/Family record where this Fact came from
443
     *
444
     * @return Individual|Family|Source|Repository|Media|Note|GedcomRecord
445
     */
446
    public function record()
447
    {
448
        return $this->record;
449
    }
450
451
    /**
452
     * Get the name of this fact type, for use as a label.
453
     *
454
     * @return string
455
     */
456
    public function label(): string
457
    {
458
        // Custom FACT/EVEN - with a TYPE
459
        if (($this->tag === 'FACT' || $this->tag === 'EVEN') && $this->attribute('TYPE') !== '') {
460
            return I18N::translate(e($this->attribute('TYPE')));
461
        }
462
463
        return GedcomTag::getLabel($this->tag, $this->record);
464
    }
465
466
    /**
467
     * This is a newly deleted fact, pending approval.
468
     *
469
     * @return void
470
     */
471
    public function setPendingDeletion(): void
472
    {
473
        $this->pending_deletion = true;
474
        $this->pending_addition = false;
475
    }
476
477
    /**
478
     * Is this a newly deleted fact, pending approval.
479
     *
480
     * @return bool
481
     */
482
    public function isPendingDeletion(): bool
483
    {
484
        return $this->pending_deletion;
485
    }
486
487
    /**
488
     * This is a newly added fact, pending approval.
489
     *
490
     * @return void
491
     */
492
    public function setPendingAddition(): void
493
    {
494
        $this->pending_addition = true;
495
        $this->pending_deletion = false;
496
    }
497
498
    /**
499
     * Is this a newly added fact, pending approval.
500
     *
501
     * @return bool
502
     */
503
    public function isPendingAddition(): bool
504
    {
505
        return $this->pending_addition;
506
    }
507
508
    /**
509
     * Source citations linked to this fact
510
     *
511
     * @return string[]
512
     */
513
    public function getCitations(): array
514
    {
515
        preg_match_all('/\n(2 SOUR @(' . Gedcom::REGEX_XREF . ')@(?:\n[3-9] .*)*)/', $this->gedcom(), $matches, PREG_SET_ORDER);
516
        $citations = [];
517
        foreach ($matches as $match) {
518
            $source = Source::getInstance($match[2], $this->record()->tree());
519
            if ($source && $source->canShow()) {
520
                $citations[] = $match[1];
521
            }
522
        }
523
524
        return $citations;
525
    }
526
527
    /**
528
     * Notes (inline and objects) linked to this fact
529
     *
530
     * @return string[]|Note[]
531
     */
532
    public function getNotes(): array
533
    {
534
        $notes = [];
535
        preg_match_all('/\n2 NOTE ?(.*(?:\n3.*)*)/', $this->gedcom(), $matches);
536
        foreach ($matches[1] as $match) {
537
            $note = preg_replace("/\n3 CONT ?/", "\n", $match);
538
            if (preg_match('/@(' . Gedcom::REGEX_XREF . ')@/', $note, $nmatch)) {
539
                $note = Note::getInstance($nmatch[1], $this->record()->tree());
540
                if ($note && $note->canShow()) {
541
                    // A note object
542
                    $notes[] = $note;
543
                }
544
            } else {
545
                // An inline note
546
                $notes[] = $note;
547
            }
548
        }
549
550
        return $notes;
551
    }
552
553
    /**
554
     * Media objects linked to this fact
555
     *
556
     * @return Media[]
557
     */
558
    public function getMedia(): array
559
    {
560
        $media = [];
561
        preg_match_all('/\n2 OBJE @(' . Gedcom::REGEX_XREF . ')@/', $this->gedcom(), $matches);
562
        foreach ($matches[1] as $match) {
563
            $obje = Media::getInstance($match, $this->record()->tree());
564
            if ($obje && $obje->canShow()) {
565
                $media[] = $obje;
566
            }
567
        }
568
569
        return $media;
570
    }
571
572
    /**
573
     * A one-line summary of the fact - for charts, etc.
574
     *
575
     * @return string
576
     */
577
    public function summary(): string
578
    {
579
        $attributes = [];
580
        $target     = $this->target();
581
        if ($target instanceof GedcomRecord) {
582
            $attributes[] = $target->fullName();
583
        } else {
584
            // Fact value
585
            $value = $this->value();
586
            if ($value !== '' && $value !== 'Y') {
587
                $attributes[] = '<span dir="auto">' . e($value) . '</span>';
588
            }
589
            // Fact date
590
            $date = $this->date();
591
            if ($date->isOK()) {
592
                if ($this->record() instanceof Individual && in_array($this->getTag(), Gedcom::BIRTH_EVENTS, true) && $this->record()->tree()->getPreference('SHOW_PARENTS_AGE')) {
593
                    $attributes[] = $date->display() . FunctionsPrint::formatParentsAges($this->record(), $date);
594
                } else {
595
                    $attributes[] = $date->display();
596
                }
597
            }
598
            // Fact place
599
            if ($this->place()->gedcomName() !== '') {
600
                $attributes[] = $this->place()->shortName();
601
            }
602
        }
603
604
        $class = 'fact_' . $this->getTag();
605
        if ($this->isPendingAddition()) {
606
            $class .= ' wt-new';
607
        } elseif ($this->isPendingDeletion()) {
608
            $class .= ' wt-old';
609
        }
610
611
        return
612
            '<div class="' . $class . '">' .
613
            /* I18N: a label/value pair, such as “Occupation: Farmer”. Some languages may need to change the punctuation. */
614
            I18N::translate('<span class="label">%1$s:</span> <span class="field" dir="auto">%2$s</span>', $this->label(), implode(' — ', $attributes)) .
615
            '</div>';
616
    }
617
618
    /**
619
     * Helper functions to sort facts
620
     *
621
     * @return Closure
622
     */
623
    private static function dateComparator(): Closure
624
    {
625
        return static function (Fact $a, Fact $b): int {
626
            if ($a->date()->isOK() && $b->date()->isOK()) {
627
                // If both events have dates, compare by date
628
                $ret = Date::compare($a->date(), $b->date());
629
630
                if ($ret === 0) {
631
                    // If dates overlap, compare by fact type
632
                    $ret = self::typeComparator()($a, $b);
633
634
                    // If the fact type is also the same, retain the initial order
635
                    if ($ret === 0) {
636
                        $ret = $a->sortOrder <=> $b->sortOrder;
637
                    }
638
                }
639
640
                return $ret;
641
            }
642
643
            // One or both events have no date - retain the initial order
644
            return $a->sortOrder <=> $b->sortOrder;
645
        };
646
    }
647
648
    /**
649
     * Helper functions to sort facts.
650
     *
651
     * @return Closure
652
     */
653
    public static function typeComparator(): Closure
654
    {
655
        static $factsort = [];
656
657
        if ($factsort === []) {
658
            $factsort = array_flip(self::FACT_ORDER);
659
        }
660
661
        return static function (Fact $a, Fact $b) use ($factsort): int {
662
            // Facts from same families stay grouped together
663
            // Keep MARR and DIV from the same families from mixing with events from other FAMs
664
            // Use the original order in which the facts were added
665
            if ($a->record instanceof Family && $b->record instanceof Family && $a->record !== $b->record) {
666
                return $a->sortOrder - $b->sortOrder;
667
            }
668
669
            $atag = $a->getTag();
670
            $btag = $b->getTag();
671
672
            // Events not in the above list get mapped onto one that is.
673
            if (!array_key_exists($atag, $factsort)) {
674
                if (preg_match('/^(_(BIRT|MARR|DEAT|BURI)_)/', $atag, $match)) {
675
                    $atag = $match[1];
676
                } else {
677
                    $atag = '_????_';
678
                }
679
            }
680
681
            if (!array_key_exists($btag, $factsort)) {
682
                if (preg_match('/^(_(BIRT|MARR|DEAT|BURI)_)/', $btag, $match)) {
683
                    $btag = $match[1];
684
                } else {
685
                    $btag = '_????_';
686
                }
687
            }
688
689
            // - Don't let dated after DEAT/BURI facts sort non-dated facts before DEAT/BURI
690
            // - Treat dated after BURI facts as BURI instead
691
            if ($a->attribute('DATE') !== '' && $factsort[$atag] > $factsort['BURI'] && $factsort[$atag] < $factsort['CHAN']) {
692
                $atag = 'BURI';
693
            }
694
695
            if ($b->attribute('DATE') !== '' && $factsort[$btag] > $factsort['BURI'] && $factsort[$btag] < $factsort['CHAN']) {
696
                $btag = 'BURI';
697
            }
698
699
            $ret = $factsort[$atag] - $factsort[$btag];
700
701
            // If facts are the same then put dated facts before non-dated facts
702
            if ($ret == 0) {
703
                if ($a->attribute('DATE') !== '' && $b->attribute('DATE') === '') {
704
                    return -1;
705
                }
706
707
                if ($b->attribute('DATE') !== '' && $a->attribute('DATE') === '') {
708
                    return 1;
709
                }
710
711
                // If no sorting preference, then keep original ordering
712
                $ret = $a->sortOrder - $b->sortOrder;
713
            }
714
715
            return $ret;
716
        };
717
    }
718
719
    /**
720
     * A multi-key sort
721
     * 1. First divide the facts into two arrays one set with dates and one set without dates
722
     * 2. Sort each of the two new arrays, the date using the compare date function, the non-dated
723
     * using the compare type function
724
     * 3. Then merge the arrays back into the original array using the compare type function
725
     *
726
     * @param Collection<Fact> $unsorted
727
     *
728
     * @return Collection<Fact>
729
     */
730
    public static function sortFacts(Collection $unsorted): Collection
731
    {
732
        $dated    = [];
733
        $nondated = [];
734
        $sorted   = [];
735
736
        // Split the array into dated and non-dated arrays
737
        $order = 0;
738
739
        foreach ($unsorted as $fact) {
740
            $fact->sortOrder = $order;
741
            $order++;
742
743
            if ($fact->date()->isOK()) {
744
                $dated[] = $fact;
745
            } else {
746
                $nondated[] = $fact;
747
            }
748
        }
749
750
        usort($dated, self::dateComparator());
751
        usort($nondated, self::typeComparator());
752
753
        // Merge the arrays
754
        $dc = count($dated);
755
        $nc = count($nondated);
756
        $i  = 0;
757
        $j  = 0;
758
759
        // while there is anything in the dated array continue merging
760
        while ($i < $dc) {
761
            // compare each fact by type to merge them in order
762
            if ($j < $nc && self::typeComparator()($dated[$i], $nondated[$j]) > 0) {
763
                $sorted[] = $nondated[$j];
764
                $j++;
765
            } else {
766
                $sorted[] = $dated[$i];
767
                $i++;
768
            }
769
        }
770
771
        // get anything that might be left in the nondated array
772
        while ($j < $nc) {
773
            $sorted[] = $nondated[$j];
774
            $j++;
775
        }
776
777
        return new Collection($sorted);
778
    }
779
780
    /**
781
     * Sort fact/event tags using the same order that we use for facts.
782
     *
783
     * @param Collection<string> $unsorted
784
     *
785
     * @return Collection<string>
786
     */
787
    public static function sortFactTags(Collection $unsorted): Collection
788
    {
789
        $tag_order = array_flip(self::FACT_ORDER);
790
791
        return $unsorted->sort(static function (string $x, string $y) use ($tag_order): int {
792
            $sort_x = $tag_order[$x] ?? $tag_order['_????_'];
793
            $sort_y = $tag_order[$y] ?? $tag_order['_????_'];
794
795
            return $sort_x - $sort_y;
796
        });
797
    }
798
799
    /**
800
     * Allow native PHP functions such as array_unique() to work with objects
801
     *
802
     * @return string
803
     */
804
    public function __toString(): string
805
    {
806
        return $this->id . '@' . $this->record->xref();
807
    }
808
}
809