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