Completed
Push — master ( d2d28e...1c2760 )
by mw
35:37
created

includes/datavalues/SMW_DV_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\DataValues\ValueFormatters\DataValueFormatter;
4
use SMW\Libs\Time\Timezone;
5
6
/**
7
 * @ingroup SMWDataValues
8
 */
9
10
/**
11
 * This datavalue captures values of dates and times, in many formats,
12
 * throughout history and pre-history. The implementation can handle dates
13
 * across history with full precision for storing, and substantial precision
14
 * for sorting and querying. The range of supported past dates should encompass
15
 * the Beginning of Time according to most of today's theories. The range of
16
 * supported future dates is limited more strictly, but it does also allow
17
 * year numbers in the order of 10^9.
18
 *
19
 * The implementation notices and stores whether parts of a date/time have been
20
 * omitted (as in "2008" or "May 2007"). For all exporting and sorting
21
 * purposes, incomplete dates are completed with defaults (usually using the
22
 * earliest possible time, i.e. interpreting "2008" as "Jan 1 2008 00:00:00").
23
 * The information on what was unspecified is kept internally for improving
24
 * behavior e.g. for outputs (defaults are not printed when querying for a
25
 * value). This largely uses the precision handling of SMWDITime.
26
 *
27
 *
28
 * Date formats
29
 *
30
 * Dates can be given in many formats, using numbers, month names, and
31
 * abbreviated month names. The preferred interpretation of ambiguous dates
32
 * ("1 2 2008" or even "1 2 3 BC") is controlled by the language file, as is
33
 * the local naming of months. English month names are always supported.
34
 *
35
 * Dates can be given in Gregorian or Julian calendar, set by the token "Jl"
36
 * or "Gr" in the input. If neither is set, a default is chosen: inputs after
37
 * October 15, 1582 (the time when the Gregorian calendar was first inaugurated
38
 * in some parts of the world) are considered Gr, earlier inputs are considered
39
 * Jl. In addition to Jl and Gr, we support "OS" (Old Style English dates that
40
 * refer to the use of Julian calendar with a displaced change of year on March
41
 * 24), JD (direct numerical input in Julian Day notation), and MJD (direct
42
 * numerical input in Modified Julian Day notation as used in aviation and
43
 * space flight).
44
 *
45
 * The class does not support the input of negative year numbers but uses the
46
 * markers "BC"/"BCE" and "AD"/"CE" instead. There is no year 0 in Gregorian or
47
 * Julian calendars, but the class graciously considers this input to mean year
48
 * 1 BC(E).
49
 *
50
 * For prehisoric dates before 9999 BC(E) only year numbers are allowed
51
 * (nothing else makes much sense). At this time, the years of Julian and
52
 * Gregorian calendar still overlap significantly, so the transition to a
53
 * purely solar annotation of prehistoric years is smooth. Technically, the
54
 * class will consider prehistoric dates as Gregorian but very ancient times
55
 * may be interpreted as desired (probably with reference to a physical notion
56
 * of time that is not dependent on revolutions of earth around the sun).
57
 *
58
 *
59
 * Time formats
60
 *
61
 * Times can be in formats like "23:12:45" and "12:30" possibly with additional
62
 * modifiers "am" or "pm". Timezones are supported: the class knows many
63
 * international timezone monikers (e.g. CET or GMT) and also allows time
64
 * offsets directly after a time (e.g. "10:30-3:30" or "14:45:23+2"). Such
65
 * offsets always refer to UTC. Timezones are only used on input and are not
66
 * stored as part of the value.
67
 *
68
 * Time offsets take leap years into account, e.g. the date
69
 * "Feb 28 2004 23:00+2:00" is equivalent to "29 February 2004 01:00:00", while
70
 * "Feb 28 1900 23:00+2:00" is equivalent to "1 March 1900 01:00:00".
71
 *
72
 * Military time format is supported. This consists of 4 or 6 numeric digits
73
 * followed by a one-letter timezone code (e.g. 1240Z is equivalent to 12:40
74
 * UTC).
75
 *
76
 *
77
 * I18N
78
 *
79
 * Currently, neither keywords like "BCE", "Jl", or "pm", nor timezone monikers
80
 * are internationalized. Timezone monikers may not require this, other than
81
 * possibly for Cyrillic (added when needed). Month names are fully
82
 * internationalized, but English names and abbreviations will also work in all
83
 * languages. The class also supports ordinal day-of-month annotations like
84
 * "st" and "rd", again only for English.
85
 *
86
 * I18N includes the preferred order of dates, e.g. to interpret "5 6 2010".
87
 *
88
 * @todo Theparsing process can encounter many kinds of well-defined problems
89
 * but uses only one error message. More detailed reporting should be done.
90
 * @todo Try to reuse more of MediaWiki's records, e.g. to obtain month names
91
 * or to format dates. The problem is that MW is based on SIO timestamps that
92
 * don't extend to very ancient or future dates, and that MW uses PHP functions
93
 * that are bound to UNIX time.
94
 *
95
 * @author Markus Krötzsch
96
 * @author Fabian Howahl
97
 * @author Terry A. Hurlbut
98
 * @ingroup SMWDataValues
99
 */
