Completed
Push — develop ( 9087a8...c9b4ef )
by Greg
16:31 queued 05:44
created

CalendarDate::romanNumeralsToNumber()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 6
nc 3
nop 1
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2018 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees\Date;
17
18
use FishareBest\ExtCalendar\CalendarInterface;
19
use FishareBest\ExtCalendar\JewishCalendar;
20
use Fisharebest\Webtrees\DebugBar;
21
use Fisharebest\Webtrees\I18N;
22
23
/**
24
 * Classes for Gedcom Date/Calendar functionality.
25
 *
26
 * CalendarDate is a base class for classes such as GregorianDate, etc.
27
 * + All supported calendars have non-zero days/months/years.
28
 * + We store dates as both Y/M/D and Julian Days.
29
 * + For imprecise dates such as "JAN 2000" we store the start/end julian day.
30
 *
31
 * NOTE: Since different calendars start their days at different times, (civil
32
 * midnight, solar midnight, sunset, sunrise, etc.), we convert on the basis of
33
 * midday.
34
 */
35
class CalendarDate {
36
	/** @var int[] Convert GEDCOM month names to month numbers */
37
	public static $MONTH_ABBREV = ['' => 0, 'JAN' => 1, 'FEB' => 2, 'MAR' => 3, 'APR' => 4, 'MAY' => 5, 'JUN' => 6, 'JUL' => 7, 'AUG' => 8, 'SEP' => 9, 'OCT' => 10, 'NOV' => 11, 'DEC' => 12];
38
39
	/** @var string[] Convert numbers to/from roman numerals */
40
	protected static $roman_numerals = [1000 => 'M', 900 => 'CM', 500 => 'D', 400 => 'CD', 100 => 'C', 90 => 'XC', 50 => 'L', 40 => 'XL', 10 => 'X', 9 => 'IX', 5 => 'V', 4 => 'IV', 1 => 'I'];
41
42
	/** @var CalendarInterface The calendar system used to represent this date */
43
	protected $calendar;
44
45
	/** @var int Year number */
46
	public $y;
47
48
	/** @var int Month number */
49
	public $m;
50
51
	/** @var int Day number */
52
	public $d;
53
54
	/** @var int Earliest Julian day number (start of month/year for imprecise dates) */
55
	public $minJD;
56
57
	/** @var int Latest Julian day number (end of month/year for imprecise dates) */
58
	public $maxJD;
59
60
	/**
61
	 * Create a date from either:
62
	 * a Julian day number
63
	 * day/month/year strings from a GEDCOM date
64
	 * another CalendarDate object
65
	 *
66
	 * @param array|int|CalendarDate $date
67
	 */
68
	public function __construct($date) {
69
		// Construct from an integer (a julian day number)
70
		if (is_integer($date)) {
71
			$this->minJD                       = $date;
72
			$this->maxJD                       = $date;
73
			list($this->y, $this->m, $this->d) = $this->calendar->jdToYmd($date);
74
75
			return;
76
		}
77
78
		// Construct from an array (of three gedcom-style strings: "1900", "FEB", "4")
79
		if (is_array($date)) {
80
			$this->d = (int) $date[2];
81
			if (array_key_exists($date[1], static::$MONTH_ABBREV)) {
82
				$this->m = static::$MONTH_ABBREV[$date[1]];
83
			} else {
84
				$this->m = 0;
85
				$this->d = 0;
86
			}
87
			$this->y = $this->extractYear($date[0]);
88
89
			// Our simple lookup table above does not take into account Adar and leap-years.
90
			if ($this->m === 6 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->y)) {
91
				$this->m = 7;
92
			}
93
94
			$this->setJdFromYmd();
95
96
			return;
97
		}
98
99
		$this->minJD = $date->minJD;
100
		$this->maxJD = $date->maxJD;
101
102
		// Construct from an equivalent xxxxDate object
103
		if (get_class($this) == get_class($date)) {
104
			$this->y = $date->y;
105
			$this->m = $date->m;
106
			$this->d = $date->d;
107
108
			return;
109
		}
110
111
		// Not all dates can be converted
112
		if (!$this->inValidRange()) {
113
			$this->y = 0;
114
			$this->m = 0;
115
			$this->d = 0;
116
117
			return;
118
		}
119
120
		// ...else construct an inequivalent xxxxDate object
121
		if ($date->y == 0) {
122
			// Incomplete date - convert on basis of anniversary in current year
123
			$today = $date->calendar->jdToYmd(unixtojd());
124
			$jd    = $date->calendar->ymdToJd($today[0], $date->m, $date->d == 0 ? $today[2] : $date->d);
125
		} else {
126
			// Complete date
127
			$jd = (int) (($date->maxJD + $date->minJD) / 2);
128
		}
129
		list($this->y, $this->m, $this->d) = $this->calendar->jdToYmd($jd);
130
		// New date has same precision as original date
131
		if ($date->y == 0) {
132
			$this->y = 0;
133
		}
134
		if ($date->m == 0) {
135
			$this->m = 0;
136
		}
137
		if ($date->d == 0) {
138
			$this->d = 0;
139
		}
140
		$this->setJdFromYmd();
141
	}
