• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tempestphp / tempest-framework / 14639629920

24 Apr 2025 10:41AM UTC coverage: 80.923%. First build
14639629920

Pull #1158

github

web-flow
Merge 11a3af0ac into 06be1af98
Pull Request #1158: feat(datetime): add datetime component

1099 of 1227 new or added lines in 39 files covered. (89.57%)

12849 of 15878 relevant lines covered (80.92%)

100.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.0
/src/Tempest/DateTime/src/functions.php
1
<?php
2

3
namespace Tempest\DateTime {
4
    use IntlCalendar;
5
    use IntlDateFormatter;
6
    use IntlTimeZone;
7
    use RuntimeException;
8
    use Tempest\DateTime\DateStyle;
9
    use Tempest\DateTime\Exception\ParserException;
10
    use Tempest\DateTime\FormatPattern;
11
    use Tempest\DateTime\SecondsStyle;
12
    use Tempest\DateTime\Timestamp;
13
    use Tempest\DateTime\TimeStyle;
14
    use Tempest\DateTime\Timezone;
15
    use Tempest\Support\Language\Locale;
16

17
    use function hrtime;
18
    use function microtime;
19

20
    use const Tempest\DateTime\NANOSECONDS_PER_SECOND;
21

22
    /**
23
     * Get the current date and time as a {@see \Tempest\DateTime\DateTime} object.
24
     */
25
    function now(): DateTime
26
    {
NEW
27
        return DateTime::now();
×
28
    }
29

30
    /**
31
     * Check if the given year is a leap year.
32
     *
33
     * Returns true if the specified year is a leap year according to the Gregorian
34
     * calendar; otherwise, returns false.
35
     *
36
     * @return bool True if the year is a leap year, false otherwise.
37
     */
38
    function is_leap_year(int $year): bool
39
    {
40
        return ($year % 4) === 0 && (($year % 100) !== 0 || ($year % 400) === 0);
134✔
41
    }
42

43
    /**
44
     * @internal
45
     *
46
     * @mago-expect best-practices/no-else-clause
47
     */
48
    function format_rfc3339(Timestamp $timestamp, ?SecondsStyle $secondsStyle = null, bool $useZ = false, ?Timezone $timezone = null): string
49
    {
50
        $secondsStyle ??= SecondsStyle::fromTimestamp($timestamp);
2✔
51

52
        if (null === $timezone) {
2✔
53
            $timezone = Timezone::UTC;
1✔
54
        } elseif ($useZ) {
1✔
NEW
55
            $useZ = Timezone::UTC === $timezone;
×
56
        }
57

58
        $seconds = $timestamp->getSeconds();
2✔
59
        $nanoseconds = $timestamp->getNanoseconds();
2✔
60

61
        // Intl formatter cannot handle nanoseconds and microseconds, do it manually instead.
62
        $fraction = substr((string) $nanoseconds, 0, $secondsStyle->value);
2✔
63

64
        if ($fraction !== '') {
2✔
65
            $fraction = '.' . $fraction;
1✔
66
        }
67

68
        $pattern = match ($useZ) {
2✔
69
            true => "yyyy-MM-dd'T'HH:mm:ss@ZZZZZ",
1✔
70
            false => "yyyy-MM-dd'T'HH:mm:ss@xxx",
2✔
71
        };
2✔
72

73
        $formatter = namespace\create_intl_date_formatter(
2✔
74
            pattern: $pattern,
2✔
75
            timezone: $timezone,
2✔
76
        );
2✔
77

78
        $rfcString = $formatter->format($seconds);
2✔
79

80
        return str_replace('@', $fraction, $rfcString);
2✔
81
    }
82

83
    /**
84
     * @internal
85
     */
86
    function create_intl_date_formatter(
87
        ?DateStyle $dateStyle = null,
88
        ?TimeStyle $timeStyle = null,
89
        null|FormatPattern|string $pattern = null,
90
        ?Timezone $timezone = null,
91
        ?Locale $locale = null,
92
    ): IntlDateFormatter {
93
        if ($pattern instanceof FormatPattern) {
24✔
94
            $pattern = $pattern->value;
16✔
95
        }
96

97
        $dateStyle ??= DateStyle::default();
24✔
98
        $timeStyle ??= TimeStyle::default();
24✔
99
        $locale ??= Locale::default();
24✔
100
        $timezone ??= Timezone::default();
24✔
101

102
        return new IntlDateFormatter(
24✔
103
            locale: $locale->value,
24✔
104
            dateType: match ($dateStyle) {
24✔
105
                DateStyle::NONE => IntlDateFormatter::NONE,
24✔
106
                DateStyle::SHORT => IntlDateFormatter::SHORT,
24✔
107
                DateStyle::MEDIUM => IntlDateFormatter::MEDIUM,
24✔
NEW
108
                DateStyle::LONG => IntlDateFormatter::LONG,
×
NEW
109
                DateStyle::FULL => IntlDateFormatter::FULL,
×
NEW
110
                DateStyle::RELATIVE_SHORT => IntlDateFormatter::RELATIVE_SHORT,
×
NEW
111
                DateStyle::RELATIVE_MEDIUM => IntlDateFormatter::RELATIVE_MEDIUM,
×
NEW
112
                DateStyle::RELATIVE_LONG => IntlDateFormatter::RELATIVE_LONG,
×
113
                DateStyle::RELATIVE_FULL => IntlDateFormatter::RELATIVE_FULL,
24✔
114
            },
24✔
115
            timeType: match ($timeStyle) {
24✔
116
                TimeStyle::NONE => IntlDateFormatter::NONE,
24✔
117
                TimeStyle::SHORT => IntlDateFormatter::SHORT,
24✔
118
                TimeStyle::MEDIUM => IntlDateFormatter::MEDIUM,
24✔
119
                TimeStyle::LONG => IntlDateFormatter::LONG,
1✔
120
                TimeStyle::FULL => IntlDateFormatter::FULL,
24✔
121
            },
24✔
122
            timezone: namespace\to_intl_timezone($timezone),
24✔
123
            calendar: IntlDateFormatter::GREGORIAN,
24✔
124
            pattern: $pattern,
24✔
125
        );
24✔
126
    }
127

128
    /**
129
     * @internal
130
     */
131
    function default_timezone(): Timezone
132
    {
133
        /**
134
         * `date_default_timezone_get` function might return any of the "Others" timezones
135
         * mentioned in PHP doc: https://www.php.net/manual/en/timezones.others.php.
136
         *
137
         * Those timezones are not supported by Tempest (aside from UTC), as they are considered "legacy".
138
         */
139
        $timezoneId = date_default_timezone_get();
107✔
140

141
        return Timezone::tryFrom($timezoneId) ?? Timezone::UTC;
107✔
142
    }
143

144
    /**
145
     * @return array{int, int}
146
     *
147
     * @internal
148
     *
149
     * @mago-expect best-practices/no-boolean-literal-comparison
150
     * @mago-expect best-practices/no-else-clause
151
     */
152
    function high_resolution_time(): array
153
    {
154
        /**
155
         * @var null|array{int, int} $offset
156
         */
157
        static $offset = null;
60✔
158

159
        if ($offset === null) {
60✔
160
            $offset = hrtime();
1✔
161

162
            if ($offset === false) { // @phpstan-ignore-line identical.alwaysFalse
1✔
NEW
163
                throw new \RuntimeException('The system does not provide a monotonic timer.');
×
164
            }
165

166
            $time = system_time();
1✔
167

168
            $offset = [
1✔
169
                $time[0] - $offset[0],
1✔
170
                $time[1] - $offset[1],
1✔
171
            ];
1✔
172
        }
173

174
        [$secondsOffset, $nanosecondsOffset] = $offset;
60✔
175
        [$seconds, $nanoseconds] = hrtime();
60✔
176

177
        $nanosecondsAdjusted = $nanoseconds + $nanosecondsOffset;
60✔
178

179
        if ($nanosecondsAdjusted >= NANOSECONDS_PER_SECOND) {
60✔
180
            ++$seconds;
32✔
181
            $nanosecondsAdjusted -= NANOSECONDS_PER_SECOND;
32✔
182
        } elseif ($nanosecondsAdjusted < 0) {
28✔
NEW
183
            --$seconds;
×
NEW
184
            $nanosecondsAdjusted += NANOSECONDS_PER_SECOND;
×
185
        }
186

187
        $seconds += $secondsOffset;
60✔
188
        $nanoseconds = $nanosecondsAdjusted;
60✔
189

190
        return [$seconds, $nanoseconds];
60✔
191
    }
192

193
    /**
194
     * @internal
195
     *
196
     * @mago-expect best-practices/no-boolean-literal-comparison
197
     */
198
    function intl_parse(
199
        string $rawString,
200
        ?DateStyle $dateStyle = null,
201
        ?TimeStyle $timeStyle = null,
202
        null|FormatPattern|string $pattern = null,
203
        ?Timezone $timezone = null,
204
        ?Locale $locale = null,
205
    ): int {
206
        $formatter = namespace\create_intl_date_formatter($dateStyle, $timeStyle, $pattern, $timezone, $locale);
16✔
207

208
        $timestamp = $formatter->parse($rawString);
16✔
209

210
        if ($timestamp === false) {
16✔
211
            // Only show pattern in the exception if it was provided.
212
            if (null !== $pattern) {
2✔
213
                $formatter_pattern = ($pattern instanceof FormatPattern) ? $pattern->value : $pattern;
2✔
214

215
                throw new ParserException(sprintf(
2✔
216
                    "Unable to interpret '%s' as a valid date/time using pattern '%s'.",
2✔
217
                    $rawString,
2✔
218
                    $formatter_pattern,
2✔
219
                ));
2✔
220
            }
221

NEW
222
            throw new ParserException("Unable to interpret '{$rawString}' as a valid date/time.");
×
223
        }
224

225
        return (int) $timestamp;
14✔
226
    }
227

228
    /**
229
     * @return array{int, int}
230
     *
231
     * @internal
232
     */
233
    function system_time(): array
234
    {
235
        $time = microtime();
66✔
236

237
        $parts = explode(' ', $time);
66✔
238
        $seconds = (int) $parts[1];
66✔
239
        $nanoseconds = (int) (((float) $parts[0]) * ((float) NANOSECONDS_PER_SECOND));
66✔
240

241
        return [$seconds, $nanoseconds];
66✔
242
    }
243

244
    /**
245
     * @internal
246
     */
247
    function to_intl_timezone(Timezone $timezone): IntlTimeZone
248
    {
249
        $value = $timezone->value;
137✔
250

251
        if (str_starts_with($value, '+') || str_starts_with($value, '-')) {
137✔
252
            $value = 'GMT' . $value;
1✔
253
        }
254

255
        $tz = IntlTimeZone::createTimeZone($value);
137✔
256

257
        if ($tz === null) { // @phpstan-ignore-line identical.alwaysFalse
137✔
NEW
258
            throw new \RuntimeException(sprintf(
×
NEW
259
                'Failed to create intl timezone from timezone "%s" ("%s" / "%s").',
×
NEW
260
                $timezone->name,
×
NEW
261
                $timezone->value,
×
NEW
262
                $value,
×
NEW
263
            ));
×
264
        }
265

266
        if ($tz->getID() === 'Etc/Unknown' && $tz->getRawOffset() === 0) {
137✔
NEW
267
            throw new \RuntimeException(sprintf(
×
NEW
268
                'Failed to create a valid intl timezone, unknown timezone "%s" ("%s" / "%s") given.',
×
NEW
269
                $timezone->name,
×
NEW
270
                $timezone->value,
×
NEW
271
                $value,
×
NEW
272
            ));
×
273
        }
274

275
        return $tz;
137✔
276
    }
277

278
    /**
279
     * @internal
280
     *
281
     * @mago-expect best-practices/no-else-clause
282
     */
283
    function create_intl_calendar_from_date_time(
284
        Timezone $timezone,
285
        int $year,
286
        int $month,
287
        int $day,
288
        int $hours,
289
        int $minutes,
290
        int $seconds,
291
    ): IntlCalendar {
292
        /**
293
         * @var IntlCalendar $calendar
294
         */
295
        $calendar = IntlCalendar::createInstance(to_intl_timezone($timezone));
43✔
296

297
        $calendar->setDateTime($year, $month - 1, $day, $hours, $minutes, $seconds);
43✔
298

299
        return $calendar;
43✔
300
    }
301
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc