Passed
Push — master ( bd73b6...71c175 )
by y
06:37
created

DateTimeTrait::subMonth()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
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\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
     * @param int|DateInterval $arg Seconds, or a `DateInterval`
308
     * @param int ...$args Minutes, Hours, Days, Months, Years. Ignored if `$arg` is a `DateInterval`
309
     */
310
    public function modify($arg, int ...$args)
311
    {
312
        static $intervals = ['SECOND', 'MINUTE', 'HOUR', 'DAY', 'MONTH', 'YEAR'];
313
        if ($arg instanceof DateInterval) {
314
            $args = [$arg->s, $arg->i, $arg->h, $arg->d, $arg->m, $arg->y];
315
        } else {
316
            array_unshift($args, $arg); // re-keys
317
        }
318
319
        // remove zeroes and reverse so larger intervals happen first
320
        $args = array_reverse(array_filter($args), true);
321
322
        // sqlite allows us to do it all in one go
323
        if ($this->db->isSQLite()) {
324
            $spec = [$this];
325
            foreach ($args as $i => $int) {
326
                $spec[] = sprintf('%s %s', $int > 0 ? "+{$int}" : $int, $intervals[$i]);
327
            }
328
            return DateTime::factory($this->db, sprintf('DATETIME(%s)', implode(',', $spec)));
329
        }
330
331
        // mysql requires wrapping
332
        $spec = $this;
333
        foreach ($args as $i => $int) {
334
            $spec = sprintf('DATE_%s(%s, INTERVAL %s %s)',
335
                $spec,
336
                $int > 0 ? 'ADD' : 'SUB',
337
                abs($int),
338
                $intervals[$i]
339
            );
340
        }
341
        return DateTime::factory($this->db, $spec);
342
    }
343
344
    /**
345
     * `01` to `12`
346
     *
347
     * @return Num
348
     */
349
    public function month()
350
    {
351
        return Num::factory($this->db, $this->dateFormat('%m'));
352
    }
353
354
    /**
355
     * `00` to `59`
356
     *
357
     * @return Num
358
     */
359
    public function seconds()
360
    {
361
        return Num::factory($this->db, $this->dateFormat('%S'));
362
    }
363
364
    /**
365
     * @return DateTime
366
     */
367
    public function subDay()
368
    {
369
        return $this->subDays(1);
370
    }
371
372
    /**
373
     * @param int $days
374
     * @return DateTime
375
     */
376
    public function subDays(int $days)
377
    {
378
        return $this->modify(0, 0, 0, $days * -1);
379
    }
380
381
    /**
382
     * @return DateTime
383
     */
384
    public function subHour()
385
    {
386
        return $this->subHours(1);
387
    }
388
389
    /**
390
     * @param int $hours
391
     * @return DateTime
392
     */
393
    public function subHours(int $hours)
394
    {
395
        return $this->modify(0, 0, $hours * -1);
396
    }
397
398
    /**
399
     * @param int $minutes
400
     * @return DateTime
401
     */
402
    public function subMinutes(int $minutes)
403
    {
404
        return $this->modify(0, $minutes * -1);
405
    }
406
407
    /**
408
     * @return DateTime
409
     */
410
    public function subMonth()
411
    {
412
        return $this->subMonths(1);
413
    }
414
415
    /**
416
     * @param int $months
417
     * @return DateTime
418
     */
419
    public function subMonths(int $months)
420
    {
421
        return $this->modify(0, 0, 0, 0, $months * -1);
422
    }
423
424
    /**
425
     * @param int $seconds
426
     * @return DateTime
427
     */
428
    public function subSeconds(int $seconds)
429
    {
430
        return $this->modify($seconds * -1);
431
    }
432
433
    /**
434
     * @return DateTime
435
     */
436
    public function subYear()
437
    {
438
        return $this->subYears(1);
439
    }
440
441
    /**
442
     * @param int $years
443
     * @return DateTime
444
     */
445
    public function subYears(int $years)
446
    {
447
        return $this->modify(0, 0, 0, 0, 0, $years * -1);
448
    }
449
450
    /**
451
     * `00:00:00` to `23:59:59`
452
     *
453
     * @return Text
454
     */
455
    public function time()
456
    {
457
        return $this->dateFormat([
458
            'mysql' => '%H:%i:%S',
459
            'sqlite' => '%H:%M:%S'
460
        ]);
461
    }
462
463
    /**
464
     * Unix timestamp.
465
     *
466
     * @return Num
467
     */
468
    public function timestamp()
469
    {
470
        if ($this->db->isSQLite()) {
471
            return Num::factory($this->db, "STRFTIME('%s',{$this})");
472
        }
473
        return Num::factory($this->db, "UNIX_TIMESTAMP({$this})");
474
    }
475
476
    /**
477
     * Changes the timezone from UTC to the local timezone.
478
     *
479
     * - SQLite: Uses the operating system's timezone.
480
     * - MySQL: Uses PHP's timezone.
481
     *
482
     * > Warning: Chaining this multiple times will further change the timezone offset.
483
     *
484
     * @return DateTime
485
     */
486
    public function toLocalTz()
487
    {
488
        if ($this->db->isSQLite()) {
489
            // docs:
490
            // > The "localtime" modifier (12) assumes the time value to its left is in
491
            // > Universal Coordinated Time (UTC) and adjusts that time value so that it is in localtime.
492
            // > If "localtime" follows a time that is not UTC, then the behavior is undefined.
493
            return DateTime::factory($this->db, "DATETIME({$this},'localtime')");
494
        }
495
        $local = date_default_timezone_get();
496
        return DateTime::factory($this->db, "CONVERT_TZ({$this},'UTC','{$local}')");
497
    }
498
499
    /**
500
     * Changes the timezone from the local timezone to UTC.
501
     *
502
     * > Warning: Datetimes are already stored and retrieved as UTC.
503
     * > Only use this if you know the expression is in the local timezone.
504
     *
505
     * > Warning: Chaining this multiple times will further change the timezone offset.
506
     *
507
     * @return DateTime
508
     */
509
    public function toUtc()
510
    {
511
        if ($this->db->isSQLite()) {
512
            // docs:
513
            // > "utc" assumes that the time value to its left is in the local timezone
514
            // > and adjusts that time value to be in UTC. If the time to the left is not in localtime,
515
            // > then the result of "utc" is undefined.
516
            return DateTime::factory($this->db, "DATETIME({$this},'utc')");
517
        }
518
        $local = date_default_timezone_get();
519
        return DateTime::factory($this->db, "CONVERT_TZ({$this},'{$local}','UTC')");
520
    }
521
522
    /**
523
     * `00` to `53`
524
     *
525
     * @return Num
526
     */
527
    public function weekOfYear()
528
    {
529
        return Num::factory($this->db, $this->dateFormat([
530
            'mysql' => '%U',
531
            'sqlite' => '%W'
532
        ]));
533
    }
534
535
    /**
536
     * `YYYY`
537
     *
538
     * @return Num
539
     */
540
    public function year()
541
    {
542
        return Num::factory($this->db, $this->dateFormat('%Y'));
543
    }
544
}
545