Passed
Push — master ( 598343...22ce15 )
by
unknown
03:14
created

IsoTimestampParser   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 239
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 97.67%

Importance

Changes 0
Metric Value
wmc 44
c 0
b 0
f 0
lcom 1
cbo 4
dl 0
loc 239
ccs 84
cts 86
cp 0.9767
rs 8.3396

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 2
B stringParse() 0 22 4
C getPrecision() 0 36 12
A getPrecisionFromYear() 0 14 3
B getCalendarModel() 0 26 6
D splitTimeString() 0 47 17

How to fix   Complexity   

Complex Class

Complex classes like IsoTimestampParser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use IsoTimestampParser, and based on these observations, apply Extract Interface, too.

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 39
	public function __construct(
62
		CalendarModelParser $calendarModelParser = null,
63
		ParserOptions $options = null
64
	) {
65 39
		parent::__construct( $options );
66
67 39
		$this->defaultOption( self::OPT_CALENDAR, null );
68 39
		$this->defaultOption( self::OPT_PRECISION, null );
69
70 39
		$this->calendarModelParser = $calendarModelParser ?: new CalendarModelParser( $this->options );
71 39
	}
72
73
	/**
74
	 * @param string $value
75
	 *
76
	 * @throws InvalidArgumentException
77
	 * @throws ParseException
78
	 * @return TimeValue
79
	 */
80 84
	protected function stringParse( $value ) {
81 84
		if ( !is_string( $value ) ) {
82
			throw new InvalidArgumentException( '$value must be a string' );
83
		}
84
85
		try {
86 84
			$timeParts = $this->splitTimeString( $value );
87 20
		} catch ( ParseException $ex ) {
88 20
			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 64
		$timestamp = vsprintf( '%\'+1s%04s-%s-%sT%02s:%02s:%02sZ', $timeParts );
93 64
		$precision = $this->getPrecision( $timeParts );
94 62
		$calendarModel = $this->getCalendarModel( $timeParts );
95
96
		try {
97 62
			return new TimeValue( $timestamp, 0, 0, 0, $precision, $calendarModel );
98 2
		} catch ( IllegalValueException $ex ) {
99 2
			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 84
	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-day
114
			. '(?:T(\d{2}):?(\d{2})(?::?(\d{2}))?)?'                      // hour:minute:second
115
			. 'Z?'                                                        // time zone
116 84
			. '\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 84
			. '\s*$@iu';                                                  // trailing spaces
118
119 84
		if ( !preg_match( $pattern, $value, $matches ) ) {
120 7
			throw new ParseException( 'Malformed time' );
121
		}
122
123 77
		list( , $sign, $year, $month, $day, $hour, $minute, $second, $calendarModel ) = $matches;
124
125 77
		if ( $sign === ''
126 77
			&& strlen( $year ) < 3
127 77
			&& $year < 32
128 77
			&& $hour === ''
129
		) {
130 4
			throw new ParseException( 'Not enough information to decide if the format is YMD' );
131
		}
132
133 73
		if ( $month > 12 ) {
134 1
			throw new ParseException( 'Month out of range' );
135 72
		} elseif ( $day > 31 ) {
136 1
			throw new ParseException( 'Day out of range' );
137 71
		} elseif ( $hour > 23 ) {
138 1
			throw new ParseException( 'Hour out of range' );
139 70
		} elseif ( $minute > 59 ) {
140 1
			throw new ParseException( 'Minute out of range' );
141 69
		} elseif ( $second > 61 ) {
142 1
			throw new ParseException( 'Second out of range' );
143
		}
144
145 68
		if ( $month < 1 && $day > 0 ) {
146 1
			throw new ParseException( 'Can not have a day with no month' );
147
		}
148
149 67
		if ( $day < 1 && ( $hour > 0 || $minute > 0 || $second > 0 ) ) {
150 3
			throw new ParseException( 'Can not have hour, minute or second with no day' );
151
		}
152
153 64
		$sign = str_replace( "\xE2\x88\x92", '-', $sign );
154
155 64
		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 64
	private function getPrecision( array $timeParts ) {
165 64
		if ( $timeParts[6] > 0 ) {
166 2
			$precision = TimeValue::PRECISION_SECOND;
167 62
		} elseif ( $timeParts[5] > 0 ) {
168 1
			$precision = TimeValue::PRECISION_MINUTE;
169 61
		} elseif ( $timeParts[4] > 0 ) {
170
			$precision = TimeValue::PRECISION_HOUR;
171 61
		} elseif ( $timeParts[3] > 0
172
			// Can not have a day with no month, fall back to year precision.
173 61
			&& $timeParts[2] > 0
174
		) {
175 40
			$precision = TimeValue::PRECISION_DAY;
176 21
		} elseif ( $timeParts[2] > 0 ) {
177 4
			$precision = TimeValue::PRECISION_MONTH;
178
		} else {
179 17
			$precision = $this->getPrecisionFromYear( $timeParts[1] );
180
		}
181
182 64
		$option = $this->getOption( self::OPT_PRECISION );
183
184 64
		if ( $option !== null ) {
185 13
			if ( !is_int( $option ) && !ctype_digit( $option ) ) {
186 2
				throw new ParseException( 'Precision must be an integer' );
187
			}
188
189 11
			$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 11
			if ( $option <= $precision || $precision >= TimeValue::PRECISION_DAY ) {
194 8
				return $option;
195
			}
196
		}
197
198 54
		return $precision;
199
	}
200
201
	/**
202
	 * @param string $unsignedYear
203
	 *
204
	 * @return int One of the TimeValue::PRECISION_... constants.
205
	 */
206 17
	private function getPrecisionFromYear( $unsignedYear ) {
207
		// default to year precision for range 4000 BC to 4000
208 17
		if ( $unsignedYear <= 4000 ) {
209 8
			return TimeValue::PRECISION_YEAR;
210
		}
211
212 9
		$rightZeros = strlen( $unsignedYear ) - strlen( rtrim( $unsignedYear, '0' ) );
213 9
		$precision = TimeValue::PRECISION_YEAR - $rightZeros;
214 9
		if ( $precision < TimeValue::PRECISION_YEAR1G ) {
215 2
			$precision = TimeValue::PRECISION_YEAR1G;
216
		}
217
218 9
		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 62
	private function getCalendarModel( array $timeParts ) {
238 62
		list( $sign, $unsignedYear, , , , , , $calendarModel ) = $timeParts;
239
240 62
		if ( !empty( $calendarModel ) ) {
241 9
			return $this->calendarModelParser->parse( $calendarModel );
242
		}
243
244 53
		$option = $this->getOption( self::OPT_CALENDAR );
245
246
		// Use the calendar given in the option, if given
247 53
		if ( $option !== null ) {
248
			// The calendar model is an URI and URIs can't be case-insensitive
249
			switch ( $option ) {
250 4
				case TimeValue::CALENDAR_JULIAN:
251 2
					return TimeValue::CALENDAR_JULIAN;
252
				default:
253 2
					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 49
		return $sign === '-' || $unsignedYear <= 1582
260 14
			? TimeValue::CALENDAR_JULIAN
261 49
			: TimeValue::CALENDAR_GREGORIAN;
262
	}
263
264
}
265