Passed
Push — master ( cc7033...ce309d )
by Ondřej
01:59
created

TimeTz::occursAfter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Value;
4
5
/**
6
 * Timezone-aware representation of time of day (no date, just time).
7
 *
8
 * For a timezone-unaware time, see {@link Time}.
9
 *
10
 * The supported range for the time part is from `00:00:00` to `24:00:00`. Fractional seconds may be used.
11
 *
12
 * Note that just the offset from GMT is recorded with the time, not the timezone identifier. E.g., if the connection
13
 * is configured for the `Europe/Prague` timezone, PostgreSQL puts `+0100` or `+0200` on output (depending on whether
14
 * the daylight "savings" time is in effect). That differs from the {@link TimestampTz} type, which records the
15
 * timezone, not just its offset.
16
 *
17
 * The objects are {@link IEqualable}, which considers two time representations equal only if they have both the time
18
 * part and the timezone offset equal. The same logic is used for the PHP `==` operator. Behaviour of the PHP `<` and
19
 * `>` operators is undefined. To compare the physical time of two {@link TimeTz} objects, use the
20
 * {@link TimeTz::occursBefore()}, {@link TimeTz::occursAt()} and {@link TimeTz::occursAfter()} methods.
21
 *
22
 * The objects are immutable, i.e., operations always produce a new object.
23
 *
24
 * @see http://www.postgresql.org/docs/9.4/static/datatype-datetime.html
25
 */
