AbstractCalendarDate::format()   F
last analyzed

Complexity

Conditions 37
Paths > 20000

Size

Total Lines 107
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 37
eloc 70
nc 54525952
nop 2
dl 0
loc 107
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2023 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\Date;
21
22
use Fisharebest\ExtCalendar\CalendarInterface;
23
use Fisharebest\ExtCalendar\JewishCalendar;
24
use Fisharebest\Webtrees\Http\RequestHandlers\CalendarPage;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Registry;
27
use Fisharebest\Webtrees\Tree;
28
use InvalidArgumentException;
29
30
use function get_class;
31
use function intdiv;
32
use function is_array;
33
use function is_int;
34
use function preg_match;
35
use function route;
36
use function sprintf;
37
use function str_contains;
38
use function strpbrk;
39
use function strtr;
40
use function trim;
41
42
/**
43
 * Classes for Gedcom Date/Calendar functionality.
44
 *
45
 * CalendarDate is a base class for classes such as GregorianDate, etc.
46
 * + All supported calendars have non-zero days/months/years.
47
 * + We store dates as both Y/M/D and Julian Days.
48
 * + For imprecise dates such as "JAN 2000" we store the start/end julian day.
49
 *
50
 * NOTE: Since different calendars start their days at different times, (civil
51
 * midnight, solar midnight, sunset, sunrise, etc.), we convert on the basis of
52
 * midday.
53
 */
54
abstract class AbstractCalendarDate
55
{
56
    // GEDCOM calendar escape
57
    public const ESCAPE = '@#DUNKNOWN@';
58
59
    // Convert GEDCOM month names to month numbers.
60
    protected const MONTH_TO_NUMBER = [];
61
    protected const NUMBER_TO_MONTH = [];
62
63
    protected CalendarInterface $calendar;
64
65
    public int $year;
66
67
    public int $month;
68
69
    public int $day;
70
71
    private int $minimum_julian_day;
72
73
    private int $maximum_julian_day;
74
75
    /**
76
     * Create a date from either:
77
     * a Julian day number
78
     * day/month/year strings from a GEDCOM date
79
     * another CalendarDate object
80
     *
81
     * @param array<string>|int|AbstractCalendarDate $date
82
     */
83
    public function __construct(array|int|AbstractCalendarDate $date)
84
    {
85
        // Construct from an integer (a julian day number)
86
        if (is_int($date)) {
87
            $this->minimum_julian_day = $date;
88
            $this->maximum_julian_day = $date;
89
            [$this->year, $this->month, $this->day] = $this->calendar->jdToYmd($date);
90
91
            return;
92
        }
93
94
        // Construct from an array (of three gedcom-style strings: "1900", "FEB", "4")
95
        if (is_array($date)) {
96
            $this->day = (int) $date[2];
97
            $this->month = static::MONTH_TO_NUMBER[$date[1]] ?? 0;
98
99
            if ($this->month === 0) {
100
                $this->day   = 0;
101
            }
102
103
            $this->year = $this->extractYear($date[0]);
104
105
            // Our simple lookup table above does not take into account Adar and leap-years.
106
            if ($this->month === 6 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->year)) {
107
                $this->month = 7;
108
            }
109
110
            $this->setJdFromYmd();
111
112
            return;
113
        }
114
115
        // Construct from a CalendarDate
116
        $this->minimum_julian_day = $date->minimum_julian_day;
117
        $this->maximum_julian_day = $date->maximum_julian_day;
118
119
        // Construct from an equivalent xxxxDate object
120
        if (get_class($this) === get_class($date)) {
121
            $this->year  = $date->year;
122
            $this->month = $date->month;
123
            $this->day   = $date->day;
124
125
            return;
126
        }
127
128
        // Not all dates can be converted
129
        if (!$this->inValidRange()) {
130
            $this->year  = 0;
131
            $this->month = 0;
132
            $this->day   = 0;
133
134
            return;
135
        }
136
137
        // ...else construct an inequivalent xxxxDate object
138
        if ($date->year === 0) {
139
            // Incomplete date - convert on basis of anniversary in current year
140
            $today = $date->calendar->jdToYmd(Registry::timestampFactory()->now()->julianDay());
141
            $jd    = $date->calendar->ymdToJd($today[0], $date->month, $date->day === 0 ? $today[2] : $date->day);
142
        } else {
143
            // Complete date
144
            $jd = intdiv($date->maximum_julian_day + $date->minimum_julian_day, 2);
145
        }
146
        [$this->year, $this->month, $this->day] = $this->calendar->jdToYmd($jd);
147
        // New date has same precision as original date
148
        if ($date->year === 0) {
149
            $this->year = 0;
150
        }
151
        if ($date->month === 0) {
152
            $this->month = 0;
153
        }
154
        if ($date->day === 0) {
155
            $this->day = 0;
156
        }
157
        $this->setJdFromYmd();
158
    }
