DateTimeModifyTrait::firstDayOfYear()   A
last analyzed

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
8
/**
9
 * Date-time component modifiers.
10
 */
11
trait DateTimeModifyTrait
12
{
13
14
    use DateTimeFormatTrait;
15
16
    /**
17
     * `YYYY-MM-01`
18
     *
19
     * @return DateTime
20
     */
21
    public function firstDayOfMonth()
22
    {
23
        return DateTime::factory($this->db, $this->dateFormat('%Y-%m-01'));
24
    }
25
26
    /**
27
     * `YYYY-01-01`
28
     *
29
     * @return DateTime
30
     */
31
    public function firstDayOfYear()
32
    {
33
        return DateTime::factory($this->db, $this->dateFormat('%Y-01-01'));
34
    }
35
36
    /**
37
     * `YYYY-MM-DD`
38
     *
39
     * @return DateTime
40
     */
41
    public function lastDayOfMonth()
42
    {
43
        return $this->firstDayOfMonth()->addMonth()->subDay();
44
    }
45
46
    /**
47
     * `YYYY-12-31`
48
     *
49
     * @return DateTime
50
     */
51
    public function lastDayOfYear()
52
    {
53
        return DateTime::factory($this->db, $this->dateFormat('%Y-12-31'));
54
    }
55
56
    /**
57
     * Applies date-time modifiers.
58
     *
59
     * `$s` can be a `DateInterval` or `DateInterval` description (e.g. `"+1 day"`).
60
     * If so, the rest of the arguments are ignored.
61
     *
62
     * > Note: Modifiers are processed from greatest-to-least interval scope,
63
     * > meaning years are applied first and seconds are applied last.
64
     *
65
     * @param int|string|DateInterval $s Seconds, or `DateInterval` related
66
     * @param int $m Minutes
67
     * @param int $h Hours
68
     * @param int $D Days
69
     * @param int $M Months
70
     * @param int $Y Years
71
     * @return DateTime
72
     */
73
    public function modify($s, int $m = 0, int $h = 0, int $D = 0, int $M = 0, int $Y = 0)
74
    {
75
        // interval units. process larger intervals first.
76
        static $units = ['YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE', 'SECOND'];
77
        if (is_string($s)) {
78
            $s = DateInterval::createFromDateString($s);
79
            assert($s instanceof DateInterval);
80
        }
81
        if ($s instanceof DateInterval) {
82
            $ints = [$s->y, $s->m, $s->d, $s->h, $s->i, $s->s];
83
        } else {
84
            $ints = [$Y, $M, $D, $h, $m, $s];
85
        }
86
87
        // key by units and remove zeroes
88
        $ints = array_filter(array_combine($units, $ints));
89
90
        if ($this->db->isSQLite()) {
91
            return $this->modify_sqlite($ints);
92
        }
93
        return $this->modify_mysql($ints);
94
    }
95
96
    /**
97
     * MySQL requires nesting.
98
     *
99
     * @param int[] $ints
100
     * @return DateTime
101
     * @internal
102
     */
103
    protected function modify_mysql(array $ints)
104
    {
105
        $spec = $this;
106
        foreach ($ints as $unit => $int) {
107
            $spec = sprintf('DATE_%s(%s, INTERVAL %s %s)', $int > 0 ? 'ADD' : 'SUB', $spec, abs($int), $unit);
108
        }
109
        return DateTime::factory($this->db, $spec);
110
    }
111
112
    /**
113
     * SQLite allows variadic modifiers.
114
     *
115
     * @param int[] $ints
116
     * @return DateTime
117
     * @internal
118
     */
119
    protected function modify_sqlite(array $ints)
120
    {
121
        $spec = [$this];
122
        foreach ($ints as $unit => $int) {
123
            $spec[] = sprintf("'%s %s'", $int > 0 ? "+{$int}" : $int, $unit);
124
        }
125
        return DateTime::factory($this->db, sprintf('DATETIME(%s)', implode(',', $spec)));
126
    }
127
128
    /**
129
     * Manually set the date components, preserving the time.
130
     *
131
     * `NULL` can be given to preserve a component's value.
132
     *
133
     * @param null|int $day
134
     * @param null|int $month
135
     * @param null|int $year
136
     * @return DateTime
137
     */
138
    public function setDate(int $day = null, int $month = null, int $year = null)
139
    {
140
        $day ??= '%D';
141
        $month ??= '%m';
142
        $year ??= '%Y';
143
        if (is_int($day)) {
144
            assert($day >= 1 and $day <= 31);
145
            $day = sprintf('%02d', $day);
146
        }
147
        if (is_int($month)) {
148
            assert($month >= 1 and $month <= 12);
149
            $month = sprintf('%02d', $month);
150
        }
151
        if (is_int($year)) {
152
            assert($year >= 0 and $year <= 9999);
153
            $year = sprintf('%04d', $year);
154
        }
155
        return DateTime::factory($this->db, $this->dateFormat([
156
            'mysql' => "{$year}-{$month}-{$day} %H:%i:%S",
157
            'sqlite' => "{$year}-{$month}-{$day} %H:%M:%S",
158
        ]));
159
    }
160
161
    /**
162
     * @param int $day
163
     * @return DateTime
164
     */
165
    public function setDay(int $day)
166
    {
167
        assert($day >= 1 and $day <= 31);
168
        $day = sprintf('%02d', $day);
169
        return DateTime::factory($this->db, $this->dateFormat([
170
            'mysql' => "%Y-%m-{$day} %H:%i:%S",
171
            'sqlite' => "%Y-%m-{$day} %H:%M:%S",
172
        ]));
173
    }
174
175
    /**
176
     * @param int $hours
177
     * @return DateTime
178
     */
179
    public function setHours(int $hours)
180
    {
181
        assert($hours >= 0 and $hours <= 23);
182
        $hours = sprintf('%02d', $hours);
183
        return DateTime::factory($this->db, $this->dateFormat([
184
            'mysql' => "%Y-%m-%d {$hours}:%i:%S",
185
            'sqlite' => "%Y-%m-%d {$hours}:%M:%S"
186
        ]));
187
    }
188
189
    /**
190
     * @param int $minutes
191
     * @return DateTime
192
     */
193
    public function setMinutes(int $minutes)
194
    {
195
        assert($minutes >= 0 and $minutes <= 59);
196
        $minutes = sprintf('%02d', $minutes);
197
        return DateTime::factory($this->db, $this->dateFormat("%Y-%m-%d %H:{$minutes}:%S"));
198
    }
199
200
    /**
201
     * @param int $month
202
     * @return DateTime
203
     */
204
    public function setMonth(int $month)
205
    {
206
        assert($month >= 1 and $month <= 12);
207
        $month = sprintf('%02d', $month);
208
        return DateTime::factory($this->db, $this->dateFormat([
209
            'mysql' => "%Y-{$month}-%d %H:%i:%S",
210
            'sqlite' => "%Y-{$month}-%d %H:%M:%S",
211
        ]));
212
    }
213
214
    /**
215
     * @param int $seconds
216
     * @return DateTime
217
     */
218
    public function setSeconds(int $seconds)
219
    {
220
        assert($seconds >= 0 and $seconds <= 59);
221
        $seconds = sprintf('%02d', $seconds);
222
        return DateTime::factory($this->db, $this->dateFormat([
223
            'mysql' => "%Y-%m-%d %H:%i:{$seconds}",
224
            'sqlite' => "%Y-%m-%d %H:%M:{$seconds}"
225
        ]));
226
    }
227
228
    /**
229
     * Manually set the time components, preserving the date.
230
     *
231
     * `NULL` can be given to preserve a component's value.
232
     *
233
     * @param null|int $seconds
234
     * @param null|int $minutes
235
     * @param null|int $hours
236
     * @return DateTime
237
     */
238
    public function setTime(int $seconds = null, int $minutes = null, int $hours = null)
239
    {
240
        $seconds ??= '%S';
241
        $minutes ??= [
242
            'mysql' => '%i',
243
            'sqlite' => '%M',
244
        ][$this->db->getDriver()];
245
        $hours ??= '%H';
246
247
        if (is_int($seconds)) {
248
            assert($seconds >= 0 and $seconds <= 59);
249
            $seconds = sprintf('%02d', $seconds);
250
        }
251
        if (is_int($minutes)) {
252
            assert($minutes >= 0 and $minutes <= 59);
253
            $minutes = sprintf('%02d', $minutes);
254
        }
255
        if (is_int($hours)) {
256
            assert($hours >= 0 and $hours <= 23);
257
            $hours = sprintf('%02d', $hours);
258
        }
259
260
        return DateTime::factory($this->db, $this->dateFormat("%Y-%m-%d {$hours}:{$minutes}:{$seconds}"));
261
    }
262
263
    /**
264
     * @param int $year
265
     * @return DateTime
266
     */
267
    public function setYear(int $year)
268
    {
269
        assert($year >= 0 and $year <= 9999);
270
        $year = sprintf('%04d', $year);
271
        return DateTime::factory($this->db, $this->dateFormat([
272
            'mysql' => "{$year}-%m-%d %H:%i:%S",
273
            'sqlite' => "{$year}-%m-%d %H:%M:%S",
274
        ]));
275
    }
276
277
    /**
278
     * Changes the timezone from local to UTC.
279
     *
280
     * SQLite uses the system's timezone as the "local" timezone,
281
     * whereas MySQL allows you to specify it.
282
     *
283
     * > Warning: Datetimes are already stored and retrieved as UTC.
284
     * > Only use this if you know the expression is in the local timezone.
285
     *
286
     * > Warning: Chaining this multiple times will further change the timezone offset.
287
     *
288
     * @param null|string $mysqlLocalTz The "local" timezone name or offset given to MySQL. Defaults to PHP's current timezone.
289
     * @return DateTime
290
     */
291
    public function toUTC(string $mysqlLocalTz = null)
292
    {
293
        return DateTime::fromFormat($this->db, [
294
            'mysql' => "CONVERT_TZ(%s,'%s','UTC')",
295
            'sqlite' => "DATETIME(%s,'utc')"
296
        ], $this, $mysqlLocalTz ?? date_default_timezone_get());
297
    }
298
}
299