Passed
Push — master ( e50bdb...a6f0ce )
by Ondřej
03:20
created

TimestampTz::nowMicro()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
1
<?php
2
namespace Ivory\Value;
3
4
/**
5
 * Timezone-aware representation of date and time according to the
6
 * {@link https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar proleptic} Gregorian calendar.
7
 *
8
 * For a timezone-unaware date/time, see {@link Timestamp}.
9
 *
10
 * As in PostgreSQL, there are two special date/time values, `-infinity` and `infinity`, representing a date/time
11
 * respectively before or after any other date/time. There are special factory methods
12
 * {@link TimestampTz::minusInfinity()} and {@link TimestampTz::infinity()} for getting these values.
13
 *
14
 * All the operations work correctly beyond the UNIX timestamp range bounded by 32bit integers, i.e., it is no problem
15
 * calculating with year 12345, for example.
16
 *
17
 * Note the date/time value is immutable, i.e., once constructed, its value cannot be changed.
18
 *
19
 * @see http://www.postgresql.org/docs/9.4/static/datetime-units-history.html
20
 */
21
class TimestampTz extends TimestampBase
22
{
23
    /**
24
     * @return TimestampTz date/time representing the current moment, with precision to microseconds (or, more
25
     *                       specifically, with the precision supported by the hosting platform)
26
     */
27 View Code Duplication
    public static function now(): TimestampTz
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
28
    {
29
        if (PHP_VERSION_ID >= 70100) {
30
            return new TimestampTz(0, new \DateTimeImmutable('now'));
31
        }
32
        else {
33
            // up to PHP 7.0, new \DateTimeImmutable('now') had only precision up to seconds
34
            list($micro, $sec) = explode(' ', microtime());
35
            $microFrac = substr($micro, 1); // cut off the whole part (always a zero)
36
            $inputStr = date('Y-m-d\TH:i:s', $sec) . $microFrac;
37
            return new TimestampTz(0, new \DateTimeImmutable($inputStr));
38
        }
39
    }
40
41
    /**
42
     * Creates a date/time from an ISO 8601 string.
43
     *
44
     * As ISO 8601, the input shall be formatted as, e.g., `2016-03-30T18:30:42Z`. This method also accepts a space as
45
     * the date/time separator instead of the `T` letter. Timezone information may be omitted, in which case the current
46
     * timezone is used. As ISO 8601 says, either `Z` or `±hh[[:]mm]` is expected as the timezone specification.
47
     *
48
     * Years beyond 4 digits are supported, i.e., `'12345-01-30'` is a valid input, representing a date of year 12345.
49
     *
50
     * As defined by ISO 8601, years before Christ are expected to be represented by numbers prefixed with a minus sign,
51
     * `0000` representing year 1 BC, `-0001` standing for year 2 BC, etc.
52
     *
53
     * Years anno Domini, i.e., the positive years, may optionally be prefixed with a plus sign.
54
     *
55
     * @param string $isoDateTimeString
56
     * @return TimestampTz
57
     * @throws \InvalidArgumentException on invalid input
58
     */
59
    public static function fromISOString(string $isoDateTimeString): TimestampTz
60
    {
61
        $dt = self::isoStringToDateTime($isoDateTimeString);
62
        return new TimestampTz(0, $dt);
63
    }
64
65
    /**
66
     * @param \DateTimeInterface $dateTime
67
     * @return TimestampTz date/time represented by the given <tt>$dateTime</tt> object
68
     */
69
    public static function fromDateTime(\DateTimeInterface $dateTime): TimestampTz
70
    {
71
        if ($dateTime instanceof \DateTimeImmutable) {
72
            return new TimestampTz(0, $dateTime);
73
        } elseif ($dateTime instanceof \DateTime) {
74
            return new TimestampTz(0, \DateTimeImmutable::createFromMutable($dateTime));
75
        } else {
76
            // there should not be any other implementation of \DateTimeInterface, but PHP is not that predictable
77
            return self::fromParts(
78
                $dateTime->format('Y'),
79
                $dateTime->format('n'),
80
                $dateTime->format('j'),
81
                $dateTime->format('G'),
82
                $dateTime->format('i'),
83
                $dateTime->format('s') + ($dateTime->format('u') ? $dateTime->format('u') / 1000000 : 0),
84
                $dateTime->format('e')
85
            );
86
        }
87
    }
88
89
    /**
90
     * Creates a date/time from the given year, month, day, hour, minute, second, and timezone.
91
     *
92
     * Invalid combinations of months and days, as well as hours, minutes and seconds outside their standard ranges,
93
     * are accepted similarly to the {@link mktime()} function.
94
     * E.g., `$year 2015, $month 14, $day 32, $hour 25, $minute -2, second 70` will be silently converted to
95
     * `2016-03-04 00:59:10`. If this is unacceptable, use the strict variant {@link TimestampTz::fromPartsStrict()}
96
     * instead.
97
     *
98
     * Years before Christ shall be represented by negative numbers. E.g., year 42 BC shall be given as -42.
99
     *
100
     * Note that, in the Gregorian calendar, there is no year 0. Thus, `$year == 0` will be rejected with an
101
     * `\InvalidArgumentException`.
102
     *
103
     * @param int $year
104
     * @param int $month
105
     * @param int $day
106
     * @param int $hour
107
     * @param int $minute
108
     * @param int|float $second
109
     * @param \DateTimeZone|string|int $timezone either the \DateTimeZone object, or
110
     *                                             a string containing the timezone name or its abbreviation (e.g.,
111
     *                                             <tt>Europe/Prague</tt> or <tt>CEST</tt>) or ISO-style offset from GMT
112
     *                                             (e.g., <tt>+02:00</tt> or <tt>+0200</tt> or <tt>+02</tt>), or
113
     *                                             an integer specifying the offset from GMT in seconds
114
     * @return TimestampTz
115
     * @throws \InvalidArgumentException if <tt>$year</tt> is zero or <tt>$timezone</tt> is not recognized by PHP
116
     */
117
    public static function fromParts(
118
        int $year,
119
        int $month,
120
        int $day,
121
        int $hour,
122
        int $minute,
123
        $second,
124
        $timezone
125
    ): TimestampTz {
126
        if ($year == 0) {
127
            throw new \InvalidArgumentException('$year zero is undefined');
128
        }
129
130
        $tz = self::parseTimezone($timezone);
131
        $z = ($year > 0 ? $year : $year + 1);
132
133
        if (self::inRanges($month, $day, $hour, $minute, $second)) {
134
            // works even for months without 31 days
135
            $dt = self::isoStringToDateTime(
136
                sprintf(
137
                    '%s%04d-%02d-%02d %02d:%02d:%s',
138
                    ($z < 0 ? '-' : ''), abs($z), $month, $day, $hour, $minute, self::floatToTwoPlaces($second)
139
                ),
140
                $tz
141
            );
142
            return new TimestampTz(0, $dt);
143
        } else {
144
            $dt = self::isoStringToDateTime(
145
                sprintf('%s%04d-01-01 00:00:00', ($z < 0 ? '-' : ''), abs($z)),
146
                $tz
147
            );
148
            return (new TimestampTz(0, $dt))
149
                ->addParts(0, $month - 1, $day - 1, $hour, $minute, $second);
150
        }
151
    }
152
153
    /**
154
     * Creates a date/time from the given year, month, day, hour, minute, second, and timezone while strictly checking
155
     * for the validity of the data.
156
     *
157
     * For a friendlier variant, accepting even out-of-range values (doing the adequate calculations), see
158
     * {@link TimestampTz::fromParts()}.
159
     *
160
     * Years before Christ shall be represented by negative numbers. E.g., year 42 BC shall be given as -42.
161
     *
162
     * Note that, in the Gregorian calendar, there is no year 0. Thus, `$year == 0` will be rejected with an
163
     * `\InvalidArgumentException`.
164
     *
165
     * @param int $year
166
     * @param int $month
167
     * @param int $day
168
     * @param int $hour
169
     * @param int $minute
170
     * @param int|float $second
171
     * @param \DateTimeZone|string|int $timezone either the \DateTimeZone object, or
172
     *                                             a string containing the timezone name or its abbreviation (e.g.,
173
     *                                             <tt>Europe/Prague</tt> or <tt>CEST</tt>) or ISO-style offset from GMT
174
     *                                             (e.g., <tt>+02:00</tt> or <tt>+0200</tt> or <tt>+02</tt>), or
175
     *                                             an integer specifying the offset from GMT in seconds
176
     * @return TimestampTz
177
     * @throws \InvalidArgumentException if <tt>$year</tt> is zero or <tt>$timezone</tt> is not recognized by PHP
178
     */
179
    public static function fromPartsStrict(
180
        int $year,
181
        int $month,
182
        int $day,
183
        int $hour,
184
        int $minute,
185
        $second,
186
        $timezone
187
    ): TimestampTz {
188
        self::assertRanges($year, $month, $day, $hour, $minute, $second);
189
        $tz = self::parseTimezone($timezone);
190
        $z = ($year > 0 ? $year : $year + 1);
191
192
        $dt = self::isoStringToDateTime(
193
            sprintf(
194
                '%s%04d-%02d-%02d %02d:%02d:%s',
195
                ($z < 0 ? '-' : ''), abs($z), $month, $day, $hour, $minute, self::floatToTwoPlaces($second)
196
            ),
197
            $tz
198
        );
199
        if ($dt->format('j') != ($day + ($hour == 24 ? 1 : 0))) {
200
            throw new \OutOfRangeException('$day out of range');
201
        }
202
203
        return new TimestampTz(0, $dt);
204
    }
205
206
    private static function parseTimezone($timezone): \DateTimeZone
207
    {
208
        if ($timezone instanceof \DateTimeZone) {
209
            return $timezone;
210
        }
211
212
        if (filter_var($timezone, FILTER_VALIDATE_INT) !== false) {
213
            $tzSpec = ($timezone >= 0 ? '+' : '-') . gmdate('H:i', abs($timezone));
214
        } elseif (preg_match('~^([^:]+:\d+):\d+$~', $timezone, $m)) {
215
            $tzSpec = $m[1];
216
            $msg = "PHP's DateTimeZone is unable to represent GMT offsets with precision to seconds. "
217
                . "Cutting '$timezone' to '$tzSpec'";
218
            trigger_error($msg, E_USER_WARNING);
219
        } else {
220
            $tzSpec = $timezone;
221
        }
222
223
        try {
224
            return new \DateTimeZone($tzSpec);
225
        } catch (\Exception $e) {
226
            throw new \InvalidArgumentException('$timezone', 0, $e);
227
        }
228
    }
229
230
    final protected function getISOFormat(): string
231
    {
232
        return 'Y-m-d\TH:i:s' . ($this->dt->format('u') ? '.u' : '') . 'O';
233
    }
234
235
    /**
236
     * @return string the timezone offset of this time from the Greenwich Mean Time formatted according to ISO 8601
237
     *                  using no delimiter, e.g., <tt>+0200</tt> or <tt>-0830</tt>
238
     */
239
    public function getOffsetISOString(): string
240
    {
241
        return $this->dt->format('O');
242
    }
243
244
    /**
245
     * @return mixed[]|null a list of seven items: year, month, day, hours, minutes, seconds, and timezone of this
246
     *                       date/time, all of which are integers except the seconds part, which might be a float if
247
     *                       containing the fractional part, and the timezone part, which is a {@link \DateTimeZone}
248
     *                       object;
249
     *                     <tt>null</tt> iff the date/time is not finite
250
     */
251 View Code Duplication
    public function toParts()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
252
    {
253
        if ($this->inf) {
254
            return null;
255
        } else {
256
            $y = (int)$this->dt->format('Y');
257
            $u = $this->dt->format('u');
258
            return [
259
                ($y > 0 ? $y : $y - 1),
260
                (int)$this->dt->format('n'),
261
                (int)$this->dt->format('j'),
262
                (int)$this->dt->format('G'),
263
                (int)$this->dt->format('i'),
264
                $this->dt->format('s') + ($u ? $u / 1000000 : 0),
265
                $this->dt->getTimezone(),
266
            ];
267
        }
268
    }
269
}
270