Passed
Push — master ( 94f321...811ae6 )
by Ondřej
02:56 queued 12s
created

TimeBase   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 181
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 56
c 2
b 0
f 1
dl 0
loc 181
rs 10
wmc 30

11 Methods

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