142
143
	/**
144
	 * Is the current year a leap year?
145
	 *
146
	 * @return bool
147
	 */
148
	public function isLeapYear() {
149
		return $this->calendar->isLeapYear($this->y);
150
	}
151
152
	/**
153
	 * Set the object’s Julian day number from a potentially incomplete year/month/day
154
	 */
155
	public function setJdFromYmd() {
156
		if ($this->y == 0) {
157
			$this->minJD = 0;
158
			$this->maxJD = 0;
159
		} elseif ($this->m == 0) {
160
			$this->minJD = $this->calendar->ymdToJd($this->y, 1, 1);
161
			$this->maxJD = $this->calendar->ymdToJd($this->nextYear($this->y), 1, 1) - 1;
162
		} elseif ($this->d == 0) {
163
			list($ny, $nm) = $this->nextMonth();
164
			$this->minJD   = $this->calendar->ymdToJd($this->y, $this->m, 1);
165
			$this->maxJD   = $this->calendar->ymdToJd($ny, $nm, 1) - 1;
166
		} else {
167
			$this->minJD = $this->calendar->ymdToJd($this->y, $this->m, $this->d);
168
			$this->maxJD = $this->minJD;
169
		}
170
	}
171
172
	/**
173
	 * Full month name in nominative case.
174
	 *
175
	 * We put these in the base class, to save duplicating it in the Julian and Gregorian calendars.
176
	 *
177
	 * @param int  $month_number
178
	 * @param bool $leap_year    Some calendars use leap months
179
	 *
180
	 * @return string
181
	 */
182
	public static function monthNameNominativeCase($month_number, $leap_year) {
0 ignored issues
show
Unused Code introduced by
The parameter $leap_year is not used and could be removed. ( Ignorable by Annotation )

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

182
	public static function monthNameNominativeCase($month_number, /** @scrutinizer ignore-unused */ $leap_year) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
183
		static $translated_month_names;
184
185
		if ($translated_month_names === null) {
186
			$translated_month_names = [
187
				0  => '',
188
				1  => I18N::translateContext('NOMINATIVE', 'January'),
189
				2  => I18N::translateContext('NOMINATIVE', 'February'),
190
				3  => I18N::translateContext('NOMINATIVE', 'March'),
191
				4  => I18N::translateContext('NOMINATIVE', 'April'),
192
				5  => I18N::translateContext('NOMINATIVE', 'May'),
193
				6  => I18N::translateContext('NOMINATIVE', 'June'),
194
				7  => I18N::translateContext('NOMINATIVE', 'July'),
195
				8  => I18N::translateContext('NOMINATIVE', 'August'),
196
				9  => I18N::translateContext('NOMINATIVE', 'September'),
197
				10 => I18N::translateContext('NOMINATIVE', 'October'),
198
				11 => I18N::translateContext('NOMINATIVE', 'November'),
199
				12 => I18N::translateContext('NOMINATIVE', 'December'),
200
			];
201
		}
202
203
		return $translated_month_names[$month_number];
204
	}
205
206
	/**
207
	 * Full month name in genitive case.
208
	 *
209
	 * We put these in the base class, to save duplicating it in the Julian and Gregorian calendars.
210
	 *
211
	 * @param int  $month_number
212
	 * @param bool $leap_year    Some calendars use leap months
213
	 *
214
	 * @return string
215
	 */