159
160
    /**
161
     * @return int
162
     */
163
    public function maximumJulianDay(): int
164
    {
165
        return $this->maximum_julian_day;
166
    }
167
168
    /**
169
     * @return int
170
     */
171
    public function year(): int
172
    {
173
        return $this->year;
174
    }
175
176
    /**
177
     * @return int
178
     */
179
    public function month(): int
180
    {
181
        return $this->month;
182
    }
183
184
    /**
185
     * @return int
186
     */
187
    public function day(): int
188
    {
189
        return $this->day;
190
    }
191
192
    /**
193
     * @return int
194
     */
195
    public function minimumJulianDay(): int
196
    {
197
        return $this->minimum_julian_day;
198
    }
199
200
    /**
201
     * Is the current year a leap year?
202
     *
203
     * @return bool
204
     */
205
    public function isLeapYear(): bool
206
    {
207
        return $this->calendar->isLeapYear($this->year);
208
    }
209
210
    /**
211
     * Set the object’s Julian day number from a potentially incomplete year/month/day
212
     *
213
     * @return void
214
     */
215
    public function setJdFromYmd(): void
216
    {
217
        if ($this->year === 0) {
218
            $this->minimum_julian_day = 0;
219
            $this->maximum_julian_day = 0;
220
        } elseif ($this->month === 0) {
221
            $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, 1, 1);
222
            $this->maximum_julian_day = $this->calendar->ymdToJd($this->nextYear($this->year), 1, 1) - 1;
223
        } elseif ($this->day === 0) {
224
            [$ny, $nm] = $this->nextMonth();
225
            $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, $this->month, 1);
226
            $this->maximum_julian_day = $this->calendar->ymdToJd($ny, $nm, 1) - 1;
227
        } else {
228
            $this->minimum_julian_day = $this->calendar->ymdToJd($this->year, $this->month, $this->day);
229
            $this->maximum_julian_day = $this->minimum_julian_day;
230
        }
231
    }
232
233
    /**
234
     * Full day of the week
235
     *
236
     * @param int $day_number
237
     *
238
     * @return string
239
     */
240
    public function dayNames(int $day_number): string
241
    {
242
        static $translated_day_names;
243
244
        if ($translated_day_names === null) {
245
            $translated_day_names = [
246
                0 => I18N::translate('Monday'),
247
                1 => I18N::translate('Tuesday'),
248
                2 => I18N::translate('Wednesday'),
249
                3 => I18N::translate('Thursday'),
250
                4 => I18N::translate('Friday'),
251
                5 => I18N::translate('Saturday'),
252
                6 => I18N::translate('Sunday'),
253
            ];
254
        }
255
256
        return $translated_day_names[$day_number];
257
    }
258
259
    /**
260
     * Abbreviated day of the week
261
     *
262
     * @param int $day_number
263
     *
264
     * @return string
265
     */
266
    protected function dayNamesAbbreviated(int $day_number): string
