Date   F
last analyzed

Complexity

Total Complexity 61

Size/Duplication

Total Lines 378
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 171
c 1
b 0
f 0
dl 0
loc 378
rs 3.52
wmc 61

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 4
A __clone() 0 5 2
C compare() 0 48 13
A maximumDate() 0 3 1
A addYears() 0 12 1
A minimumJulianDay() 0 3 1
A minimumDate() 0 3 1
A maximumJulianDay() 0 3 1
A isOK() 0 3 2
A gregorianYear() 0 10 2
A julianDay() 0 3 1
F display() 0 135 32

How to fix   Complexity   

Complex Class

Complex classes like Date 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 Date, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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 Fisharebest\ExtCalendar\GregorianCalendar;
23
use Fisharebest\Webtrees\Date\AbstractCalendarDate;
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\Date\AbstractCalendarDate was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
25
/**
26
 * A representation of GEDCOM dates and date ranges.
27
 *
28
 * Since different calendars start their days at different times, (civil
29
 * midnight, solar midnight, sunset, sunrise, etc.), we convert on the basis of
30
 * midday.
31
 *
32
 * We assume that years start on the first day of the first month. Where
33
 * this is not the case (e.g. England prior to 1752), we need to use modified
34
 * years or the OS/NS notation "4 FEB 1750/51".
35
 */
