Passed
Push — master ( 79af1c...08a8fa )
by y
01:29
created

DateTimeModifyTrait::setDate()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 20
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 7
eloc 15
c 1
b 0
f 1
nc 8
nop 3
dl 0
loc 20
rs 8.8333
1
<?php
2
3
namespace Helix\DB\Fluent\DateTime;
4
5
use DateInterval;
6
use Helix\DB\Fluent\AbstractTrait;
7
use Helix\DB\Fluent\DateTime;
8
use Helix\DB\Fluent\Str;
9
10
/**
11
 * Date-time modifiers.
12
 */
13
trait DateTimeModifyTrait
14
{
15
16
    use AbstractTrait;
17
18
    /**
19
     * @param string|string[] $format
20
     * @return Str
21
     */
22
    abstract public function dateFormat($format);
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
     * @return DateTime
60
     */
61
    public function addMinute()
62
    {
63
        return $this->addMinutes(1);
64
    }
65
66
    /**
67
     * @param int $minutes
68
     * @return DateTime
69
     */
70
    public function addMinutes(int $minutes)
71
    {
72
        return $this->modify(0, $minutes);
73
    }
74
75
    /**
76
     * @return DateTime
77
     */
78
    public function addMonth()
79
    {
80
        return $this->addMonths(1);
81
    }
82
83
    /**
84
     * @param int $months
85
     * @return DateTime
86
     */
87
    public function addMonths(int $months)
88
    {
89
        return $this->modify(0, 0, 0, 0, $months);
90
    }
91
92
    /**
93
     * @return DateTime
94
     */
95
    public function addSecond()
96
    {
97
        return $this->addSeconds(1);
98
    }
99
100
    /**
101
     * @param int $seconds
102
     * @return DateTime
103
     */
104
    public function addSeconds(int $seconds)
105
    {
106
        return $this->modify($seconds);
107
    }
108
109
    /**
110
     * @return DateTime
111
     */
112
    public function addYear()
113
    {
114
        return $this->addYears(1);
115
    }
116
117
    /**
118
     * @param int $years
119
     * @return DateTime
120
     */
121
    public function addYears(int $years)
122
    {
123
        return $this->modify(0, 0, 0, 0, 0, $years);
124
    }
125
126
    /**
127
     * `YYYY-MM-01`
128
     *
129
     * @return DateTime
130
     */
131
    public function firstDayOfMonth()
132
    {
133
        return DateTime::factory($this->db, $this->dateFormat('%Y-%m-01'));
134
    }
135
136
    /**
137
     * `YYYY-01-01`
138
     *
139
     * @return DateTime
140
     */
141
    public function firstDayOfYear()
142
    {
143
        return DateTime::factory($this->db, $this->dateFormat('%Y-01-01'));
144
    }
145
146
    /**
147
     * `YYYY-MM-DD`
148
     *
149
     * @return DateTime
150
     */
151
    public function lastDayOfMonth()
152
    {
153
        return $this->firstDayOfMonth()->addMonth()->subDay();
154
    }
155
156
    /**
157
     * `YYYY-12-31`
158
     *
159
     * @return DateTime
160
     */
161
    public function lastDayOfYear()
162
    {
163
        return DateTime::factory($this->db, $this->dateFormat('%Y-12-31'));
164
    }
165
166
    /**
167
     * Applies date-time modifiers.
168
     *
169
     * `$s` can be a `DateInterval` or `DateInterval` description (e.g. `"+1 day"`).
170
     * If so, the rest of the arguments are ignored.
171
     *
172
     * > Note: Modifiers are processed from greatest-to-least interval scope,
173
     * > meaning years are applied first and seconds are applied last.
174
     *
175
     * @param int|string|DateInterval $s Seconds, or `DateInterval` related
176
     * @param int $m Minutes
177
     * @param int $h Hours
178
     * @param int $D Days
179
     * @param int $M Months
180
     * @param int $Y Years
181
     * @return DateTime
182
     */
183
    public function modify($s, int $m = 0, int $h = 0, int $D = 0, int $M = 0, int $Y = 0)
184
    {
185
        // interval units. process larger intervals first.
186
        static $units = ['YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND'];
187
        if (is_string($s)) {
188
            $s = DateInterval::createFromDateString($s);
189
            assert($s instanceof DateInterval);
190
        }
191
        if ($s instanceof DateInterval) {
192
            $ints = [$s->y, $s->m, $s->d, $s->h, $s->i, $s->s];
193
        } else {
194
            $ints = [$Y, $M, $D, $h, $m, $s];
195
        }
196
197
        // key by units and remove zeroes
198
        $ints = array_filter(array_combine($units, $ints));
199
200
        if ($this->db->isSQLite()) {
201
            return $this->modify_sqlite($ints);
202
        }
203
        return $this->modify_mysql($ints);
204
    }
205
206
    /**
207
     * MySQL requires nesting.
208
     *
209
     * @param int[] $ints
210
     * @return DateTime
211
     * @internal
212
     */
213
    protected function modify_mysql(array $ints)
214
    {
215
        $spec = $this;
216
        foreach ($ints as $unit => $int) {
217
            $spec = sprintf('DATE_%s(%s, INTERVAL %s %s)', $int > 0 ? 'ADD' : 'SUB', $spec, abs($int), $unit);
218
        }
219
        return DateTime::factory($this->db, $spec);
220
    }
221
222
    /**
223
     * SQLite allows variadic modifiers.
224
     *
225
     * @param int[] $ints
226
     * @return DateTime
227
     * @internal
228
     */
229
    protected function modify_sqlite(array $ints)
230
    {
231
        $spec = [$this];
232
        foreach ($ints as $unit => $int) {
233
            $spec[] = sprintf("'%s %s'", $int > 0 ? "+{$int}" : $int, $unit);
234
        }
235
        return DateTime::factory($this->db, sprintf('DATETIME(%s)', implode(',', $spec)));
236
    }
237
238
    /**
239
     * Manually set the date components, preserving the time.
240
     *
241
     * `NULL` can be given to preserve a component's value.
242
     *
243
     * @param null|int $day
244
     * @param null|int $month
245
     * @param null|int $year
246
     * @return DateTime
247
     */
248
    public function setDate(int $day = null, int $month = null, int $year = null)
249
    {
250
        $day ??= '%D';
251
        $month ??= '%m';
252
        $year ??= '%Y';
253
        if (is_int($day)) {
254
            assert($day >= 1 and $day <= 31);
255
            $day = sprintf('%02d', $day);
256
        }
257
        if (is_int($month)) {
258
            assert($month >= 1 and $month <= 12);
259
            $month = sprintf('%02d', $month);
260
        }
261
        if (is_int($year)) {
262
            assert($year >= 0 and $year <= 9999);
263
            $year = sprintf('%04d', $year);
264
        }
265
        return DateTime::factory($this->db, $this->dateFormat([
266
            'mysql' => "{$year}-{$month}-{$day} %H:%i:%S",
267
            'sqlite' => "{$year}-{$month}-{$day} %H:%M:%S",
268
        ]));
269
    }
270
271
    /**
272
     * @param int $day
273
     * @return DateTime
274
     */
275
    public function setDay(int $day)
276
    {
277
        assert($day >= 1 and $day <= 31);
278
        $day = sprintf('%02d', $day);
279
        return DateTime::factory($this->db, $this->dateFormat([
280
            'mysql' => "%Y-%m-{$day} %H:%i:%S",
281
            'sqlite' => "%Y-%m-{$day} %H:%M:%S",
282
        ]));
283
    }
284
285
    /**
286
     * @param int $hours
287
     * @return DateTime
288
     */
289
    public function setHours(int $hours)
290
    {
291
        assert($hours >= 0 and $hours <= 23);
292
        $hours = sprintf('%02d', $hours);
293
        return DateTime::factory($this->db, $this->dateFormat([
294
            'mysql' => "%Y-%m-%d {$hours}:%i:%S",
295
            'sqlite' => "%Y-%m-%d {$hours}:%M:%S"
296
        ]));
297
    }
298
299
    /**
300
     * @param int $minutes
301
     * @return DateTime
302
     */
303
    public function setMinutes(int $minutes)
304
    {
305
        assert($minutes >= 0 and $minutes <= 59);
306
        $minutes = sprintf('%02d', $minutes);
307
        return DateTime::factory($this->db, $this->dateFormat("%Y-%m-%d %H:{$minutes}:%S"));
308
    }
309
310
    /**
311
     * @param int $month
312
     * @return DateTime
313
     */
314
    public function setMonth(int $month)
315
    {
316
        assert($month >= 1 and $month <= 12);
317
        $month = sprintf('%02d', $month);
318
        return DateTime::factory($this->db, $this->dateFormat([
319
            'mysql' => "%Y-{$month}-%d %H:%i:%S",
320
            'sqlite' => "%Y-{$month}-%d %H:%M:%S",
321
        ]));
322
    }
323
324
    /**
325
     * @param int $seconds
326
     * @return DateTime
327
     */
328
    public function setSeconds(int $seconds)
329
    {
330
        assert($seconds >= 0 and $seconds <= 59);
331
        $seconds = sprintf('%02d', $seconds);
332
        return DateTime::factory($this->db, $this->dateFormat([
333
            'mysql' => "%Y-%m-%d %H:%i:{$seconds}",
334
            'sqlite' => "%Y-%m-%d %H:%M:{$seconds}"
335
        ]));
336
    }
337
338
    /**
339
     * Manually set the time components, preserving the date.
340
     *
341
     * `NULL` can be given to preserve a component's value.
342
     *
343
     * @param null|int $seconds
344
     * @param null|int $minutes
345
     * @param null|int $hours
346
     * @return DateTime
347
     */
348
    public function setTime(int $seconds = null, int $minutes = null, int $hours = null)
349
    {
350
        $seconds ??= '%S';
351
        $minutes ??= [
352
            'mysql' => '%i',
353
            'sqlite' => '%M',
354
        ][$this->db->getDriver()];
355
        $hours ??= '%H';
356
357
        if (is_int($seconds)) {
358
            assert($seconds >= 0 and $seconds <= 59);
359
            $seconds = sprintf('%02d', $seconds);
360
        }
361
        if (is_int($minutes)) {
362
            assert($minutes >= 0 and $minutes <= 59);
363
            $minutes = sprintf('%02d', $minutes);
364
        }
365
        if (is_int($hours)) {
366
            assert($hours >= 0 and $hours <= 23);
367
            $hours = sprintf('%02d', $hours);
368
        }
369
370
        return DateTime::factory($this->db, $this->dateFormat("%Y-%m-%d {$hours}:{$minutes}:{$seconds}"));
371
    }
372
373
    /**
374
     * @param int $year
375
     * @return DateTime
376
     */
377
    public function setYear(int $year)
378
    {
379
        assert($year >= 0 and $year <= 9999);
380
        $year = sprintf('%04d', $year);
381
        return DateTime::factory($this->db, $this->dateFormat([
382
            'mysql' => "{$year}-%m-%d %H:%i:%S",
383
            'sqlite' => "{$year}-%m-%d %H:%M:%S",
384
        ]));
385
    }
386
387
    /**
388
     * @return DateTime
389
     */
390
    public function subDay()
391
    {
392
        return $this->subDays(1);
393
    }
394
395
    /**
396
     * @param int $days
397
     * @return DateTime
398
     */
399
    public function subDays(int $days)
400
    {
401
        return $this->modify(0, 0, 0, $days * -1);
402
    }
403
404
    /**
405
     * @return DateTime
406
     */
407
    public function subHour()
408
    {
409
        return $this->subHours(1);
410
    }
411
412
    /**
413
     * @param int $hours
414
     * @return DateTime
415
     */
416
    public function subHours(int $hours)
417
    {
418
        return $this->modify(0, 0, $hours * -1);
419
    }
420
421
    /**
422
     * @return DateTime
423
     */
424
    public function subMinute()
425
    {
426
        return $this->subMinutes(1);
427
    }
428
429
    /**
430
     * @param int $minutes
431
     * @return DateTime
432
     */
433
    public function subMinutes(int $minutes)
434
    {
435
        return $this->modify(0, $minutes * -1);
436
    }
437
438
    /**
439
     * @return DateTime
440
     */
441
    public function subMonth()
442
    {
443
        return $this->subMonths(1);
444
    }
445
446
    /**
447
     * @param int $months
448
     * @return DateTime
449
     */
450
    public function subMonths(int $months)
451
    {
452
        return $this->modify(0, 0, 0, 0, $months * -1);
453
    }
454
455
    /**
456
     * @return DateTime
457
     */
458
    public function subSecond()
459
    {
460
        return $this->subSeconds(1);
461
    }
462
463
    /**
464
     * @param int $seconds
465
     * @return DateTime
466
     */
467
    public function subSeconds(int $seconds)
468
    {
469
        return $this->modify($seconds * -1);
470
    }
471
472
    /**
473
     * @return DateTime
474
     */
475
    public function subYear()
476
    {
477
        return $this->subYears(1);
478
    }
479
480
    /**
481
     * @param int $years
482
     * @return DateTime
483
     */
484
    public function subYears(int $years)
485
    {
486
        return $this->modify(0, 0, 0, 0, 0, $years * -1);
487
    }
488
489
    /**
490
     * Changes the timezone from local to UTC.
491
     *
492
     * SQLite uses the system's timezone as the "local" timezone,
493
     * whereas MySQL allows you to specify it.
494
     *
495
     * > Warning: Datetimes are already stored and retrieved as UTC.
496
     * > Only use this if you know the expression is in the local timezone.
497
     *
498
     * > Warning: Chaining this multiple times will further change the timezone offset.
499
     *
500
     * @param null|string $mysqlLocalTz The "local" timezone name or offset given to MySQL. Defaults to PHP's current timezone.
501
     * @return DateTime
502
     */
503
    public function toUTC(string $mysqlLocalTz = null)
504
    {
505
        return DateTime::fromFormat($this->db, [
506
            'mysql' => "CONVERT_TZ(%s,'%s','UTC')",
507
            'sqlite' => "DATETIME(%s,'utc')"
508
        ], $this, $mysqlLocalTz ?? date_default_timezone_get());
509
    }
510
}
511