Test Failed
Pull Request — master (#140)
by
unknown
09:55
created

TimeValueCalculator::timestampAbsCeiling()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 14
ccs 0
cts 0
cp 0
rs 9.7998
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
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
		} else {
67 34
			$timestamp = $this->timestampAbsFloor( $timestamp, $precision );
68 34
		}
69 34
		$unixTimestamp = $this->getSecondsSinceUnixEpoch( $timestamp, $timeValue->getTimezone() );
70
		$unixTimestamp -= $timeValue->getBefore() * $this->getSecondsForPrecision( $precision );
71 34
		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 68
	 * @param TimeValue $timeValue
80 68
	 *
81 68
	 * @return float seconds since 1970-01-01T00:00:00Z
82 68
	 */
83 68
	public function getHigherTimestamp( TimeValue $timeValue ) {
84 68
		$precision = $timeValue->getPrecision();
85
		$timestamp = $timeValue->getTime();
86
		if ( strcmp( substr( $timestamp, 0, 1 ), '-' ) === 0 && $precision < TimeValue::PRECISION_YEAR ) {
87
			$timestamp = $this->timestampAbsFloor( $timestamp, $precision );
88
		} else {
89
			$timestamp = $this->timestampAbsCeiling( $timestamp, $precision );
90
		}
91
		$unixTimestamp = $this->getSecondsSinceUnixEpoch( $timestamp, $timeValue->getTimezone() );
92
		$unixTimestamp += $timeValue->getAfter() * $this->getSecondsForPrecision( $precision );
93 68
		return $unixTimestamp;
94 68
	}
95 68
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
	private function getSecondsSinceUnixEpoch( $time, $timezone = 0 ) {
104 8
		// Validation is done in TimeValue. As long if we found enough numbers we are fine.
105 8
		if ( !preg_match( '/([-+]?\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)\D+(\d+)/', $time, $matches )
106 3
		) {
107 3
			throw new InvalidArgumentException( "Failed to parse time value $time." );
108 3
		}
109
		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 5
		$year = $this->isLeapYear( $fullYear ) ? 1972 : 1970;
114 1
115 4
		$defaultTimezone = date_default_timezone_get();
116 1
		date_default_timezone_set( 'UTC' );
117 3
		// With day/month set to 0 mktime would calculate the last day of the previous month/year.
118 1
		// In the context of this calculation we must assume 0 means "start of the month/year".
119 2
		$timestamp = mktime( $hour, $minute, $second, max( 1, $month ), max( 1, $day ), $year );
120 1
		date_default_timezone_set( $defaultTimezone );
121 1
122 1
		if ( $timestamp === false ) {
123
			throw new InvalidArgumentException( "Failed to get epoche from time value $time." );
124
		}
125
126
		$missingYears = ( $fullYear < 0 ? $fullYear + 1 : $fullYear ) - $year;
127
		$missingLeapDays = $this->getNumberOfLeapYears( $fullYear )
128
			- $this->getNumberOfLeapYears( $year );
129
130
		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
	public function isLeapYear( $year ) {
139
		$year = $year < 0 ? ceil( $year ) + 1 : floor( $year );
140
		$isMultipleOf4 = fmod( $year, 4 ) === 0.0;
141
		$isMultipleOf100 = fmod( $year, 100 ) === 0.0;
142
		$isMultipleOf400 = fmod( $year, 400 ) === 0.0;
143
		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
	public function getNumberOfLeapYears( $year ) {
153
		$year = $year < 0 ? ceil( $year ) + 1 : floor( $year );
154
		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
	public function getSecondsForPrecision( $precision ) {
164
		if ( $precision <= TimeValue::PRECISION_YEAR ) {
165
			return self::SECONDS_PER_GREGORIAN_YEAR * pow(
166
					10,
167
					TimeValue::PRECISION_YEAR - $precision
168
				);
169
		}
170
171
		switch ( $precision ) {
172
			case TimeValue::PRECISION_SECOND:
173
				return 1.0;
174
			case TimeValue::PRECISION_MINUTE:
175
				return 60.0;
176
			case TimeValue::PRECISION_HOUR:
177
				return 3600.0;
178
			case TimeValue::PRECISION_DAY:
179
				return 86400.0;
180
			case TimeValue::PRECISION_MONTH:
181
				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
	private function timestampAbsFloor( $timestamp, $precision ) {
194
		// The year is padded with zeros to have 16 digits
195
		$timestamp = substr_replace(
196
			$timestamp,
197
			str_repeat( '0', self::MAX_LENGTH_TIMESTAMP - strlen( $timestamp ) ),
198
			1,
199
			0
200
		);
201
		$numCharsToModify = $this->charsAffectedByPrecision( $precision );
202
		$timestamp = substr( $timestamp, 0, -$numCharsToModify ) .
203
			substr( self::TIMESTAMP_ZERO, -$numCharsToModify );
204
		return $timestamp;
205
	}
206
207
	/**
208
	 * @param $timestamp
209
	 * @param $precision
210
	 *
211
	 * @return string
212
	 */
213
	private function timestampAbsCeiling( $timestamp, $precision ) {
214
		// The year is padded with zeros to have 16 digits
215
		$timestamp = substr_replace(
216
			$timestamp,
217
			str_repeat( '0', self::MAX_LENGTH_TIMESTAMP - strlen( $timestamp ) ),
218
			1,
219
			0
220
		);
221
		$numCharsToModify = $this->charsAffectedByPrecision( $precision );
222
		// WARNING: Day 31 will be applied to all months
223
		$timestamp = substr( $timestamp, 0, -$numCharsToModify ) .
224
			substr( self::HIGHEST_TIMESTAMP, -$numCharsToModify );
225
		return $timestamp;
226
	}
227
228
	/**
229
	 * @param $precision
230
	 *
231
	 * @return int
232
	 */
233
	private function charsAffectedByPrecision( $precision ) {
234
		$numCharsAffected = 1;
235
		switch ( $precision ) {
236
			case TimeValue::PRECISION_MINUTE:
237
				$numCharsAffected = 3;
238
				break;
239
			case TimeValue::PRECISION_HOUR:
240
				$numCharsAffected = 6;
241
				break;
242
			case TimeValue::PRECISION_DAY:
243
				$numCharsAffected = 9;
244
				break;
245
			case TimeValue::PRECISION_MONTH:
246
				$numCharsAffected = 12;
247
				break;
248
			case TimeValue::PRECISION_YEAR:
249
				$numCharsAffected = 15;
250
				break;
251
			case TimeValue::PRECISION_YEAR10:
252
				$numCharsAffected = 17;
253
				break;
254
			case TimeValue::PRECISION_YEAR100:
255
				$numCharsAffected = 18;
256
				break;
257
			case TimeValue::PRECISION_YEAR1K:
258
				$numCharsAffected = 19;
259
				break;
260
			case TimeValue::PRECISION_YEAR10K:
261
				$numCharsAffected = 20;
262
				break;
263
			case TimeValue::PRECISION_YEAR100K:
264
				$numCharsAffected = 21;
265
				break;
266
			case TimeValue::PRECISION_YEAR1M:
267
				$numCharsAffected = 22;
268
				break;
269
			case TimeValue::PRECISION_YEAR10M:
270
				$numCharsAffected = 23;
271
				break;
272
			case TimeValue::PRECISION_YEAR100M:
273
				$numCharsAffected = 24;
274
				break;
275
			case TimeValue::PRECISION_YEAR1G:
276
				$numCharsAffected = 25;
277
				break;
278
		}
279
		return $numCharsAffected;
280
	}
281
282
}
283