Completed
Push — master ( 18fd13...95cdad )
by Marius
24s
created

IsoTimestampParser::splitTimeString()   D

Complexity

Conditions 17
Paths 10

Size

Total Lines 47
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 47
rs 4.9547
cc 17
eloc 32
nc 10
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace ValueParsers;
4
5
use DataValues\IllegalValueException;
6
use DataValues\TimeValue;
7
use InvalidArgumentException;
8
9
/**
10
 * ValueParser that parses various string representations of time values, in YMD ordered formats
11
 * resembling ISO 8601, e.g. +2013-01-01T00:00:00Z. While the parser tries to be relaxed, certain
12
 * aspects of the ISO norm are obligatory: The order must be YMD. All elements but the year must
13
 * have 2 digits. The separation characters must be dashes (in the date part), "T" and colons (in
14
 * the time part).
15
 *
16
 * The parser refuses to parse strings that can be parsed differently by other, locale-aware
17
 * parsers, e.g. 01-02-03 can be in YMD, DMY or MDY order depending on the language.
18
 *
19
 * @since 0.7 renamed from TimeParser to IsoTimestampParser.
20
 *
21
 * @license GPL-2.0+
22
 * @author Addshore
23
 * @author Thiemo Mättig
24
 * @author Daniel Kinzler
25
 */
