Completed
Pull Request — master (#83)
by no
05:18 queued 02:36
created

DateFormatParser   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 248
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 48
c 1
b 0
f 1
lcom 1
cbo 3
dl 0
loc 248
rs 8.4865

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
C stringParse() 0 41 8
C parseDateFormat() 0 77 26
A getMonthNamesPattern() 0 11 2
A findMonthMatch() 0 9 3
A parseFormattedNumber() 0 11 2
A getNumberCharacters() 0 10 2
A getDateFormat() 0 3 1
A getDigitTransformTable() 0 3 1
A getMonthNames() 0 3 2

How to fix   Complexity   

Complex Class

Complex classes like DateFormatParser 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 DateFormatParser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace ValueParsers;
4
5
use DataValues\TimeValue;
6
7
/**
8
 * This parser is in essence the inverse operation of Language::sprintfDate.
9
 *
10
 * @see Language::sprintfDate
11
 *
12
 * @since 0.8.1
13
 *
14
 * @licence GNU GPL v2+
15
 * @author Thiemo Mättig
16
 */
17
class DateFormatParser extends StringValueParser {
18
19
	const FORMAT_NAME = 'datetime';
20
21
	const OPT_DATE_FORMAT = 'dateFormat';
22
23
	/**
24
	 * Option for unlocalizing non-canonical digits. Must be an array of strings, mapping canonical
25
	 * digit characters ("1", "2" and so on, possibly including "." and ",") to localized
26
	 * characters.
27
	 */
28
	const OPT_DIGIT_TRANSFORM_TABLE = 'digitTransformTable';
29
30
	/**
31
	 * Option for localized month names. Should be a two-dimensional array, the first dimension
32
	 * mapping the month's numbers 1 to 12 to arrays of localized month names, possibly including
33
	 * full month names, genitive names and abbreviations. Can also be a one-dimensional array of
34
	 * strings.
35
	 */
36
	const OPT_MONTH_NAMES = 'monthNames';
37
38
	public function __construct( ParserOptions $options = null ) {
39
		parent::__construct( $options );
40
41
		$this->defaultOption( self::OPT_DATE_FORMAT, 'j F Y' );
42
		// FIXME: Should not be an option. Options should be trivial, never arrays or objects!
43
		$this->defaultOption( self::OPT_DIGIT_TRANSFORM_TABLE, null );
44
		$this->defaultOption( self::OPT_MONTH_NAMES, null );
45
	}
46
47
	/**
48
	 * @see StringValueParser::stringParse
49
	 *
50
	 * @param string $value
51
	 *
52
	 * @throws ParseException
53
	 * @return TimeValue
54
	 */
55
	protected function stringParse( $value ) {
56
		$pattern = $this->parseDateFormat( $this->getDateFormat() );
57
58
		if ( @preg_match( '<^\p{Z}*' . $pattern . '$>iu', $value, $matches )
59
			&& isset( $matches['year'] )
60
		) {
61
			$precision = TimeValue::PRECISION_YEAR;
62
			$time = array( $this->parseFormattedNumber( $matches['year'] ), 0, 0, 0, 0, 0 );
63
64
			if ( isset( $matches['month'] ) ) {
65
				$precision = TimeValue::PRECISION_MONTH;
66
				$time[1] = $this->findMonthMatch( $matches );
67
			}
68
69
			if ( isset( $matches['day'] ) ) {
70
				$precision = TimeValue::PRECISION_DAY;
71
				$time[2] = $this->parseFormattedNumber( $matches['day'] );
72
			}
73
74
			if ( isset( $matches['hour'] ) ) {
75
				$precision = TimeValue::PRECISION_HOUR;
76
				$time[3] = $this->parseFormattedNumber( $matches['hour'] );
77
			}
78
79
			if ( isset( $matches['minute'] ) ) {
80
				$precision = TimeValue::PRECISION_MINUTE;
81
				$time[4] = $this->parseFormattedNumber( $matches['minute'] );
82
			}
83
84
			if ( isset( $matches['second'] ) ) {
85
				$precision = TimeValue::PRECISION_SECOND;
86
				$time[5] = $this->parseFormattedNumber( $matches['second'] );
87
			}
88
89
			$timestamp = vsprintf( '+%04s-%02s-%02sT%02s:%02s:%02sZ', $time );
90
			return new TimeValue( $timestamp, 0, 0, 0, $precision, TimeValue::CALENDAR_GREGORIAN );
91
		}
92
93
		throw new ParseException( "Failed to parse $value ("
94
			. $this->parseFormattedNumber( $value ) . ')', $value );
95
	}
96
97
	/**
98
	 * @see Language::sprintfDate
99
	 *
100
	 * @param string $format A date format, as described in Language::sprintfDate.
101
	 *
102
	 * @return string Regular expression
103
	 */
104
	private function parseDateFormat( $format ) {
105
		$length = strlen( $format );
106
		$numberPattern = '[' . $this->getNumberCharacters() . ']';
107
		$pattern = '';
108
109
		for ( $p = 0; $p < $length; $p++ ) {
110
			$code = $format[$p];
111
112
			if ( $code === 'x' && $p < $length - 1 ) {
113
				$code .= $format[++$p];
114
			}
115
116
			if ( preg_match( '<^x[ijkmot]$>', $code ) && $p < $length - 1 ) {
117
				$code .= $format[++$p];
118
			}
119
120
			switch ( $code ) {
121
				case 'Y':
122
					$pattern .= '(?P<year>' . $numberPattern . '+)\p{Z}*';
123
					break;
124
				case 'F':
125
				case 'm':
126
				case 'M':
127
				case 'n':
128
				case 'xg':
129
					$pattern .= '(?P<month>' . $numberPattern . '{1,2}'
130
						. $this->getMonthNamesPattern()
131
						. ')\p{P}*\p{Z}*';
132
					break;
133
				case 'd':
134
				case 'j':
135
					$pattern .= '(?P<day>' . $numberPattern . '{1,2})\p{P}*\p{Z}*';
136
					break;
137
				case 'G':
138
				case 'H':
139
					$pattern .= '(?P<hour>' . $numberPattern . '{1,2})\p{Z}*';
140
					break;
141
				case 'i':
142
					$pattern .= '(?P<minute>' . $numberPattern . '{1,2})\p{Z}*';
143
					break;
144
				case 's':
145
					$pattern .= '(?P<second>' . $numberPattern . '{1,2})\p{Z}*';
146
					break;
147
				case '\\':
148
					if ( $p < $length - 1 ) {
149
						$pattern .= preg_quote( $format[++$p] );
150
					} else {
151
						$pattern .= '\\';
152
					}
153
					break;
154
				case '"':
155
					$endQuote = strpos( $format, '"', $p + 1 );
156
					if ( $endQuote !== false ) {
157
						$pattern .= preg_quote( substr( $format, $p + 1, $endQuote - $p - 1 ) );
158
						$p = $endQuote;
159
					} else {
160
						$pattern .= '"';
161
					}
162
					break;
163
				case 'xn':
164
				case 'xN':
165
					// We can ignore raw and raw toggle when parsing, because we always accept
166
					// canonical digits.
167
					break;
168
				default:
169
					if ( preg_match( '<^\p{P}+$>u', $format[$p] ) ) {
170
						$pattern .= '\p{P}*';
171
					} elseif ( preg_match( '<^\p{Z}+$>u', $format[$p] ) ) {
172
						$pattern .= '\p{Z}*';
173
					} else {
174
						$pattern .= preg_quote( $format[$p] );
175
					}
176
			}
177
		}
178
179
		return $pattern;
180
	}
181
182
	/**
183
	 * @return string
184
	 */
185
	private function getMonthNamesPattern() {
186
		$pattern = '';
187
188
		foreach ( $this->getMonthNames() as $i => $monthNames ) {
189
			$pattern .= '|(?P<month' . $i . '>'
190
				. implode( '|', array_map( 'preg_quote', (array)$monthNames ) )
191
				. ')';
192
		}
193
194
		return $pattern;
195
	}
196
197
	/**
198
	 * @param string[] $matches
199
	 *
200
	 * @return int
201
	 */
202
	private function findMonthMatch( $matches ) {
203
		for ( $i = 1; $i <= 12; $i++ ) {
204
			if ( !empty( $matches['month' . $i] ) ) {
205
				return $i;
206
			}
207
		}
208
209
		return $this->parseFormattedNumber( $matches['month'] );
210
	}
211
212
	/**
213
	 * @param string $number
214
	 *
215
	 * @return string
216
	 */
217
	private function parseFormattedNumber( $number ) {
218
		$transformTable = $this->getDigitTransformTable();
219
220
		if ( is_array( $transformTable ) ) {
221
			// Eliminate empty array values (bug T66347).
222
			$transformTable = array_filter( $transformTable );
223
			$number = strtr( $number, array_flip( $transformTable ) );
224
		}
225
226
		return $number;
227
	}
228
229
	/**
230
	 * @return string
231
	 */
232
	private function getNumberCharacters() {
233
		$numberCharacters = '\d';
234
235
		$transformTable = $this->getDigitTransformTable();
236
		if ( is_array( $transformTable ) ) {
237
			$numberCharacters .= preg_quote( implode( '', $transformTable ) );
238
		}
239
240
		return $numberCharacters;
241
	}
242
243
	/**
244
	 * @return string
245
	 */
246
	private function getDateFormat() {
247
		return $this->getOption( self::OPT_DATE_FORMAT );
248
	}
249
250
	/**
251
	 * @return string[]|null
252
	 */
253
	private function getDigitTransformTable() {
254
		return $this->getOption( self::OPT_DIGIT_TRANSFORM_TABLE );
255
	}
256
257
	/**
258
	 * @return array[]
259
	 */
260
	private function getMonthNames() {
261
		return $this->getOption( self::OPT_MONTH_NAMES ) ?: array();
262
	}
263
264
}
265