26
class TimeTz extends TimeBase
27
{
28
    /** @var int difference to UTC in seconds; positive to the east of GMT, negative to the west of GMT */
29
    private $offset;
30
31
    /**
32
     * Creates a timezone-aware time object from a string containing the time and the timezone offset.
33
     *
34
     * The accepted format is `H:M[:S[.p]][offset]`, where `H` holds hours (0-24), `M` minutes (0-59), `S` seconds
35
     * (0-60), `p` fractional seconds, `offset` is the timezone offset written in the ISO 8601 format (e.g., `+2:00` or
36
     * `-800`). If the timezone offset is not given, the current offset is used.
37
     *
38
     * Note that, although 60 is accepted in the seconds part, it gets automatically converted to 0 of the next minute,
39
     * as neither PostgreSQL supports leap seconds.
40
     *
41
     * @param string $timeString
42
     * @return TimeTz
43
     * @throws \InvalidArgumentException on invalid input
44
     * @throws \OutOfRangeException when some of the parts is outside its range
45
     */
46
    public static function fromString(string $timeString): TimeTz
47
    {
48
        $re = '~^
49
                (\d+):                  # hours
50
                (\d+)                   # minutes
51
                (?::(\d+(?:\.\d*)?))?   # optional seconds, possibly with fractional part
52
                (?P<zone>               # optional timezone specification
53
                 Z|                     # ...either the "Z" letter or offset
54
                 (?P<offh>[-+]\d{1,2})  # ...or two digits for hours offset
55
                 (?::?(?P<offm>\d{2}))? # ...and possibly two digits for minutes offset, optionally separated by a colon
56
                )?
57
                $~x';
58
        if (!preg_match($re, $timeString, $m)) {
59
            throw new \InvalidArgumentException('$timeString');
60
        }
61
62
        $hour = $m[1];
63
        $min = $m[2];
64
        $sec = (!empty($m[3]) ? $m[3] : 0);
65
66
        if ($hour == 24) {
67
            if ($min > 0 || $sec > 0) {
68
                throw new \OutOfRangeException('with hour 24, the minutes and seconds must be zero');
69
            }
70
        } elseif ($hour < 0 || $hour > 24) {
71
            throw new \OutOfRangeException('hours');
72
        }
73
74
        if ($min < 0 || $min > 59) {
75
            throw new \OutOfRangeException('minutes');
76
        }
77
78
        if ($sec < 0 || $sec >= 61) {
79
            throw new \OutOfRangeException('seconds');
80
        }
81
82
        if (!isset($m['zone'])) {
83
            $offset = (new \DateTime())->getOffset();
84
        } elseif ($m['zone'] == 'Z') {
85
            $offset = 0;
86
        } else {
87
            $offset = $m['offh'] * 60 * 60;
88
            if (isset($m['offm'])) {
89
                $offset += ($m['offh'] >= 0 ? 1 : -1) * $m['offm'] * 60;
90
            }
91
        }
92
93
        return new TimeTz($hour * 60 * 60 + $min * 60 + $sec, $offset);
94
    }
95
96
    /**
97
     * Creates a timezone-aware time object from the timezone offset and specified hours, minutes, and seconds.
98
     *
99
     * Any part exceeding its standard range overflows in the expected way to the higher part. E.g., it is possible to
100
     * pass 70 seconds, which results, in 1 minute 10 seconds. Moreover, the arithmetic also works for subtracting
101
     * negative minutes or seconds. Still, the overall time must fit between 00:00:00 and 24:00:00.
102
     *
103
     * The overflow rule applies to leap seconds as well as to any other value, i.e., 60 seconds get converted to 0 of
104
     * the next minute, as neither PostgreSQL supports leap seconds.
105
     *
106
     * @param int $hour
107
     * @param int $minute
108
     * @param int|float $second
109
     * @param int $offset the timezone offset of this time from the Greenwich Mean Time, in seconds;
110
     *                    positive for east of Greenwich, negative for west of Greenwich
111
     * @return TimeTz
112
     * @throws \OutOfRangeException when the resulting time underruns 00:00:00 or exceeds 24:00:00
113
     */
114
    public static function fromParts(int $hour, int $minute, $second, int $offset): TimeTz
115
    {
116
        return new TimeTz(self::partsToSec($hour, $minute, $second), $offset);
117
    }
118
119
    /**
120
     * Creates a timezone-aware time object from the timezone offset and specified hours, minutes, and seconds with
121
     * range checks for each of them.
122
     *
123
     * Note that, although 60 is accepted in the seconds part, it gets automatically converted to 0 of the next minute,
124
     * as neither PostgreSQL supports leap seconds. In this case it is possible to get time even greater than 24:00:00
125
     * (but still less than 24:00:01).
126
     *
127
     * @param int $hour 0-24, but when 24, the others must be zero
128
     * @param int $minute 0-59
129
     * @param int|float $second greater than or equal to 0, less than 61
130
     * @param int $offset the timezone offset of this time from the Greenwich Mean Time, in seconds;
131
     *                    positive for east of Greenwich, negative for west of Greenwich
132
     * @return TimeTz
133
     * @throws \OutOfRangeException when some of the parts is outside its range
134
     */
135
    public static function fromPartsStrict(int $hour, int $minute, $second, int $offset): TimeTz
136
    {
137
        $sec = self::partsToSecStrict($hour, $minute, $second);
138
        return new TimeTz($sec, $offset);
139
    }
140
141
    /**
142
     * Extracts the time part and timezone offset from a {@link \DateTime} or {@link \DateTimeImmutable} object.
143
     *
144
     * @param \DateTimeInterface $dateTime
145
     * @return TimeTz
146
     */
147
    public static function fromDateTime(\DateTimeInterface $dateTime): TimeTz
148
    {
149
        return self::fromString($dateTime->format('H:i:s.uP'));
150
    }
151
152
    /**
153
     * Extracts the time part from a UNIX timestamp as the time in the UTC timezone.
154
     *
155
     * Negative timestamps are supported. E.g., timestamp `-30.1` results in time `23:59:29.9`.
156
     *
157
     * Note there is one exception: the timestamp `1970-01-02 00:00:00 UTC` gets extracted as time `24:00:00` so that
158
     * there is symmetry with {@link TimeTz::toUnixTimestamp()}. Other timestamps are processed as expected, i.e., the
159
     * day part gets truncated and the result being less than `24:00:00`.
160
     *
161
     * @param int|float $timestamp
162
     * @return TimeTz
163
     */
164
    public static function fromUnixTimestamp($timestamp): TimeTz
165
    {
166
        $sec = self::cutUnixTimestampToSec($timestamp);
167
        return new TimeTz($sec, 0);
168
    }
169
170
    /**
171
     * @internal Only for the purpose of Ivory itself.
172
     * @param int|float $sec
173
     * @param int $offset
174
     */
175
    protected function __construct($sec, int $offset)
176
    {
177
        parent::__construct($sec);
178
        $this->offset = $offset;
179
    }
180
181
    /**
182
     * @return int the timezone offset of this time from the Greenwich Mean Time, in seconds;
183
     *             positive for east of Greenwich, negative for west of Greenwich
184
     */
185
    final public function getOffset(): int
186
    {
187
        return $this->offset;
188
    }
189
190
    /**
191
     * @return string the timezone offset of this time from the Greenwich Mean Time formatted according to ISO 8601
192
     *                  using no delimiter, e.g., <tt>+0200</tt> or <tt>-0830</tt>
193
     */
194
    final public function getOffsetISOString(): string
195
    {
196
        return sprintf('%s%02d%02d',
197
            ($this->offset >= 0 ? '+' : '-'),
198
            abs($this->offset) / (60 * 60),
199
            (abs($this->offset) / 60) % 60
200
        );
201
    }
202
203
    public function toUnixTimestamp($date = null)
204
    {
205
        return parent::toUnixTimestamp($date) - $this->offset;
206
    }
207
208
    /**
209
     * @param TimeTz $other
210
     * @return bool whether this and the other time happen in the exact same moment
211
     */
212
    public function occursAt(TimeTz $other): bool
213
    {
214
        return ($this->toUnixTimestamp() == $other->toUnixTimestamp());
215
    }
216
217
    /**
218
     * @param TimeTz $other
219
     * @return bool whether this time happens before the given other time
220
     */
221
    public function occursBefore(TimeTz $other): bool
222
    {
223
        return ($this->toUnixTimestamp() < $other->toUnixTimestamp());
224
    }
225
226
    /**
227
     * @param TimeTz $other
228
     * @return bool whether this time happens after the given other time
229
     */
230
    public function occursAfter(TimeTz $other): bool
231
    {
232
        return ($this->toUnixTimestamp() > $other->toUnixTimestamp());
233
    }
234
235
    /**
236
     * @param string $timeFmt the format string as accepted by {@link date()}
237
     * @return string the time formatted according to <tt>$timeFmt</tt>
238
     */
239
    public function format(string $timeFmt): string
240
    {
241
        $ts = new \DateTime($this->toString()); // OPT: \DateTime::createFromFormat() is supposed to be twice as fast as new \DateTime()
242
        $ts->setDate(1970, 1, 1);
243
        return $ts->format($timeFmt);
244
    }
245
246
    /**
247
     * @return string the ISO representation of this time, in format <tt>HH:MM:SS[.p](+|-)hhmm</tt>, where <tt>hh</tt>
248
     *                  and <tt>mm</tt> represent the timezone offset in hours and minutes, respectively;
249
     *                the fractional seconds part is only used if non-zero
250
     */
251
    public function toString(): string
252
    {
253
        return parent::toString() . $this->getOffsetISOString();
254
    }
255
}
256