|
1
|
|
|
<?php |
|
2
|
|
|
namespace Ivory\Value; |
|
3
|
|
|
|
|
4
|
|
|
use Ivory\Utils\ComparableWithPhpOperators; |
|
5
|
|
|
use Ivory\Utils\IComparable; |
|
6
|
|
|
|
|
7
|
|
|
/** |
|
8
|
|
|
* Common base for time representations. |
|
9
|
|
|
* |
|
10
|
|
|
* @internal Only for the purpose of Ivory itself. |
|
11
|
|
|
*/ |
|
12
|
|
|
abstract class TimeBase implements IComparable |
|
13
|
|
|
{ |
|
14
|
|
|
use ComparableWithPhpOperators; |
|
15
|
|
|
|
|
16
|
|
|
/** Number of decimal digits of precision in the fractional seconds part. */ |
|
17
|
|
|
const PRECISION = 6; |
|
18
|
|
|
|
|
19
|
|
|
/** @var int|float */ |
|
20
|
|
|
protected $sec; |
|
21
|
|
|
|
|
22
|
|
|
|
|
23
|
|
|
/** |
|
24
|
|
|
* @internal |
|
25
|
|
|
* @param int $hour |
|
26
|
|
|
* @param int $minute |
|
27
|
|
|
* @param int|float $second |
|
28
|
|
|
* @return int|float |
|
29
|
|
|
* @throws \OutOfRangeException |
|
30
|
|
|
*/ |
|
31
|
|
|
protected static function partsToSec($hour, $minute, $second) |
|
32
|
|
|
{ |
|
33
|
|
|
$s = $hour * 60 * 60 + $minute * 60 + $second; |
|
34
|
|
|
|
|
35
|
|
|
if ($s < 0) { |
|
36
|
|
|
throw new \OutOfRangeException('The resulting time underruns 00:00:00'); |
|
37
|
|
|
} elseif ($s > 24 * 60 * 60) { |
|
38
|
|
|
throw new \OutOfRangeException('The resulting time exceeds 24:00:00'); |
|
39
|
|
|
} else { |
|
40
|
|
|
return $s; |
|
41
|
|
|
} |
|
42
|
|
|
} |
|
43
|
|
|
|
|
44
|
|
|
/** |
|
45
|
|
|
* @internal |
|
46
|
|
|
* @param int $hour |
|
47
|
|
|
* @param int $minute |
|
48
|
|
|
* @param int|float $second |
|
49
|
|
|
* @return int|float |
|
50
|
|
|
* @throws \OutOfRangeException |
|
51
|
|
|
*/ |
|
52
|
|
|
protected static function partsToSecStrict($hour, $minute, $second) |
|
53
|
|
|
{ |
|
54
|
|
View Code Duplication |
if ($hour == 24) { |
|
|
|
|
|
|
55
|
|
|
if ($minute > 0 || $second > 0) { |
|
56
|
|
|
throw new \OutOfRangeException('with hour 24, the minutes and seconds must be zero'); |
|
57
|
|
|
} |
|
58
|
|
|
} elseif ($hour < 0 || $hour > 24) { |
|
59
|
|
|
throw new \OutOfRangeException('hours'); |
|
60
|
|
|
} |
|
61
|
|
|
|
|
62
|
|
|
if ($minute < 0 || $minute > 59) { |
|
63
|
|
|
throw new \OutOfRangeException('minutes'); |
|
64
|
|
|
} |
|
65
|
|
|
|
|
66
|
|
|
if ($second < 0 || $second >= 61) { |
|
67
|
|
|
throw new \OutOfRangeException('seconds'); |
|
68
|
|
|
} |
|
69
|
|
|
|
|
70
|
|
|
return $hour * 60 * 60 + $minute * 60 + $second; |
|
71
|
|
|
} |
|
72
|
|
|
|
|
73
|
|
|
/** |
|
74
|
|
|
* @internal |
|
75
|
|
|
* @param int|float $timestamp |
|
76
|
|
|
* @return int |
|
77
|
|
|
*/ |
|
78
|
|
|
protected static function cutUnixTimestampToSec($timestamp) |
|
79
|
|
|
{ |
|
80
|
|
|
if ($timestamp == 24 * 60 * 60) { |
|
81
|
|
|
return $timestamp; |
|
82
|
|
|
} |
|
83
|
|
|
|
|
84
|
|
|
$dayRes = (int)($timestamp - ($timestamp % (24 * 60 * 60))); |
|
85
|
|
|
$sec = $timestamp - $dayRes; |
|
86
|
|
|
if ($sec < 0) { |
|
87
|
|
|
$sec += 24 * 60 * 60; |
|
88
|
|
|
} |
|
89
|
|
|
|
|
90
|
|
|
return $sec; |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
|
/** |
|
94
|
|
|
* @internal Only for the purpose of Ivory itself. |
|
95
|
|
|
* @param int|float $sec |
|
96
|
|
|
*/ |
|
97
|
|
|
protected function __construct($sec) |
|
98
|
|
|
{ |
|
99
|
|
|
$this->sec = $sec; |
|
100
|
|
|
} |
|
101
|
|
|
|
|
102
|
|
|
/** |
|
103
|
|
|
* @return int the hours part of the time (0-24) |
|
104
|
|
|
*/ |
|
105
|
|
|
public function getHours() |
|
106
|
|
|
{ |
|
107
|
|
|
return (int)($this->sec / (60 * 60)); |
|
108
|
|
|
} |
|
109
|
|
|
|
|
110
|
|
|
/** |
|
111
|
|
|
* @return int the minutes part of the time (0-59) |
|
112
|
|
|
*/ |
|
113
|
|
|
public function getMinutes() |
|
114
|
|
|
{ |
|
115
|
|
|
return ($this->sec / 60) % 60; |
|
116
|
|
|
} |
|
117
|
|
|
|
|
118
|
|
|
/** |
|
119
|
|
|
* @return int|float the seconds part of the time (0-59), potentially with the fractional part, if any |
|
120
|
|
|
*/ |
|
121
|
|
|
public function getSeconds() |
|
122
|
|
|
{ |
|
123
|
|
|
return $this->sec - $this->getMinutes() * 60 - $this->getHours() * 60 * 60; |
|
124
|
|
|
} |
|
125
|
|
|
|
|
126
|
|
|
/** |
|
127
|
|
|
* @param Date|string|null $date the date for the resulting timestamp; |
|
128
|
|
|
* besides a {@link Date} object, an ISO date string is accepted - see |
|
129
|
|
|
* {@link Date::fromISOString()}; |
|
130
|
|
|
* the given date (if any) must be finite; |
|
131
|
|
|
* if not given the time on 1970-01-01 is returned, which is effectively the amount of |
|
132
|
|
|
* time, in seconds, between the time this object represents and <tt>00:00:00</tt> |
|
133
|
|
|
* @return float|int the UNIX timestamp of this time on the given day |
|
134
|
|
|
* @throws \InvalidArgumentException if the date is infinite or if the <tt>$date</tt> string is not a valid ISO date |
|
135
|
|
|
* string |
|
136
|
|
|
*/ |
|
137
|
|
|
public function toUnixTimestamp($date = null) |
|
138
|
|
|
{ |
|
139
|
|
|
if ($date === null) { |
|
140
|
|
|
return $this->sec; |
|
141
|
|
|
} else { |
|
142
|
|
|
if (!$date instanceof Date) { |
|
143
|
|
|
$date = Date::fromISOString($date); |
|
144
|
|
|
} |
|
145
|
|
|
$dayTs = $date->toUnixTimestamp(); |
|
146
|
|
|
if ($dayTs !== null) { |
|
147
|
|
|
return $dayTs + $this->sec; |
|
148
|
|
|
} else { |
|
149
|
|
|
throw new \InvalidArgumentException('infinite date'); |
|
150
|
|
|
} |
|
151
|
|
|
} |
|
152
|
|
|
} |
|
153
|
|
|
|
|
154
|
|
|
/** |
|
155
|
|
|
* @param string $timeFmt the format string as accepted by {@link date()} |
|
156
|
|
|
* @return string the time formatted according to <tt>$timeFmt</tt> |
|
157
|
|
|
*/ |
|
158
|
|
|
public function format($timeFmt) |
|
159
|
|
|
{ |
|
160
|
|
|
$ts = $this->toUnixTimestamp(); |
|
161
|
|
|
|
|
162
|
|
|
// microseconds are not supported by gmdate(), and constructing a new \DateTime object would be overkill |
|
163
|
|
|
if (strpos($timeFmt, 'u') !== false) { |
|
164
|
|
|
$frac = round($ts - (int)$ts, self::PRECISION); |
|
165
|
|
|
$fracPart = ($frac ? substr($frac, 2) : '0'); // cut off the leading "0." for non-zero fractional seconds |
|
166
|
|
|
$fracStr = str_pad($fracPart, 6, '0', STR_PAD_RIGHT); |
|
167
|
|
|
|
|
168
|
|
|
$re = '~ |
|
169
|
|
|
(?<!\\\\) # not prefixed with a backslash |
|
170
|
|
|
((?:\\\\\\\\)*) # any number of pairs of backslashes, each meaning a single literal backslash |
|
171
|
|
|
u # the microseconds format character to be replaced |
|
172
|
|
|
~x'; |
|
173
|
|
|
$timeFmt = preg_replace($re, '${1}' . $fracStr, $timeFmt); |
|
174
|
|
|
} |
|
175
|
|
|
|
|
176
|
|
|
return gmdate($timeFmt, $ts); |
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
|
|
/** |
|
180
|
|
|
* @return string the ISO representation of this time, in format <tt>HH:MM:SS[.p]</tt>; |
|
181
|
|
|
* the fractional seconds part is only used if non-zero |
|
182
|
|
|
*/ |
|
183
|
|
|
public function toString() |
|
184
|
|
|
{ |
|
185
|
|
|
$frac = round($this->sec - (int)$this->sec, self::PRECISION); |
|
186
|
|
|
return sprintf( |
|
187
|
|
|
'%02d:%02d:%02d%s', |
|
188
|
|
|
$this->getHours(), $this->getMinutes(), $this->getSeconds(), |
|
189
|
|
|
($frac ? substr($frac, 1) : '') // cut off the leading "0" for non-zero fractional seconds |
|
190
|
|
|
); |
|
191
|
|
|
} |
|
192
|
|
|
} |
|
193
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.