100
class SMWTimeValue extends SMWDataValue {
101
	protected $m_dataitem_greg = null;
102
	protected $m_dataitem_jul = null;
103
104
	protected $m_wikivalue; // a suitable wiki input value
105
106
	// The following are constant (array-valued constants are not supported, hence the declaration as private static variable):
107
	protected static $m_months = array( 'January', 'February', 'March', 'April' , 'May' , 'June' , 'July' , 'August' , 'September' , 'October' , 'November' , 'December' );
108
	protected static $m_monthsshort = array( 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' );
109
	protected static $m_formats = array( SMW_Y => array( 'y' ), SMW_YM => array( 'y', 'm' ), SMW_MY => array( 'm', 'y' ), SMW_YDM => array( 'y', 'd', 'm' ), SMW_YMD => array( 'y', 'm', 'd' ), SMW_DMY => array( 'd', 'm', 'y' ), SMW_MDY => array( 'm', 'd', 'y' ) );
110
111
	/// Moment of switchover to Gregorian calendar.
112
	const J1582 = 2299160.5;
113
	/// Offset of Julian Days for Modified JD inputs.
114
	const MJD_EPOCH = 2400000.5;
115
	/// The year before which we do not accept anything but year numbers and largely discourage calendar models.
116
	const PREHISTORY = -10000;
117
118 42
	protected function parseUserValue( $value ) {
119
120 42
		$value = $this->convertDoubleWidth( $value );
121 42
		$this->m_wikivalue = $value;
122
123 42
		if ( $this->m_caption === false ) { // Store the caption now.
124 42
			$this->m_caption = $value;
125
		}
126 42
		$this->m_dataitem = null;
127
128 42
		$datecomponents = array();
129 42
		$calendarmodel = $era = $hours = $minutes = $seconds = $microseconds = $timeoffset = $timezone = false;
130 42
		if ( $this->isInterpretableAsYearOnly( $value ) ) {
131 15
			$this->m_dataitem = new SMWDITime( $this->getCalendarModel( null, $value, null, null ), $value );
132 35
		} elseif ( $this->isInterpretableAsTimestamp( $value ) ) {
133 3
			$this->m_dataitem = SMWDITime::newFromTimestamp( $value );
134 35
		} elseif ( $this->parseDateString( $value, $datecomponents, $calendarmodel, $era, $hours, $minutes, $seconds, $microseconds, $timeoffset, $timezone ) ) {
135 35
			if ( ( $calendarmodel === false ) && ( $era === false ) && ( count( $datecomponents ) == 1 ) && ( intval( end( $datecomponents ) ) >= 100000 ) ) {
136
				$calendarmodel = 'JD'; // default to JD input if a single number was given as the date
137
			}
138
139 35
			if ( ( $calendarmodel == 'JD' ) || ( $calendarmodel == 'MJD' ) ) {
140 1
				if ( ( $era === false ) && ( $hours === false ) && ( $timeoffset == 0 ) ) {
141
					try {
142 1
						$jd = floatval( isset( $datecomponents[1] ) ? $datecomponents[0] . '.' . $datecomponents[1] : $datecomponents[0] );
143 1
						if ( $calendarmodel == 'MJD' ) {
144 1
							$jd += self::MJD_EPOCH;
145
						}
146 1
						$this->m_dataitem = SMWDITime::newFromJD( $jd, SMWDITime::CM_GREGORIAN, SMWDITime::PREC_YMDT, $timezone );
147
					} catch ( SMWDataItemException $e ) {
148 1
						$this->addErrorMsg( array( 'smw-datavalue-time-invalid-jd', $this->m_wikivalue, $e->getMessage() ) );
149
					}
150
				} else {
151 1
					$this->addErrorMsg( array( 'smw-datavalue-time-invalid-jd', $this->m_wikivalue, "NO_EXCEPTION" ) );
152
				}
153
			} else {
154 35
				$this->setDateFromParsedValues( $datecomponents, $calendarmodel, $era, $hours, $minutes, $seconds, $microseconds, $timeoffset, $timezone );
155
			}
156
		}
157
158 42
		if ( is_null( $this->m_dataitem ) ) { // make sure that m_dataitem is set in any case
159 4
			$this->m_dataitem = new SMWDITime( SMWDITime::CM_GREGORIAN, 32202 );
160
		}
161 42
	}
162
163
	/**
164
	 * Parse the given string to check if it a date/time value.
165
	 * The function sets the provided call-by-ref values to the respective
166
	 * values. If errors are encountered, they are added to the objects
167
	 * error list and false is returned. Otherwise, true is returned.
168
	 * @param $string string input time representation, e.g. "12 May 2007 13:45:23-3:30"
169
	 * @param $datecomponents array of strings that might belong to the specification of a date
170
	 * @param $calendarmodesl string if model was set in input, otherwise false
171
	 * @param $era string '+' or '-' if provided, otherwise false
172
	 * @param $hours integer set to a value between 0 and 24
173
	 * @param $minutes integer set to a value between 0 and 59
174
	 * @param $seconds integer set to a value between 0 and 59, or false if not given
175
	 * @param $timeoffset double set to a value for time offset (e.g. 3.5), or false if not given
176
	 * @return boolean stating if the parsing succeeded
177
	 * @todo This method in principle allows date parsing to be internationalized further. Should be done.
178
	 */
179 35
	protected function parseDateString( $string, &$datecomponents, &$calendarmodel, &$era, &$hours, &$minutes, &$seconds, &$microseconds, &$timeoffset, &$timezone ) {
180
181 35
		$calendarmodel = $timezoneoffset = $era = $ampm = false;
182 35
		$hours = $minutes = $seconds = $microseconds = $timeoffset = $timezone = false;
183
184
		// Fetch possible "America/Argentina/Mendoza"
185 35
		$timzoneIdentifier = substr( $string, strrpos( $string, ' ' ) + 1 );
186
187 35
		if ( Timezone::isValid( $timzoneIdentifier ) ) {
188 3
			$string = str_replace( $timzoneIdentifier, '', $string );
189 3
			$timezoneoffset = Timezone::getOffsetByAbbreviation( $timzoneIdentifier ) / 3600;
190 3
			$timezone = Timezone::getIdByAbbreviation( $timzoneIdentifier );
191
		}
192
193
		// crude preprocessing for supporting different date separation characters;
194
		// * this does not allow localized time notations such as "10.34 pm"
195
		// * this creates problems with keywords that contain "." such as "p.m."
196
		// * yet "." is an essential date separation character in languages such as German
197 35
		$parsevalue = str_replace( array( '/', '.', '&nbsp;', ',', '年', '月', '日', '時', '分' ), array( '-', ' ', ' ', ' ', ' ', ' ', ' ', ':', ' ' ), $string );
198
199 35
		$matches = preg_split( "/([T]?[0-2]?[0-9]:[\:0-9]+[+\-]?[0-2]?[0-9\:]+|[\p{L}]+|[0-9]+|[ ])/u", $parsevalue, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
200 35
		$datecomponents = array();
201 35
		$unclearparts = array();
202 35
		$matchisnumber = false; // used for looking back; numbers are days/months/years by default but may be re-interpreted if certain further symbols are found
203 35
		$matchisdate = false; // used for ensuring that date parts are in one block
204
205 35
		foreach ( $matches as $match ) {
206 35
			$prevmatchwasnumber = $matchisnumber;
207 35
			$prevmatchwasdate   = $matchisdate;
208 35
			$matchisnumber = $matchisdate = false;
209
210 35
			if ( $match == ' ' ) {
211 32
				$matchisdate = $prevmatchwasdate; // spaces in dates do not end the date
212 35
			} elseif ( $match == '-' ) { // can only occur separately between date components
213 8
				$datecomponents[] = $match; // we check later if this makes sense
214 8
				$matchisdate = true;
215 35
			} elseif ( is_numeric( $match ) &&
216 35
			           ( $prevmatchwasdate || count( $datecomponents ) == 0 ) ) {
217 34
				$datecomponents[] = $match;
218 34
				$matchisnumber = true;
219 34
				$matchisdate = true;
220 32
			} elseif ( $era === false && in_array( $match, array( 'AD', 'CE' ) ) ) {
221 2
				$era = '+';
222 32
			} elseif ( $era === false && in_array( $match, array( 'BC', 'BCE' ) ) ) {
223 4
				$era = '-';
224 32
			} elseif ( $calendarmodel === false && in_array( $match, array( 'Gr', 'GR' , 'He', 'Jl', 'JL', 'MJD', 'JD', 'OS' ) ) ) {
225 3
				$calendarmodel = $match;
226 32
			} elseif ( $ampm === false && ( strtolower( $match ) === 'am' || strtolower( $match ) === 'pm' ) ) {
227 8
				$ampm = strtolower( $match );
228 32
			} elseif ( $hours === false && self::parseTimeString( $match, $hours, $minutes, $seconds, $timeoffset ) ) {
229
				// nothing to do
230 32
			} elseif ( $hours !== false && $timezoneoffset === false && Timezone::isValid( $match ) ) {
231
				// only accept timezone if time has already been set
232
				$timezoneoffset = Timezone::getOffsetByAbbreviation( $match ) / 3600;
233
				$timezone = Timezone::getIdByAbbreviation( $match );
234 32
			} elseif ( $prevmatchwasnumber && $hours === false && $timezoneoffset === false &&
235 32
					Timezone::isMilitary( $match ) &&
236 32
					self::parseMilTimeString( end( $datecomponents ), $hours, $minutes, $seconds ) ) {
237
					// military timezone notation is found after a number -> re-interpret the number as military time
238
					array_pop( $datecomponents );
239
					$timezoneoffset = Timezone::getOffsetByAbbreviation( $match ) / 3600;
240
					$timezone = Timezone::getIdByAbbreviation( $match );
241 32
			} elseif ( ( $prevmatchwasdate || count( $datecomponents ) == 0 ) &&
242 32
				   $this->parseMonthString( $match, $monthname ) ) {
243 31
				$datecomponents[] = $monthname;
244 31
				$matchisdate = true;
245 4
			} elseif ( $prevmatchwasnumber && $prevmatchwasdate && in_array( $match, array( 'st', 'nd', 'rd', 'th' ) ) ) {
246
				$datecomponents[] = 'd' . strval( array_pop( $datecomponents ) ); // must be a day; add standard marker
247
				$matchisdate = true;
248 4
			} elseif ( count( $match ) == 1 ) {
249 4
				$microseconds = $match;
250
			} else {
251 35
				$unclearparts[] = $match;
252
			}
253
		}
254
255
256
		// Useful for debugging:
257
		// 		print "\n\n Results \n\n";
258
		// 		debug_zval_dump( $datecomponents );
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
259
		// 		print "\ncalendarmodel: $calendarmodel   \ntimezoneoffset: $timezoneoffset  \nera: $era  \nampm: $ampm  \nh: $hours  \nm: $minutes  \ns:$seconds  \ntimeoffset: $timeoffset  \n";
260
		// 		debug_zval_dump( $unclearparts );
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
261
262
		// Abort if we found unclear or over-specific information:
263 35
		if ( count( $unclearparts ) != 0 ) {
264
			$this->addErrorMsg( array( 'smw-datavalue-time-invalid-values', $this->m_wikivalue, implode( ', ', $unclearparts ) ) );
265
			return false;
266
		}
267
268 35
		if ( ( $timezoneoffset !== false && $timeoffset !== false ) ) {
269
			$this->addErrorMsg( array( 'smw-datavalue-time-invalid-offset-zone-usage', $this->m_wikivalue ) );
270
			return false;
271
		}
272
273 35
		if ( ( $timezoneoffset !== false && $timeoffset !== false ) ) {
274
			$this->addErrorMsg( array( 'smw-datavalue-time-invalid-offset-zone-usage', $this->m_wikivalue ) );
275
			return false;
276
		}
277
278 35
		$timeoffset = $timeoffset + $timezoneoffset;
279
		// Check if the a.m. and p.m. information is meaningful
280
281 35
		if ( $ampm !== false && ( $hours > 12 || $hours == 0 ) ) { // Note: the == 0 check subsumes $hours===false
282 1
			$this->addErrorMsg( array( 'smw-datavalue-time-invalid-ampm', $this->m_wikivalue, $hours ) );
283 1
			return false;
284 35
		} elseif ( $ampm == 'am' && $hours == 12 ) {
285
			$hours = 0;
286 35
		} elseif ( $ampm == 'pm' && $hours < 12 ) {
287 8
			$hours += 12;
288
		}
289
290 35
		return true;
291
	}
292
293
	/**
294
	 * Parse the given string to check if it encodes an international time.
295
	 * If successful, the function sets the provided call-by-ref values to
296
	 * the respective numbers and returns true. Otherwise, it returns
297
	 * false and does not set any values.
298
	 * @note This method is only temporarily public for enabling SMWCompatibilityHelpers. Do not use it directly in your code.
299
	 *
300
	 * @param $string string input time representation, e.g. "13:45:23-3:30"
301
	 * @param $hours integer between 0 and 24
302
	 * @param $minutes integer between 0 and 59
303
	 * @param $seconds integer between 0 and 59, or false if not given
304
	 * @param $timeoffset double for time offset (e.g. 3.5), or false if not given
305
	 * @return boolean stating if the parsing succeeded
306
	 */
307 32
	public static function parseTimeString( $string, &$hours, &$minutes, &$seconds, &$timeoffset ) {
308 32
		if ( !preg_match( "/^[T]?([0-2]?[0-9]):([0-5][0-9])(:[0-5][0-9])?(([+\-][0-2]?[0-9])(:(30|00))?)?$/u", $string, $match ) ) {
309 32
			return false;
310
		} else {
311 17
			$nhours = intval( $match[1] );
312 17
			$nminutes = $match[2] ? intval( $match[2] ) : false;
313 17
			if ( ( count( $match ) > 3 ) && ( $match[3] !== '' ) ) {
314 7
				$nseconds = intval( substr( $match[3], 1 ) );
315
			} else {
316 14
				$nseconds = false;
317
			}
318 17
			if ( ( $nhours < 25 ) && ( ( $nhours < 24 ) || ( $nminutes + $nseconds == 0 ) ) ) {
319 17
				$hours = $nhours;
320 17
				$minutes = $nminutes;
321 17
				$seconds = $nseconds;
322 17
				if ( ( count( $match ) > 5 ) && ( $match[5] !== '' ) ) {
323 3
					$timeoffset = intval( $match[5] );
324 3
					if ( ( count( $match ) > 7 ) && ( $match[7] == '30' ) ) {
325 3
						$timeoffset += 0.5;
326
					}
327
				} else {
328 17
					$timeoffset = false;
329
				}
330 17
				return true;
331
			} else {
332
				return false;
333
			}
334
		}
335
	}
336
337
	/**
338
	 * Parse the given string to check if it encodes a "military time".
339
	 * If successful, the function sets the provided call-by-ref values to
340
	 * the respective numbers and returns true. Otherwise, it returns
341
	 * false and does not set any values.
342
	 * @param $string string input time representation, e.g. "134523"
343
	 * @param $hours integer between 0 and 24
344
	 * @param $minutes integer between 0 and 59
345
	 * @param $seconds integer between 0 and 59, or false if not given
346
	 * @return boolean stating if the parsing succeeded
347
	 */
348
	protected static function parseMilTimeString( $string, &$hours, &$minutes, &$seconds ) {
349
		if ( !preg_match( "/^([0-2][0-9])([0-5][0-9])([0-5][0-9])?$/u", $string, $match ) ) {
350
			return false;
351
		} else {
352
			$nhours = intval( $match[1] );
353
			$nminutes = $match[2] ? intval( $match[2] ) : false;
354
			$nseconds = ( ( count( $match ) > 3 ) && $match[3] ) ? intval( $match[3] ) : false;
355
			if ( ( $nhours < 25 ) && ( ( $nhours < 24 ) || ( $nminutes + $nseconds == 0 ) ) ) {
356
				$hours = $nhours;
357
				$minutes = $nminutes;
358
				$seconds = $nseconds;
359
				return true;
360
			} else {
361
				return false;
362
			}
363
		}
364
	}
365
366
	/**
367
	 * Parse the given string to check if it refers to the string name ot
368
	 * abbreviation of a month name. If yes, it is replaced by a normalized
369
	 * month name (placed in the call-by-ref parameter) and true is
370
	 * returned. Otherwise, false is returned and $monthname is not changed.
371
	 * @param $string string month name or abbreviation to parse
372
	 * @param $monthname string with standard 3-letter English month abbreviation
373
	 * @return boolean stating whether a month was found
374
	 */
375 32
	protected static function parseMonthString( $string, &$monthname ) {
376
		/**
377
		 * @var SMWLanguage $smwgContLang
378
		 */
379 32
		global $smwgContLang;
380
381 32
		$monthnum = $smwgContLang->findMonth( $string ); // takes precedence over English month names!
382
383 32
		if ( $monthnum !== false ) {
384 31
			$monthnum -= 1;
385
		} else {
386 4
			$monthnum = array_search( $string, self::$m_months ); // check English names
387
		}
388
389 32
		if ( $monthnum !== false ) {
390 31
			$monthname = self::$m_monthsshort[$monthnum];
391 31
			return true;
392 3
		} elseif ( array_search( $string, self::$m_monthsshort ) !== false ) {
393
			$monthname = $string;
394
			return true;
395
		} else {
396 3
			return false;
397
		}
398
	}
399
400
	/**
401
	 * Validate and interpret the date components as retrieved when parsing
402
	 * a user input. The method takes care of guessing how a list of values
403
	 * such as "10 12 13" is to be interpreted using the current language
404
	 * settings. The result is stored in the call-by-ref parameter
405
	 * $date that uses keys 'y', 'm', 'd' and contains the respective
406
	 * numbers as values, or false if not specified. If errors occur, error
407
	 * messages are added to the objects list of errors, and false is
408
	 * returned. Otherwise, true is returned.
409
	 * @param $datecomponents array of strings that might belong to the specification of a date
410
	 * @param $date array set to result
411
	 * @return boolean stating if successful
412
	 */
413 35
	protected function interpretDateComponents( $datecomponents, &$date ) {
414 35
		global $smwgContLang;
415
		// The following code segment creates a bit vector to encode
416
		// which role each digit of the entered date can take (day,
417
		// year, month). The vector starts with 1 and contains three
418
		// bits per date component, set ot true whenever this component
419
		// could be a month, a day, or a year (this is the order).
420
		// Examples:
421
		//   100 component could only be a month
422
		//   010 component could only be a day
423
		//   001 component could only be a year
424
		//   011 component could be a day or a year but no month etc.
425
		// For three components, we thus get a 10 digit bit vector.
426 35
		$datevector = 1;
427 35
		$propercomponents = array();
428 35
		$justfounddash = true; // avoid two dashes in a row, or dashes at the end
429 35
		$error = false;
430 35
		$numvalue = 0;
431 35
		foreach ( $datecomponents as $component ) {
432 34
			if ( $component == "-" ) {
433 8
				if ( $justfounddash ) {
434 1
					$error = true;
435 1
					break;
436
				}
437 7
				$justfounddash = true;
438
			} else {
439 34
				$justfounddash = false;
440 34
				$datevector = ( $datevector << 3 ) | $this->checkDateComponent( $component, $numvalue );
441 34
				$propercomponents[] = $numvalue;
442
			}
443
		}
444
445 35
		if ( ( $error ) || ( $justfounddash ) || ( count( $propercomponents ) == 0 ) || ( count( $propercomponents ) > 3 ) ) {
446
447 3
			$msgKey = 'smw-datavalue-time-invalid-date-components';
448
449 3
			if ( $justfounddash ) {
450 3
				$msgKey .= '-dash';
451
			} elseif ( count( $propercomponents ) == 0 ) {
452
				$msgKey .= '-empty';
453
			} elseif ( count( $propercomponents ) > 3 ) {
454
				$msgKey .= '-three';
455
			} else{
456
				$msgKey .= '-common';
457
			}
458
459 3
			$this->addErrorMsg( array( $msgKey, $this->m_wikivalue ) );
460 3
			return false;
461
		}
462
463
		// Now use the bitvector to find the preferred interpretation of the date components:
464 34
		$dateformats = $smwgContLang->getDateFormats();
465 34
		$date = array( 'y' => false, 'm' => false, 'd' => false );
466 34
		foreach ( $dateformats[count( $propercomponents ) - 1] as $formatvector ) {
467 34
			if ( !( ~$datevector & $formatvector ) ) { // check if $formatvector => $datevector ("the input supports the format")
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
468 34
				$i = 0;
469 34
				foreach ( self::$m_formats[$formatvector] as $fieldname ) {
470 34
					$date[$fieldname] = $propercomponents[$i];
471 34
					$i += 1;
472
				}
473 34
				break;
474
			}
475
		}
476 34
		if ( $date['y'] === false ) { // no band matches the entered date
477
			$this->addErrorMsg( array( 'smw-datavalue-time-invalid-date-components-sequence', $this->m_wikivalue ) );
478
			return false;
479
		}
480 34
		return true;
481
	}
482
483
	/**
484
	 * Initialise data from the provided intermediate results after
485
	 * parsing, assuming that a conventional date notation is used.
486
	 * If errors occur, error messages are added to the objects list of
487
	 * errors, and false is returned. Otherwise, true is returned.
488
	 * @param $datecomponents array of strings that might belong to the specification of a date
489
	 * @param $calendarmodesl string if model was set in input, otherwise false
490
	 * @param $era string '+' or '-' if provided, otherwise false
491
	 * @param $hours integer value between 0 and 24
492
	 * @param $minutes integer value between 0 and 59
493
	 * @param $seconds integer value between 0 and 59, or false if not given
494
	 * @param $timeoffset double value for time offset (e.g. 3.5), or false if not given
495
	 * @return boolean stating if successful
496
	 */
497 35
	protected function setDateFromParsedValues( $datecomponents, $calendarmodel, $era, $hours, $minutes, $seconds, $microseconds, $timeoffset, $timezone ) {
498 35
		$date = false;
499 35
		if ( !$this->interpretDateComponents( $datecomponents, $date ) ) {
500 3
			return false;
501
		}
502
503
		// Handle BC: the year is negative.
504 34
		if ( ( $era == '-' ) && ( $date['y'] > 0 ) ) { // see class documentation on BC, "year 0", and ISO conformance ...
505 4
			$date['y'] = -( $date['y'] );
506
		}
507
508
		// Keep information about the era
509 34
		if ( ( $era == '+' ) && ( $date['y'] > 0 ) ) {
510 1
			$date['y'] = $era . $date['y'];
511
		}
512
513
		// Old Style is a special case of Julian calendar model where the change of the year was 25 March:
514 34
		if ( ( $calendarmodel == 'OS' ) &&
515 34
		     ( ( $date['m'] < 3 ) || ( ( $date['m'] == 3 ) && ( $date['d'] < 25 ) ) ) ) {
516
			$date['y']++;
517
		}
518
519 34
		$calmod = $this->getCalendarModel( $calendarmodel, $date['y'], $date['m'], $date['d'] );
520
		try {
521 34
			$this->m_dataitem = new SMWDITime( $calmod, $date['y'], $date['m'], $date['d'], $hours, $minutes, $seconds . '.' . $microseconds, $timezone );
522
		} catch ( SMWDataItemException $e ) {
523
			$this->addErrorMsg( array( 'smw-datavalue-time-invalid', $this->m_wikivalue, $e->getMessage() ) );
524
			return false;
525
		}
526
527
		// Having more than years or specifying a calendar model does
528
		// not make sense for prehistoric dates, and our calendar
529
		// conversion would not be reliable if JD numbers get too huge:
530 34
		if ( ( $date['y'] <= self::PREHISTORY ) &&
531 34
		     ( ( $this->m_dataitem->getPrecision() > SMWDITime::PREC_Y ) || ( $calendarmodel !== false ) ) ) {
532
			$this->addErrorMsg( array( 'smw-datavalue-time-invalid-prehistoric', $this->m_wikivalue ) );
533
			return false;
534
		}
535 34
		if ( $timeoffset != 0 ) {
536 3
			$newjd = $this->m_dataitem->getJD() - $timeoffset / 24;
537
			try {
538 3
				$this->m_dataitem = SMWDITime::newFromJD( $newjd, $calmod, $this->m_dataitem->getPrecision(), $timezone );
539
			} catch ( SMWDataItemException $e ) {
540
				$this->addErrorMsg( array( 'smw-datavalue-time-invalid-jd', $this->m_wikivalue, $e->getMessage() ) );
541
				return false;
542
			}
543
		}
544 34
		return true;
545
	}
546
547
	/**
548
	 * Check which roles a string component might play in a date, and
549
	 * set the call-by-ref parameter to the proper numerical
550
	 * representation. The component string has already been normalized to
551
	 * be either a plain number, a month name, or a plain number with "d"
552
	 * pre-pended. The result is a bit vector to indicate the possible
553
	 * interpretations.
554
	 * @param $component string
555
	 * @param $numvalue integer representing the components value
556
	 * @return integer that encodes a three-digit bit vector
557
	 */
558 34
	protected static function checkDateComponent( $component, &$numvalue ) {
559 34
		if ( $component === '' ) { // should not happen
560
			$numvalue = 0;
561
			return 0;
562 34
		} elseif ( is_numeric( $component ) ) {
563 34
			$numvalue = intval( $component );
564 34
			if ( ( $numvalue >= 1 ) && ( $numvalue <= 12 ) ) {
565 29
				return SMW_DAY_MONTH_YEAR; // can be a month, day or year
566 34
			} elseif ( ( $numvalue >= 1 ) && ( $numvalue <= 31 ) ) {
567 19
				return SMW_DAY_YEAR; // can be day or year
568
			} else { // number can just be a year
569 34
				return SMW_YEAR;
570
			}
571 31
		} elseif ( $component { 0 } == 'd' ) { // already marked as day
572
			if ( is_numeric( substr( $component, 1 ) ) ) {
573
				$numvalue = intval( substr( $component, 1 ) );
574
				return ( ( $numvalue >= 1 ) && ( $numvalue <= 31 ) ) ? SMW_DAY : 0;
575
			} else {
576
				return 0;
577
			}
578
		} else {
579 31
			$monthnum = array_search( $component, self::$m_monthsshort );
580 31
			if ( $monthnum !== false ) {
581 31
				$numvalue = $monthnum + 1;
582 31
				return SMW_MONTH;
583
			} else {
584
				return 0;
585
			}
586
		}
587
	}
588
589
	/**
590
	 * Determine the calender model under which an input should be
591
	 * interpreted based on the given input data.
592
	 * @param $presetmodel mixed string related to a user input calendar model (OS, Jl, Gr) or false
593
	 * @param $year integer of the given year (adjusted for BC(E), i.e. possibly negative)
594
	 * @param $month mixed integer of the month or false
595
	 * @param $day mixed integer of the day or false
596
	 * @return integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN
597
	 */
598 41
	protected function getCalendarModel( $presetmodel, $year, $month, $day ) {
599 41
		if ( $presetmodel == 'OS' ) { // Old Style is a notational convention of Julian dates only
600
			$presetmodel = 'Jl';
601
		}
602 41
		if ( $presetmodel === 'Gr' || $presetmodel === 'GR' ) {
603 3
			return SMWDITime::CM_GREGORIAN;
604 41
		} elseif (  $presetmodel === 'Jl' || $presetmodel === 'JL' ) {
605 3
			return SMWDITime::CM_JULIAN;
606
		}
607 40
		if ( ( $year > 1582 ) ||
608 12
		     ( ( $year == 1582 ) && ( $month > 10 ) ) ||
609 40
		     ( ( $year == 1582 ) && ( $month == 10 ) && ( $day > 4 ) ) ) {
610 40
			return SMWDITime::CM_GREGORIAN;
611 12
		} elseif ( $year > self::PREHISTORY ) {
612 12
			return SMWDITime::CM_JULIAN;
613
		} else {
614
			// proleptic Julian years at some point deviate from the count of complete revolutions of the earth around the sun
615
			// hence assume that earlier date years are Gregorian (where this effect is very weak only)
616
			// This is mostly for internal use since we will not allow users to specify calendar models at this scale
617 3
			return SMWDITime::CM_GREGORIAN;
618
		}
619
	}
620
621
	/**
622
	 * @see SMWDataValue::loadDataItem
623
	 *
624
	 * {@inheritDoc}
625
	 */
626 58
	protected function loadDataItem( SMWDataItem $dataItem ) {
627
628 58
		if ( $dataItem->getDIType() !== SMWDataItem::TYPE_TIME ) {
629
			return false;
630
		}
631
632 58
		$this->m_dataitem = $dataItem;
633 58
		$this->m_caption = false;
634 58
		$this->m_wikivalue = false;
635
636 58
		return true;
637
	}
638
639
	/**
640
	 * @see SMWDataValue::getShortWikiText
641
	 *
642
	 * {@inheritDoc}
643
	 */
644 31
	public function getShortWikiText( $linker = null ) {
645 31
		return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_SHORT, $linker );
646
	}
647
648
	/**
649
	 * @see SMWDataValue::getShortHTMLText
650
	 *
651
	 * {@inheritDoc}
652
	 */
653
	public function getShortHTMLText( $linker = null ) {
654
		return $this->getDataValueFormatter()->format( DataValueFormatter::HTML_SHORT, $linker );
655
	}
656
657
	/**
658
	 * @see SMWDataValue::getLongWikiText
659
	 *
660
	 * {@inheritDoc}
661
	 */
662 13
	public function getLongWikiText( $linker = null ) {
663 13
		return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_LONG, $linker );
664
	}
665
666
	/**
667
	 * @see SMWDataValue::getLongHTMLText
668
	 *
669
	 * {@inheritDoc}
670
	 */
671 3
	public function getLongHTMLText( $linker = null ) {
672 3
		return $this->getDataValueFormatter()->format( DataValueFormatter::HTML_LONG, $linker );
673
	}
674
675
	/**
676
	 * @todo The preferred caption may not be suitable as a wiki value (i.e. not parsable).
677
	 * @see SMWDataValue::getLongHTMLText
678
	 *
679
	 * {@inheritDoc}
680
	 */
681 10
	public function getWikiValue() {
682 10
		return $this->m_wikivalue ? $this->m_wikivalue : strip_tags( $this->getLongWikiText() );
683
	}
684
685
	/**
686
	 * @see SMWDataValue::isNumeric
687
	 *
688
	 * {@inheritDoc}
689
	 */
690
	public function isNumeric() {
691
		return true;
692
	}
693
694
	/**
695
	 * Return the year number in the given calendar model, or false if
696
	 * this number is not available (typically when attempting to get
697
	 * prehistoric Julian calendar dates). As everywhere in this class,
698
	 * there is no year 0.
699
	 * @param $calendarmodel integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN
700
	 * @return mixed typically a number but possibly false
701
	 */
702 1
	public function getYear( $calendarmodel = SMWDITime::CM_GREGORIAN ) {
703 1
		$di = $this->getDataItemForCalendarModel( $calendarmodel );
704 1
		if ( !is_null( $di ) ) {
705 1
			return $di->getYear();
706
		} else {
707
			return false;
708
		}
709
	}
710
711
	/**
712
	 * Return the month number in the given calendar model, or false if
713
	 * this number is not available (typically when attempting to get
714
	 * prehistoric Julian calendar dates).
715
	 * @param $calendarmodel integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN
716
	 * @param $default value to return if month is not set at our level of precision
717
	 * @return mixed typically a number but possibly anything given as $default
718
	 */
719 1
	public function getMonth( $calendarmodel = SMWDITime::CM_GREGORIAN, $default = 1 ) {
720 1
		$di = $this->getDataItemForCalendarModel( $calendarmodel );
721 1
		if ( !is_null( $di ) ) {
722 1
			return ( $di->getPrecision() >= SMWDITime::PREC_YM ) ? $di->getMonth() : $default;
723
		} else {
724
			return false;
725
		}
726
	}
727
728
	/**
729
	 * Return the day number in the given calendar model, or false if this
730
	 * number is not available (typically when attempting to get
731
	 * prehistoric Julian calendar dates).
732
	 * @param $calendarmodel integer either SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN
733
	 * @param $default value to return if day is not set at our level of precision
734
	 * @return mixed typically a number but possibly anything given as $default
735
	 */
736 1
	public function getDay( $calendarmodel = SMWDITime::CM_GREGORIAN, $default = 1 ) {
737 1
		$di = $this->getDataItemForCalendarModel( $calendarmodel );
738 1
		if ( !is_null( $di ) ) {
739 1
			return ( $di->getPrecision() >= SMWDITime::PREC_YMD ) ? $di->getDay() : $default;
740
		} else {
741
			return false;
742
		}
743
	}
744
745
	/**
746
	 * @see TimeValueFormatter::getTimeStringFromDataItem
747
	 *
748
	 * @return
749
	 */
750 1
	public function getTimeString( $default = '00:00:00' ) {
751 1
		return $this->getDataValueFormatter()->getTimeString( $default );
752
	}
753
754
	/**
755
	 * @deprecated This method is now called getISO8601Date(). It will vanish before SMW 1.7.
756
	 */
757
	public function getXMLSchemaDate( $mindefault = true ) {
758
		return $this->getISO8601Date( $mindefault );
759
	}
760
761
	/**
762
	 * @see TimeValueFormatter::getISO8601DateFromDataItem
763
	 *
764
	 * @param $mindefault boolean determining whether values below the
765
	 * precision of our input should be completed with minimal or maximal
766
	 * conceivable values
767
	 *
768
	 * @return string
769
	 */
770 45
	public function getISO8601Date( $mindefault = true ) {
771 45
		return $this->getDataValueFormatter()->getISO8601Date( $mindefault );
772
	}
773
774
	/**
775
	 * @see TimeValueFormatter::getMediaWikiDateFromDataItem
776
	 *
777
	 * @return string
778
	 */
779 1
	public function getMediaWikiDate() {
780 1
		return $this->getDataValueFormatter()->getMediaWikiDate();
781
	}
782
783
	/**
784
	 * Get the current data in the specified calendar model. Conversion is
785
	 * not done for prehistoric dates (where it might lead to precision
786
	 * errors and produce results that are not meaningful). In this case,
787
	 * null might be returned if no data in the specified format is
788
	 * available.
789
	 * @param $calendarmodel integer one of SMWDITime::CM_GREGORIAN or SMWDITime::CM_JULIAN
790
	 * @return SMWDITime
791
	 */
792 57
	public function getDataItemForCalendarModel( $calendarmodel ) {
793 57
		if ( $this->m_dataitem->getYear() <= self::PREHISTORY ) {
794 3
			return ( $this->m_dataitem->getCalendarModel() == $calendarmodel ) ? $this->m_dataitem : null;
795 57
		} elseif ( $calendarmodel == SMWDITime::CM_GREGORIAN ) {
796 57
			if ( is_null( $this->m_dataitem_greg ) ) {
797 57
				$this->m_dataitem_greg = $this->m_dataitem->getForCalendarModel( SMWDITime::CM_GREGORIAN );
798
			}
799 57
			return $this->m_dataitem_greg;
800
		} else {
801 5
			if ( is_null( $this->m_dataitem_jul ) ) {
802 5
				$this->m_dataitem_jul = $this->m_dataitem->getForCalendarModel( SMWDITime::CM_JULIAN );
803
			}
804 5
			return $this->m_dataitem_jul;
805
		}
806
	}
807
808 42
	private function isInterpretableAsYearOnly( $value ) {
809 42
		return strpos( $value, ' ' ) === false && is_numeric( strval( $value ) ) && ( strval( $value ) < 0 || strlen( $value ) < 6 );
810
	}
811
812 35
	private function isInterpretableAsTimestamp( $value ) {
813
		// 1200-11-02T12:03:25 or 20120320055913
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
814 35
		return ( ( strlen( $value ) > 4 && substr( $value, 10, 1 ) === 'T' ) || strlen( $value ) == 14 ) && wfTimestamp( TS_MW, $value ) !== false;
815
	}
816
817
}
818