216
	protected function monthNameGenitiveCase($month_number, $leap_year) {
0 ignored issues
show
Unused Code introduced by
The parameter $leap_year is not used and could be removed. ( Ignorable by Annotation )

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

216
	protected function monthNameGenitiveCase($month_number, /** @scrutinizer ignore-unused */ $leap_year) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
217
		static $translated_month_names;
218
219
		if ($translated_month_names === null) {
220
			$translated_month_names = [
221
				0  => '',
222
				1  => I18N::translateContext('GENITIVE', 'January'),
223
				2  => I18N::translateContext('GENITIVE', 'February'),
224
				3  => I18N::translateContext('GENITIVE', 'March'),
225
				4  => I18N::translateContext('GENITIVE', 'April'),
226
				5  => I18N::translateContext('GENITIVE', 'May'),
227
				6  => I18N::translateContext('GENITIVE', 'June'),
228
				7  => I18N::translateContext('GENITIVE', 'July'),
229
				8  => I18N::translateContext('GENITIVE', 'August'),
230
				9  => I18N::translateContext('GENITIVE', 'September'),
231
				10 => I18N::translateContext('GENITIVE', 'October'),
232
				11 => I18N::translateContext('GENITIVE', 'November'),
233
				12 => I18N::translateContext('GENITIVE', 'December'),
234
			];
235
		}
236
237
		return $translated_month_names[$month_number];
238
	}
239
240
	/**
241
	 * Full month name in locative case.
242
	 *
243
	 * We put these in the base class, to save duplicating it in the Julian and Gregorian calendars.
244
	 *
245
	 * @param int  $month_number
246
	 * @param bool $leap_year    Some calendars use leap months
247
	 *
248
	 * @return string
249
	 */
250
	protected function monthNameLocativeCase($month_number, $leap_year) {
0 ignored issues
show
Unused Code introduced by
The parameter $leap_year is not used and could be removed. ( Ignorable by Annotation )

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

250
	protected function monthNameLocativeCase($month_number, /** @scrutinizer ignore-unused */ $leap_year) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
251
		static $translated_month_names;
252
253
		if ($translated_month_names === null) {
254
			$translated_month_names = [
255
				0  => '',
256
				1  => I18N::translateContext('LOCATIVE', 'January'),
257
				2  => I18N::translateContext('LOCATIVE', 'February'),
258
				3  => I18N::translateContext('LOCATIVE', 'March'),
259
				4  => I18N::translateContext('LOCATIVE', 'April'),
260
				5  => I18N::translateContext('LOCATIVE', 'May'),
261
				6  => I18N::translateContext('LOCATIVE', 'June'),
262
				7  => I18N::translateContext('LOCATIVE', 'July'),
263
				8  => I18N::translateContext('LOCATIVE', 'August'),
264
				9  => I18N::translateContext('LOCATIVE', 'September'),
265
				10 => I18N::translateContext('LOCATIVE', 'October'),
266
				11 => I18N::translateContext('LOCATIVE', 'November'),
267
				12 => I18N::translateContext('LOCATIVE', 'December'),
268
			];
269
		}
270
271
		return $translated_month_names[$month_number];
272
	}
273
274
	/**
275
	 * Full month name in instrumental case.
276
	 *
277
	 * We put these in the base class, to save duplicating it in the Julian and Gregorian calendars.
278
	 *
279
	 * @param int  $month_number
280
	 * @param bool $leap_year    Some calendars use leap months
281
	 *
282
	 * @return string
283
	 */
284
	protected function monthNameInstrumentalCase($month_number, $leap_year) {
0 ignored issues
show
Unused Code introduced by
The parameter $leap_year is not used and could be removed. ( Ignorable by Annotation )

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

284
	protected function monthNameInstrumentalCase($month_number, /** @scrutinizer ignore-unused */ $leap_year) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
285
		static $translated_month_names;
286
287
		if ($translated_month_names === null) {
288
			$translated_month_names = [
289
				0  => '',
290
				1  => I18N::translateContext('INSTRUMENTAL', 'January'),
291
				2  => I18N::translateContext('INSTRUMENTAL', 'February'),
292
				3  => I18N::translateContext('INSTRUMENTAL', 'March'),
293
				4  => I18N::translateContext('INSTRUMENTAL', 'April'),
294
				5  => I18N::translateContext('INSTRUMENTAL', 'May'),
295
				6  => I18N::translateContext('INSTRUMENTAL', 'June'),
296
				7  => I18N::translateContext('INSTRUMENTAL', 'July'),
297
				8  => I18N::translateContext('INSTRUMENTAL', 'August'),
298
				9  => I18N::translateContext('INSTRUMENTAL', 'September'),
299
				10 => I18N::translateContext('INSTRUMENTAL', 'October'),
300
				11 => I18N::translateContext('INSTRUMENTAL', 'November'),
301
				12 => I18N::translateContext('INSTRUMENTAL', 'December'),
302
			];
303
		}
304
305
		return $translated_month_names[$month_number];
306
	}
