Passed
Push — master ( 6db175...709bfd )
by y
01:41
created

DateTimeTrait::modify()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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