267
    {
268
        static $translated_day_names;
269
270
        if ($translated_day_names === null) {
271
            $translated_day_names = [
272
                /* I18N: abbreviation for Monday */
273
                0 => I18N::translate('Mon'),
274
                /* I18N: abbreviation for Tuesday */
275
                1 => I18N::translate('Tue'),
276
                /* I18N: abbreviation for Wednesday */
277
                2 => I18N::translate('Wed'),
278
                /* I18N: abbreviation for Thursday */
279
                3 => I18N::translate('Thu'),
280
                /* I18N: abbreviation for Friday */
281
                4 => I18N::translate('Fri'),
282
                /* I18N: abbreviation for Saturday */
283
                5 => I18N::translate('Sat'),
284
                /* I18N: abbreviation for Sunday */
285
                6 => I18N::translate('Sun'),
286
            ];
287
        }
288
289
        return $translated_day_names[$day_number];
290
    }
291
292
    /**
293
     * Most years are 1 more than the previous, but not always (e.g. 1BC->1AD)
294
     *
295
     * @param int $year
296
     *
297
     * @return int
298
     */
299
    protected function nextYear(int $year): int
300
    {
301
        return $year + 1;
302
    }
303
304
    /**
305
     * Calendars that use suffixes, etc. (e.g. “B.C.”) or OS/NS notation should redefine this.
306
     *
307
     * @param string $year
308
     *
309
     * @return int
310
     */
311
    protected function extractYear(string $year): int
312
    {
313
        return (int) $year;
314
    }
315
316
    /**
317
     * Compare two dates, for sorting
318
     *
319
     * @param AbstractCalendarDate $d1
320
     * @param AbstractCalendarDate $d2
321
     *
322
     * @return int
323
     */
324
    public static function compare(AbstractCalendarDate $d1, AbstractCalendarDate $d2): int
325
    {
326
        if ($d1->maximum_julian_day < $d2->minimum_julian_day) {
327
            return -1;
328
        }
329
330
        if ($d2->maximum_julian_day < $d1->minimum_julian_day) {
331
            return 1;
332
        }
333
334
        return 0;
335
    }
336
337
    /**
338
     * Calculate the years/months/days between this date and another date.
339
     * Results assume you add the days first, then the months.
340
     * 4 February -> 3 July is 27 days (3 March) and 4 months.
341
     * It is not 4 months (4 June) and 29 days.
342
     *
343
     * @param AbstractCalendarDate $date
344
     *
345
     * @return array<int> Age in years/months/days
346
     */
347
    public function ageDifference(AbstractCalendarDate $date): array
348
    {
349
        // Incomplete dates
350
        if ($this->year === 0 || $date->year === 0) {
351
            return [-1, -1, -1];
352
        }
353
354
        // Overlapping dates
355
        if (self::compare($this, $date) === 0) {
356
            return [0, 0, 0];
357
        }
358
359
        // Perform all calculations using the calendar of the first date
360
        [$year1, $month1, $day1] = $this->calendar->jdToYmd($this->minimum_julian_day);
361
        [$year2, $month2, $day2] = $this->calendar->jdToYmd($date->minimum_julian_day);
362
363
        $years  = $year2 - $year1;
364
        $months = $month2 - $month1;
365
        $days   = $day2 - $day1;
366
367
        if ($days < 0) {
368
            $days += $this->calendar->daysInMonth($year1, $month1);
369
            $months--;
370
        }
371
372
        if ($months < 0) {
373
            $months += $this->calendar->monthsInYear($year2);
374
            $years--;
375
        }
376
377
        return [$years, $months, $days];
378
    }
379
380
    /**
381
     * Convert a date from one calendar to another.
382
     *
383
     * @param string $calendar
384
     *
385
     * @return AbstractCalendarDate
386
     */
387
    public function convertToCalendar(string $calendar): AbstractCalendarDate