307
308
	/**
309
	 * Abbreviated month name
310
	 *
311
	 * @param int  $month_number
312
	 * @param bool $leap_year    Some calendars use leap months
313
	 *
314
	 * @return string
315
	 */
316
	protected function monthNameAbbreviated($month_number, $leap_year) {
0 ignored issues
show
Unused Code introduced by
The parameter $leap_year is not used and could be removed. ( Ignorable by Annotation )

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

316
	protected function monthNameAbbreviated($month_number, /** @scrutinizer ignore-unused */ $leap_year) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
317
		static $translated_month_names;
318
319
		if ($translated_month_names === null) {
320
			$translated_month_names = [
321
				0  => '',
322
				1  => I18N::translateContext('Abbreviation for January', 'Jan'),
323
				2  => I18N::translateContext('Abbreviation for February', 'Feb'),
324
				3  => I18N::translateContext('Abbreviation for March', 'Mar'),
325
				4  => I18N::translateContext('Abbreviation for April', 'Apr'),
326
				5  => I18N::translateContext('Abbreviation for May', 'May'),
327
				6  => I18N::translateContext('Abbreviation for June', 'Jun'),
328
				7  => I18N::translateContext('Abbreviation for July', 'Jul'),
329
				8  => I18N::translateContext('Abbreviation for August', 'Aug'),
330
				9  => I18N::translateContext('Abbreviation for September', 'Sep'),
331
				10 => I18N::translateContext('Abbreviation for October', 'Oct'),
332
				11 => I18N::translateContext('Abbreviation for November', 'Nov'),
333
				12 => I18N::translateContext('Abbreviation for December', 'Dec'),
334
			];
335
		}
336
337
		return $translated_month_names[$month_number];
338
	}
339
340
	/**
341
	 * Full day of th eweek
342
	 *
343
	 * @param int $day_number
344
	 *
345
	 * @return string
346
	 */
347
	public function dayNames($day_number) {
348
		static $translated_day_names;
349
350
		if ($translated_day_names === null) {
351
			$translated_day_names = [
352
				0 => I18N::translate('Monday'),
353
				1 => I18N::translate('Tuesday'),
354
				2 => I18N::translate('Wednesday'),
355
				3 => I18N::translate('Thursday'),
356
				4 => I18N::translate('Friday'),
357
				5 => I18N::translate('Saturday'),
358
				6 => I18N::translate('Sunday'),
359
			];
360
		}
361
362
		return $translated_day_names[$day_number];
363
	}
364
365
	/**
366
	 * Abbreviated day of the week
367
	 *
368
	 * @param int $day_number
369
	 *
370
	 * @return string
371
	 */
372
	protected function dayNamesAbbreviated($day_number) {
373
		static $translated_day_names;
374
375
		if ($translated_day_names === null) {
376
			$translated_day_names = [
377
				0 => /* I18N: abbreviation for Monday */ I18N::translate('Mon'),
378
				1 => /* I18N: abbreviation for Tuesday */ I18N::translate('Tue'),
379
				2 => /* I18N: abbreviation for Wednesday */ I18N::translate('Wed'),
380
				3 => /* I18N: abbreviation for Thursday */ I18N::translate('Thu'),
381
				4 => /* I18N: abbreviation for Friday */ I18N::translate('Fri'),
382
				5 => /* I18N: abbreviation for Saturday */ I18N::translate('Sat'),
383
				6 => /* I18N: abbreviation for Sunday */ I18N::translate('Sun'),
384
			];
385
		}
386
387
		return $translated_day_names[$day_number];
388
	}
389
390
	/**
391
	 * Most years are 1 more than the previous, but not always (e.g. 1BC->1AD)
392
	 *
393
	 * @param int $year
394
	 *
395
	 * @return int
396
	 */
397
	protected function nextYear($year) {
398
		return $year + 1;
399
	}
400
401
	/**
402
	 * Calendars that use suffixes, etc. (e.g. “B.C.”) or OS/NS notation should redefine this.
403
	 *
404
	 * @param string $year
405
	 *
406
	 * @return int
407
	 */
408
	protected function extractYear($year) {
409
		return (int) $year;
410
	}
