Completed
Pull Request — master (#140)
by
unknown
06:34 queued 04:42
created

TimeValueCalculator::getHigherTimestamp()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 9
cts 9
cp 1
rs 9.8666
c 0
b 0
f 0
cc 3
nc 2
nop 1
crap 3
1
<?php
2
3
namespace DataValues;
4
5
use InvalidArgumentException;
6
7
/**
8
 * Logical and mathematical helper functions for calculations with and conversions from TimeValue
9
 * objects.
10
 *
11
 * @since 0.6
12
 *
13
 * @license GPL-2.0+
14
 * @author Thiemo Kreuz
15
 */
16
class TimeValueCalculator {
17
18
	/**
19
	 * Average length of a year in the Gregorian calendar in seconds, calculated via
20
	 * 365 + 1 / 4 - 1 / 100 + 1 / 400 = 365.2425 days.
21
	 */
22
	const SECONDS_PER_GREGORIAN_YEAR = 31556952;
23
24
	/**
25
	 * Maximum length for a timestamp.
26
	 */
27
	private $MAX_LENGTH_TIMESTAMP = 33;
28
29
	/**
30
	 * Lowest positive timestamp.
31
	 */
32
	private $TIMESTAMP_ZERO = '+0000000000000000-01-01T00:00:00Z';
33
34
	/**
35
	 * Highest positive timestamp strictly earlier than the lowest positive timestamp with
36
	 * a length of $MAX_LENGTH_TIMESTAMP + 1.
37
	 */
38
	private $HIGHEST_TIMESTAMP = '+9999999999999999-12-31T23:59:59Z';
39
40
	/**
41
	 * This returns a Unix timestamp from a TimeValue similar to PHP's mk_time() (or strtotime()),
42
	 * but with no range limitations. Data type is float because PHP's 32 bit integer would
43
	 * clip in the year 2038.
44
	 *
45
	 * @param TimeValue $timeValue
46
	 *
47
	 * @return float seconds since 1970-01-01T00:00:00Z
48
	 */
49 36
	public function getTimestamp( TimeValue $timeValue ) {
50 36
		return $this->getSecondsSinceUnixEpoch( $timeValue->getTime(), $timeValue->getTimezone() );
51
	}
52
53
	/**
54
	 * Returns the lowest possible Unix timestamp from a TimeValue considering its precision
55
	 * and its before value. Data type is float because PHP's 32 bit integer would clip in the
56
	 * year 2038.
57
	 *
58
	 * @param TimeValue $timeValue
59
	 *
60
	 * @return float seconds since 1970-01-01T00:00:00Z
61
	 */
62 1
	public function getLowerTimestamp( TimeValue $timeValue ) {
63 1
		$precision = $timeValue->getPrecision();
64 1
		$timestamp = $timeValue->getTime();
65 1
		if ( $timestamp[0] === '-' && $precision < TimeValue::PRECISION_YEAR ) {
66 1
			$timestamp = $this->timestampAbsCeiling( $timestamp, $precision );
67
		} else {
68 1
			$timestamp = $this->timestampAbsFloor( $timestamp, $precision );
69
		}
70 1
		$unixTimestamp = $this->getSecondsSinceUnixEpoch( $timestamp, $timeValue->getTimezone() );
71 1
		$unixTimestamp -= $timeValue->getBefore() * $this->getSecondsForPrecision( $precision );
72 1
		return $unixTimestamp;
73
	}
74
75
	/**
76
	 * Returns the highest possible Unix timestamp from a TimeValue considering its precision
77
	 * and its after value. Data type is float because PHP's 32 bit integer would clip in the
78
	 * year 2038.
79
	 *
80
	 * @param TimeValue $timeValue
81
	 *
82
	 * @return float seconds since 1970-01-01T00:00:00Z
83
	 */
84 1
	public function getHigherTimestamp( TimeValue $timeValue ) {
85 1
		$precision = $timeValue->getPrecision();
86 1
		$timestamp = $timeValue->getTime();
87 1
		if ( $timestamp[0] === '-' && $precision < TimeValue::PRECISION_YEAR ) {
88 1
			$timestamp = $this->timestampAbsFloor( $timestamp, $precision );
89
		} else {
90 1
			$timestamp = $this->timestampAbsCeiling( $timestamp, $precision );
91
		}
92 1
		$unixTimestamp = $this->getSecondsSinceUnixEpoch( $timestamp, $timeValue->getTimezone() );
93 1
		$unixTimestamp += $timeValue->getAfter() * $this->getSecondsForPrecision( $precision );
94 1
		return $unixTimestamp;
95
	}
96
97
	/**
98
	 * @param string $time an ISO 8601 date and time
99
	 * @param int $timezone offset from UTC in minutes
100
	 *
101
	 * @throws InvalidArgumentException
102
	 * @return float seconds since 1970-01-01T00:00:00Z
103
	 */
104 36
	private function getSecondsSinceUnixEpoch( $time, $timezone = 0 ) {
105
		// Validation is done in TimeValue. As long if we found enough numbers we are fine.
106 36
		if ( !preg_match( '/([-+]?\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)/', $time, $matches )
107
		) {
108
			throw new InvalidArgumentException( "Failed to parse time value $time." );
109
		}
110 36
		list( , $fullYear, $month, $day, $hour, $minute, $second ) = $matches;
111
112
		// We use mktime only for the month, day and time calculation. Set the year to the smallest
113
		// possible in the 1970-2038 range to be safe, even if it's 1901-2038 since PHP 5.1.0.
114 36
		$year = $this->isLeapYear( $fullYear ) ? 1972 : 1970;
115
116 36
		$defaultTimezone = date_default_timezone_get();
117 36
		date_default_timezone_set( 'UTC' );
118
		// With day/month set to 0 mktime would calculate the last day of the previous month/year.
119
		// In the context of this calculation we must assume 0 means "start of the month/year".
120 36
		$timestamp = mktime( $hour, $minute, $second, max( 1, $month ), max( 1, $day ), $year );
121 36
		date_default_timezone_set( $defaultTimezone );
122
123 36
		if ( $timestamp === false ) {
124
			throw new InvalidArgumentException( "Failed to get epoche from time value $time." );
125
		}
126
127 36
		$missingYears = ( $fullYear < 0 ? $fullYear + 1 : $fullYear ) - $year;
128 36
		$missingLeapDays = $this->getNumberOfLeapYears( $fullYear )
129 36
			- $this->getNumberOfLeapYears( $year );
130
131 36
		return $timestamp + ( $missingYears * 365 + $missingLeapDays ) * 86400 - $timezone * 60;
132
	}
133
134
	/**
135
	 * @param float $year
136
	 *
137
	 * @return bool if the year is a leap year in the Gregorian calendar
138
	 */
139 70
	public function isLeapYear( $year ) {
140 70
		$year = $year < 0 ? ceil( $year ) + 1 : floor( $year );
141 70
		$isMultipleOf4   = fmod( $year,   4 ) === 0.0;
142 70
		$isMultipleOf100 = fmod( $year, 100 ) === 0.0;
143 70
		$isMultipleOf400 = fmod( $year, 400 ) === 0.0;
144 70
		return $isMultipleOf4 && !$isMultipleOf100 || $isMultipleOf400;
145
	}
146
147
	/**
148
	 * @param float $year
149
	 *
150
	 * @return float The number of leap years since year 1. To be more precise: The number of
151
	 * leap days in the range between 31 December of year 1 and 31 December of the given year.
152
	 */
153 70
	public function getNumberOfLeapYears( $year ) {
154 70
		$year = $year < 0 ? ceil( $year ) + 1 : floor( $year );
155 70
		return floor( $year / 4 ) - floor( $year / 100 ) + floor( $year / 400 );
156
	}
157
158
	/**
159
	 * @param int $precision One of the TimeValue::PRECISION_... constants
160
	 *
161
	 * @throws InvalidArgumentException
162
	 * @return float number of seconds in one unit of the given precision
163
	 */
164 10
	public function getSecondsForPrecision( $precision ) {
165 10
		if ( $precision <= TimeValue::PRECISION_YEAR ) {
166 5
			return self::SECONDS_PER_GREGORIAN_YEAR * pow(
167 5
				10,
168 5
				TimeValue::PRECISION_YEAR - $precision
169
			);
170
		}
171
172
		switch ( $precision ) {
173 7
			case TimeValue::PRECISION_SECOND:
174 3
				return 1;
175 6
			case TimeValue::PRECISION_MINUTE:
176 3
				return 60;
177 5
			case TimeValue::PRECISION_HOUR:
178 3
				return 3600;
179 4
			case TimeValue::PRECISION_DAY:
180 3
				return 86400;
181 3
			case TimeValue::PRECISION_MONTH:
182 3
				return self::SECONDS_PER_GREGORIAN_YEAR / 12;
183
		}
184
185
		throw new InvalidArgumentException( "Unable to get seconds for precision $precision." );
186
	}
187
188
	/**
189
	 * @param $timestamp
190
	 * @param $precision
191
	 *
192
	 * @return string
193
	 */
194 2
	private function timestampAbsFloor( $timestamp, $precision ) {
195
		// The year is padded with zeros to have 16 digits
196 2
		$timestamp = substr_replace(
197 2
			$timestamp,
198 2
			str_repeat( '0', $this->MAX_LENGTH_TIMESTAMP - strlen( $timestamp ) ),
199 2
			1,
200
			0
201
		);
202 2
		$numCharsToModify = $this->charsAffectedByPrecision( $precision );
203 2
		$timestamp = substr( $timestamp, 0, -$numCharsToModify ) .
204 2
			substr( $this->TIMESTAMP_ZERO, -$numCharsToModify );
205 2
		return $timestamp;
206
	}
207
208
	/**
209
	 * @param $timestamp
210
	 * @param $precision
211
	 *
212
	 * @return string
213
	 */
214 2
	private function timestampAbsCeiling( $timestamp, $precision ) {
215
		// The year is padded with zeros to have 16 digits
216 2
		$timestamp = substr_replace(
217 2
			$timestamp,
218 2
			str_repeat( '0', $this->MAX_LENGTH_TIMESTAMP - strlen( $timestamp ) ),
219 2
			1,
220
			0
221
		);
222 2
		$numCharsToModify = $this->charsAffectedByPrecision( $precision );
223
		// WARNING: Day 31 will be applied to all months
224 2
		$timestampCeiling = substr( $timestamp, 0, -$numCharsToModify ) .
225 2
			substr( $this->HIGHEST_TIMESTAMP, -$numCharsToModify );
226 2
		if ( $precision === TimeValue::PRECISION_MONTH ) {
227 1
			$month = (int)substr(
228 1
				$timestamp,
229 1
				-$this->charsAffectedByPrecision( TimeValue::PRECISION_YEAR ),
230 1
				2
231
			);
232 1
			$year = (int)substr(
233 1
				$timestamp,
234 1
				1,
235 1
				-$this->charsAffectedByPrecision( TimeValue::PRECISION_YEAR ) - 1
236
			);
237 1
			$daysInMonth = cal_days_in_month( CAL_GREGORIAN, $month, $year );
238 1
			$timestampCeiling = substr( $timestamp, 0, -$numCharsToModify ) .
239 1
				$daysInMonth .
240 1
				substr( $this->HIGHEST_TIMESTAMP, -$numCharsToModify + 2 );
241
		}
242 2
		return $timestampCeiling;
243
	}
244
245
	/**
246
	 * @param $precision
247
	 *
248
	 * @return int
249
	 */
250 2
	private function charsAffectedByPrecision( $precision ) {
251 2
		$numCharsAffected = 1;
252
		switch ( $precision ) {
253 2
			case TimeValue::PRECISION_MINUTE:
254 2
				$numCharsAffected = 3;
255 2
				break;
256 2
			case TimeValue::PRECISION_HOUR:
257 2
				$numCharsAffected = 6;
258 2
				break;
259 2
			case TimeValue::PRECISION_DAY:
260 2
				$numCharsAffected = 9;
261 2
				break;
262 2
			case TimeValue::PRECISION_MONTH:
263 2
				$numCharsAffected = 12;
264 2
				break;
265 2
			case TimeValue::PRECISION_YEAR:
266 2
				$numCharsAffected = 15;
267 2
				break;
268 2
			case TimeValue::PRECISION_YEAR10:
269 2
				$numCharsAffected = 17;
270 2
				break;
271 2
			case TimeValue::PRECISION_YEAR100:
272 2
				$numCharsAffected = 18;
273 2
				break;
274 2
			case TimeValue::PRECISION_YEAR1K:
275 2
				$numCharsAffected = 19;
276 2
				break;
277 2
			case TimeValue::PRECISION_YEAR10K:
278 2
				$numCharsAffected = 20;
279 2
				break;
280 2
			case TimeValue::PRECISION_YEAR100K:
281 2
				$numCharsAffected = 21;
282 2
				break;
283 2
			case TimeValue::PRECISION_YEAR1M:
284 2
				$numCharsAffected = 22;
285 2
				break;
286 2
			case TimeValue::PRECISION_YEAR10M:
287 2
				$numCharsAffected = 23;
288 2
				break;
289 2
			case TimeValue::PRECISION_YEAR100M:
290 2
				$numCharsAffected = 24;
291 2
				break;
292 2
			case TimeValue::PRECISION_YEAR1G:
293 2
				$numCharsAffected = 25;
294 2
				break;
295
		}
296 2
		return $numCharsAffected;
297
	}
298
299
}
300