1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace ValueParsers; |
4
|
|
|
|
5
|
|
|
use DataValues\IllegalValueException; |
6
|
|
|
use DataValues\TimeValue; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* ValueParser that parses various string representations of time values, in YMD ordered formats |
10
|
|
|
* resembling ISO 8601, e.g. +2013-01-01T00:00:00Z. While the parser tries to be relaxed, certain |
11
|
|
|
* aspects of the ISO norm are obligatory: The order must be YMD. All elements but the year must |
12
|
|
|
* have 2 digits. The separation characters must be dashes (in the date part), "T" and colons (in |
13
|
|
|
* the time part). |
14
|
|
|
* |
15
|
|
|
* The parser refuses to parse strings that can be parsed differently by other, locale-aware |
16
|
|
|
* parsers, e.g. 01-02-03 can be in YMD, DMY or MDY order depending on the language. |
17
|
|
|
* |
18
|
|
|
* @since 0.7 renamed from TimeParser to IsoTimestampParser. |
19
|
|
|
* |
20
|
|
|
* @license GPL-2.0+ |
21
|
|
|
* @author Addshore |
22
|
|
|
* @author Thiemo Kreuz |
23
|
|
|
* @author Daniel Kinzler |
24
|
|
|
*/ |
25
|
|
|
class IsoTimestampParser extends StringValueParser { |
26
|
|
|
|
27
|
|
|
const FORMAT_NAME = 'iso-timestamp'; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* Option to override the precision auto-detection and set a specific precision. Should be an |
31
|
|
|
* integer or string containing one of the TimeValue::PRECISION_... constants. |
32
|
|
|
*/ |
33
|
|
|
const OPT_PRECISION = 'precision'; |
34
|
|
|
|
35
|
|
|
/** |
36
|
|
|
* Option to override the calendar model auto-detection and set a specific calendar model URI. |
37
|
|
|
* Should be one of the TimeValue::CALENDAR_... constants. |
38
|
|
|
*/ |
39
|
|
|
const OPT_CALENDAR = 'calendar'; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @deprecated since 0.7.1, use TimeValue::CALENDAR_GREGORIAN instead |
43
|
|
|
*/ |
44
|
|
|
const CALENDAR_GREGORIAN = TimeValue::CALENDAR_GREGORIAN; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @deprecated since 0.7.1, use TimeValue::CALENDAR_JULIAN instead |
48
|
|
|
*/ |
49
|
|
|
const CALENDAR_JULIAN = TimeValue::CALENDAR_JULIAN; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var CalendarModelParser |
53
|
|
|
*/ |
54
|
|
|
private $calendarModelParser; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* @param CalendarModelParser|null $calendarModelParser |
58
|
|
|
* @param ParserOptions|null $options |
59
|
|
|
*/ |
60
|
40 |
|
public function __construct( |
61
|
|
|
CalendarModelParser $calendarModelParser = null, |
62
|
|
|
ParserOptions $options = null |
63
|
|
|
) { |
64
|
40 |
|
parent::__construct( $options ); |
65
|
|
|
|
66
|
40 |
|
$this->defaultOption( self::OPT_CALENDAR, null ); |
67
|
40 |
|
$this->defaultOption( self::OPT_PRECISION, null ); |
68
|
|
|
|
69
|
40 |
|
$this->calendarModelParser = $calendarModelParser ?: new CalendarModelParser( $this->options ); |
70
|
40 |
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* @param string $value |
74
|
|
|
* |
75
|
|
|
* @throws ParseException |
76
|
|
|
* @return TimeValue |
77
|
|
|
*/ |
78
|
86 |
|
protected function stringParse( $value ) { |
79
|
|
|
try { |
80
|
86 |
|
$timeParts = $this->splitTimeString( $value ); |
81
|
20 |
|
} catch ( ParseException $ex ) { |
82
|
20 |
|
throw new ParseException( $ex->getMessage(), $value, self::FORMAT_NAME ); |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
// Pad sign with 1 plus, year with 4 zeros and hour, minute and second with 2 zeros |
86
|
66 |
|
$timestamp = vsprintf( '%\'+1s%04s-%s-%sT%02s:%02s:%02sZ', $timeParts ); |
87
|
66 |
|
$precision = $this->getPrecision( $timeParts ); |
88
|
64 |
|
$calendarModel = $this->getCalendarModel( $timeParts ); |
89
|
|
|
|
90
|
|
|
try { |
91
|
63 |
|
return new TimeValue( $timestamp, 0, 0, 0, $precision, $calendarModel ); |
92
|
2 |
|
} catch ( IllegalValueException $ex ) { |
93
|
2 |
|
throw new ParseException( $ex->getMessage(), $value, self::FORMAT_NAME ); |
94
|
|
|
} |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* @param string $value |
99
|
|
|
* |
100
|
|
|
* @throws ParseException |
101
|
|
|
* @return string[] Array with index 0 => sign, 1 => year, 2 => month, 3 => day, 4 => hour, |
102
|
|
|
* 5 => minute, 6 => second and 7 => calendar model. |
103
|
|
|
*/ |
104
|
86 |
|
private function splitTimeString( $value ) { |
105
|
|
|
$pattern = '@^\s*' // leading spaces |
106
|
|
|
. "([-+\xE2\x88\x92]?)\\s*" // sign |
107
|
|
|
. '(\d{1,16})-(\d{2})-(\d{2})' // year-month-day |
108
|
|
|
. '(?:T(\d{2}):?(\d{2})(?::?(\d{2}))?)?' // hour:minute:second |
109
|
|
|
. 'Z?' // time zone |
110
|
86 |
|
. '\s*\(?\s*' . CalendarModelParser::MODEL_PATTERN . '\s*\)?' // calendar model |
|
|
|
|
111
|
86 |
|
. '\s*$@iu'; // trailing spaces |
112
|
|
|
|
113
|
86 |
|
if ( !preg_match( $pattern, $value, $matches ) ) { |
114
|
7 |
|
throw new ParseException( 'Malformed time' ); |
115
|
|
|
} |
116
|
|
|
|
117
|
79 |
|
list( , $sign, $year, $month, $day, $hour, $minute, $second, $calendarModel ) = $matches; |
118
|
|
|
|
119
|
79 |
|
if ( $sign === '' |
120
|
79 |
|
&& strlen( $year ) < 3 |
121
|
79 |
|
&& $year < 32 |
122
|
79 |
|
&& $hour === '' |
123
|
|
|
) { |
124
|
4 |
|
throw new ParseException( 'Not enough information to decide if the format is YMD' ); |
125
|
|
|
} |
126
|
|
|
|
127
|
75 |
|
if ( $month > 12 ) { |
128
|
1 |
|
throw new ParseException( 'Month out of range' ); |
129
|
74 |
|
} elseif ( $day > 31 ) { |
130
|
1 |
|
throw new ParseException( 'Day out of range' ); |
131
|
73 |
|
} elseif ( $hour > 23 ) { |
132
|
1 |
|
throw new ParseException( 'Hour out of range' ); |
133
|
72 |
|
} elseif ( $minute > 59 ) { |
134
|
1 |
|
throw new ParseException( 'Minute out of range' ); |
135
|
71 |
|
} elseif ( $second > 61 ) { |
136
|
1 |
|
throw new ParseException( 'Second out of range' ); |
137
|
|
|
} |
138
|
|
|
|
139
|
70 |
|
if ( $month < 1 && $day > 0 ) { |
140
|
1 |
|
throw new ParseException( 'Can not have a day with no month' ); |
141
|
|
|
} |
142
|
|
|
|
143
|
69 |
|
if ( $day < 1 && ( $hour > 0 || $minute > 0 || $second > 0 ) ) { |
144
|
3 |
|
throw new ParseException( 'Can not have hour, minute or second with no day' ); |
145
|
|
|
} |
146
|
|
|
|
147
|
66 |
|
$sign = str_replace( "\xE2\x88\x92", '-', $sign ); |
148
|
|
|
|
149
|
66 |
|
return array( $sign, $year, $month, $day, $hour, $minute, $second, $calendarModel ); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* @param string[] $timeParts Array with index 0 => sign, 1 => year, 2 => month, etc. |
154
|
|
|
* |
155
|
|
|
* @throws ParseException |
156
|
|
|
* @return int One of the TimeValue::PRECISION_... constants. |
157
|
|
|
*/ |
158
|
66 |
|
private function getPrecision( array $timeParts ) { |
159
|
66 |
|
if ( $timeParts[6] > 0 ) { |
160
|
2 |
|
$precision = TimeValue::PRECISION_SECOND; |
161
|
64 |
|
} elseif ( $timeParts[5] > 0 ) { |
162
|
1 |
|
$precision = TimeValue::PRECISION_MINUTE; |
163
|
63 |
|
} elseif ( $timeParts[4] > 0 ) { |
164
|
|
|
$precision = TimeValue::PRECISION_HOUR; |
165
|
63 |
|
} elseif ( $timeParts[3] > 0 |
166
|
|
|
// Can not have a day with no month, fall back to year precision. |
167
|
63 |
|
&& $timeParts[2] > 0 |
168
|
|
|
) { |
169
|
42 |
|
$precision = TimeValue::PRECISION_DAY; |
170
|
21 |
|
} elseif ( $timeParts[2] > 0 ) { |
171
|
4 |
|
$precision = TimeValue::PRECISION_MONTH; |
172
|
|
|
} else { |
173
|
17 |
|
$precision = $this->getPrecisionFromYear( $timeParts[1] ); |
174
|
|
|
} |
175
|
|
|
|
176
|
66 |
|
$option = $this->getOption( self::OPT_PRECISION ); |
177
|
|
|
|
178
|
66 |
|
if ( $option !== null ) { |
179
|
13 |
|
if ( !is_int( $option ) && !ctype_digit( $option ) ) { |
180
|
2 |
|
throw new ParseException( 'Precision must be an integer' ); |
181
|
|
|
} |
182
|
|
|
|
183
|
11 |
|
$option = (int)$option; |
184
|
|
|
|
185
|
|
|
// It's impossible to increase the detected precision via option, e.g. from year to month if |
186
|
|
|
// no month is given. If a day is given it can be increased, relevant for midnight. |
187
|
11 |
|
if ( $option <= $precision || $precision >= TimeValue::PRECISION_DAY ) { |
188
|
8 |
|
return $option; |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
|
192
|
56 |
|
return $precision; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* @param string $unsignedYear |
197
|
|
|
* |
198
|
|
|
* @return int One of the TimeValue::PRECISION_... constants. |
199
|
|
|
*/ |
200
|
17 |
|
private function getPrecisionFromYear( $unsignedYear ) { |
201
|
|
|
// default to year precision for range 4000 BC to 4000 |
202
|
17 |
|
if ( $unsignedYear <= 4000 ) { |
203
|
8 |
|
return TimeValue::PRECISION_YEAR; |
204
|
|
|
} |
205
|
|
|
|
206
|
9 |
|
$rightZeros = strlen( $unsignedYear ) - strlen( rtrim( $unsignedYear, '0' ) ); |
207
|
9 |
|
$precision = TimeValue::PRECISION_YEAR - $rightZeros; |
208
|
9 |
|
if ( $precision < TimeValue::PRECISION_YEAR1G ) { |
209
|
2 |
|
$precision = TimeValue::PRECISION_YEAR1G; |
210
|
|
|
} |
211
|
|
|
|
212
|
9 |
|
return $precision; |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Determines the calendar model. The calendar model is determined as follows: |
217
|
|
|
* |
218
|
|
|
* - if $timeParts[7] is set, use $this->calendarModelParser to parse it into a URI. |
219
|
|
|
* - otherwise, if $this->getOption( self::OPT_CALENDAR ) is not null, use |
220
|
|
|
* $this->calendarModelParser to parse it into a URI. |
221
|
|
|
* - otherwise, use self::CALENDAR_JULIAN for dates before 1583, and self::CALENDAR_GREGORIAN |
222
|
|
|
* for later dates. |
223
|
|
|
* |
224
|
|
|
* @note Keep this in sync with HtmlTimeFormatter::getDefaultCalendar(). |
225
|
|
|
* |
226
|
|
|
* @param string[] $timeParts as returned by splitTimeString() |
227
|
|
|
* |
228
|
|
|
* @return string URI |
229
|
|
|
*/ |
230
|
64 |
|
private function getCalendarModel( array $timeParts ) { |
231
|
64 |
|
list( $sign, $unsignedYear, , , , , , $calendarModel ) = $timeParts; |
232
|
|
|
|
233
|
64 |
|
if ( !empty( $calendarModel ) ) { |
234
|
9 |
|
return $this->calendarModelParser->parse( $calendarModel ); |
235
|
|
|
} |
236
|
|
|
|
237
|
55 |
|
$option = $this->getOption( self::OPT_CALENDAR ); |
238
|
|
|
|
239
|
55 |
|
if ( $option !== null ) { |
240
|
6 |
|
return $this->calendarModelParser->parse( $option ); |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
// The Gregorian calendar was introduced in October 1582, |
244
|
|
|
// so we'll default to Julian for all years before that. |
245
|
49 |
|
return $sign === '-' || $unsignedYear <= 1582 |
246
|
14 |
|
? TimeValue::CALENDAR_JULIAN |
247
|
49 |
|
: TimeValue::CALENDAR_GREGORIAN; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
} |
251
|
|
|
|
This class constant has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the constant will be removed from the class and what other constant to use instead.