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