411
412
	/**
413
	 * Compare two dates, for sorting
414
	 *
415
	 * @param CalendarDate $d1
416
	 * @param CalendarDate $d2
417
	 *
418
	 * @return int
419
	 */
420
	public static function compare(CalendarDate $d1, CalendarDate $d2) {
421
		if ($d1->maxJD < $d2->minJD) {
422
			return -1;
423
		} elseif ($d2->maxJD < $d1->minJD) {
424
			return 1;
425
		} else {
426
			return 0;
427
		}
428
	}
429
430
	/**
431
	 * How long between an event and a given julian day
432
	 * Return result as either a number of years or
433
	 * a gedcom-style age string.
434
	 *
435
	 * @todo JewishDate needs to redefine this to cope with leap months
436
	 *
437
	 * @param bool $full             true=gedcom style, false=just years
438
	 * @param int  $jd               date for calculation
439
	 * @param bool $warn_on_negative show a warning triangle for negative ages
440
	 *
441
	 * @return string
442
	 */
443
	public function getAge($full, $jd, $warn_on_negative = true) {
444
		if ($this->y == 0 || $jd == 0) {
445
			return $full ? '' : '0';
446
		}
447
		if ($this->minJD < $jd && $this->maxJD > $jd) {
448
			return $full ? '' : '0';
449
		}
450
		if ($this->minJD == $jd) {
451
			return $full ? '' : '0';
452
		}
453
		if ($warn_on_negative && $jd < $this->minJD) {
454
			return '<i class="icon-warning"></i>';
455
		}
456
		list($y, $m, $d) = $this->calendar->jdToYmd($jd);
457
		$dy              = $y - $this->y;
458
		$dm              = $m - max($this->m, 1);
459
		$dd              = $d - max($this->d, 1);
460
		if ($dd < 0) {
461
			$dm--;
462
		}
463
		if ($dm < 0) {
464
			$dm += $this->calendar->monthsInYear();
465
			$dy--;
466
		}
467
		// Not a full age? Then just the years
468
		if (!$full) {
469
			return $dy;
470
		}
471
		// Age in years?
472
		if ($dy > 1) {
473
			return $dy . 'y';
474
		}
475
		$dm += $dy * $this->calendar->monthsInYear();
476
		// Age in months?
477
		if ($dm > 1) {
478
			return $dm . 'm';
479
		}
480
481
		// Age in days?
482
		return ($jd - $this->minJD) . 'd';
483
	}
484
485
	/**
486
	 * Convert a date from one calendar to another.
487
	 *
488
	 * @param string $calendar
489
	 *
490
	 * @return CalendarDate
491
	 */
492
	public function convertToCalendar($calendar) {
493
		switch ($calendar) {
494
			case 'gregorian':
495
				return new GregorianDate($this);
496
			case 'julian':
497
				return new JulianDate($this);
498
			case 'jewish':
499
				return new JewishDate($this);
500
			case 'french':
501
				return new FrenchDate($this);
502
			case 'hijri':
503
				return new HijriDate($this);
504
			case 'jalali':
505
				return new JalaliDate($this);
506
			default:
507
				return $this;
508
		}
509
	}
510
511
	/**
512
	 * Is this date within the valid range of the calendar
513
	 *
514
	 * @return bool
515
	 */
516
	public function inValidRange() {
517
		return $this->minJD >= $this->calendar->jdStart() && $this->maxJD <= $this->calendar->jdEnd();
518
	}
519
520
	/**
521
	 * How many months in a year
522
	 *
523
	 * @return int
524
	 */
525
	public function monthsInYear() {
526
		return $this->calendar->monthsInYear();
527
	}
528
529
	/**
530
	 * How many days in the current month
531
	 *
532
	 * @return int
533
	 */
534
	public function daysInMonth() {
535
		try {
536
			return $this->calendar->daysInMonth($this->y, $this->m);
537
		} catch (\InvalidArgumentException $ex) {
538
			DebugBar::addThrowable($ex);
539
540
			// calendar.php calls this with "DD MMM" dates, for which we cannot calculate
541
			// the length of a month. Should we validate this before calling this function?
542
			return 0;
543
		}
544
	}
545
546
	/**
547
	 * How many days in the current week
548
	 *
549
	 * @return int
550
	 */
551
	public function daysInWeek() {
552
		return $this->calendar->daysInWeek();
553
	}
