TimestampBase::isoStringToDateTime()   A
last analyzed

Complexity

Conditions 5
Paths 12

Size

Total Lines 35
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 1
Metric Value
cc 5
eloc 17
c 1
b 1
f 1
nc 12
nop 2
dl 0
loc 35
rs 9.3888
1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Value;
4
5
/**
6
 * @internal Only for the purpose of Ivory itself.
7
 */
8
abstract class TimestampBase extends DateBase
9
{
10
    /**
11
     * @param int|float $timestamp the UNIX timestamp;
12
     *                       besides an integer, a float is also accepted, which may contain fractional seconds;
13
     *                       note that a UNIX timestamp represents the number of seconds since 1970-01-01 UTC, i.e., it
14
     *                         corresponds to usage of PHP functions {@link gmmktime()} and {@link gmdate()} rather than
15
     *                         {@link mktime()} or {@link date()}
16
     * @return static
17
     */
18
    public static function fromUnixTimestamp($timestamp): TimestampBase
19
    {
20
        $str = gmdate('Y-m-d H:i:s', (int)$timestamp);
21
        $tz = self::getUTCTimeZone();
22
        try {
23
            $datetime = new \DateTimeImmutable($str, $tz);
24
        } catch (\Exception $e) {
25
            throw new \LogicException('Date/time error', 0, $e);
26
        }
27
        $timestamp = new static(0, $datetime);
28
29
        // gmdate() only accepts an integer - add the fractional part separately
30
        $frac = $timestamp - (int)$timestamp;
31
        if ($frac) {
32
            $timestamp = $timestamp->addSecond($frac);
33
        }
34
        return $timestamp;
35
    }
36
37
    protected static function isoStringToDateTime(
38
        string $isoDateTimeString,
39
        ?\DateTimeZone $forcedTimezone = null
40
    ): \DateTimeImmutable {
41
        // check out for more than 4 digits for the year - something date_create_immutable() does not handle properly
42
        $addYears = 0;
43
        $dateCreateInput = preg_replace_callback(
44
            '~\d{5,}(?=(?:-\d+-\d+|\d{4})(?:\s+|T)\d)~', // supports both dash-separated date/time parts and also the
45
                                                         // form without dash separators
46
            function ($y) use (&$addYears) {
47
                $res = $y[0] % 10000;
48
                $addYears = $y[0] - $res;
49
                return $res;
50
            },
51
            $isoDateTimeString,
52
            1
53
        );
54
55
        if ($forcedTimezone !== null) {
56
            // the date_create_immutable() prefers the timezone (if any) in the input string - which we must cut off
57
            $dateCreateInput = preg_replace('~Z|[-+]\d{2}(?::?\d{2})?$~', '', $dateCreateInput, 1);
58
        }
59
60
        if ($addYears) {
61
            $sgn = ($dateCreateInput[0] == '-' ? '-' : '+');
62
            $dateCreateInput .= " $sgn$addYears years";
63
        }
64
65
        // using the procedural style as it does not throw the generic \Exception
66
        $dt = date_create_immutable($dateCreateInput, $forcedTimezone);
67
        if ($dt === false) {
68
            throw new \InvalidArgumentException('$isoDateString');
69
        }
70
71
        return $dt;
72
    }
73
74
    protected static function floatToTwoPlaces($float): string
75
    {
76
        if ($float >= 10) {
77
            return (string)$float;
78
        } else {
79
            return '0' . (float)$float;
80
        }
81
    }
82
83
    protected static function inRanges(int $month, int $day, int $hour, int $minute, $second): bool
84
    {
85
        return (
86
            $month >= 1 && $month <= 12 &&
87
            $day >= 1 && $day <= 31 &&
88
            (
89
                ($hour >= 0 && $hour <= 23 && $minute >= 0 && $minute <= 59 && $second >= 0 && $second < 61)
90
                ||
91
                ($hour == 24 && $minute == 0 && $second == 0)
92
            )
93
        );
94
    }
95
96
    protected static function assertRanges(int $year, int $month, int $day, int $hour, int $minute, $second): void
97
    {
98
        if ($year == 0) {
99
            throw new \InvalidArgumentException('$year zero is undefined');
100
        }
101
        if ($month < 1 || $month > 12) {
102
            throw new \OutOfRangeException('$month out of range');
103
        }
104
        if ($day < 1 || $day > 31) { // days in the month shall be verified ex post
105
            throw new \OutOfRangeException('$day out of range');
106
        }
107
        if ($hour < 0 || $hour > 24 || ($hour == 24 && $minute > 0 && $second > 0)) {
108
            throw new \OutOfRangeException('$hour out of range');
109
        }
110
        if ($minute < 0 || $minute > 60) {
111
            throw new \OutOfRangeException('$minute out of range');
112
        }
113
        if ($second < 0 || $second >= 61) {
114
            throw new \OutOfRangeException('$second out of range');
115
        }
116
    }
117
118
119
    /**
120
     * @return int|null the hour part of the date/time;
121
     *                  <tt>null</tt> iff the date/time is not finite
122
     */
123
    final public function getHour(): ?int
124
    {
125
        return ($this->inf ? null : (int)$this->dt->format('G'));
126
    }
127
128
    /**
129
     * @return int|null the minute part of the date/time;
130
     *                  <tt>null</tt> iff the date/time is not finite
131
     */
132
    final public function getMinute(): ?int
133
    {
134
        return ($this->inf ? null : (int)$this->dt->format('i'));
135
    }
136
137
    /**
138
     * @return int|float|null the second part of the date/time, including the fractional seconds;
139
     *                        <tt>null</tt> iff the date/time is not finite
140
     */
141
    final public function getSecond()
142
    {
143
        if ($this->inf) {
144
            return null;
145
        } elseif ($this->dt->format('u')) {
146
            return (float)$this->dt->format('s.u');
147
        } else {
148
            return (int)$this->dt->format('s');
149
        }
150
    }
151
152
    /**
153
     * Adds a given number of years, months, days, hours, minutes, and seconds to this date/time and returns the result.
154
     *
155
     * Only affects finite date/times - an infinite date/time is returned as is.
156
     *
157
     * Note that addition of months respects the month days, and might actually change the day part. Example:
158
     * - adding 1 month to `2015-05-31` results in `2015-07-01` (June only has 30 days).
159
     *
160
     * Addition of years and months is done prior to addition of days, so the actual number of days in month is
161
     * evaluated with respect to the new month. Examples:
162
     * - adding 2 months and 1 day to `2015-05-31` results in `2015-08-01`,
163
     * - adding 1 month and 1 day to `2015-05-31` results in `2015-07-02` (June only has 30 days),
164
     * - adding 1 month and 1 day to `2015-02-28` results in `2015-03-29`,
165
     * - adding 1 year and 1 day to `2015-02-28` results in `2016-02-29`,
166
     * - adding 1 year and 1 day to `2016-02-28` results in `2017-03-01`.
167
     *
168
     * @param int $years
169
     * @param int $months
170
     * @param int $days
171
     * @param int $hours
172
     * @param int $minutes
173
     * @param int|float $seconds
174
     * @return static the date/time <tt>$years</tt> years, <tt>$months</tt> months, <tt>$days</tt> days,
175
     *                  <tt>$hours</tt> hours, <tt>$minutes</tt> minutes, and <tt>$seconds</tt> seconds after this
176
     *                  date/time
177
     */
178
    public function addParts(int $years, int $months, int $days, int $hours, int $minutes, $seconds): TimestampBase
179
    {
180
        return $this->addPartsImpl($years, $months, $days, $hours, $minutes, $seconds);
181
    }
182
183
    /**
184
     * Adds a given number of hours (1 by default) to this date/time and returns the result. Only affects finite dates.
185
     *
186
     * @param int $hours
187
     * @return static the date <tt>$hours</tt> hours after (or before, if negative) this date/time
188
     */
189
    public function addHour(int $hours = 1): TimestampBase
190
    {
191
        return $this->addPartsImpl(0, 0, 0, $hours, 0, 0);
192
    }
193
194
    /**
195
     * Adds a given number of minutes (1 by default) to this date/time and returns the result. Only affects finite
196
     * dates.
197
     *
198
     * @param int $minutes
199
     * @return static the date <tt>$minutes</tt> minutes after (or before, if negative) this date/time
200
     */
201
    public function addMinute(int $minutes = 1): TimestampBase
202
    {
203
        return $this->addPartsImpl(0, 0, 0, 0, $minutes, 0);
204
    }
205
206
    /**
207
     * Adds a given number of seconds (1 by default) to this date/time and returns the result. Only affects finite
208
     * dates.
209
     *
210
     * @param int|float $seconds
211
     * @return static the date <tt>$seconds</tt> seconds after (or before, if negative) this date/time
212
     */
213
    public function addSecond($seconds = 1): TimestampBase
214
    {
215
        return $this->addPartsImpl(0, 0, 0, 0, 0, $seconds);
216
    }
217
}
218