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