Completed
Push — master ( 3837d1...eff53c )
by mw
164:04 queued 128:48
created

includes/datavalues/SMW_DV_Time.php (2 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 43
	protected function parseUserValue( $value ) {
119
120 43
		$value = $this->convertDoubleWidth( $value );
121 43
		$this->m_wikivalue = $value;
122
123 43
		if ( $this->m_caption === false ) { // Store the caption now.
124 43
			$this->m_caption = $value;
125
		}
126 43
		$this->m_dataitem = null;
127
128 43
		$datecomponents = array();
129 43
		$calendarmodel = $era = $hours = $minutes = $seconds = $microseconds = $timeoffset = $timezone = false;
130 43
		if ( $this->isInterpretableAsYearOnly( $value ) ) {
131 15
			$this->m_dataitem = new SMWDITime( $this->getCalendarModel( null, $value, null, null ), $value );
132 36
		} elseif ( $this->isInterpretableAsTimestamp( $value ) ) {
133 3
			$this->m_dataitem = SMWDITime::newFromTimestamp( $value );
134 36
		} elseif ( $this->parseDateString( $value, $datecomponents, $calendarmodel, $era, $hours, $minutes, $seconds, $microseconds, $timeoffset, $timezone ) ) {
135 36
			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 36
			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 36
				$this->setDateFromParsedValues( $datecomponents, $calendarmodel, $era, $hours, $minutes, $seconds, $microseconds, $timeoffset, $timezone );
155
			}
156
		}
157
158 43
		if ( is_null( $this->m_dataitem ) ) { // make sure that m_dataitem is set in any case
159 5
			$this->m_dataitem = new SMWDITime( SMWDITime::CM_GREGORIAN, 32202 );
160
		}
161 43
	}
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 36
	protected function parseDateString( $string, &$datecomponents, &$calendarmodel, &$era, &$hours, &$minutes, &$seconds, &$microseconds, &$timeoffset, &$timezone ) {
180
181 36
		$calendarmodel = $timezoneoffset = $era = $ampm = false;
182 36
		$hours = $minutes = $seconds = $microseconds = $timeoffset = $timezone = false;
183
184
		// Fetch possible "America/Argentina/Mendoza"
185 36
		$timzoneIdentifier = substr( $string, strrpos( $string, ' ' ) + 1 );
186
187 36
		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 36
		$parsevalue = str_replace( array( '/', '.', '&nbsp;', ',', '年', '月', '日', '時', '分' ), array( '-', ' ', ' ', ' ', ' ', ' ', ' ', ':', ' ' ), $string );
198
199 36
		$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 36
		$datecomponents = array();
201 36
		$unclearparts = array();
202 36
		$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 36
		$matchisdate = false; // used for ensuring that date parts are in one block
204
205 36
		foreach ( $matches as $match ) {
206 36
			$prevmatchwasnumber = $matchisnumber;
207 36
			$prevmatchwasdate   = $matchisdate;
208 36
			$matchisnumber = $matchisdate = false;
209
210 36
			if ( $match == ' ' ) {
211 33
				$matchisdate = $prevmatchwasdate; // spaces in dates do not end the date
212 36
			} 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 36
			} elseif ( is_numeric( $match ) &&
216 36
			           ( $prevmatchwasdate || count( $datecomponents ) == 0 ) ) {
217 35
				$datecomponents[] = $match;
218 35
				$matchisnumber = true;
219 35
				$matchisdate = true;
220 33
			} elseif ( $era === false && in_array( $match, array( 'AD', 'CE' ) ) ) {
221 2
				$era = '+';
222 33
			} elseif ( $era === false && in_array( $match, array( 'BC', 'BCE' ) ) ) {
223 4
				$era = '-';
224 33
			} elseif ( $calendarmodel === false && in_array( $match, array( 'Gr', 'GR' , 'He', 'Jl', 'JL', 'MJD', 'JD', 'OS' ) ) ) {
225 3
				$calendarmodel = $match;
226 33
			} elseif ( $ampm === false && ( strtolower( $match ) === 'am' || strtolower( $match ) === 'pm' ) ) {
227 8
				$ampm = strtolower( $match );
228 33
			} elseif ( $hours === false && self::parseTimeString( $match, $hours, $minutes, $seconds, $timeoffset ) ) {
229
				// nothing to do
230 33
			} 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 33
			} elseif ( $prevmatchwasnumber && $hours === false && $timezoneoffset === false &&
235 33
					Timezone::isMilitary( $match ) &&
236 33
					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 33
			} elseif ( ( $prevmatchwasdate || count( $datecomponents ) == 0 ) &&
242 33
				   $this->parseMonthString( $match, $monthname ) ) {
243 32
				$datecomponents[] = $monthname;
244 32
				$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 36
				$unclearparts[] = $match;
252
			}
253
		}
254
255
256
		// Useful for debugging:
