Passed
Push — master ( 71c175...699f14 )
by y
01:29
created

DateTimeTrait::toUTC()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 11
rs 10
1
<?php
2
3
namespace Helix\DB\Fluent\DateTime;
4
5
use DateInterval;
6
use Helix\DB\Fluent\DateTime;
7
use Helix\DB\Fluent\Num;
8
use Helix\DB\Fluent\Text;
9
use Helix\DB\Fluent\Value\ValueTrait;
10
11
/**
12
 * Date-time expression manipulation.
13
 *
14
 * Each DBMS has its own quirks with dates, which is beyond the scope of this library.
15
 *
16
 * @see https://sqlite.org/lang_datefunc.html
17
 * @see https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-format
18
 */
19
trait DateTimeTrait
20
{
21
22
    use ValueTrait;
23
24
    /**
25
     * @return DateTime
26
     */
27
    public function addDay()
28
    {
29
        return $this->addDays(1);
30
    }
31
32
    /**
33
     * @param int $days
34
     * @return DateTime
35
     */
36
    public function addDays(int $days)
37
    {
38
        return $this->modify(0, 0, 0, $days);
39
    }
40
41
    /**
42
     * @return DateTime
43
     */
44
    public function addHour()
45
    {
46
        return $this->addHours(1);
47
    }
48
49
    /**
50
     * @param int $hours
51
     * @return DateTime
52
     */
53
    public function addHours(int $hours)
54
    {
55
        return $this->modify(0, 0, $hours);
56
    }
57
58
    /**
59
     * @param int $minutes
60
     * @return DateTime
61
     */
62
    public function addMinutes(int $minutes)
63
    {
64
        return $this->modify(0, $minutes);
65
    }
66
67
    /**
68
     * @return DateTime
69
     */
70
    public function addMonth()
71
    {
72
        return $this->addMonths(1);
73
    }
74
75
    /**
76
     * @param int $months
77
     * @return DateTime
78
     */
79
    public function addMonths(int $months)
80
    {
81
        return $this->modify(0, 0, 0, 0, $months);
82
    }
83
84
    /**
85
     * @param int $seconds
86
     * @return DateTime
87
     */
88
    public function addSeconds(int $seconds)
89
    {
90
        return $this->modify($seconds);
91
    }
92
93
    /**
94
     * @return DateTime
95
     */
96
    public function addYear()
97
    {
98
        return $this->addYears(1);
99
    }
100
101
    /**
102
     * @param int $years
103
     * @return DateTime
104
     */
105
    public function addYears(int $years)
106
    {
107
        return $this->modify(0, 0, 0, 0, 0, $years);
108
    }
109
110
    /**
111
     * `YYYY-MM-DD`
112
     *
113
     * Because this format is reentrant, a {@link DateTime} is returned.
114
     *
115
     * @return DateTime
116
     */
117
    public function date()
118
    {
119
        return DateTime::factory($this->db, "DATE({$this})");
120
    }
121
122
    /**
123
     * Date formatting expression using a driver-appropriate function.
124
     *
125
     * @param string|string[] $format Format, or formats keyed by driver name.
126
     * @return Text
127
     */
128
    public function dateFormat($format)
129
    {
130
        if (is_array($format)) {
131
            $format = $format[$this->db->getDriver()];
132
        }
133
        $format = $this->db->quote($format);
134
        if ($this->db->isSQLite()) {
135
            return Text::factory($this->db, "STRFTIME({$format},{$this})");
136
        }
137
        return Text::factory($this->db, "DATE_FORMAT({$this},{$format})");
138
    }
139
140
    /**
141
     * `YYYY-MM-DD hh:mm:ss`
142
     *
143
     * Because this format is reentrant, a {@link DateTime} is returned.
144
     *
145
     * @return DateTime
146
     */
147
    public function datetime()
148
    {
149
        if ($this->db->isSQLite()) {
150
            return DateTime::factory($this->db, "DATETIME({$this})");
151
        }
152
        return DateTime::factory($this->db, "DATE_FORMAT({$this},'%Y-%m-%d %H:%i:%S')");
153
    }
154
155
    /**
156
     * `01` to `31`
157
     *
158
     * @return Num
159
     */
160
    public function day()
161
    {
162
        return Num::factory($this->db, $this->dateFormat('%d'));
163
    }
164
165
    /**
166
     * `0` to `6` (Sunday is `0`)
167
     *
168
     * @return Num
169
     */
170
    public function dayOfWeek()
171
    {
172
        return Num::factory($this->db, $this->dateFormat('%w'));
173
    }
174
175
    /**
176
     * `001` to `366` (365 + 1 during leap year)
177
     *
178
     * @return Num
179
     */
180
    public function dayOfYear()
181
    {
182
        return Num::factory($this->db, $this->dateFormat('%j'));
183
    }
184
185
    /**
186
     * Date-time difference (`$x - $this`) in fractional days elapsed.
187
     *
188
     * @param null|DateTime $x Defaults to the current time.
189
     * @return Num
190
     */
191
    public function diffDays(DateTime $x = null)
192
    {
193
        $x ??= $this->db->now();
194
        return $x->julian()->sub($this->julian());
195
    }
196
197
    /**
198
     * Date-time difference (`$x - $this`) in fractional hours elapsed.
199
     *
200
     * @param null|DateTime $x Defaults to the current time.
201
     * @return Num
202
     */
203
    public function diffHours(DateTime $x = null)
204
    {
205
        return $this->diffDays($x)->mul(24);
206
    }
207
208
    /**
209
     * Date-time difference (`$x - $this`) in fractional minutes elapsed.
210
     *
211
     * @param null|DateTime $x Defaults to the current time.
212
     * @return Num
213
     */
214
    public function diffMinutes(DateTime $x = null)
215
    {
216
        return $this->diffDays($x)->mul(24 * 60);
217
    }
218
219
    /**
220
     * Date-time difference (`$x - $this`) in fractional months elapsed.
221
     *
222
     * @param null|DateTime $x Defaults to the current time.
223
     * @return Num
224
     */
225
    public function diffMonths(DateTime $x = null)
226
    {
227
        return $this->diffDays($x)->div(365.2425 / 12);
228
    }
229
230
    /**
231
     * Date-time difference (`$x - $this`) in whole seconds elapsed (rounded).
232
     *
233
     * @param null|DateTime $x Defaults to the current time.
234
     * @return Num
235
     */
236
    public function diffSeconds(DateTime $x = null)
237
    {
238
        return $this->diffDays($x)->mul(24 * 60 * 60)->round();
239
    }
240
241
    /**
242
     * Date-time difference (`$x - $this`) in fractional years elapsed.
243
     *
244
     * @param null|DateTime $x Defaults to the current time.
245
     * @return Num
246
     */
247
    public function diffYears(DateTime $x = null)
248
    {
249
        return $this->diffDays($x)->div(365.2425);
250
    }
251
252
    /**
253
     * `00` to `23`
254
     *
255
     * @return Num
256
     */
257
    public function hours()
258
    {
259
        return Num::factory($this->db, $this->dateFormat('%H'));
260
    }
261
262
    /**
263
     * ISO-8601 compatible datetime string, offset `Z` (UTC/Zulu)
264
     *
265
     * https://en.wikipedia.org/wiki/ISO_8601
266
     *
267
     * @return Text
268
     */
269
    public function iso8601()
270
    {
271
        return $this->dateFormat([
272
            'mysql' => '%Y-%m-%dT%H:%i:%SZ',
273
            'sqlite' => '%Y-%m-%dT%H:%M:%SZ',
274
        ]);
275
    }
276
277
    /**
278
     * Julian day number (fractional).
279
     *
280
     * @return Num
281
     */
282
    public function julian()
283
    {
284
        if ($this->db->isSQLite()) {
285
            return Num::factory($this->db, "JULIANDAY({$this})");
286
        }
287
        // julian "year zero" offset, plus number of fractional days since "year zero".
288
        return Num::factory($this->db, "(1721059.5 + (TO_SECONDS({$this}) / 86400))");
289
    }
290
291
    /**
292
     * `00` to `59`
293
     *
294
     * @return Num
295
     */
296
    public function minutes()
297
    {
298
        return Num::factory($this->db, $this->dateFormat([
299
            'mysql' => '%i',
300
            'sqlite' => '%M'
301
        ]));
302
    }
303
304
    /**
305
     * Applies date-time modifiers.
306
     *
307
     * Each argument can be zero, positive, or negative.
308
     *
309
     * `$s` can also be a `DateInterval` or `DateInterval` description (e.g. `"+1 day"`).
310
     * If so, the rest of the arguments are ignored.
311
     *
312
     * @param int|string|DateInterval $s Seconds, or `DateInterval` related
313
     * @param int $m Minutes
314
     * @param int $h Hours
315
     * @param int $D Days
316
     * @param int $M Months
317
     * @param int $Y Years
318
     * @return DateTime
319
     */
320
    public function modify($s, int $m = 0, int $h = 0, int $D = 0, int $M = 0, int $Y = 0)
321
    {
322
        static $units = ['SECOND', 'MINUTE', 'HOUR', 'DAY', 'MONTH', 'YEAR'];
323
        if (is_string($s)) {
324
            $s = DateInterval::createFromDateString($s);
325
            assert($s instanceof DateInterval);
326
        }
327
        if ($s instanceof DateInterval) {
328
            $ints = [$s->s, $s->i, $s->h, $s->d, $s->m, $s->y];
329
        } else {
330
            $ints = func_get_args();
331
        }
332
333
        // remove zeroes and reverse so larger intervals happen first
334
        $ints = array_reverse(array_filter($ints), true);
335
336
        // sqlite allows us to do it all in one go
337
        if ($this->db->isSQLite()) {
338
            $spec = [$this];
339
            foreach ($ints as $i => $int) {
340
                $spec[] = sprintf("'%s %s'", $int > 0 ? "+{$int}" : $int, $units[$i]);
341
            }
342
            return DateTime::factory($this->db, sprintf('DATETIME(%s)', implode(',', $spec)));
343
        }
344
345
        // mysql requires wrapping
346
        $spec = "CAST({$this} AS DATETIME)";
347
        foreach ($ints as $i => $int) {
348
            $spec = sprintf('DATE_%s(%s, INTERVAL %s %s)',
349
                $spec,
350
                $int > 0 ? 'ADD' : 'SUB',
351
                abs($int),
352
                $units[$i]
353
            );
354
        }
355
        return DateTime::factory($this->db, $spec);
356
    }
357
358
    /**
359
     * `01` to `12`
360
     *
361
     * @return Num
362
     */
363
    public function month()
364
    {
365
        return Num::factory($this->db, $this->dateFormat('%m'));
366
    }
367
368
    /**
369
     * `00` to `59`
370
     *
371
     * @return Num
372
     */
373
    public function seconds()
374
    {
375
        return Num::factory($this->db, $this->dateFormat('%S'));
376
    }
377
378
    /**
379
     * @return DateTime
380
     */
381
    public function subDay()
382
    {
383
        return $this->subDays(1);
384
    }
385
386
    /**
387
     * @param int $days
388
     * @return DateTime
389
     */
390
    public function subDays(int $days)
391
    {
392
        return $this->modify(0, 0, 0, $days * -1);
393
    }
394
395
    /**
396
     * @return DateTime
397
     */
398
    public function subHour()
399
    {
400
        return $this->subHours(1);
401
    }
402
403
    /**
404
     * @param int $hours
405
     * @return DateTime
406
     */
407
    public function subHours(int $hours)
408
    {
409
        return $this->modify(0, 0, $hours * -1);
410
    }
411
412
    /**
413
     * @param int $minutes
414
     * @return DateTime
415
     */
416
    public function subMinutes(int $minutes)
417
    {
418
        return $this->modify(0, $minutes * -1);
419
    }
420
421
    /**
422
     * @return DateTime
423
     */
424
    public function subMonth()
425
    {
426
        return $this->subMonths(1);
427
    }
428
429
    /**
430
     * @param int $months
431
     * @return DateTime
432
     */
433
    public function subMonths(int $months)
434
    {
435
        return $this->modify(0, 0, 0, 0, $months * -1);
436
    }
437
438
    /**
439
     * @param int $seconds
440
     * @return DateTime
441
     */
442
    public function subSeconds(int $seconds)
443
    {
444
        return $this->modify($seconds * -1);
445
    }
446
447
    /**
448
     * @return DateTime
449
     */
450
    public function subYear()
451
    {
452
        return $this->subYears(1);
453
    }
454
455
    /**
456
     * @param int $years
457
     * @return DateTime
458
     */
459
    public function subYears(int $years)
460
    {
461
        return $this->modify(0, 0, 0, 0, 0, $years * -1);
462
    }
463
464
    /**
465
     * `00:00:00` to `23:59:59`
466
     *
467
     * @return Text
468
     */
469
    public function time()
470
    {
471
        return $this->dateFormat([
472
            'mysql' => '%H:%i:%S',
473
            'sqlite' => '%H:%M:%S'
474
        ]);
475
    }
476
477
    /**
478
     * Unix timestamp.
479
     *
480
     * @return Num
481
     */
482
    public function timestamp()
483
    {
484
        if ($this->db->isSQLite()) {
485
            return Num::factory($this->db, "STRFTIME('%s',{$this})");
486
        }
487
        return Num::factory($this->db, "UNIX_TIMESTAMP({$this})");
488
    }
489
490
    /**
491
     * Changes the timezone from the local timezone to UTC.
492
     *
493
     * > Warning: Datetimes are already stored and retrieved as UTC.
494
     * > Only use this if you know the expression is in the local timezone.
495
     *
496
     * > Warning: Chaining this multiple times will further change the timezone offset.
497
     *
498
     * @return DateTime
499
     */
500
    public function toUTC()
501
    {
502
        if ($this->db->isSQLite()) {
503
            // docs:
504
            // > "utc" assumes that the time value to its left is in the local timezone
505
            // > and adjusts that time value to be in UTC. If the time to the left is not in localtime,
506
            // > then the result of "utc" is undefined.
507
            return DateTime::factory($this->db, "DATETIME({$this},'utc')");
508
        }
509
        $local = date_default_timezone_get();
510
        return DateTime::factory($this->db, "CONVERT_TZ({$this},'{$local}','UTC')");
511
    }
512
513
    /**
514
     * `00` to `53`
515
     *
516
     * @return Num
517
     */
518
    public function weekOfYear()
519
    {
520
        return Num::factory($this->db, $this->dateFormat([
521
            'mysql' => '%U',
522
            'sqlite' => '%W'
523
        ]));
524
    }
525
526
    /**
527
     * `YYYY`
528
     *
529
     * @return Num
530
     */
531
    public function year()
532
    {
533
        return Num::factory($this->db, $this->dateFormat('%Y'));
534
    }
535
}
536