Passed
Push — master ( c6f47c...6c478e )
by y
01:29
created

DateTimeTrait::firstDayOfYear()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
c 1
b 1
f 0
nc 1
nop 0
dl 0
loc 3
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\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 ?? DateTime::now($this->db))->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
     * `YYYY-MM-01`
251
     *
252
     * @return DateTime
253
     */
254
    public function firstDayOfMonth()
255
    {
256
        return DateTime::factory($this->db, $this->dateFormat('%Y-%m-01'));
257
    }
258
259
    /**
260
     * `YYYY-01-01`
261
     *
262
     * @return DateTime
263
     */
264
    public function firstDayOfYear()
265
    {
266
        return DateTime::factory($this->db, $this->dateFormat('%Y-01-01'));
267
    }
268
269
    /**
270
     * `00` to `23`
271
     *
272
     * @return Num
273
     */
274
    public function hours()
275
    {
276
        return Num::factory($this->db, $this->dateFormat('%H'));
277
    }
278
279
    /**
280
     * ISO-8601 compatible datetime string, offset `Z` (UTC/Zulu)
281
     *
282
     * https://en.wikipedia.org/wiki/ISO_8601
283
     *
284
     * @return Str
285
     */
286
    public function iso8601()
287
    {
288
        return $this->dateFormat([
289
            'mysql' => '%Y-%m-%dT%H:%i:%SZ',
290
            'sqlite' => '%Y-%m-%dT%H:%M:%SZ',
291
        ]);
292
    }
293
294
    /**
295
     * Julian day number (fractional).
296
     *
297
     * @return Num
298
     */
299
    public function julian()
300
    {
301
        return Num::fromFormat($this->db, [
302
            // mysql: julian "year zero" offset, plus number of fractional days since "year zero".
303
            'mysql' => "(1721059.5 + (TO_SECONDS(%s) / 86400))",
304
            'sqlite' => "JULIANDAY(%s)"
305
        ], $this);
306
    }
307
308
    /**
309
     * `00` to `59`
310
     *
311
     * @return Num
312
     */
313
    public function minutes()
314
    {
315
        return Num::factory($this->db, $this->dateFormat([
316
            'mysql' => '%i',
317
            'sqlite' => '%M'
318
        ]));
319
    }
320
321
    /**
322
     * Applies date-time modifiers.
323
     *
324
     * `$s` can be a `DateInterval` or `DateInterval` description (e.g. `"+1 day"`).
325
     * If so, the rest of the arguments are ignored.
326
     *
327
     * @param int|string|DateInterval $s Seconds, or `DateInterval` related
328
     * @param int $m Minutes
329
     * @param int $h Hours
330
     * @param int $D Days
331
     * @param int $M Months
332
     * @param int $Y Years
333
     * @return DateTime
334
     */
335
    public function modify($s, int $m = 0, int $h = 0, int $D = 0, int $M = 0, int $Y = 0)
336
    {
337
        // interval units. process larger intervals first.
338
        static $units = ['YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND'];
339
        if (is_string($s)) {
340
            $s = DateInterval::createFromDateString($s);
341
            assert($s instanceof DateInterval);
342
        }
343
        if ($s instanceof DateInterval) {
344
            $ints = [$s->y, $s->m, $s->d, $s->h, $s->i, $s->s];
345
        } else {
346
            $ints = [$Y, $M, $D, $h, $m, $s];
347
        }
348
349
        // key by units and remove zeroes
350
        $ints = array_filter(array_combine($units, $ints));
351
352
        if ($this->db->isSQLite()) {
353
            return $this->modify_sqlite($ints);
354
        }
355
        return $this->modify_mysql($ints);
356
    }
357
358
    /**
359
     * MySQL requires nesting.
360
     *
361
     * @param int[] $ints
362
     * @return DateTime
363
     * @internal
364
     */
365
    protected function modify_mysql(array $ints)
366
    {
367
        $spec = $this;
368
        foreach ($ints as $unit => $int) {
369
            $spec = sprintf('DATE_%s(%s, INTERVAL %s %s)', $int > 0 ? 'ADD' : 'SUB', $spec, abs($int), $unit);
370
        }
371
        return DateTime::factory($this->db, $spec);
372
    }
373
374
    /**
375
     * SQLite allows variadic modifiers.
376
     *
377
     * @param int[] $ints
378
     * @return DateTime
379
     * @internal
380
     */
381
    protected function modify_sqlite(array $ints)
382
    {
383
        $spec = [$this];
384
        foreach ($ints as $unit => $int) {
385
            $spec[] = sprintf("'%s %s'", $int > 0 ? "+{$int}" : $int, $unit);
386
        }
387
        return DateTime::factory($this->db, sprintf('DATETIME(%s)', implode(',', $spec)));
388
    }