554
555
	/**
556
	 * Format a date, using similar codes to the PHP date() function.
557
	 *
558
	 * @param string $format    See http://php.net/date
559
	 * @param string $qualifier GEDCOM qualifier, so we can choose the right case for the month name.
560
	 *
561
	 * @return string
562
	 */
563
	public function format($format, $qualifier = '') {
564
		// Don’t show exact details for inexact dates
565
		if (!$this->d) {
566
			// The comma is for US "M D, Y" dates
567
			$format = preg_replace('/%[djlDNSwz][,]?/', '', $format);
568
		}
569
		if (!$this->m) {
570
			$format = str_replace(['%F', '%m', '%M', '%n', '%t'], '', $format);
571
		}
572
		if (!$this->y) {
573
			$format = str_replace(['%t', '%L', '%G', '%y', '%Y'], '', $format);
574
		}
575
		// If we’ve trimmed the format, also trim the punctuation
576
		if (!$this->d || !$this->m || !$this->y) {
577
			$format = trim($format, ',. ;/-');
578
		}
579
		if ($this->d && preg_match('/%[djlDNSwz]/', $format)) {
580
			// If we have a day-number *and* we are being asked to display it, then genitive
581
			$case = 'GENITIVE';
582
		} else {
583
			switch ($qualifier) {
584
				case 'TO':
585
				case 'ABT':
586
				case 'FROM':
587
					$case = 'GENITIVE';
588
					break;
589
				case 'AFT':
590
					$case = 'LOCATIVE';
591
					break;
592
				case 'BEF':
593
				case 'BET':
594
				case 'AND':
595
					$case = 'INSTRUMENTAL';
596
					break;
597
				case '':
598
				case 'INT':
599
				case 'EST':
600
				case 'CAL':
601
				default: // There shouldn't be any other options...
602
					$case = 'NOMINATIVE';
603
					break;
604
			}
605
		}
606
		// Build up the formatted date, character at a time
607
		preg_match_all('/%[^%]/', $format, $matches);
608
		foreach ($matches[0] as $match) {
609
			switch ($match) {
610
				case '%d':
611
					$format = str_replace($match, $this->formatDayZeros(), $format);
612
					break;
613
				case '%j':
614
					$format = str_replace($match, $this->formatDay(), $format);
615
					break;
616
				case '%l':
617
					$format = str_replace($match, $this->formatLongWeekday(), $format);
618
					break;
619
				case '%D':
620
					$format = str_replace($match, $this->formatShortWeekday(), $format);
621
					break;
622
				case '%N':
623
					$format = str_replace($match, $this->formatIsoWeekday(), $format);
624
					break;
625
				case '%w':
626
					$format = str_replace($match, $this->formatNumericWeekday(), $format);
627
					break;
628
				case '%z':
629
					$format = str_replace($match, $this->formatDayOfYear(), $format);
630
					break;
631
				case '%F':
632
					$format = str_replace($match, $this->formatLongMonth($case), $format);
633
					break;
634
				case '%m':
635
					$format = str_replace($match, $this->formatMonthZeros(), $format);
636
					break;
637
				case '%M':
638
					$format = str_replace($match, $this->formatShortMonth(), $format);
639
					break;
640
				case '%n':
641
					$format = str_replace($match, $this->formatMonth(), $format);
642
					break;
643
				case '%t':
644
					$format = str_replace($match, $this->daysInMonth(), $format);
645
					break;
646
				case '%L':
647
					$format = str_replace($match, (int) $this->isLeapYear(), $format);
648
					break;
649
				case '%Y':
650
					$format = str_replace($match, $this->formatLongYear(), $format);
651
					break;
652
				case '%y':
653
					$format = str_replace($match, $this->formatShortYear(), $format);
654
					break;
655
					// These 4 extensions are useful for re-formatting gedcom dates.
656
				case '%@':
657
					$format = str_replace($match, $this->calendar->gedcomCalendarEscape(), $format);
658
					break;
659
				case '%A':
660
					$format = str_replace($match, $this->formatGedcomDay(), $format);
661
					break;
662
				case '%O':
663
					$format = str_replace($match, $this->formatGedcomMonth(), $format);
664
					break;
665
				case '%E':
666
					$format = str_replace($match, $this->formatGedcomYear(), $format);
667
					break;
668
			}
669
		}
670
671
		return $format;
672
	}
673
674
	/**
675
	 * Generate the %d format for a date.
676
	 *
677
	 * @return string
678
	 */
