Passed
Push — develop ( 0f8720...91e6f7 )
by Greg
09:53
created

Fact   F

Complexity

Total Complexity 112

Size/Duplication

Total Lines 740
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 323
c 4
b 0
f 0
dl 0
loc 740
rs 2
wmc 112

26 Methods

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

756
        return new Collection(/** @scrutinizer ignore-type */ $sorted);
Loading history...
757
    }
758
759
    /**
760
     * Sort fact/event tags using the same order that we use for facts.
761
     *
762
     * @param Collection<int,string> $unsorted
763
     *
764
     * @return Collection<int,string>
765
     */
766
    public static function sortFactTags(Collection $unsorted): Collection
767
    {
768
        $tag_order = array_flip(self::FACT_ORDER);
769
770
        return $unsorted->sort(static function (string $x, string $y) use ($tag_order): int {
771
            $sort_x = $tag_order[$x] ?? $tag_order['_????_'];
772
            $sort_y = $tag_order[$y] ?? $tag_order['_????_'];
773
774
            return $sort_x - $sort_y;
775
        });
776
    }
777
778
    /**
779
     * Allow native PHP functions such as array_unique() to work with objects
780
     *
781
     * @return string
782
     */
783
    public function __toString(): string
784
    {
785
        return $this->id . '@' . $this->record->xref();
786
    }
787
}
788