388
    {
389
        switch ($calendar) {
390
            case 'gregorian':
391
                return new GregorianDate($this);
392
            case 'julian':
393
                return new JulianDate($this);
394
            case 'jewish':
395
                return new JewishDate($this);
396
            case 'french':
397
                return new FrenchDate($this);
398
            case 'hijri':
399
                return new HijriDate($this);
400
            case 'jalali':
401
                return new JalaliDate($this);
402
            default:
403
                return $this;
404
        }
405
    }
406
407
    /**
408
     * Is this date within the valid range of the calendar
409
     *
410
     * @return bool
411
     */
412
    public function inValidRange(): bool
413
    {
414
        return $this->minimum_julian_day >= $this->calendar->jdStart() && $this->maximum_julian_day <= $this->calendar->jdEnd();
415
    }
416
417
    /**
418
     * How many months in a year
419
     *
420
     * @return int
421
     */
422
    public function monthsInYear(): int
423
    {
424
        return $this->calendar->monthsInYear();
425
    }
426
427
    /**
428
     * How many days in the current month
429
     *
430
     * @return int
431
     */
432
    public function daysInMonth(): int
433
    {
434
        try {
435
            return $this->calendar->daysInMonth($this->year, $this->month);
436
        } catch (InvalidArgumentException) {
437
            // calendar.php calls this with "DD MMM" dates, for which we cannot calculate
438
            // the length of a month. Should we validate this before calling this function?
439
            return 0;
440
        }
441
    }
442
443
    /**
444
     * How many days in the current week
445
     *
446
     * @return int
447
     */
448
    public function daysInWeek(): int
449
    {
450
        return $this->calendar->daysInWeek();
451
    }
452
453
    /**
454
     * Format a date, using similar codes to the PHP date() function.
455
     *
456
     * @param string $format    See https://php.net/date
457
     * @param string $qualifier GEDCOM qualifier, so we can choose the right case for the month name.
458
     *
459
     * @return string
460
     */
461
    public function format(string $format, string $qualifier = ''): string
