Passed
Push — ymdMonthNames ( 4f0b3e...7ee93b )
by no
08:06
created

YearMonthDayTimeParser::getCalendarModel()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 6
rs 9.4286
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
3
namespace ValueParsers;
4
5
use DataValues\TimeValue;
6
7
/**
8
 * A straight time parser with a strict rule set that only accepts YMD, DMY, MDY and YDM formatted
9
 * dates if they can not be confused with an other format.
10
 *
11
 * @since 0.8.1
12
 *
13
 * @license GPL-2.0+
14
 * @author Thiemo Kreuz
15
 */
16
class YearMonthDayTimeParser extends StringValueParser {
17
18
	const FORMAT_NAME = 'year-month-day';
19
20
	/**
21
	 * @var ValueParser
22
	 */
23
	private $eraParser;
24
25
	/**
26
	 * @var int[]
27
	 */
28
	private $months;
29
30
	/**
31
	 * @var ValueParser
32
	 */
33
	private $isoTimestampParser;
34
35
	/**
36
	 * @param ValueParser|null $eraParser String parser that detects signs, "BC" suffixes and such and
37
	 * returns an array with the detected sign character and the remaining value.
38
	 * @param int[] $months Array mapping localized month names (possibly including full month
39
	 * names, genitive names and abbreviations) to the numbers 1 to 12.
40
	 * @param ParserOptions|null $options
41
	 */
42
	public function __construct(
43
		ValueParser $eraParser = null,
44
		array $months = [],
45
		ParserOptions $options = null
46
	) {
47
		parent::__construct( $options );
48
49
		$this->eraParser = $eraParser ?: new EraParser();
50
		$this->months = $months;
51
		$this->isoTimestampParser = new IsoTimestampParser( null, $this->options );
52
	}
53
54
	/**
55
	 * @param string $value
56
	 *
57
	 * @throws ParseException
58
	 * @return TimeValue
59
	 */
60
	protected function stringParse( $value ) {
61
		try {
62
			list( $sign, $preparsedValue ) = $this->eraParser->parse( $value );
63
			list( $signedYear, $month, $day ) = $this->parseYearMonthDay( $preparsedValue );
64
65
			if ( substr( $signedYear, 0, 1 ) !== '-' ) {
66
				$signedYear = $sign . $signedYear;
67
			} elseif ( $sign === '-' ) {
68
				throw new ParseException( 'Two eras found' );
69
			}
70
71
			return $this->newTimeValue( $signedYear, $month, $day );
72
		} catch ( ParseException $ex ) {
73
			throw new ParseException( $ex->getMessage(), $value, self::FORMAT_NAME );
74
		}
75
	}
76
77
	/**
78
	 * @param string $value
79
	 *
80
	 * @throws ParseException
81
	 * @return string[]
82
	 */
83
	private function parseYearMonthDay( $value ) {
84
		$monthPattern = '(?:\d+|'
85
			. implode( '|', array_map( 'preg_quote', array_keys( $this->months ) ) )
86
			. ')';
87
		if ( !preg_match(
88
			'<^\D*?(-?' . $monthPattern . ')\D+(' . $monthPattern . ')\D+?(-?' . $monthPattern . ')\D*$>',
89
			$value,
90
			$matches
91
		) ) {
92
			throw new ParseException( 'Can not find three numbers' );
93
		}
94
95
		$monthAt = null;
96
		foreach ( $matches as $i => &$match ) {
97
			if ( isset( $this->months[$match] ) ) {
98
				if ( $monthAt !== null ) {
99
					throw new ParseException( 'Two months found' );
100
				}
101
				$match = $this->months[$match];
102
				$monthAt = $i;
103
			}
104
		}
105
106
		// A 32 in the first spot can not be confused with anything.
107
		if ( $matches[1] < 1 || $matches[1] > 31 ) {
108
			if ( $matches[3] > 12 || $matches[2] == $matches[3] ) {
109
				list( , $signedYear, $month, $day ) = $matches;
110
			} elseif ( $matches[2] > 12 ) {
111
				list( , $signedYear, $day, $month ) = $matches;
112
			} else {
113
				throw new ParseException( 'Can not distinguish YDM and YMD' );
114
			}
115
		} elseif ( $matches[3] < 1 || $matches[3] > 59
116
			// A 59 in the third spot may be a second, but can not if the first number is > 24.
117
			// A 31 in the last spot may be the day, but can not if it's negative.
118
			|| ( abs( $matches[1] ) > 24 && $matches[3] > 31 )
119
		) {
120
			if ( $matches[1] > 12 || $matches[1] == $matches[2] ) {
121
				list( , $day, $month, $signedYear ) = $matches;
122
			} elseif ( $matches[2] > 12 ) {
123
				list( , $month, $day, $signedYear ) = $matches;
124
			} else {
125
				throw new ParseException( 'Can not distinguish DMY and MDY' );
126
			}
127
		} else {
128
			// Formats DYM and MYD do not exist.
129
			throw new ParseException( 'Can not identify year' );
130
		}
131
132
		return [ $signedYear, $month, $day ];
133
	}
134
135
	/**
136
	 * @param string $signedYear
137
	 * @param string $month
138
	 * @param string $day
139
	 *
140
	 * @throws ParseException
141
	 * @return TimeValue
142
	 */
143
	private function newTimeValue( $signedYear, $month, $day ) {
144
		if ( $month < 1 || $month > 12 ) {
145
			throw new ParseException( 'Month out of range' );
146
		} elseif ( $day < 1 || $day > 31 ) {
147
			throw new ParseException( 'Day out of range' );
148
		}
149
150
		return $this->isoTimestampParser->parse(
151
			sprintf( '%s-%02s-%02sT00:00:00Z', $signedYear, $month, $day )
152
		);
153
	}
154
155
}
156