Completed
Push — precisionOption ( 604f81 )
by no
02:21
created

IsoTimestampParser   B

Complexity

Total Complexity 40

Size/Duplication

Total Lines 221
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 21
Bugs 3 Features 0
Metric Value
wmc 40
c 21
b 3
f 0
lcom 1
cbo 4
dl 0
loc 221
rs 8.2608

6 Methods

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

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 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
 * @licence GNU GPL v2+
22
 * @author Addshore
23
 * @author Thiemo Mättig
24
 * @author Daniel Kinzler
25
 */
26
class IsoTimestampParser extends StringValueParser {
27
28
	const FORMAT_NAME = 'time';
29
30
	const OPT_PRECISION = 'precision';
31
	const OPT_CALENDAR = 'calendar';
32
33
	/**
34
	 * @deprecated since 0.7.1, use TimeValue::CALENDAR_GREGORIAN instead
35
	 */
36
	const CALENDAR_GREGORIAN = TimeValue::CALENDAR_GREGORIAN;
37
38
	/**
39
	 * @deprecated since 0.7.1, use TimeValue::CALENDAR_JULIAN instead
40
	 */
41
	const CALENDAR_JULIAN = TimeValue::CALENDAR_JULIAN;
42
43
	/**
44
	 * @var CalendarModelParser
45
	 */
46
	private $calendarModelParser;
47
48
	/**
49
	 * @param CalendarModelParser|null $calendarModelParser
50
	 * @param ParserOptions|null $options
51
	 */
52
	public function __construct(
53
		CalendarModelParser $calendarModelParser = null,
54
		ParserOptions $options = null
55
	) {
56
		parent::__construct( $options );
57
58
		$this->defaultOption( self::OPT_CALENDAR, null );
59
		$this->defaultOption( self::OPT_PRECISION, null );
60
61
		$this->calendarModelParser = $calendarModelParser ?: new CalendarModelParser( $this->options );
62
	}
63
64
	/**
65
	 * @param string $value
66
	 *
67
	 * @throws InvalidArgumentException
68
	 * @throws ParseException
69
	 * @return TimeValue
70
	 */
71
	protected function stringParse( $value ) {
72
		if ( !is_string( $value ) ) {
73
			throw new InvalidArgumentException( '$value must be a string' );
74
		}
75
76
		try {
77
			$timeParts = $this->splitTimeString( $value );
78
		} catch ( ParseException $ex ) {
79
			throw new ParseException( $ex->getMessage(), $value, self::FORMAT_NAME );
80
		}
81
82
		// Pad sign with 1 plus, year with 4 zeros and hour, minute and second with 2 zeros
83
		$timestamp = vsprintf( '%\'+1s%04s-%s-%sT%02s:%02s:%02sZ', $timeParts );
84
		$precision = $this->getPrecision( $timeParts );
85
		$calendarModel = $this->getCalendarModel( $timeParts );
86
87
		try {
88
			return new TimeValue( $timestamp, 0, 0, 0, $precision, $calendarModel );
89
		} catch ( IllegalValueException $ex ) {
90
			throw new ParseException( $ex->getMessage(), $value, self::FORMAT_NAME );
91
		}
92
	}
93
94
	/**
95
	 * @param string $value
96
	 *
97
	 * @throws ParseException
98
	 * @return string[] Array with index 0 => sign, 1 => year, 2 => month, 3 => day, 4 => hour,
99
	 * 5 => minute, 6 => second and 7 => calendar model.
100
	 */
101
	private function splitTimeString( $value ) {
102
		$pattern = '@^\s*'                                                //leading spaces
103
			. "([-+\xE2\x88\x92]?)\\s*"                                   //sign
104
			. '(\d{1,16})-(\d{2})-(\d{2})'                                //year, month and day
105
			. '(?:T(\d{2}):?(\d{2})(?::?(\d{2}))?)?'                      //hour, minute and second
106
			. 'Z?'                                                        //time zone
107
			. '\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...
108
			. '\s*$@iu';                                                  //trailing spaces
109
110
		if ( !preg_match( $pattern, $value, $matches ) ) {
111
			throw new ParseException( 'Malformed time' );
112
		}
113
114
		list( , $sign, $year, $month, $day, $hour, $minute, $second, $calendarModel ) = $matches;
115
116
		if ( $sign === ''
117
			&& strlen( $year ) < 3
118
			&& $year < 32
119
			&& $hour === ''
120
		) {
121
			throw new ParseException( 'Not enough information to decide if the format is YMD' );
122
		} elseif ( $month > 12 ) {
123
			throw new ParseException( 'Month out of range' );
124
		} elseif ( $day > 0 && $month < 1 ) {
125
			throw new ParseException( 'Can not have a day with no month' );
126
		} elseif ( $day > 31 ) {
127
			throw new ParseException( 'Day out of range' );
128
		} elseif ( $hour > 23 ) {
129
			throw new ParseException( 'Hour out of range' );
130
		} elseif ( $minute > 59 ) {
131
			throw new ParseException( 'Minute out of range' );
132
		} elseif ( $second > 61 ) {
133
			throw new ParseException( 'Second out of range' );
134
		}
135
136
		$sign = str_replace( "\xE2\x88\x92", '-', $sign );
137
138
		return array( $sign, $year, $month, $day, $hour, $minute, $second, $calendarModel );
139
	}
140
141
	/**
142
	 * @param string[] $timeParts Array with index 0 => sign, 1 => year, 2 => month, etc.
143
	 *
144
	 * @return int One of the TimeValue::PRECISION_... constants.
145
	 */
146
	private function getPrecision( array $timeParts ) {
147
		if ( $timeParts[6] > 0 ) {
148
			$precision = TimeValue::PRECISION_SECOND;
149
		} elseif ( $timeParts[5] > 0 ) {
150
			$precision = TimeValue::PRECISION_MINUTE;
151
		} elseif ( $timeParts[4] > 0 ) {
152
			$precision = TimeValue::PRECISION_HOUR;
153
		} elseif ( $timeParts[3] > 0
154
			// Can not have a day with no month, fall back to year precision.
155
			&& $timeParts[2] > 0
156
		) {
157
			$precision = TimeValue::PRECISION_DAY;
158
		} elseif ( $timeParts[2] > 0 ) {
159
			$precision = TimeValue::PRECISION_MONTH;
160
		} else {
161
			$precision = $this->getPrecisionFromYear( $timeParts[1] );
162
		}
163
164
		$option = $this->getOption( self::OPT_PRECISION );
165
166
		if ( $option !== null ) {
167
			if ( !is_int( $option ) && !ctype_digit( $option ) ) {
168
				throw new ParseException( 'Precision must be an integer' );
169
			}
170
171
			$option = (int)$option;
172
173
			// It's impossible to increase the detected precision via option, e.g. from year to month if
174
			// no month is given. If a day is given it can be increased, relevant for midnight.
175
			if ( $option <= $precision || $precision >= TimeValue::PRECISION_DAY ) {
176
				return $option;
177
			}
178
		}
179
180
		return $precision;
181
	}
182
183
	/**
184
	 * @param string $unsignedYear
185
	 *
186
	 * @return int One of the TimeValue::PRECISION_... constants.
187
	 */
188
	private function getPrecisionFromYear( $unsignedYear ) {
189
		// default to year precision for range 4000 BC to 4000
190
		if ( $unsignedYear <= 4000 ) {
191
			return TimeValue::PRECISION_YEAR;
192
		}
193
194
		$rightZeros = strlen( $unsignedYear ) - strlen( rtrim( $unsignedYear, '0' ) );
195
		$precision = TimeValue::PRECISION_YEAR - $rightZeros;
196
		if ( $precision < TimeValue::PRECISION_YEAR1G ) {
197
			$precision = TimeValue::PRECISION_YEAR1G;
198
		}
199
200
		return $precision;
201
	}
202
203
	/**
204
	 * Determines the calendar model. The calendar model is determined as follows:
205
	 *
206
	 * - if $timeParts[7] is set, use $this->calendarModelParser to parse it into a URI.
207
	 * - otherwise, if $this->getOption( self::OPT_CALENDAR ) is not null, return
208
	 *   self::CALENDAR_JULIAN if the option is self::CALENDAR_JULIAN, and self::CALENDAR_GREGORIAN
209
	 *   otherwise.
210
	 * - otherwise, use self::CALENDAR_JULIAN for dates before 1583, and self::CALENDAR_GREGORIAN
211
	 *   for later dates.
212
	 *
213
	 * @note Keep this in sync with HtmlTimeFormatter::getDefaultCalendar().
214
	 *
215
	 * @param string[] $timeParts as returned by splitTimeString()
216
	 *
217
	 * @return string URI
218
	 */
219
	private function getCalendarModel( array $timeParts ) {
220
		list( $sign, $unsignedYear, , , , , , $calendarModel ) = $timeParts;
221
222
		if ( !empty( $calendarModel ) ) {
223
			return $this->calendarModelParser->parse( $calendarModel );
224
		}
225
226
		$option = $this->getOption( self::OPT_CALENDAR );
227
228
		// Use the calendar given in the option, if given
229
		if ( $option !== null ) {
230
			// The calendar model is an URI and URIs can't be case-insensitive
231
			switch ( $option ) {
232
				case TimeValue::CALENDAR_JULIAN:
233
					return TimeValue::CALENDAR_JULIAN;
234
				default:
235
					return TimeValue::CALENDAR_GREGORIAN;
236
			}
237
		}
238
239
		// The Gregorian calendar was introduced in October 1582,
240
		// so we'll default to Julian for all years before that.
241
		return $sign === '-' || $unsignedYear <= 1582
242
			? TimeValue::CALENDAR_JULIAN
243
			: TimeValue::CALENDAR_GREGORIAN;
244
	}
245
246
}
247