257
		// 		print "\n\n Results \n\n";
258
		// 		debug_zval_dump( $datecomponents );
259
		// 		print "\ncalendarmodel: $calendarmodel   \ntimezoneoffset: $timezoneoffset  \nera: $era  \nampm: $ampm  \nh: $hours  \nm: $minutes  \ns:$seconds  \ntimeoffset: $timeoffset  \n";
260
		// 		debug_zval_dump( $unclearparts );
261
262
		// Abort if we found unclear or over-specific information:
263 36
		if ( count( $unclearparts ) != 0 ) {
264
			$this->addErrorMsg( array( 'smw-datavalue-time-invalid-values', $this->m_wikivalue, implode( ', ', $unclearparts ) ) );
265
			return false;
266
		}
267
268 36
		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 36
		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 36
		$timeoffset = $timeoffset + $timezoneoffset;
279
		// Check if the a.m. and p.m. information is meaningful
280
281 36
		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 36
		} elseif ( $ampm == 'am' && $hours == 12 ) {
285
			$hours = 0;
286 36
		} elseif ( $ampm == 'pm' && $hours < 12 ) {
287 8
			$hours += 12;
288
		}
289
290 36
		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 33
	public static function parseTimeString( $string, &$hours, &$minutes, &$seconds, &$timeoffset ) {
308 33
		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 33
			return false;
310
		} else {
311 18
			$nhours = intval( $match[1] );
312 18
			$nminutes = $match[2] ? intval( $match[2] ) : false;
313 18
			if ( ( count( $match ) > 3 ) && ( $match[3] !== '' ) ) {
314 8
				$nseconds = intval( substr( $match[3], 1 ) );
315
			} else {
316 14
				$nseconds = false;
317
			}
318 18
			if ( ( $nhours < 25 ) && ( ( $nhours < 24 ) || ( $nminutes + $nseconds == 0 ) ) ) {
319 18
				$hours = $nhours;
320 18
				$minutes = $nminutes;
321 18
				$seconds = $nseconds;
322 18
				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 18
					$timeoffset = false;
329
				}
330 18
				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 33
	protected static function parseMonthString( $string, &$monthname ) {
376
		/**
377
		 * @var SMWLanguage $smwgContLang
378
		 */
379 33
		global $smwgContLang;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
380
381 33
		$monthnum = $smwgContLang->findMonth( $string ); // takes precedence over English month names!
382
383 33
		if ( $monthnum !== false ) {
384 32
			$monthnum -= 1;
385
		} else {
386 4
			$monthnum = array_search( $string, self::$m_months ); // check English names
387
		}
388
389 33
		if ( $monthnum !== false ) {
390 32
			$monthname = self::$m_monthsshort[$monthnum];
391 32
			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 36
	protected function interpretDateComponents( $datecomponents, &$date ) {
414 36
		global $smwgContLang;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
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 36
		$datevector = 1;
427 36
		$propercomponents = array();
428 36
		$justfounddash = true; // avoid two dashes in a row, or dashes at the end
429 36
		$error = false;
430 36
		$numvalue = 0;
431 36
		foreach ( $datecomponents as $component ) {
432 35
			if ( $component == "-" ) {
433 8
				if ( $justfounddash ) {
434 1
					$error = true;
435 1
					break;
436
				}
437 7
				$justfounddash = true;
438
			} else {
439 35
				$justfounddash = false;
440 35
				$datevector = ( $datevector << 3 ) | $this->checkDateComponent( $component, $numvalue );
441 35
				$propercomponents[] = $numvalue;
442
			}
443
		}
444
445 36
		if ( ( $error ) || ( $justfounddash ) || ( count( $propercomponents ) == 0 ) || ( count( $propercomponents ) > 3 ) ) {
446
447 4
			$msgKey = 'smw-datavalue-time-invalid-date-components';
448
449 4
			if ( $justfounddash ) {
450 4
				$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 4
			$this->addErrorMsg( array( $msgKey, $this->m_wikivalue ) );
460 4
			return false;
461
		}
462
463
		// Now use the bitvector to find the preferred interpretation of the date components:
464 35
		$dateformats = $smwgContLang->getDateFormats();
465 35
		$date = array( 'y' => false, 'm' => false, 'd' => false );
466 35
		foreach ( $dateformats[count( $propercomponents ) - 1] as $formatvector ) {
467 35
			if ( !( ~$datevector & $formatvector ) ) { // check if $formatvector => $datevector ("the input supports the format")
468 35
				$i = 0;
469 35
				foreach ( self::$m_formats[$formatvector] as $fieldname ) {
470 35
					$date[$fieldname] = $propercomponents[$i];
471 35
					$i += 1;
472
				}
473 35
				break;
474
			}
475
		}
476 35
		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 35
		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 36
	protected function setDateFromParsedValues( $datecomponents, $calendarmodel, $era, $hours, $minutes, $seconds, $microseconds, $timeoffset, $timezone ) {
498 36
		$date = false;
499 36
		if ( !$this->interpretDateComponents( $datecomponents, $date ) ) {
500 4
			return false;
501
		}
502
503
		// Handle BC: the year is negative.
504 35
		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 35
		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 35
		if ( ( $calendarmodel == 'OS' ) &&
515 35
		     ( ( $date['m'] < 3 ) || ( ( $date['m'] == 3 ) && ( $date['d'] < 25 ) ) ) ) {
516
			$date['y']++;
517
		}
518
519 35
		$calmod = $this->getCalendarModel( $calendarmodel, $date['y'], $date['m'], $date['d'] );
520
		try {
521 35
			$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 35
		if ( ( $date['y'] <= self::PREHISTORY ) &&
531 35
		     ( ( $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 35
		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 35
		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 35
	protected static function checkDateComponent( $component, &$numvalue ) {
559 35
		if ( $component === '' ) { // should not happen
560
			$numvalue = 0;
561
			return 0;
562 35
		} elseif ( is_numeric( $component ) ) {
563 35
			$numvalue = intval( $component );
564 35
			if ( ( $numvalue >= 1 ) && ( $numvalue <= 12 ) ) {
565 30
				return SMW_DAY_MONTH_YEAR; // can be a month, day or year
566 35
			} 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 35
				return SMW_YEAR;
570
			}
571 32
		} 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 32
			$monthnum = array_search( $component, self::$m_monthsshort );
580 32
			if ( $monthnum !== false ) {
581 32
				$numvalue = $monthnum + 1;
582 32
				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 42
	protected function getCalendarModel( $presetmodel, $year, $month, $day ) {
599 42
		if ( $presetmodel == 'OS' ) { // Old Style is a notational convention of Julian dates only
600
			$presetmodel = 'Jl';
601
		}
602 42
		if ( $presetmodel === 'Gr' || $presetmodel === 'GR' ) {
603 3
			return SMWDITime::CM_GREGORIAN;
604 42
		} elseif (  $presetmodel === 'Jl' || $presetmodel === 'JL' ) {
605 3
			return SMWDITime::CM_JULIAN;
606
		}
607 41
		if ( ( $year > 1582 ) ||
608 12
		     ( ( $year == 1582 ) && ( $month > 10 ) ) ||
609 41
		     ( ( $year == 1582 ) && ( $month == 10 ) && ( $day > 4 ) ) ) {
610 41
			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 61
	protected function loadDataItem( SMWDataItem $dataItem ) {
627
628 61
		if ( $dataItem->getDIType() !== SMWDataItem::TYPE_TIME ) {
629
			return false;
630
		}
631
632 61
		$this->m_dataitem = $dataItem;
633 61
		$this->m_caption = false;
634 61
		$this->m_wikivalue = false;
635
636 61
		return true;
637
	}
638
639
	/**
640
	 * @see SMWDataValue::getShortWikiText
641
	 *
642
	 * {@inheritDoc}
643
	 */
644 32
	public function getShortWikiText( $linker = null ) {
645 32
		return $this->getDataValueFormatter()->format( DataValueFormatter::WIKI_SHORT, $linker );
646
	}
647
648
	/**
649
	 * @see SMWDataValue::getShortHTMLText
650
	 *
651
	 * {@inheritDoc}
652
	 */
653 1
	public function getShortHTMLText( $linker = null ) {
654 1
		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 4
	public function getLongHTMLText( $linker = null ) {
672 4
		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 47
	public function getISO8601Date( $mindefault = true ) {
771 47
		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 60
	public function getDataItemForCalendarModel( $calendarmodel ) {
793 60
		if ( $this->m_dataitem->getYear() <= self::PREHISTORY ) {
794 3
			return ( $this->m_dataitem->getCalendarModel() == $calendarmodel ) ? $this->m_dataitem : null;
795 60
		} elseif ( $calendarmodel == SMWDITime::CM_GREGORIAN ) {
796 60
			if ( is_null( $this->m_dataitem_greg ) ) {
797 60
				$this->m_dataitem_greg = $this->m_dataitem->getForCalendarModel( SMWDITime::CM_GREGORIAN );
798
			}
799 60
			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 43
	private function isInterpretableAsYearOnly( $value ) {
809 43
		return strpos( $value, ' ' ) === false && is_numeric( strval( $value ) ) && ( strval( $value ) < 0 || strlen( $value ) < 6 );
810
	}
811
812 36
	private function isInterpretableAsTimestamp( $value ) {
813
		// 1200-11-02T12:03:25 or 20120320055913
814 36
		return ( ( strlen( $value ) > 4 && substr( $value, 10, 1 ) === 'T' ) || strlen( $value ) == 14 ) && wfTimestamp( TS_MW, $value ) !== false;
815
	}
816
817
}
818