Completed
Push — master ( 14d2bd...06e609 )
by mw
81:37 queued 59:24
created

includes/dataitems/SMW_DI_Time.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
use SMW\DataItemException;
4
use SMW\JulianDay;
5
6
/**
7
 * This class implements time data items.
8
 * Such data items represent a unique point in time, given in either Julian or
9
 * Gregorian notation (possibly proleptic), and a precision setting that states
10
 * which of the components year, month, day, time were specified expicitly.
11
 * Even when not specified, the data item always assumes default values for the
12
 * missing parts, so the item really captures one point in time, no intervals.
13
 * Times are always assumed to be in UTC.
14
 *
15
 * "Y0K issue": Neither the Gregorian nor the Julian calendar assume a year 0,
16
 * i.e. the year 1 BC(E) was followed by 1 AD/CE. See
17
 * http://en.wikipedia.org/wiki/Year_zero
18
 * This implementation adheres to this convention and disallows year 0. The
19
 * stored year numbers use positive numbers for CE and negative numbers for
20
 * BCE. This is not just relevant for the question of how many years have
21
 * (exactly) passed since a given date, but also for the location of leap
22
 * years.
23
 *
24
 * @since 1.6
25
 *
26
 * @author Markus Krötzsch
27
 * @ingroup SMWDataItems
28
 */