679
	protected function formatDayZeros() {
680
		if ($this->d > 9) {
681
			return I18N::digits($this->d);
682
		} else {
683
			return I18N::digits('0' . $this->d);
0 ignored issues
show
Bug introduced by
'0' . $this->d of type string is incompatible with the type integer expected by parameter $n of Fisharebest\Webtrees\I18N::digits(). ( Ignorable by Annotation )

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

683
			return I18N::digits(/** @scrutinizer ignore-type */ '0' . $this->d);
Loading history...
684
		}
685
	}
686
687
	/**
688
	 * Generate the %j format for a date.
689
	 *
690
	 * @return string
691
	 */
692
	protected function formatDay() {
693
		return I18N::digits($this->d);
694
	}
695
696
	/**
697
	 * Generate the %l format for a date.
698
	 *
699
	 * @return string
700
	 */
701
	protected function formatLongWeekday() {
702
		return $this->dayNames($this->minJD % $this->calendar->daysInWeek());
703
	}
704
705
	/**
706
	 * Generate the %D format for a date.
707
	 *
708
	 * @return string
709
	 */
710
	protected function formatShortWeekday() {
711
		return $this->dayNamesAbbreviated($this->minJD % $this->calendar->daysInWeek());
712
	}
713
714
	/**
715
	 * Generate the %N format for a date.
716
	 *
717
	 * @return string
718
	 */
719
	protected function formatIsoWeekday() {
720
		return I18N::digits($this->minJD % 7 + 1);
721
	}
722
723
	/**
724
	 * Generate the %w format for a date.
725
	 *
726
	 * @return string
727
	 */
728
	protected function formatNumericWeekday() {
729
		return I18N::digits(($this->minJD + 1) % $this->calendar->daysInWeek());
730
	}
731
732
	/**
733
	 * Generate the %z format for a date.
734
	 *
735
	 * @return string
736
	 */
737
	protected function formatDayOfYear() {
738
		return I18N::digits($this->minJD - $this->calendar->ymdToJd($this->y, 1, 1));
739
	}
740
741
	/**
742
	 * Generate the %n format for a date.
743
	 *
744
	 * @return string
745
	 */
746
	protected function formatMonth() {
747
		return I18N::digits($this->m);
748
	}
749
750
	/**
751
	 * Generate the %m format for a date.
752
	 *
753
	 * @return string
754
	 */
755
	protected function formatMonthZeros() {
756
		if ($this->m > 9) {
757
			return I18N::digits($this->m);
758
		} else {
759
			return I18N::digits('0' . $this->m);
0 ignored issues
show
Bug introduced by
'0' . $this->m of type string is incompatible with the type integer expected by parameter $n of Fisharebest\Webtrees\I18N::digits(). ( Ignorable by Annotation )

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

759
			return I18N::digits(/** @scrutinizer ignore-type */ '0' . $this->m);
Loading history...
760
		}
761
	}
762
763
	/**
764
	 * Generate the %F format for a date.
765
	 *
766
	 * @param string $case Which grammatical case shall we use
767
	 *
768
	 * @return string
769
	 */
770
	protected function formatLongMonth($case = 'NOMINATIVE') {
771
		switch ($case) {
772
			case 'GENITIVE':
773
				return $this->monthNameGenitiveCase($this->m, $this->isLeapYear());
774
			case 'NOMINATIVE':
775
				return $this->monthNameNominativeCase($this->m, $this->isLeapYear());
776
			case 'LOCATIVE':
777
				return $this->monthNameLocativeCase($this->m, $this->isLeapYear());
778
			case 'INSTRUMENTAL':
779
				return $this->monthNameInstrumentalCase($this->m, $this->isLeapYear());
780
			default:
781
				throw new \InvalidArgumentException($case);
782
		}
783
	}
784
785
	/**
786
	 * Generate the %M format for a date.
787
	 *
788
	 * @return string
789
	 */
790
	protected function formatShortMonth() {
791
		return $this->monthNameAbbreviated($this->m, $this->isLeapYear());
792
	}
793
794
	/**
795
	 * Generate the %y format for a date.
796
	 * NOTE Short year is NOT a 2-digit year. It is for calendars such as hebrew
797
	 * which have a 3-digit form of 4-digit years.
798
	 *
799
	 * @return string
800
	 */
801
	protected function formatShortYear() {
802
		return $this->formatLongYear();
803
	}
804
805
	/**
806
	 * Generate the %A format for a date.
807
	 *
808
	 * @return string
809
	 */