36
class Date
37
{
38
    // Optional qualifier, such as BEF, FROM, ABT
39
    public string $qual1 = '';
40
41
    // The first (or only) date
42
    private AbstractCalendarDate $date1;
43
44
    // Optional qualifier, such as TO, AND
45
    public string $qual2 = '';
46
47
    // Optional second date
48
    private AbstractCalendarDate|null $date2 = null;
49
50
    // Optional text, as included with an INTerpreted date
51
    private string $text = '';
52
53
    /**
54
     * Create a date, from GEDCOM data.
55
     *
56
     * @param string $date A date in GEDCOM format
57
     */
58
    public function __construct(string $date)
59
    {
60
        $calendar_date_factory = Registry::calendarDateFactory();
61
62
        // Extract any explanatory text
63
        if (preg_match('/^(.*) ?[(](.*)[)]/', $date, $match)) {
64
            $date       = $match[1];
65
            $this->text = $match[2];
66
        }
67
        if (preg_match('/^(FROM|BET) (.+) (AND|TO) (.+)/', $date, $match)) {
68
            $this->qual1 = $match[1];
69
            $this->date1 = $calendar_date_factory->make($match[2]);
70
            $this->qual2 = $match[3];
71
            $this->date2 = $calendar_date_factory->make($match[4]);
72
        } elseif (preg_match('/^(TO|FROM|BEF|AFT|CAL|EST|INT|ABT) (.+)/', $date, $match)) {
73
            $this->qual1 = $match[1];
74
            $this->date1 = $calendar_date_factory->make($match[2]);
75
        } else {
76
            $this->date1 = $calendar_date_factory->make($date);
77
        }
78
    }
79
80
    /**
81
     * When we copy a date object, we need to create copies of
82
     * its child objects.
83
     */
84
    public function __clone()
85
    {
86
        $this->date1 = clone $this->date1;
87
        if ($this->date2 !== null) {
88
            $this->date2 = clone $this->date2;
89
        }
90
    }
91
92
    /**
93
     * Convert a date to the preferred format and calendar(s) display.
94
     *
95
     * @param Tree|null   $tree              Wrap the date in a link to the calendar page for the tree
96
     * @param string|null $date_format       Override the default date format
97
     * @param bool        $convert_calendars Convert the date into other calendars (requires a tree)
98
     *
99
     * @return string
100
     */
101
    public function display(Tree|null $tree = null, string|null $date_format = null, bool $convert_calendars = false): string
102
    {
103
        if ($tree instanceof Tree) {
104
            $CALENDAR_FORMAT = $tree->getPreference('CALENDAR_FORMAT');
105
        } else {
106
            $CALENDAR_FORMAT = 'none';
107
        }
108
109
        $date_format ??= I18N::dateFormat();
0 ignored issues
show
Bug introduced by
The type Fisharebest\Webtrees\I18N was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
110
111
        if ($convert_calendars) {
112
            $calendar_format = explode('_and_', $CALENDAR_FORMAT);
113
        } else {
114
            $calendar_format = [];
115
        }
116
117
        // Two dates with text before, between and after
118
        $q1 = $this->qual1;
119
        $d1 = $this->date1->format($date_format, $this->qual1);
120
        $q2 = $this->qual2;
121
        if ($this->date2 === null) {
122
            $d2 = '';
123
        } else {
124
            $d2 = $this->date2->format($date_format, $this->qual2);
125
        }
126
        // Con vert to other calendars, if requested
127
        $conv1 = '';
128
        $conv2 = '';
129
        foreach ($calendar_format as $cal_fmt) {
130
            if ($cal_fmt !== 'none') {
131
                $d1conv = $this->date1->convertToCalendar($cal_fmt);
132
                if ($d1conv->inValidRange()) {
133
                    $d1tmp = $d1conv->format($date_format, $this->qual1);
134
                } else {
135
                    $d1tmp = '';
136
                }
137
                if ($this->date2 === null) {
138
                    $d2conv = null;
139
                    $d2tmp  = '';
140
                } else {
141
                    $d2conv = $this->date2->convertToCalendar($cal_fmt);
142
                    if ($d2conv->inValidRange()) {
143
                        $d2tmp = $d2conv->format($date_format, $this->qual2);
144
                    } else {
145
                        $d2tmp = '';
146
                    }
147
                }
148
                // If the date is different from the unconverted date, add it to the date string.
149
                if ($d1 !== $d1tmp && $d1tmp !== '') {
150
                    if ($tree instanceof Tree) {
151
                        if ($CALENDAR_FORMAT !== 'none') {
152
                            $conv1 .= ' <span dir="' . I18N::direction() . '">(<a href="' . e($d1conv->calendarUrl($date_format, $tree)) . '" rel="nofollow">' . $d1tmp . '</a>)</span>';
153
                        } else {
154
                            $conv1 .= ' <span dir="' . I18N::direction() . '"><br><a href="' . e($d1conv->calendarUrl($date_format, $tree)) . '" rel="nofollow">' . $d1tmp . '</a></span>';
155
                        }
156
                    } else {
157
                        $conv1 .= ' <span dir="' . I18N::direction() . '">(' . $d1tmp . ')</span>';
158
                    }
159
                }
160
                if ($this->date2 !== null && $d2 !== $d2tmp && $d1tmp !== '') {
161
                    if ($tree instanceof Tree) {
162
                        $conv2 .= ' <span dir="' . I18N::direction() . '">(<a href="' . e($d2conv->calendarUrl($date_format, $tree)) . '" rel="nofollow">' . $d2tmp . '</a>)</span>';
163
                    } else {
164
                        $conv2 .= ' <span dir="' . I18N::direction() . '">(' . $d2tmp . ')</span>';
165
                    }
166
                }
167
            }
168
        }
169
170
        // Add URLs, if requested
171
        if ($tree instanceof Tree) {
172
            $d1 = '<a href="' . e($this->date1->calendarUrl($date_format, $tree)) . '" rel="nofollow">' . $d1 . '</a>';
173
            if ($this->date2 instanceof AbstractCalendarDate) {
174
                $d2 = '<a href="' . e($this->date2->calendarUrl($date_format, $tree)) . '" rel="nofollow">' . $d2 . '</a>';
175
            }
176
        }
177
178
        // Localise the date
179
        switch ($q1 . $q2) {
180
            case '':
181
                $tmp = $d1 . $conv1;
182
                if ($this->text !== '') {
183
                    $tmp .= '(' . e($this->text) . ')';
184
                }
185
                break;
186
            case 'ABT':
187
                /* I18N: Gedcom ABT dates */
188
                $tmp = I18N::translate('about %s', $d1 . $conv1);
189
                break;
190
            case 'CAL':
191
                /* I18N: Gedcom CAL dates */
192
                $tmp = I18N::translate('calculated %s', $d1 . $conv1);
193
                break;
194
            case 'EST':
195
                /* I18N: Gedcom EST dates */
196
                $tmp = I18N::translate('estimated %s', $d1 . $conv1);
197
                break;
198
            case 'INT':
199
                /* I18N: Gedcom INT dates */
200
                $tmp = I18N::translate('interpreted %s (%s)', $d1 . $conv1, e($this->text));
201
                break;
202
            case 'BEF':
203
                /* I18N: Gedcom BEF dates */
204
                $tmp = I18N::translate('before %s', $d1 . $conv1);
205
                break;
206
            case 'AFT':
207
                /* I18N: Gedcom AFT dates */
208
                $tmp = I18N::translate('after %s', $d1 . $conv1);
209
                break;
210
            case 'FROM':
211
                /* I18N: Gedcom FROM dates */
212
                $tmp = I18N::translate('from %s', $d1 . $conv1);
213
                break;
214
            case 'TO':
215
                /* I18N: Gedcom TO dates */
216
                $tmp = I18N::translate('to %s', $d1 . $conv1);
217
                break;
218
            case 'BETAND':
219
                /* I18N: Gedcom BET-AND dates */
220
                $tmp = I18N::translate('between %s and %s', $d1 . $conv1, $d2 . $conv2);
221
                break;
222
            case 'FROMTO':
223
                /* I18N: Gedcom FROM-TO dates */
224
                $tmp = I18N::translate('from %s to %s', $d1 . $conv1, $d2 . $conv2);
225
                break;
226
            default:
227
                $tmp = I18N::translate('Invalid date');
228
                break;
229
        }
230
231
        if (strip_tags($tmp) === '') {
232
            return '';
233
        }
234
235
        return '<span class="date">' . $tmp . '</span>';
236
    }
237
238
    /**
239
     * Get the earliest calendar date from this GEDCOM date.
240
     *
241
     * In the date “FROM 1900 TO 1910”, this would be 1900.
242
     *
243
     * @return AbstractCalendarDate
244
     */
245
    public function minimumDate(): AbstractCalendarDate
246
    {
247
        return $this->date1;
248
    }
249
250
    /**
251
     * Get the latest calendar date from this GEDCOM date.
252
     *
253
     * In the date “FROM 1900 TO 1910”, this would be 1910.
254
     *
255
     * @return AbstractCalendarDate
256
     */
257
    public function maximumDate(): AbstractCalendarDate
258
    {
259
        return $this->date2 ?? $this->date1;
260
    }
261
262
    /**
263
     * Get the earliest Julian day number from this GEDCOM date.
264
     *
265
     * @return int
266
     */
267
    public function minimumJulianDay(): int
268
    {
269
        return $this->minimumDate()->minimumJulianDay();
270
    }
271
272
    /**
273
     * Get the latest Julian day number from this GEDCOM date.
274
     *
275
     * @return int
276
     */
277
    public function maximumJulianDay(): int
278
    {
279
        return $this->maximumDate()->maximumJulianDay();
280
    }
281
282
    /**
283
     * Get the middle Julian day number from the GEDCOM date.
284
     *
285
     * For a month-only date, this would be somewhere around the 16th day.
286
     * For a year-only date, this would be somewhere around 1st July.
287
     *
288
     * @return int
289
     */
290
    public function julianDay(): int
291
    {
292
        return intdiv($this->minimumJulianDay() + $this->maximumJulianDay(), 2);
293
    }
294
295
    /**
296
     * Offset this date by N years, and round to the whole year.
297
     *
298
     * This is typically used to create an estimated death date,
299
     * which is before a certain number of years after the birth date.
300
     *
301
     * @param int    $years     a number of years, positive or negative
302
     * @param string $qualifier typically “BEF” or “AFT”
303
     *
304
     * @return Date
305
     */
306
    public function addYears(int $years, string $qualifier = ''): Date
307
    {
308
        $tmp               = clone $this;
309
        $tmp->date1->year  += $years;
310
        $tmp->date1->month = 0;
311
        $tmp->date1->day   = 0;
312
        $tmp->date1->setJdFromYmd();
313
        $tmp->qual1 = $qualifier;
314
        $tmp->qual2 = '';
315
        $tmp->date2 = null;
316
317
        return $tmp;
318
    }
319
320
    /**
321
     * Compare two dates, so they can be sorted.
322
     *
323
     * return -1 if $a<$b
324
     * return +1 if $b>$a
325
     * return  0 if dates same/overlap
326
     * BEF/AFT sort as the day before/after
327
     *
328
     * @param Date $a
329
     * @param Date $b
330
     *
331
     * @return int
332
     */
333
    public static function compare(Date $a, Date $b): int
334
    {
335
        // Get min/max JD for each date.
336
        switch ($a->qual1) {
337
            case 'BEF':
338
                $amin = $a->minimumJulianDay() - 1;
339
                $amax = $amin;
340
                break;
341
            case 'AFT':
342
                $amax = $a->maximumJulianDay() + 1;
343
                $amin = $amax;
344
                break;
345
            default:
346
                $amin = $a->minimumJulianDay();
347
                $amax = $a->maximumJulianDay();
348
                break;
349
        }
350
        switch ($b->qual1) {
351
            case 'BEF':
352
                $bmin = $b->minimumJulianDay() - 1;
353
                $bmax = $bmin;
354
                break;
355
            case 'AFT':
356
                $bmax = $b->maximumJulianDay() + 1;
357
                $bmin = $bmax;
358
                break;
359
            default:
360
                $bmin = $b->minimumJulianDay();
361
                $bmax = $b->maximumJulianDay();
362
                break;
363
        }
364
        if ($amax < $bmin) {
365
            return -1;
366
        }
367
368
        if ($amin > $bmax && $bmax > 0) {
369
            return 1;
370
        }
371
372
        if ($amin < $bmin && $amax <= $bmax) {
373
            return -1;
374
        }
375
376
        if ($amin > $bmin && $amax >= $bmax && $bmax > 0) {
377
            return 1;
378
        }
379
380
        return 0;
381
    }
382
383
    /**
384
     * Check whether a gedcom date contains usable calendar date(s).
385
     *
386
     * An incomplete date such as "12 AUG" would be invalid, as
387
     * we cannot sort it.
388
     *
389
     * @return bool
390
     */
391
    public function isOK(): bool
392
    {
393
        return $this->minimumJulianDay() && $this->maximumJulianDay();
394
    }
395
396
    /**
397
     * Calculate the gregorian year for a date. This should NOT be used internally
398
     * within WT - we should keep the code "calendar neutral" to allow support for
399
     * jewish/arabic users. This is only for interfacing with external entities,
400
     * such as the ancestry.com search interface or the dated fact icons.
401
     *
402
     * @return int
403
     */
404
    public function gregorianYear(): int
405
    {
406
        if ($this->isOK()) {
407
            $gregorian_calendar = new GregorianCalendar();
408
            [$year] = $gregorian_calendar->jdToYmd($this->julianDay());
409
410
            return $year;
411
        }
412
413
        return 0;
414
    }
415
}
416