462
    {
463
        // Dates can include additional punctuation and symbols. e.g.
464
        // %F %j, %Y
465
        // %Y. %F %d.
466
        // %Y年 %n月 %j日
467
        // %j. %F %Y
468
        // Don’t show exact details or unnecessary punctuation for inexact dates.
469
        if ($this->day === 0) {
470
            $format = strtr($format, ['%d' => '', '日' => '', '%j,' => '', '%j' => '', '%l' => '', '%D' => '', '%N' => '', '%S' => '', '%w' => '', '%z' => '']);
471
        }
472
        if ($this->month === 0) {
473
            $format = strtr($format, ['%F' => '', '%m' => '', '%M' => '', '月' => '', '%n' => '', '%t' => '']);
474
        }
475
        if ($this->year === 0) {
476
            $format = strtr($format, ['%t' => '', '%L' => '', '%G' => '', '%y' => '', '年' => '', '%Y' => '']);
477
        }// 年 %n月%j日
478
        $format = trim($format, ',. /-');
479
480
        if ($this->day !== 0 && preg_match('/%[djlDNSwz]/', $format)) {
481
            // If we have a day-number *and* we are being asked to display it, then genitive
482
            $case = 'GENITIVE';
483
        } else {
484
            switch ($qualifier) {
485
                case 'TO':
486
                case 'ABT':
487
                case 'FROM':
488
                    $case = 'GENITIVE';
489
                    break;
490
                case 'AFT':
491
                    $case = 'LOCATIVE';
492
                    break;
493
                case 'BEF':
494
                case 'BET':
495
                case 'AND':
496
                    $case = 'INSTRUMENTAL';
497
                    break;
498
                case '':
499
                case 'INT':
500
                case 'EST':
501
                case 'CAL':
502
                default: // There shouldn't be any other options...
503
                    $case = 'NOMINATIVE';
504
                    break;
505
            }
506
        }
507
        // Build up the formatted date, character at a time
508
        if (str_contains($format, '%d')) {
509
            $format = strtr($format, ['%d' => $this->formatDayZeros()]);
510
        }
511
        if (str_contains($format, '%j')) {
512
            $format = strtr($format, ['%j' => $this->formatDay()]);
513
        }
514
        if (str_contains($format, '%l')) {
515
            $format = strtr($format, ['%l' => $this->formatLongWeekday()]);
516
        }
517
        if (str_contains($format, '%D')) {
518
            $format = strtr($format, ['%D' => $this->formatShortWeekday()]);
519
        }
520
        if (str_contains($format, '%N')) {
521
            $format = strtr($format, ['%N' => $this->formatIsoWeekday()]);
522
        }
523
        if (str_contains($format, '%w')) {
524
            $format = strtr($format, ['%w' => $this->formatNumericWeekday()]);
525
        }
526
        if (str_contains($format, '%z')) {
527
            $format = strtr($format, ['%z' => $this->formatDayOfYear()]);
528
        }
529
        if (str_contains($format, '%F')) {
530
            $format = strtr($format, ['%F' => $this->formatLongMonth($case)]);
531
        }
532
        if (str_contains($format, '%m')) {
533
            $format = strtr($format, ['%m' => $this->formatMonthZeros()]);
534
        }
535
        if (str_contains($format, '%M')) {
536
            $format = strtr($format, ['%M' => $this->formatShortMonth()]);
537
        }
538
        if (str_contains($format, '%n')) {
539
            $format = strtr($format, ['%n' => $this->formatMonth()]);
540
        }
541
        if (str_contains($format, '%t')) {
542
            $format = strtr($format, ['%t' => (string) $this->daysInMonth()]);
543
        }
544
        if (str_contains($format, '%L')) {
545
            $format = strtr($format, ['%L' => $this->isLeapYear() ? '1' : '0']);
546
        }
547
        if (str_contains($format, '%Y')) {
548
            $format = strtr($format, ['%Y' => $this->formatLongYear()]);
549
        }
550
        if (str_contains($format, '%y')) {
551
            $format = strtr($format, ['%y' => $this->formatShortYear()]);
552
        }
553
        // These 4 extensions are useful for re-formatting gedcom dates.
554
        if (str_contains($format, '%@')) {
555
            $format = strtr($format, ['%@' => $this->formatGedcomCalendarEscape()]);
556
        }
557
        if (str_contains($format, '%A')) {
558
            $format = strtr($format, ['%A' => $this->formatGedcomDay()]);
559
        }
560
        if (str_contains($format, '%O')) {
561
            $format = strtr($format, ['%O' => $this->formatGedcomMonth()]);
562
        }
563
        if (str_contains($format, '%E')) {
564
            $format = strtr($format, ['%E' => $this->formatGedcomYear()]);
565
        }
566
567
        return $format;
568
    }
569
570
    /**
571
     * Generate the %d format for a date.
572
     *
573
     * @return string
574
     */
575
    protected function formatDayZeros(): string
576
    {
577
        if ($this->day > 9) {
578
            return I18N::digits($this->day);
579
        }
580
581
        return I18N::digits('0' . $this->day);
582
    }
583
584
    /**
585
     * Generate the %j format for a date.
586
     *
587
     * @return string
588
     */
589
    protected function formatDay(): string
590
    {
591
        return I18N::digits($this->day);
592
    }
593
594
    /**
595
     * Generate the %l format for a date.
596
     *
597
     * @return string
598
     */
599
    protected function formatLongWeekday(): string
600
    {
601
        return $this->dayNames($this->minimum_julian_day % $this->calendar->daysInWeek());
602
    }
603
604
    /**
605
     * Generate the %D format for a date.
606
     *
607
     * @return string
608
     */
609
    protected function formatShortWeekday(): string
610
    {
611
        return $this->dayNamesAbbreviated($this->minimum_julian_day % $this->calendar->daysInWeek());
612
    }
613
614
    /**
615
     * Generate the %N format for a date.
616
     *
617
     * @return string
618
     */
619
    protected function formatIsoWeekday(): string
620
    {
621
        return I18N::digits($this->minimum_julian_day % 7 + 1);
622
    }
