blob: 2902a8b103c9104d6b3a074b0daf632e0b9f8c0b [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3/**
4 * This file is part of the Carbon package.
5 *
6 * (c) Brian Nesbitt <brian@nesbot.com>
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010011
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020012namespace Carbon\Traits;
13
14use Carbon\CarbonConverterInterface;
15use Carbon\CarbonInterface;
16use Carbon\CarbonInterval;
17use Carbon\Exceptions\UnitException;
18use Closure;
19use DateInterval;
20use ReturnTypeWillChange;
21
22/**
23 * Trait Units.
24 *
25 * Add, subtract and set units.
26 */
27trait Units
28{
29 /**
30 * Add seconds to the instance using timestamp. Positive $value travels
31 * forward while negative $value travels into the past.
32 *
33 * @param string $unit
34 * @param int $value
35 *
36 * @return static
37 */
38 public function addRealUnit($unit, $value = 1)
39 {
40 switch ($unit) {
41 // @call addRealUnit
42 case 'micro':
43
44 // @call addRealUnit
45 case 'microsecond':
46 /* @var CarbonInterface $this */
47 $diff = $this->microsecond + $value;
48 $time = $this->getTimestamp();
49 $seconds = (int) floor($diff / static::MICROSECONDS_PER_SECOND);
50 $time += $seconds;
51 $diff -= $seconds * static::MICROSECONDS_PER_SECOND;
52 $microtime = str_pad((string) $diff, 6, '0', STR_PAD_LEFT);
53 $tz = $this->tz;
54
55 return $this->tz('UTC')->modify("@$time.$microtime")->tz($tz);
56
57 // @call addRealUnit
58 case 'milli':
59 // @call addRealUnit
60 case 'millisecond':
61 return $this->addRealUnit('microsecond', $value * static::MICROSECONDS_PER_MILLISECOND);
62
63 break;
64
65 // @call addRealUnit
66 case 'second':
67 break;
68
69 // @call addRealUnit
70 case 'minute':
71 $value *= static::SECONDS_PER_MINUTE;
72
73 break;
74
75 // @call addRealUnit
76 case 'hour':
77 $value *= static::MINUTES_PER_HOUR * static::SECONDS_PER_MINUTE;
78
79 break;
80
81 // @call addRealUnit
82 case 'day':
83 $value *= static::HOURS_PER_DAY * static::MINUTES_PER_HOUR * static::SECONDS_PER_MINUTE;
84
85 break;
86
87 // @call addRealUnit
88 case 'week':
89 $value *= static::DAYS_PER_WEEK * static::HOURS_PER_DAY * static::MINUTES_PER_HOUR * static::SECONDS_PER_MINUTE;
90
91 break;
92
93 // @call addRealUnit
94 case 'month':
95 $value *= 30 * static::HOURS_PER_DAY * static::MINUTES_PER_HOUR * static::SECONDS_PER_MINUTE;
96
97 break;
98
99 // @call addRealUnit
100 case 'quarter':
101 $value *= static::MONTHS_PER_QUARTER * 30 * static::HOURS_PER_DAY * static::MINUTES_PER_HOUR * static::SECONDS_PER_MINUTE;
102
103 break;
104
105 // @call addRealUnit
106 case 'year':
107 $value *= 365 * static::HOURS_PER_DAY * static::MINUTES_PER_HOUR * static::SECONDS_PER_MINUTE;
108
109 break;
110
111 // @call addRealUnit
112 case 'decade':
113 $value *= static::YEARS_PER_DECADE * 365 * static::HOURS_PER_DAY * static::MINUTES_PER_HOUR * static::SECONDS_PER_MINUTE;
114
115 break;
116
117 // @call addRealUnit
118 case 'century':
119 $value *= static::YEARS_PER_CENTURY * 365 * static::HOURS_PER_DAY * static::MINUTES_PER_HOUR * static::SECONDS_PER_MINUTE;
120
121 break;
122
123 // @call addRealUnit
124 case 'millennium':
125 $value *= static::YEARS_PER_MILLENNIUM * 365 * static::HOURS_PER_DAY * static::MINUTES_PER_HOUR * static::SECONDS_PER_MINUTE;
126
127 break;
128
129 default:
130 if ($this->localStrictModeEnabled ?? static::isStrictModeEnabled()) {
131 throw new UnitException("Invalid unit for real timestamp add/sub: '$unit'");
132 }
133
134 return $this;
135 }
136
137 /* @var CarbonInterface $this */
138 return $this->setTimestamp((int) ($this->getTimestamp() + $value));
139 }
140
141 public function subRealUnit($unit, $value = 1)
142 {
143 return $this->addRealUnit($unit, -$value);
144 }
145
146 /**
147 * Returns true if a property can be changed via setter.
148 *
149 * @param string $unit
150 *
151 * @return bool
152 */
153 public static function isModifiableUnit($unit)
154 {
155 static $modifiableUnits = [
156 // @call addUnit
157 'millennium',
158 // @call addUnit
159 'century',
160 // @call addUnit
161 'decade',
162 // @call addUnit
163 'quarter',
164 // @call addUnit
165 'week',
166 // @call addUnit
167 'weekday',
168 ];
169
170 return \in_array($unit, $modifiableUnits) || \in_array($unit, static::$units);
171 }
172
173 /**
174 * Call native PHP DateTime/DateTimeImmutable add() method.
175 *
176 * @param DateInterval $interval
177 *
178 * @return static
179 */
180 public function rawAdd(DateInterval $interval)
181 {
182 return parent::add($interval);
183 }
184
185 /**
186 * Add given units or interval to the current instance.
187 *
188 * @example $date->add('hour', 3)
189 * @example $date->add(15, 'days')
190 * @example $date->add(CarbonInterval::days(4))
191 *
192 * @param string|DateInterval|Closure|CarbonConverterInterface $unit
193 * @param int $value
194 * @param bool|null $overflow
195 *
196 * @return static
197 */
198 #[ReturnTypeWillChange]
199 public function add($unit, $value = 1, $overflow = null)
200 {
201 if (\is_string($unit) && \func_num_args() === 1) {
202 $unit = CarbonInterval::make($unit);
203 }
204
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200205 if ($unit instanceof CarbonConverterInterface) {
206 return $this->resolveCarbon($unit->convertDate($this, false));
207 }
208
209 if ($unit instanceof Closure) {
210 return $this->resolveCarbon($unit($this, false));
211 }
212
213 if ($unit instanceof DateInterval) {
214 return parent::add($unit);
215 }
216
217 if (is_numeric($unit)) {
218 [$value, $unit] = [$unit, $value];
219 }
220
221 return $this->addUnit($unit, $value, $overflow);
222 }
223
224 /**
225 * Add given units to the current instance.
226 *
227 * @param string $unit
228 * @param int $value
229 * @param bool|null $overflow
230 *
231 * @return static
232 */
233 public function addUnit($unit, $value = 1, $overflow = null)
234 {
235 $date = $this;
236
237 if (!is_numeric($value) || !(float) $value) {
238 return $date->isMutable() ? $date : $date->avoidMutation();
239 }
240
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100241 $unit = self::singularUnit($unit);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200242 $metaUnits = [
243 'millennium' => [static::YEARS_PER_MILLENNIUM, 'year'],
244 'century' => [static::YEARS_PER_CENTURY, 'year'],
245 'decade' => [static::YEARS_PER_DECADE, 'year'],
246 'quarter' => [static::MONTHS_PER_QUARTER, 'month'],
247 ];
248
249 if (isset($metaUnits[$unit])) {
250 [$factor, $unit] = $metaUnits[$unit];
251 $value *= $factor;
252 }
253
254 if ($unit === 'weekday') {
255 $weekendDays = static::getWeekendDays();
256
257 if ($weekendDays !== [static::SATURDAY, static::SUNDAY]) {
258 $absoluteValue = abs($value);
259 $sign = $value / max(1, $absoluteValue);
260 $weekDaysCount = 7 - min(6, \count(array_unique($weekendDays)));
261 $weeks = floor($absoluteValue / $weekDaysCount);
262
263 for ($diff = $absoluteValue % $weekDaysCount; $diff; $diff--) {
264 /** @var static $date */
265 $date = $date->addDays($sign);
266
267 while (\in_array($date->dayOfWeek, $weekendDays)) {
268 $date = $date->addDays($sign);
269 }
270 }
271
272 $value = $weeks * $sign;
273 $unit = 'week';
274 }
275
276 $timeString = $date->toTimeString();
277 } elseif ($canOverflow = \in_array($unit, [
278 'month',
279 'year',
280 ]) && ($overflow === false || (
281 $overflow === null &&
282 ($ucUnit = ucfirst($unit).'s') &&
283 !($this->{'local'.$ucUnit.'Overflow'} ?? static::{'shouldOverflow'.$ucUnit}())
284 ))) {
285 $day = $date->day;
286 }
287
288 $value = (int) $value;
289
290 if ($unit === 'milli' || $unit === 'millisecond') {
291 $unit = 'microsecond';
292 $value *= static::MICROSECONDS_PER_MILLISECOND;
293 }
294
295 // Work-around for bug https://bugs.php.net/bug.php?id=75642
296 if ($unit === 'micro' || $unit === 'microsecond') {
297 $microseconds = $this->micro + $value;
298 $second = (int) floor($microseconds / static::MICROSECONDS_PER_SECOND);
299 $microseconds %= static::MICROSECONDS_PER_SECOND;
300 if ($microseconds < 0) {
301 $microseconds += static::MICROSECONDS_PER_SECOND;
302 }
303 $date = $date->microseconds($microseconds);
304 $unit = 'second';
305 $value = $second;
306 }
307 $date = $date->modify("$value $unit");
308
309 if (isset($timeString)) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100310 $date = $date->setTimeFromTimeString($timeString);
311 } elseif (isset($canOverflow, $day) && $canOverflow && $day !== $date->day) {
312 $date = $date->modify('last day of previous month');
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200313 }
314
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100315 if (!$date) {
316 throw new UnitException('Unable to add unit '.var_export(\func_get_args(), true));
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200317 }
318
319 return $date;
320 }
321
322 /**
323 * Subtract given units to the current instance.
324 *
325 * @param string $unit
326 * @param int $value
327 * @param bool|null $overflow
328 *
329 * @return static
330 */
331 public function subUnit($unit, $value = 1, $overflow = null)
332 {
333 return $this->addUnit($unit, -$value, $overflow);
334 }
335
336 /**
337 * Call native PHP DateTime/DateTimeImmutable sub() method.
338 *
339 * @param DateInterval $interval
340 *
341 * @return static
342 */
343 public function rawSub(DateInterval $interval)
344 {
345 return parent::sub($interval);
346 }
347
348 /**
349 * Subtract given units or interval to the current instance.
350 *
351 * @example $date->sub('hour', 3)
352 * @example $date->sub(15, 'days')
353 * @example $date->sub(CarbonInterval::days(4))
354 *
355 * @param string|DateInterval|Closure|CarbonConverterInterface $unit
356 * @param int $value
357 * @param bool|null $overflow
358 *
359 * @return static
360 */
361 #[ReturnTypeWillChange]
362 public function sub($unit, $value = 1, $overflow = null)
363 {
364 if (\is_string($unit) && \func_num_args() === 1) {
365 $unit = CarbonInterval::make($unit);
366 }
367
368 if ($unit instanceof CarbonConverterInterface) {
369 return $this->resolveCarbon($unit->convertDate($this, true));
370 }
371
372 if ($unit instanceof Closure) {
373 return $this->resolveCarbon($unit($this, true));
374 }
375
376 if ($unit instanceof DateInterval) {
377 return parent::sub($unit);
378 }
379
380 if (is_numeric($unit)) {
381 [$value, $unit] = [$unit, $value];
382 }
383
384 return $this->addUnit($unit, -(float) $value, $overflow);
385 }
386
387 /**
388 * Subtract given units or interval to the current instance.
389 *
390 * @see sub()
391 *
392 * @param string|DateInterval $unit
393 * @param int $value
394 * @param bool|null $overflow
395 *
396 * @return static
397 */
398 public function subtract($unit, $value = 1, $overflow = null)
399 {
400 if (\is_string($unit) && \func_num_args() === 1) {
401 $unit = CarbonInterval::make($unit);
402 }
403
404 return $this->sub($unit, $value, $overflow);
405 }
406}