29
class SMWDITime extends SMWDataItem {
30
31
	const CM_GREGORIAN = 1;
32
	const CM_JULIAN = 2;
33
34
	const PREC_Y    = SMW_PREC_Y;
35
	const PREC_YM   = SMW_PREC_YM;
36
	const PREC_YMD  = SMW_PREC_YMD;
37
	const PREC_YMDT = SMW_PREC_YMDT;
38
39
	/**
40
	 * The year before which we do not accept anything but year numbers and
41
	 * largely discourage calendar models.
42
	 */
43
	const PREHISTORY = -10000;
44
45
	/**
46
	 * Maximal number of days in a given month.
47
	 * @var array
48
	 */
49
	protected static $m_daysofmonths = array( 1 => 31, 2 => 29, 3 => 31, 4 => 30, 5 => 31, 6 => 30, 7 => 31, 8 => 31, 9 => 30, 10 => 31, 11 => 30, 12 => 31 );
50
51
	/**
52
	 * Precision SMWDITime::PREC_Y, SMWDITime::PREC_YM,
53
	 * SMWDITime::PREC_YMD, or SMWDITime::PREC_YMDT.
54
	 * @var integer
55
	 */
56
	protected $m_precision;
57
	/**
58
	 * Calendar model: SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN.
59
	 * @var integer
60
	 */
61
	protected $m_model;
62
	/**
63
	 * Number of year, possibly negative.
64
	 * @var integer
65
	 */
66
	protected $m_year;
67
	/**
68
	 * Number of month.
69
	 * @var integer
70
	 */
71
	protected $m_month;
72
	/**
73
	 * Number of day.
74
	 * @var integer
75
	 */
76
	protected $m_day;
77
	/**
78
	 * Hours of the day.
79
	 * @var integer
80
	 */
81
	protected $m_hours;
82
	/**
83
	 * Minutes of the hour.
84
	 * @var integer
85
	 */
86
	protected $m_minutes;
87
	/**
88
	 * Seconds of the minute.
89
	 * @var integer
90
	 */
91
	protected $m_seconds;
92
93
	/**
94
	 * @var integer
95
	 */
96
	protected $timezone;
97
98
	/**
99
	 * @var integer|null
100
	 */
101
	protected $era = null;
102
103
	/**
104
	 * @var integer
105
	 */
106
	protected $julianDay = null;
107
108
	/**
109
	 * Create a time data item. All time components other than the year can
110
	 * be false to indicate that they are not specified. This will affect
111
	 * the internal precision setting. The missing values are initialised
112
	 * to minimal values (0 or 1) for internal calculations.
113
	 *
114
	 * @param $calendarmodel integer one of SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN
115
	 * @param $year integer number of the year (possibly negative)
116
	 * @param $month mixed integer number or false
117
	 * @param $day mixed integer number or false
118
	 * @param $hour mixed integer number or false
119
	 * @param $minute mixed integer number or false
120
	 * @param $second mixed integer number or false
121
	 * @param integer|false $timezone
122
	 *
123
	 * @todo Implement more validation here.
124
	 */
125 181
	public function __construct( $calendarmodel, $year, $month = false, $day = false,
126
	                             $hour = false, $minute = false, $second = false, $timezone = false ) {
127
128 181
		if ( ( $calendarmodel != self::CM_GREGORIAN ) && ( $calendarmodel != self::CM_JULIAN ) ) {
129
			throw new DataItemException( "Unsupported calendar model constant \"$calendarmodel\"." );
130
		}
131
132 181
		if ( $year == 0 ) {
133
			throw new DataItemException( "There is no year 0 in Gregorian and Julian calendars." );
134
		}
135
136 181
		$this->m_model   = $calendarmodel;
137 181
		$this->m_year    = intval( $year );
138 181
		$this->m_month   = $month != false ? intval( $month ) : 1;
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison !== instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
139 181
		$this->m_day     = $day != false ? intval( $day ) : 1;
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison !== instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
140 181
		$this->m_hours   = $hour !== false ? intval( $hour ) : 0;
141 181
		$this->m_minutes = $minute !== false ? intval( $minute ) : 0;
142 181
		$this->m_seconds = $second !== false ? floatval( $second ) : 0;
143
144 181
		$this->timezone = $timezone !== false ? intval( $timezone ) : 0;
145 181
		$year = strval( $year );
146 181
		$this->era      = $year{0} === '+' ? 1 : ( $year{0} === '-' ? -1 : 0 );
147
148
149 181
		if ( ( $this->m_hours < 0 ) || ( $this->m_hours > 23 ) ||
150 181
		     ( $this->m_minutes < 0 ) || ( $this->m_minutes > 59 ) ||
151 181
		     ( $this->m_seconds < 0 ) || ( $this->m_seconds > 59 ) ||
152 181
		     ( $this->m_month < 1 ) || ( $this->m_month > 12 ) ) {
153
			throw new DataItemException( "Part of the date is out of bounds." );
154
		}
155
156 181
		if ( $this->m_day > self::getDayNumberForMonth( $this->m_month, $this->m_year, $this->m_model ) ) {
157
			throw new DataItemException( "Month {$this->m_month} in year {$this->m_year} did not have {$this->m_day} days in this calendar model." );
158
		}
159
160 181
		if ( $month === false ) {
161 18
			$this->m_precision = self::PREC_Y;
162 175
		} elseif ( $day === false ) {
163 3
			$this->m_precision = self::PREC_YM;
164 175
		} elseif ( $hour === false ) {
165 25
			$this->m_precision = self::PREC_YMD;
166
		} else {
167 173
			$this->m_precision = self::PREC_YMDT;
168
		}
169 181
	}
170
171
	/**
172
	 * @since 1.6
173
	 *
174
	 * @return integer
175
	 */
176 177
	public function getDIType() {
177 177
		return SMWDataItem::TYPE_TIME;
178
	}
179
180
	/**
181
	 * @since 1.6
182
	 *
183
	 * @return integer
184
	 */
185 176
	public function getCalendarModel() {
186 176
		return $this->m_model;
187
	}
188
189
	/**
190
	 * @since 1.6
191
	 *
192
	 * @return integer
193
	 */
194 62
	public function getPrecision() {
195 62
		return $this->m_precision;
196
	}
197
198
	/**
199
	 * Indicates whether a user explicitly used an era marker even for a positive
200
	 * year.
201
	 *
202
	 * - [-1] indicates BC(E)
203
	 * - [0]/null indicates no era marker
204
	 * - [1] indicates AD/CE was used
205
	 *
206
	 * @since 2.4
207
	 *
208
	 * @return integer
209
	 */
210 14
	public function getEra() {
211 14
		return $this->era;
212
	}
213
214
	/**
215
	 * @since 1.6
216
	 *
217
	 * @return integer
218
	 */
219 176
	public function getYear() {
220 176
		return $this->m_year;
221
	}
222
223
	/**
224
	 * @since 1.6
225
	 *
226
	 * @return integer
227
	 */
228 176
	public function getMonth() {
229 176
		return $this->m_month;
230
	}
231
232
	/**
233
	 * @since 1.6
234
	 *
235
	 * @return integer
236
	 */
237 176
	public function getDay() {
238 176
		return $this->m_day;
239
	}
240
241
	/**
242
	 * @since 1.6
243
	 *
244
	 * @return integer
245
	 */
246 176
	public function getHour() {
247 176
		return $this->m_hours;
248
	}
249
250
	/**
251
	 * @since 1.6
252
	 *
253
	 * @return integer
254
	 */
255 176
	public function getMinute() {
256 176
		return $this->m_minutes;
257
	}
258
259
	/**
260
	 * @since 1.6
261
	 *
262
	 * @return integer
263
	 */
264 177
	public function getSecond() {
265 177
		return $this->m_seconds;
266
	}
267
268
	/**
269
	 * @since 2.4
270
	 *
271
	 * @return string
272
	 */
273 5
	public function getCalendarModelLiteral() {
274
275
		$literal = array(
276 5
			self::CM_GREGORIAN => '',
277
			self::CM_JULIAN    => 'JL'
278
		);
279
280 5
		return $literal[$this->m_model];
281
	}
282
283
	/**
284
	 * @since 2.4
285
	 *
286
	 * @param DateTime $dateTime
287
	 *
288
	 * @return SMWDITime|false
289
	 */
290 2
	public static function newFromDateTime( DateTime $dateTime ) {
291
292 2
		$calendarModel = self::CM_JULIAN;
293
294 2
		$year = $dateTime->format( 'Y' );
295 2
		$month = $dateTime->format( 'm' );
296 2
		$day = $dateTime->format( 'd' );
297
298 2
		if ( ( $year > 1582 ) ||
299 1
			( ( $year == 1582 ) && ( $month > 10 ) ) ||
300 2
			( ( $year == 1582 ) && ( $month == 10 ) && ( $day > 4 ) ) ) {
301 2
			$calendarModel = self::CM_GREGORIAN;
302
		}
303
304 2
		return self::doUnserialize( $calendarModel . '/' . $dateTime->format( 'Y/m/d/H/i/s.u' ) );
305
	}
306
307
	/**
308
	 * @since 2.4
309
	 *
310
	 * @return DateTime
311
	 */
312 10
	public function asDateTime() {
313
314 10
		$year = str_pad( $this->m_year , 4, '0', STR_PAD_LEFT );
315
316
		// Avoid "Failed to parse time string (-900-02-02 00:00:00) at
317
		// position 7 (-): Double timezone specification"
318 10
		if ( $this->m_year < 0 ) {
319 2
			$year = '-' . str_pad( $this->m_year * -1 , 4, '0', STR_PAD_LEFT );
320
		}
321
322
		// Avoid "Failed to parse time string (1300-11-02 12:03:25.888499949) at
323
		// at position 11 (1): The timezone could not ..."
324 10
		$seconds = number_format( str_pad( $this->m_seconds, 2, '0', STR_PAD_LEFT ), 7, '.', '' );
325
326 10
		$time = $year . '-' .
327 10
			str_pad( $this->m_month, 2, '0', STR_PAD_LEFT )     . '-' .
328 10
			str_pad( $this->m_day, 2, '0', STR_PAD_LEFT )       . ' ' .
329 10
			str_pad( $this->m_hours, 2, '0', STR_PAD_LEFT )     . ':' .
330 10
			str_pad( $this->m_minutes, 2, '0', STR_PAD_LEFT )   . ':' .
331 10
			$seconds;
332
333 10
		return new DateTime( $time );
334
	}
335
336
	/**
337
	 * Creates and returns a new instance of SMWDITime from a MW timestamp.
338
	 *
339
	 * @since 1.8
340
	 *
341
	 * @param string $timestamp must be in format
342
	 *
343
	 * @return SMWDITime|false
344
	 */
345 167
	public static function newFromTimestamp( $timestamp ) {
346 167
		$timestamp = wfTimestamp( TS_MW, (string)$timestamp );
347
348 167
		if ( $timestamp === false ) {
349
			return false;
350
		}
351
352 167
		return new self(
353 167
			self::CM_GREGORIAN,
354 167
			substr( $timestamp, 0, 4 ),
355 167
			substr( $timestamp, 4, 2 ),
356 167
			substr( $timestamp, 6, 2 ),
357 167
			substr( $timestamp, 8, 2 ),
358 167
			substr( $timestamp, 10, 2 ),
359 167
			substr( $timestamp, 12, 2 )
360
		);
361
	}
362
363
	/**
364
	 * Returns a MW timestamp representation of the value.
365
	 *
366
	 * @since 1.6.2
367
	 *
368
	 * @param $outputtype
369
	 *
370
	 * @return mixed
371
	 */
372 165
	public function getMwTimestamp( $outputtype = TS_UNIX ) {
373 165
		return wfTimestamp(
374
			$outputtype,
375 165
			implode( '', array(
376 165
				str_pad( $this->m_year, 4, '0', STR_PAD_LEFT ),
377 165
				str_pad( $this->m_month, 2, '0', STR_PAD_LEFT ),
378 165
				str_pad( $this->m_day, 2, '0', STR_PAD_LEFT ),
379 165
				str_pad( $this->m_hours, 2, '0', STR_PAD_LEFT ),
380 165
				str_pad( $this->m_minutes, 2, '0', STR_PAD_LEFT ),
381 165
				str_pad( $this->m_seconds, 2, '0', STR_PAD_LEFT ),
382
			) )
383
		);
384
	}
385
386
	/**
387
	 * Get the data in the specified calendar model. This might require
388
	 * conversion.
389
	 * @note Conversion can be unreliable for very large absolute year
390
	 * numbers when the internal calculations hit floating point accuracy.
391
	 * Callers might want to avoid this (calendar models make little sense
392
	 * in such cases anyway).
393
	 * @param $calendarmodel integer one of SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN
394
	 * @return SMWDITime
395
	 */
396 60
	public function getForCalendarModel( $calendarmodel ) {
397 60
		if ( $calendarmodel == $this->m_model ) {
398 60
			return $this;
399
		} else {
400 4
			return self::newFromJD( $this->getJD(), $calendarmodel, $this->m_precision );
401
		}
402
	}
403
404
	/**
405
	 * Return a number that helps comparing time data items. For
406
	 * dates in the Julian Day era (roughly from 4713 BCE onwards), we use
407
	 * the Julian Day number. For earlier dates, the (negative) year number
408
	 * with a fraction for the date is used (times are ignored). This
409
	 * avoids calculation errors that would occur for very ancient dates
410
	 * if the JD number was used there.
411
	 * @return double sortkey
412
	 */
413 172
	public function getSortKey() {
414 172
		$jd = ( $this->m_year >= -4713 ) ? $jd = $this->getJD() : -1;
415 172
		if ( $jd > 0 ) {
416 172
			return $jd;
417
		} else {
418 3
			return $this->m_year - 1 + ( $this->m_month - 1 ) / 12 + ( $this->m_day - 1 ) / 12 / 31;
419
		}
420
	}
421
422
	/**
423
	 * @since 1.6
424
	 *
425
	 * @return double
426
	 */
427 175
	public function getJD() {
428
429 175
		if ( $this->julianDay === null ) {
430 175
			$this->julianDay = JulianDay::get( $this );
431
		}
432
433 175
		return $this->julianDay;
434
	}
435
436
	/**
437
	 * @since 1.6
438
	 *
439
	 * @return string
440
	 */
441 175
	public function getSerialization() {
442 175
		$result = strval( $this->m_model ) . '/' . ( $this->era > 0 ? '+' : '' ) . strval( $this->m_year );
443
444 175
		if ( $this->m_precision >= self::PREC_YM ) {
445 170
			$result .= '/' . strval( $this->m_month );
446
		}
447
448 175
		if ( $this->m_precision >= self::PREC_YMD ) {
449 170
			$result .= '/' . strval( $this->m_day );
450
		}
451
452 175
		if ( $this->m_precision >= self::PREC_YMDT ) {
453 169
			$result .= '/' . strval( $this->m_hours ) . '/' . strval( $this->m_minutes ) . '/' . strval( $this->m_seconds ) . '/' . strval( $this->timezone );
454
		}
455
456 175
		return $result;
457
	}
458
459
	/**
460
	 * Create a data item from the provided serialization string.
461
	 *
462
	 * @return SMWDITime
463
	 */
464 45
	public static function doUnserialize( $serialization ) {
465 45
		$parts = explode( '/', $serialization, 8 );
466 45
		$values = array();
467
468 45
		if ( count( $parts ) <= 1 ) {
469 1
			throw new DataItemException( "Unserialization failed: the string \"$serialization\" is no valid URI." );
470
		}
471
472 44
		for ( $i = 0; $i < 8; $i += 1 ) {
473
474 44
			$values[$i] = false;
475
476 44
			if ( $i < count( $parts ) ) {
477
478 44
				if ( $parts[$i] !== '' && !is_numeric( $parts[$i] ) ) {
479 1
					throw new DataItemException( "Unserialization failed: the string \"$serialization\" is no valid datetime specification." );
480
				}
481
482
				// 6 == seconds, we want to keep microseconds
483 44
				$values[$i] = $i == 6 ? floatval( $parts[$i] ) : intval( $parts[$i] );
484
485
				// Find out whether the input contained an explicit AD/CE era marker
486 44
				if ( $i == 1 ) {
487 44
					$values[$i] = ( $parts[1]{0} === '+' ? '+' : '' ) . $values[$i];
488
				}
489
			}
490
		}
491
492 43
		return new self( $values[0], $values[1], $values[2], $values[3], $values[4], $values[5], $values[6], $values[7] );
493
	}
494
495
	/**
496
	 * Create a new time data item from the specified Julian Day number,
497
	 * calendar model, presicion, and type ID.
498
	 *
499
	 * @param $jdvalue double Julian Day number
500
	 * @param $calendarmodel integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN
501
	 * @param $precision integer one of SMWDITime::PREC_Y, SMWDITime::PREC_YM, SMWDITime::PREC_YMD, SMWDITime::PREC_YMDT
502
	 *
503
	 * @return SMWDITime object
504
	 */
505 7
	public static function newFromJD( $jdvalue, $calendarmodel, $precision ) {
506 7
		return JulianDay::newDiFromJD( $jdvalue, $calendarmodel, $precision );
507
	}
508
509
	/**
510
	 * Find out whether the given year number is a leap year.
511
	 * This calculation assumes that neither calendar has a year 0.
512
	 * @param $year integer year number
513
	 * @param $calendarmodel integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN
514
	 * @return boolean
515
	 */
516 7
	static public function isLeapYear( $year, $calendarmodel ) {
0 ignored issues
show
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
517 7
		$astroyear = ( $year < 1 ) ? ( $year + 1 ) : $year;
518 7
		if ( $calendarmodel == self::CM_JULIAN ) {
519 4
			return ( $astroyear % 4 ) == 0;
520
		} else {
521 6
			return ( ( $astroyear % 400 ) == 0 ) ||
522 6
			       ( ( ( $astroyear % 4 ) == 0 ) && ( ( $astroyear % 100 ) != 0 ) );
523
		}
524
	}
525
526
	/**
527
	 * Find out how many days the given month had in the given year
528
	 * based on the specified calendar model.
529
	 * This calculation assumes that neither calendar has a year 0.
530
	 * @param $month integer month number
531
	 * @param $year integer year number
532
	 * @param $calendarmodel integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN
533
	 * @return boolean
534
	 */
535 181
	static public function getDayNumberForMonth( $month, $year, $calendarmodel ) {
0 ignored issues
show
As per PSR2, the static declaration should come after the visibility declaration.
Loading history...
536 181
		if ( $month !== 2 ) {
537 180
			return self::$m_daysofmonths[$month];
538 7
		} elseif ( self::isLeapYear( $year, $calendarmodel ) ) {
539 3
			return 29;
540
		} else {
541 5
			return 28;
542
		}
543
	}
544
545
	public function equals( SMWDataItem $di ) {
546
		if ( $di->getDIType() !== SMWDataItem::TYPE_TIME ) {
547
			return false;
548
		}
549
550
		return $di->getSortKey() === $this->getSortKey();
551
	}
552
}
553