810
	protected function formatGedcomDay() {
811
		if ($this->d == 0) {
812
			return '';
813
		} else {
814
			return sprintf('%02d', $this->d);
815
		}
816
	}
817
818
	/**
819
	 * Generate the %O format for a date.
820
	 *
821
	 * @return string
822
	 */
823
	protected function formatGedcomMonth() {
824
		// Our simple lookup table doesn't work correctly for Adar on leap years
825
		if ($this->m == 7 && $this->calendar instanceof JewishCalendar && !$this->calendar->isLeapYear($this->y)) {
826
			return 'ADR';
827
		} else {
828
			return array_search($this->m, static::$MONTH_ABBREV);
829
		}
830
	}
831
832
	/**
833
	 * Generate the %E format for a date.
834
	 *
835
	 * @return string
836
	 */
837
	protected function formatGedcomYear() {
838
		if ($this->y == 0) {
839
			return '';
840
		} else {
841
			return sprintf('%04d', $this->y);
842
		}
843
	}
844
845
	/**
846
	 * Generate the %Y format for a date.
847
	 *
848
	 * @return string
849
	 */
850
	protected function formatLongYear() {
851
		return I18N::digits($this->y);
852
	}
853
854
	/**
855
	 * Which months follows this one? Calendars with leap-months should provide their own implementation.
856
	 *
857
	 * @return int[]
858
	 */
859
	protected function nextMonth() {
860
		return [$this->m === $this->calendar->monthsInYear() ? $this->nextYear($this->y) : $this->y, ($this->m % $this->calendar->monthsInYear()) + 1];
861
	}
862
863
	/**
864
	 * Convert a decimal number to roman numerals
865
	 *
866
	 * @param int $number
867
	 *
868
	 * @return string
869
	 */
870
	protected function numberToRomanNumerals($number) {
871
		if ($number < 1) {
872
			// Cannot convert zero/negative numbers
873
			return (string) $number;
874
		}
875
		$roman = '';
876
		foreach (self::$roman_numerals as $key => $value) {
877
			while ($number >= $key) {
878
				$roman .= $value;
879
				$number -= $key;
880
			}
881
		}
882
883
		return $roman;
884
	}
885
886
	/**
887
	 * Convert a roman numeral to decimal
888
	 *
889
	 * @param string $roman
890
	 *
891
	 * @return int
892
	 */
893
	protected function romanNumeralsToNumber($roman) {
894
		$num = 0;
895
		foreach (self::$roman_numerals as $key => $value) {
896
			if (strpos($roman, $value) === 0) {
897
				$num += $key;
898
				$roman = substr($roman, strlen($value));
899
			}
900
		}
901
902
		return $num;
903
	}
904
905
	/**
906
	 * Get today’s date in the current calendar.
907
	 *
908
	 * @return int[]
909
	 */
910
	public function todayYmd() {
911
		return $this->calendar->jdToYmd(unixtojd());
912
	}
913
914
	/**
915
	 * Convert to today’s date.
916
	 *
917
	 * @return CalendarDate
918
	 */
919
	public function today() {
920
		$tmp    = clone $this;
921
		$ymd    = $tmp->todayYmd();
922
		$tmp->y = $ymd[0];
923
		$tmp->m = $ymd[1];
924
		$tmp->d = $ymd[2];
925
		$tmp->setJdFromYmd();
926
927
		return $tmp;
928
	}
929
930
	/**
931
	 * Create a URL that links this date to the WT calendar
932
	 *
933
	 * @param string $date_format
934
	 *
935
	 * @return string
936
	 */
937
	public function calendarUrl($date_format) {
938
		if (strpbrk($date_format, 'dDj') && $this->d) {
939
			// If the format includes a day, and the date also includes a day, then use the day view
940
			$view = 'day';
941
		} elseif (strpbrk($date_format, 'FMmn') && $this->m) {
942
			// If the format includes a month, and the date also includes a month, then use the month view
943
			$view = 'month';
944
		} else {
945
			// Use the year view
946
			$view = 'year';
947
		}
948
949
		return
950
			'calendar.php?cal=' . rawurlencode($this->calendar->gedcomCalendarEscape()) .
951
			'&amp;year=' . $this->formatGedcomYear() .
952
			'&amp;month=' . $this->formatGedcomMonth() .
953
			'&amp;day=' . $this->formatGedcomDay() .
954
			'&amp;view=' . $view;
955
	}
956
}
957