623
624
    /**
625
     * Generate the %w format for a date.
626
     *
627
     * @return string
628
     */
629
    protected function formatNumericWeekday(): string
630
    {
631
        return I18N::digits(($this->minimum_julian_day + 1) % $this->calendar->daysInWeek());
632
    }
633
634
    /**
635
     * Generate the %z format for a date.
636
     *
637
     * @return string
638
     */
639
    protected function formatDayOfYear(): string
640
    {
641
        return I18N::digits($this->minimum_julian_day - $this->calendar->ymdToJd($this->year, 1, 1));
642
    }
643
644
    /**
645
     * Generate the %n format for a date.
646
     *
647
     * @return string
648
     */
649
    protected function formatMonth(): string
650
    {
651
        return I18N::digits($this->month);
652
    }
653
654
    /**
655
     * Generate the %m format for a date.
656
     *
657
     * @return string
658
     */
659
    protected function formatMonthZeros(): string
660
    {
661
        if ($this->month > 9) {
662
            return I18N::digits($this->month);
663
        }
664
665
        return I18N::digits('0' . $this->month);
666
    }
667
668
    /**
669
     * Generate the %F format for a date.
670
     *
671
     * @param string $case Which grammatical case shall we use
672
     *
673
     * @return string
674
     */
675
    protected function formatLongMonth(string $case = 'NOMINATIVE'): string
676
    {
677
        switch ($case) {
678
            case 'GENITIVE':
679
                return $this->monthNameGenitiveCase($this->month, $this->isLeapYear());
680
            case 'NOMINATIVE':
681
                return $this->monthNameNominativeCase($this->month, $this->isLeapYear());
682
            case 'LOCATIVE':
683
                return $this->monthNameLocativeCase($this->month, $this->isLeapYear());
684
            case 'INSTRUMENTAL':
685
                return $this->monthNameInstrumentalCase($this->month, $this->isLeapYear());
686
            default:
687
                throw new InvalidArgumentException($case);
688
        }
689
    }
690
691
    /**
692
     * Full month name in genitive case.
693
     *
694
     * @param int  $month
695
     * @param bool $leap_year Some calendars use leap months
696
     *
697
     * @return string
698
     */
699
    abstract protected function monthNameGenitiveCase(int $month, bool $leap_year): string;
700
701
    /**
702
     * Full month name in nominative case.
703
     *
704
     * @param int  $month
705
     * @param bool $leap_year Some calendars use leap months
706
     *
707
     * @return string
708
     */
709
    abstract protected function monthNameNominativeCase(int $month, bool $leap_year): string;
710
711
    /**
712
     * Full month name in locative case.
713
     *
714
     * @param int  $month
715
     * @param bool $leap_year Some calendars use leap months
716
     *
717
     * @return string
718
     */
719
    abstract protected function monthNameLocativeCase(int $month, bool $leap_year): string;
720
721
    /**
722
     * Full month name in instrumental case.
723
     *
724
     * @param int  $month
725
     * @param bool $leap_year Some calendars use leap months
726
     *
727
     * @return string
728
     */
729
    abstract protected function monthNameInstrumentalCase(int $month, bool $leap_year): string;
730
731
    /**
732
     * Abbreviated month name
733
     *
734
     * @param int  $month
735
     * @param bool $leap_year Some calendars use leap months
736
     *
737
     * @return string
738
     */
739
    abstract protected function monthNameAbbreviated(int $month, bool $leap_year): string;
740
741
    /**
742
     * Generate the %M format for a date.
743
     *
744
     * @return string
745
     */
746
    protected function formatShortMonth(): string
747
    {
748
        return $this->monthNameAbbreviated($this->month, $this->isLeapYear());
749
    }
750
751
    /**
752
     * Generate the %y format for a date.
753
     * NOTE Short year is NOT a 2-digit year. It is for calendars such as hebrew
754
     * which have a 3-digit form of 4-digit years.
755
     *
756
     * @return string
757
     */