26
class IsoTimestampParser extends StringValueParser {
27
28
	const FORMAT_NAME = 'iso-timestamp';
29
30
	/**
31
	 * Option to override the precision auto-detection and set a specific precision. Should be an
32
	 * integer or string containing one of the TimeValue::PRECISION_... constants.
33
	 */
34
	const OPT_PRECISION = 'precision';
35
36
	/**
37
	 * Option to override the calendar model auto-detection and set a specific calendar model URI.
38
	 * Should be one of the TimeValue::CALENDAR_... constants.
39
	 */
40
	const OPT_CALENDAR = 'calendar';
41
42
	/**
43
	 * @deprecated since 0.7.1, use TimeValue::CALENDAR_GREGORIAN instead
44
	 */
45
	const CALENDAR_GREGORIAN = TimeValue::CALENDAR_GREGORIAN;
46
47
	/**
48
	 * @deprecated since 0.7.1, use TimeValue::CALENDAR_JULIAN instead
49
	 */
50
	const CALENDAR_JULIAN = TimeValue::CALENDAR_JULIAN;
51
52
	/**
53
	 * @var CalendarModelParser
54
	 */
55
	private $calendarModelParser;
56
57
	/**
58
	 * @param CalendarModelParser|null $calendarModelParser
59
	 * @param ParserOptions|null $options
60
	 */
61
	public function __construct(
62
		CalendarModelParser $calendarModelParser = null,
63
		ParserOptions $options = null
64
	) {
65
		parent::__construct( $options );
66
67
		$this->defaultOption( self::OPT_CALENDAR, null );
68
		$this->defaultOption( self::OPT_PRECISION, null );
69
70
		$this->calendarModelParser = $calendarModelParser ?: new CalendarModelParser( $this->options );
71
	}
72
73
	/**
74
	 * @param string $value
75
	 *
76
	 * @throws InvalidArgumentException
77
	 * @throws ParseException
78
	 * @return TimeValue
79
	 */
80
	protected function stringParse( $value ) {
81
		if ( !is_string( $value ) ) {
82
			throw new InvalidArgumentException( '$value must be a string' );
83
		}
84
85
		try {
86
			$timeParts = $this->splitTimeString( $value );
87
		} catch ( ParseException $ex ) {
88
			throw new ParseException( $ex->getMessage(), $value, self::FORMAT_NAME );
89
		}
90
91
		// Pad sign with 1 plus, year with 4 zeros and hour, minute and second with 2 zeros
92
		$timestamp = vsprintf( '%\'+1s%04s-%s-%sT%02s:%02s:%02sZ', $timeParts );
93
		$precision = $this->getPrecision( $timeParts );
94
		$calendarModel = $this->getCalendarModel( $timeParts );
95
96
		try {
97
			return new TimeValue( $timestamp, 0, 0, 0, $precision, $calendarModel );
98
		} catch ( IllegalValueException $ex ) {
99
			throw new ParseException( $ex->getMessage(), $value, self::FORMAT_NAME );
100
		}
101
	}
102
103
	/**
104
	 * @param string $value
105
	 *
106
	 * @throws ParseException
107
	 * @return string[] Array with index 0 => sign, 1 => year, 2 => month, 3 => day, 4 => hour,
108
	 * 5 => minute, 6 => second and 7 => calendar model.
109
	 */
110
	private function splitTimeString( $value ) {
111
		$pattern = '@^\s*'                                                //leading spaces
112
			. "([-+\xE2\x88\x92]?)\\s*"                                   //sign
113
			. '(\d{1,16})-(\d{2})-(\d{2})'                                //year, month and day
114
			. '(?:T(\d{2}):?(\d{2})(?::?(\d{2}))?)?'                      //hour, minute and second
115
			. 'Z?'                                                        //time zone
116
			. '\s*\(?\s*' . CalendarModelParser::MODEL_PATTERN . '\s*\)?' //calendar model
0 ignored issues
show
Deprecated Code introduced by
The constant ValueParsers\CalendarModelParser::MODEL_PATTERN has been deprecated with message: Do not use. Regex pattern constant matching the parable calendar models
should be used as an insensitive to match all cases TODO: How crucial is it that this regex is in sync with the list below?

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.

Loading history...
117
			. '\s*$@iu';                                                  //trailing spaces
118
119
		if ( !preg_match( $pattern, $value, $matches ) ) {
120
			throw new ParseException( 'Malformed time' );
121
		}
122
123
		list( , $sign, $year, $month, $day, $hour, $minute, $second, $calendarModel ) = $matches;
124
125
		if ( $sign === ''
126
			&& strlen( $year ) < 3
127
			&& $year < 32
128
			&& $hour === ''
129
		) {
130
			throw new ParseException( 'Not enough information to decide if the format is YMD' );
131
		}
132
133
		if ( $month > 12 ) {
134
			throw new ParseException( 'Month out of range' );
135
		} elseif ( $day > 31 ) {
136
			throw new ParseException( 'Day out of range' );
137
		} elseif ( $hour > 23 ) {
138
			throw new ParseException( 'Hour out of range' );
139
		} elseif ( $minute > 59 ) {
140
			throw new ParseException( 'Minute out of range' );
141
		} elseif ( $second > 61 ) {
142
			throw new ParseException( 'Second out of range' );
143
		}
144
145
		if ( $month < 1 && $day > 0 ) {
146
			throw new ParseException( 'Can not have a day with no month' );
147
		}
148
149
		if ( $day < 1 && ( $hour > 0 || $minute > 0 || $second > 0 ) ) {
150
			throw new ParseException( 'Can not have hour, minute or second with no day' );
151
		}
152
153
		$sign = str_replace( "\xE2\x88\x92", '-', $sign );
154
155
		return array( $sign, $year, $month, $day, $hour, $minute, $second, $calendarModel );
156
	}
157
158
	/**
159
	 * @param string[] $timeParts Array with index 0 => sign, 1 => year, 2 => month, etc.
160
	 *
161
	 * @throws ParseException
162
	 * @return int One of the TimeValue::PRECISION_... constants.
163
	 */
164
	private function getPrecision( array $timeParts ) {
165
		if ( $timeParts[6] > 0 ) {
166
			$precision = TimeValue::PRECISION_SECOND;
167
		} elseif ( $timeParts[5] > 0 ) {
168
			$precision = TimeValue::PRECISION_MINUTE;
169
		} elseif ( $timeParts[4] > 0 ) {
170
			$precision = TimeValue::PRECISION_HOUR;
171
		} elseif ( $timeParts[3] > 0
172
			// Can not have a day with no month, fall back to year precision.
173
			&& $timeParts[2] > 0
174
		) {
175
			$precision = TimeValue::PRECISION_DAY;
176
		} elseif ( $timeParts[2] > 0 ) {
177
			$precision = TimeValue::PRECISION_MONTH;
178
		} else {
179
			$precision = $this->getPrecisionFromYear( $timeParts[1] );
180
		}
181
182
		$option = $this->getOption( self::OPT_PRECISION );
183
184
		if ( $option !== null ) {
185
			if ( !is_int( $option ) && !ctype_digit( $option ) ) {
186
				throw new ParseException( 'Precision must be an integer' );
187
			}
188
189
			$option = (int)$option;
190
191
			// It's impossible to increase the detected precision via option, e.g. from year to month if
192
			// no month is given. If a day is given it can be increased, relevant for midnight.
193
			if ( $option <= $precision || $precision >= TimeValue::PRECISION_DAY ) {
194
				return $option;
195
			}
196
		}
197
198
		return $precision;
199
	}
200
201
	/**
202
	 * @param string $unsignedYear
203
	 *
204
	 * @return int One of the TimeValue::PRECISION_... constants.
205
	 */
206
	private function getPrecisionFromYear( $unsignedYear ) {
207
		// default to year precision for range 4000 BC to 4000
208
		if ( $unsignedYear <= 4000 ) {
209
			return TimeValue::PRECISION_YEAR;
210
		}
211
212
		$rightZeros = strlen( $unsignedYear ) - strlen( rtrim( $unsignedYear, '0' ) );
213
		$precision = TimeValue::PRECISION_YEAR - $rightZeros;
214
		if ( $precision < TimeValue::PRECISION_YEAR1G ) {
215
			$precision = TimeValue::PRECISION_YEAR1G;
216
		}
217
218
		return $precision;
219
	}
220
221
	/**
222
	 * Determines the calendar model. The calendar model is determined as follows:
223
	 *
224
	 * - if $timeParts[7] is set, use $this->calendarModelParser to parse it into a URI.
225
	 * - otherwise, if $this->getOption( self::OPT_CALENDAR ) is not null, return
226
	 *   self::CALENDAR_JULIAN if the option is self::CALENDAR_JULIAN, and self::CALENDAR_GREGORIAN
227
	 *   otherwise.
228
	 * - otherwise, use self::CALENDAR_JULIAN for dates before 1583, and self::CALENDAR_GREGORIAN
229
	 *   for later dates.
230
	 *
231
	 * @note Keep this in sync with HtmlTimeFormatter::getDefaultCalendar().
232
	 *
233
	 * @param string[] $timeParts as returned by splitTimeString()
234
	 *
235
	 * @return string URI
236
	 */
237
	private function getCalendarModel( array $timeParts ) {
238
		list( $sign, $unsignedYear, , , , , , $calendarModel ) = $timeParts;
239
240
		if ( !empty( $calendarModel ) ) {
241
			return $this->calendarModelParser->parse( $calendarModel );
242
		}
243
244
		$option = $this->getOption( self::OPT_CALENDAR );
245
246
		// Use the calendar given in the option, if given
247
		if ( $option !== null ) {
248
			// The calendar model is an URI and URIs can't be case-insensitive
249
			switch ( $option ) {
250
				case TimeValue::CALENDAR_JULIAN:
251
					return TimeValue::CALENDAR_JULIAN;
252
				default:
253
					return TimeValue::CALENDAR_GREGORIAN;
254
			}
255
		}
256
257
		// The Gregorian calendar was introduced in October 1582,
258
		// so we'll default to Julian for all years before that.
259
		return $sign === '-' || $unsignedYear <= 1582
260
			? TimeValue::CALENDAR_JULIAN
261
			: TimeValue::CALENDAR_GREGORIAN;
262
	}
263
264
}
265