Passed
Push — monthNameProvider ( 285575...e7bdd9 )
by no
02:32
created

IsoTimestampParser::getPrecision()   C

Complexity

Conditions 12
Paths 24

Size

Total Lines 36
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 2 Features 0
Metric Value
c 6
b 2
f 0
dl 0
loc 36
rs 5.1612
cc 12
eloc 22
nc 24
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 seperation 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
		} elseif ( $month > 12 ) {
132
			throw new ParseException( 'Month out of range' );
133
		} elseif ( $day > 0 && $month < 1 ) {
134
			throw new ParseException( 'Can not have a day with no month' );
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
		$sign = str_replace( "\xE2\x88\x92", '-', $sign );
146
147
		return array( $sign, $year, $month, $day, $hour, $minute, $second, $calendarModel );
148
	}
149
150
	/**
151
	 * @param string[] $timeParts Array with index 0 => sign, 1 => year, 2 => month, etc.
152
	 *
153
	 * @throws ParseException
154
	 * @return int One of the TimeValue::PRECISION_... constants.
155
	 */
156
	private function getPrecision( array $timeParts ) {
157
		if ( $timeParts[6] > 0 ) {
158
			$precision = TimeValue::PRECISION_SECOND;
159
		} elseif ( $timeParts[5] > 0 ) {
160
			$precision = TimeValue::PRECISION_MINUTE;
161
		} elseif ( $timeParts[4] > 0 ) {
162
			$precision = TimeValue::PRECISION_HOUR;
163
		} elseif ( $timeParts[3] > 0
164
			// Can not have a day with no month, fall back to year precision.
165
			&& $timeParts[2] > 0
166
		) {
167
			$precision = TimeValue::PRECISION_DAY;
168
		} elseif ( $timeParts[2] > 0 ) {
169
			$precision = TimeValue::PRECISION_MONTH;
170
		} else {
171
			$precision = $this->getPrecisionFromYear( $timeParts[1] );
172
		}
173
174
		$option = $this->getOption( self::OPT_PRECISION );
175
176
		if ( $option !== null ) {
177
			if ( !is_int( $option ) && !ctype_digit( $option ) ) {
178
				throw new ParseException( 'Precision must be an integer' );
179
			}
180
181
			$option = (int)$option;
182
183
			// It's impossible to increase the detected precision via option, e.g. from year to month if
184
			// no month is given. If a day is given it can be increased, relevant for midnight.
185
			if ( $option <= $precision || $precision >= TimeValue::PRECISION_DAY ) {
186
				return $option;
187
			}
188
		}
189
190
		return $precision;
191
	}
192
193
	/**
194
	 * @param string $unsignedYear
195
	 *
196
	 * @return int One of the TimeValue::PRECISION_... constants.
197
	 */
198
	private function getPrecisionFromYear( $unsignedYear ) {
199
		// default to year precision for range 4000 BC to 4000
200
		if ( $unsignedYear <= 4000 ) {
201
			return TimeValue::PRECISION_YEAR;
202
		}
203
204
		$rightZeros = strlen( $unsignedYear ) - strlen( rtrim( $unsignedYear, '0' ) );
205
		$precision = TimeValue::PRECISION_YEAR - $rightZeros;
206
		if ( $precision < TimeValue::PRECISION_YEAR1G ) {
207
			$precision = TimeValue::PRECISION_YEAR1G;
208
		}
209
210
		return $precision;
211
	}
212
213
	/**
214
	 * Determines the calendar model. The calendar model is determined as follows:
215
	 *
216
	 * - if $timeParts[7] is set, use $this->calendarModelParser to parse it into a URI.
217
	 * - otherwise, if $this->getOption( self::OPT_CALENDAR ) is not null, return
218
	 *   self::CALENDAR_JULIAN if the option is self::CALENDAR_JULIAN, and self::CALENDAR_GREGORIAN
219
	 *   otherwise.
220
	 * - otherwise, use self::CALENDAR_JULIAN for dates before 1583, and self::CALENDAR_GREGORIAN
221
	 *   for later dates.
222
	 *
223
	 * @note Keep this in sync with HtmlTimeFormatter::getDefaultCalendar().
224
	 *
225
	 * @param string[] $timeParts as returned by splitTimeString()
226
	 *
227
	 * @return string URI
228
	 */
229
	private function getCalendarModel( array $timeParts ) {
230
		list( $sign, $unsignedYear, , , , , , $calendarModel ) = $timeParts;
231
232
		if ( !empty( $calendarModel ) ) {
233
			return $this->calendarModelParser->parse( $calendarModel );
234
		}
235
236
		$option = $this->getOption( self::OPT_CALENDAR );
237
238
		// Use the calendar given in the option, if given
239
		if ( $option !== null ) {
240
			// The calendar model is an URI and URIs can't be case-insensitive
241
			switch ( $option ) {
242
				case TimeValue::CALENDAR_JULIAN:
243
					return TimeValue::CALENDAR_JULIAN;
244
				default:
245
					return TimeValue::CALENDAR_GREGORIAN;
246
			}
247
		}
248
249
		// The Gregorian calendar was introduced in October 1582,
250
		// so we'll default to Julian for all years before that.
251
		return $sign === '-' || $unsignedYear <= 1582
252
			? TimeValue::CALENDAR_JULIAN
253
			: TimeValue::CALENDAR_GREGORIAN;
254
	}
255
256
}
257