389
390
    /**
391
     * `01` to `12`
392
     *
393
     * @return Num
394
     */
395
    public function month()
396
    {
397
        return Num::factory($this->db, $this->dateFormat('%m'));
398
    }
399
400
    /**
401
     * `00` to `59`
402
     *
403
     * @return Num
404
     */
405
    public function seconds()
406
    {
407
        return Num::factory($this->db, $this->dateFormat('%S'));
408
    }
409
410
    /**
411
     * @return DateTime
412
     */
413
    public function subDay()
414
    {
415
        return $this->subDays(1);
416
    }
417
418
    /**
419
     * @param int $days
420
     * @return DateTime
421
     */
422
    public function subDays(int $days)
423
    {
424
        return $this->modify(0, 0, 0, $days * -1);
425
    }
426
427
    /**
428
     * @return DateTime
429
     */
430
    public function subHour()
431
    {
432
        return $this->subHours(1);
433
    }
434
435
    /**
436
     * @param int $hours
437
     * @return DateTime
438
     */
439
    public function subHours(int $hours)
440
    {
441
        return $this->modify(0, 0, $hours * -1);
442
    }
443
444
    /**
445
     * @param int $minutes
446
     * @return DateTime
447
     */
448
    public function subMinutes(int $minutes)
449
    {
450
        return $this->modify(0, $minutes * -1);
451
    }
452
453
    /**
454
     * @return DateTime
455
     */
456
    public function subMonth()
457
    {
458
        return $this->subMonths(1);
459
    }
460
461
    /**
462
     * @param int $months
463
     * @return DateTime
464
     */
465
    public function subMonths(int $months)
466
    {
467
        return $this->modify(0, 0, 0, 0, $months * -1);
468
    }
469
470
    /**
471
     * @param int $seconds
472
     * @return DateTime
473
     */
474
    public function subSeconds(int $seconds)
475
    {
476
        return $this->modify($seconds * -1);
477
    }
478
479
    /**
480
     * @return DateTime
481
     */
482
    public function subYear()
483
    {
484
        return $this->subYears(1);
485
    }
486
487
    /**
488
     * @param int $years
489
     * @return DateTime
490
     */
491
    public function subYears(int $years)
492
    {
493
        return $this->modify(0, 0, 0, 0, 0, $years * -1);
494
    }
495
496
    /**
497
     * `00:00:00` to `23:59:59`
498
     *
499
     * @return Str
500
     */
501
    public function time()
502
    {
503
        return $this->dateFormat([
504
            'mysql' => '%H:%i:%S',
505
            'sqlite' => '%H:%M:%S'
506
        ]);
507
    }
508
509
    /**
510
     * Unix timestamp.
511
     *
512
     * @return Num
513
     */
514
    public function timestamp()
515
    {
516
        return Num::fromFormat($this->db, [
517
            'mysql' => "UNIX_TIMESTAMP(%s)",
518
            'sqlite' => "STRFTIME('%%s',%s)",
519
        ], $this);
520
    }
521
522
    /**
523
     * Changes the timezone from the local timezone to UTC.
524
     *
525
     * > Warning: Datetimes are already stored and retrieved as UTC.
526
     * > Only use this if you know the expression is in the local timezone.
527
     *
528
     * > Warning: Chaining this multiple times will further change the timezone offset.
529
     *
530
     * @return DateTime
531
     */
532
    public function toUTC()
533
    {
534
        if ($this->db->isSQLite()) {
535
            // docs:
536
            // > "utc" assumes that the time value to its left is in the local timezone
537
            // > and adjusts that time value to be in UTC. If the time to the left is not in localtime,
538
            // > then the result of "utc" is undefined.
539
            return DateTime::factory($this->db, "DATETIME({$this},'utc')");
540
        }
541
        $local = date_default_timezone_get();
542
        return DateTime::factory($this->db, "CONVERT_TZ({$this},'{$local}','UTC')");
543
    }
544
545
    /**
546
     * `00` to `53`
547
     *
548
     * @return Num
549
     */
550
    public function weekOfYear()
551
    {
552
        return Num::factory($this->db, $this->dateFormat([
553
            'mysql' => '%U',
554
            'sqlite' => '%W'
555
        ]));
556
    }
557
558
    /**
559
     * `YYYY`
560
     *
561
     * @return Num
562
     */
563
    public function year()
564
    {
565
        return Num::factory($this->db, $this->dateFormat('%Y'));
566
    }
567
}
568