MwTimeIsoFormatter::getLocalizedYear()   D
last analyzed

Complexity

Conditions 17
Paths 60

Size

Total Lines 92

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 92
rs 4.3478
c 0
b 0
f 0
cc 17
nc 60
nop 2

How to fix   Long Method    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 Wikibase\Lib\Formatters;
4
5
use DataValues\TimeValue;
6
use InvalidArgumentException;
7
use Language;
8
use ValueFormatters\FormatterOptions;
9
use ValueFormatters\ValueFormatter;
10
use ValueFormatters\ValueFormatterBase;
11
12
/**
13
 * @license GPL-2.0-or-later
14
 * @author H. Snater < [email protected] >
15
 * @author Addshore
16
 * @author Thiemo Kreuz
17
 *
18
 * @todo move me to DataValues-time
19
 */
20
class MwTimeIsoFormatter extends ValueFormatterBase {
21
22
	/**
23
	 * @var Language
24
	 */
25
	private $language;
26
27
	/**
28
	 * @param FormatterOptions|null $options
29
	 */
30
	public function __construct( FormatterOptions $options = null ) {
31
		parent::__construct( $options );
32
33
		$languageCode = $this->getOption( ValueFormatter::OPT_LANG );
34
		$this->language = Language::factory( $languageCode );
35
	}
36
37
	/**
38
	 * @see ValueFormatter::format
39
	 *
40
	 * @param TimeValue $value
41
	 *
42
	 * @throws InvalidArgumentException
43
	 * @return string Text
44
	 */
45
	public function format( $value ) {
46
		if ( !( $value instanceof TimeValue ) ) {
47
			throw new InvalidArgumentException( 'Data value type mismatch. Expected a TimeValue.' );
48
		}
49
50
		return $this->formatTimeValue( $value );
51
	}
52
53
	/**
54
	 * @param TimeValue $timeValue
55
	 *
56
	 * @return string Text
57
	 */
58
	private function formatTimeValue( TimeValue $timeValue ) {
59
		$isoTimestamp = $timeValue->getTime();
60
61
		try {
62
			return $this->getLocalizedDate( $isoTimestamp, $timeValue->getPrecision() );
63
		} catch ( InvalidArgumentException $ex ) {
64
			return $isoTimestamp;
65
		}
66
	}
67
68
	/**
69
	 * @param string $isoTimestamp
70
	 * @param int $precision
71
	 *
72
	 * @throws InvalidArgumentException
73
	 * @return string Formatted date
74
	 */
75
	private function getLocalizedDate( $isoTimestamp, $precision ) {
76
		$localizedYear = $this->getLocalizedYear( $isoTimestamp, $precision );
77
78
		if ( $precision <= TimeValue::PRECISION_YEAR ) {
79
			return $localizedYear;
80
		}
81
82
		$dateFormat = $this->getDateFormat( $precision );
83
		$mwTimestamp = $this->getMwTimestamp( $isoTimestamp, $precision );
84
		$mwYear = $this->language->sprintfDate( 'Y', $mwTimestamp );
85
		$localizedDate = $this->language->sprintfDate( $dateFormat, $mwTimestamp );
86
87
		if ( $mwYear !== $localizedYear ) {
88
			// Check if we can reliably fix the year. This should never fail as
89
			// Language::sprintfDate should always return a 4 digit year.
90
			if ( substr_count( $localizedDate, $mwYear ) !== 1 ) {
91
				throw new InvalidArgumentException( 'Cannot identify year in formatted date.' );
92
			}
93
94
			$localizedDate = str_replace( $mwYear, $localizedYear, $localizedDate );
95
		}
96
97
		return $localizedDate;
98
	}
99
100
	/**
101
	 * @param int $precision
102
	 *
103
	 * @throws InvalidArgumentException
104
	 * @return string Date format string to be used by Language::sprintfDate
105
	 */
106
	private function getDateFormat( $precision ) {
107
		$datePreference = 'default';
108
109
		$datePreferences = $this->language->getDatePreferences();
110
		if ( is_array( $datePreferences ) && in_array( 'dmy', $datePreferences ) ) {
111
			$datePreference = 'dmy';
112
		}
113
114
		if ( $precision === TimeValue::PRECISION_MONTH ) {
115
			$format = $this->language->getDateFormatString( 'monthonly', $datePreference );
116
			return sprintf( '%s Y', $this->getMonthFormat( $format ) );
117
		} elseif ( $precision === TimeValue::PRECISION_DAY ) {
118
			$format = $this->language->getDateFormatString( 'date', $datePreference );
119
			return sprintf( '%s %s Y', $this->getDayFormat( $format ), $this->getMonthFormat( $format ) );
120
		} else {
121
			throw new InvalidArgumentException( 'Unsupported precision' );
122
		}
123
	}
124
125
	/**
126
	 * @see Language::sprintfDate
127
	 *
128
	 * @param string $dateFormat
129
	 *
130
	 * @return string A date format for the day that roundtrips the Wikibase TimeParsers.
131
	 */
132
	private function getDayFormat( $dateFormat ) {
133
		if ( preg_match( '/(?:d|(?<!x)j)[.,]?/', $dateFormat, $matches ) ) {
134
			return $matches[0];
135
		}
136
137
		return 'j';
138
	}
139
140
	/**
141
	 * @see Language::sprintfDate
142
	 *
143
	 * @param string $dateFormat
144
	 *
145
	 * @return string A date format for the month that roundtrips the Wikibase TimeParsers.
146
	 */
147
	private function getMonthFormat( $dateFormat ) {
148
		if ( preg_match( '/(?:[FMn]|(?<!x)m|xg)[.,]?/', $dateFormat, $matches ) ) {
149
			return $matches[0];
150
		}
151
152
		return 'F';
153
	}
154
155
	/**
156
	 * @param string $isoTimestamp
157
	 * @param int $precision
158
	 *
159
	 * @throws InvalidArgumentException
160
	 * @return string MediaWiki time stamp in the format YYYYMMDDHHMMSS
161
	 */
162
	private function getMwTimestamp( $isoTimestamp, $precision ) {
163
		$args = $this->splitIsoTimestamp( $isoTimestamp, $precision );
164
165
		// Year must be in the range 0000 to 9999 in an MediaWiki time stamp
166
		$args[0] = substr( $args[0], -4 );
167
		// Month/day must default to 1 to not get the last day of the previous year/month
168
		$args[1] = max( 1, $args[1] );
169
		$args[2] = max( 1, $args[2] );
170
171
		return vsprintf( '%04d%02d%02d%02d%02d%02d', $args );
172
	}
173
174
	/**
175
	 * @param string $isoTimestamp
176
	 * @param int $precision
177
	 *
178
	 * @throws InvalidArgumentException
179
	 * @return string[] Year, month, day, hour, minute, second
180
	 */
181
	private function splitIsoTimestamp( $isoTimestamp, $precision ) {
182
		if ( !preg_match(
183
			'/(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)/',
184
			$isoTimestamp,
185
			$matches
186
		) ) {
187
			throw new InvalidArgumentException( 'Unable to parse time value.' );
188
		}
189
190
		list( , $year, $month, $day ) = $matches;
191
192
		if ( $year == 0 && $precision < TimeValue::PRECISION_YEAR
193
			|| $month == 0 && $precision >= TimeValue::PRECISION_MONTH
194
			|| $day == 0 && $precision >= TimeValue::PRECISION_DAY
195
		) {
196
			throw new InvalidArgumentException( 'Time value insufficient for precision.' );
197
		}
198
199
		return array_slice( $matches, 1 );
200
	}
201
202
	/**
203
	 * @param string $isoTimestamp
204
	 * @param int $precision
205
	 *
206
	 * @return string
207
	 */
208
	private function getLocalizedYear( $isoTimestamp, $precision ) {
209
		preg_match( '/^(\D*)(\d*)/', $isoTimestamp, $matches );
210
		list( , $sign, $year ) = $matches;
211
		$isBCE = $sign === '-';
212
213
		$shift = 1;
214
		$unshift = 1;
215
		$func = 'round';
216
217
		switch ( $precision ) {
218
			case TimeValue::PRECISION_YEAR1G:
219
				$msg = 'Gannum';
220
				$shift = 1e+9;
221
				break;
222
			case TimeValue::PRECISION_YEAR100M:
223
				$msg = 'Mannum';
224
				$shift = 1e+8;
225
				$unshift = 1e+2;
226
				break;
227
			case TimeValue::PRECISION_YEAR10M:
228
				$msg = 'Mannum';
229
				$shift = 1e+7;
230
				$unshift = 1e+1;
231
				break;
232
			case TimeValue::PRECISION_YEAR1M:
233
				$msg = 'Mannum';
234
				$shift = 1e+6;
235
				break;
236
			case TimeValue::PRECISION_YEAR100K:
237
				$msg = 'annum';
238
				$shift = 1e+5;
239
				$unshift = 1e+5;
240
				break;
241
			case TimeValue::PRECISION_YEAR10K:
242
				$msg = 'annum';
243
				$shift = 1e+4;
244
				$unshift = 1e+4;
245
				break;
246
			case TimeValue::PRECISION_YEAR1K:
247
				$msg = 'millennium';
248
				$func = 'ceil';
249
				$shift = 1e+3;
250
				break;
251
			case TimeValue::PRECISION_YEAR100:
252
				$msg = 'century';
253
				$func = 'ceil';
254
				$shift = 1e+2;
255
				break;
256
			case TimeValue::PRECISION_YEAR10:
257
				$msg = '10annum';
258
				$func = 'floor';
259
				$shift = 1e+1;
260
				$unshift = 1e+1;
261
				break;
262
		}
263
264
		$shifted = $this->shiftNumber( $year, $func, $shift, $unshift );
265
		if ( $shifted == 0
266
			&& ( $precision < TimeValue::PRECISION_YEAR
267
				|| ( $isBCE && $precision === TimeValue::PRECISION_YEAR )
268
			)
269
		) {
270
			// Year to small for precision, fall back to year.
271
			$msg = null;
272
		} else {
273
			$year = $shifted;
274
		}
275
276
		$year = str_pad( ltrim( $year, '0' ), 1, '0', STR_PAD_LEFT );
277
		// TODO: The year should be localized via Language::formatNum() at this point, but currently
278
		// can't because not all relevant time parsers unlocalize numbers.
279
280
		if ( empty( $msg ) ) {
281
			if ( $isBCE ) {
282
				return wfMessage(
283
					'wikibase-time-precision-BCE',
284
					$year
285
				)
286
				->inLanguage( $this->language )
287
				->text();
288
			} else {
289
				return $year;
290
			}
291
		}
292
293
		return wfMessage(
294
			'wikibase-time-precision-' . ( $isBCE ? 'BCE-' : '' ) . $msg,
295
			$year
296
		)
297
		->inLanguage( $this->language )
298
		->text();
299
	}
300
301
	/**
302
	 * @param string $number
303
	 * @param string $function
304
	 * @param float $shift
305
	 * @param float $unshift
306
	 *
307
	 * @return string
308
	 */
309
	private function shiftNumber( $number, $function, $shift, $unshift ) {
310
		if ( $shift == 1 && $unshift == 1 ) {
311
			return $number;
312
		}
313
314
		switch ( $function ) {
315
			case 'ceil':
316
				$shifted = ceil( $number / $shift ) * $unshift;
317
				break;
318
			case 'floor':
319
				$shifted = floor( $number / $shift ) * $unshift;
320
				break;
321
			default:
322
				$shifted = round( $number / $shift ) * $unshift;
323
		}
324
325
		return sprintf( '%.0f', $shifted );
326
	}
327
328
}
329