Test Failed
Pull Request — master (#140)
by
unknown
08:14
created

TimeValueCalculator::isLeapYear()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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