758
    protected function formatShortYear(): string
759
    {
760
        return $this->formatLongYear();
761
    }
762
763
    /**
764
     * Generate the %A format for a date.
765
     *
766
     * @return string
767
     */
768
    protected function formatGedcomDay(): string
769
    {
770
        if ($this->day === 0) {
771
            return '';
772
        }
773
774
        return sprintf('%02d', $this->day);
775
    }
776
777
    /**
778
     * Generate the %O format for a date.
779
     *
780
     * @return string
781
     */
782
    protected function formatGedcomMonth(): string
783
    {
784
        // Our simple lookup table doesn't work correctly for Adar on leap years
785
        if ($this->month === 7 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->year)) {
786
            return 'ADR';
787
        }
788
789
        return static::NUMBER_TO_MONTH[$this->month] ?? '';
790
    }
791
792
    /**
793
     * Generate the %E format for a date.
794
     *
795
     * @return string
796
     */
797
    protected function formatGedcomYear(): string
798
    {
799
        if ($this->year === 0) {
800
            return '';
801
        }
802
803
        return sprintf('%04d', $this->year);
804
    }
805
806
    /**
807
     * Generate the %@ format for a calendar escape.
808
     *
809
     * @return string
810
     */
811
    protected function formatGedcomCalendarEscape(): string
812
    {
813
        return static::ESCAPE;
814
    }
815
816
    /**
817
     * Generate the %Y format for a date.
818
     *
819
     * @return string
820
     */
821
    protected function formatLongYear(): string
822
    {
823
        return I18N::digits($this->year);
824
    }
825
826
    /**
827
     * Which months follows this one? Calendars with leap-months should provide their own implementation.
828
     *
829
     * @return array<int>
830
     */
831
    protected function nextMonth(): array
832
    {
833
        return [
834
            $this->month === $this->calendar->monthsInYear() ? $this->nextYear($this->year) : $this->year,
835
            $this->month % $this->calendar->monthsInYear() + 1,
836
        ];
837
    }
838
839
    /**
840
     * Get today’s date in the current calendar.
841
     *
842
     * @return array<int>
843
     */
844
    public function todayYmd(): array
845
    {
846
        return $this->calendar->jdToYmd(Registry::timestampFactory()->now()->julianDay());
847
    }
848
849
    /**
850
     * Convert to today’s date.
851
     *
852
     * @return AbstractCalendarDate
853
     */
854
    public function today(): AbstractCalendarDate
855
    {
856
        $tmp        = clone $this;
857
        $ymd        = $tmp->todayYmd();
858
        $tmp->year  = $ymd[0];
859
        $tmp->month = $ymd[1];
860
        $tmp->day   = $ymd[2];
861
        $tmp->setJdFromYmd();
862
863
        return $tmp;
864
    }
865
866
    /**
867
     * Create a URL that links this date to the WT calendar
868
     *
869
     * @param string $date_format
870
     * @param Tree   $tree
871
     *
872
     * @return string
873
     */
874
    public function calendarUrl(string $date_format, Tree $tree): string
875
    {
876
        if ($this->day !== 0 && strpbrk($date_format, 'dDj')) {
877
            // If the format includes a day, and the date also includes a day, then use the day view
878
            $view = 'day';
879
        } elseif ($this->month !== 0 && strpbrk($date_format, 'FMmn')) {
880
            // If the format includes a month, and the date also includes a month, then use the month view
881
            $view = 'month';
882
        } else {
883
            // Use the year view
884
            $view = 'year';
885
        }
886
887
        return route(CalendarPage::class, [
888
            'cal'   => $this->calendar->gedcomCalendarEscape(),
889
            'year'  => $this->formatGedcomYear(),
890
            'month' => $this->formatGedcomMonth(),
891
            'day'   => $this->formatGedcomDay(),
892
            'view'  => $view,
893
            'tree'  => $tree->name(),
894
        ]);
895
    }
896
}
897