blob: d465beac70fb9ce336a1daaa232518ca6d876a58 [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;
13
14use Carbon\Exceptions\BadFluentConstructorException;
15use Carbon\Exceptions\BadFluentSetterException;
16use Carbon\Exceptions\InvalidCastException;
17use Carbon\Exceptions\InvalidIntervalException;
18use Carbon\Exceptions\ParseErrorException;
19use Carbon\Exceptions\UnitNotConfiguredException;
20use Carbon\Exceptions\UnknownGetterException;
21use Carbon\Exceptions\UnknownSetterException;
22use Carbon\Exceptions\UnknownUnitException;
23use Carbon\Traits\IntervalRounding;
24use Carbon\Traits\IntervalStep;
25use Carbon\Traits\Mixin;
26use Carbon\Traits\Options;
27use Closure;
28use DateInterval;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +010029use DateTimeInterface;
30use DateTimeZone;
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +020031use Exception;
32use ReflectionException;
33use ReturnTypeWillChange;
34use Throwable;
35
36/**
37 * A simple API extension for DateInterval.
38 * The implementation provides helpers to handle weeks but only days are saved.
39 * Weeks are calculated based on the total days of the current instance.
40 *
41 * @property int $years Total years of the current interval.
42 * @property int $months Total months of the current interval.
43 * @property int $weeks Total weeks of the current interval calculated from the days.
44 * @property int $dayz Total days of the current interval (weeks * 7 + days).
45 * @property int $hours Total hours of the current interval.
46 * @property int $minutes Total minutes of the current interval.
47 * @property int $seconds Total seconds of the current interval.
48 * @property int $microseconds Total microseconds of the current interval.
49 * @property int $milliseconds Total microseconds of the current interval.
50 * @property int $microExcludeMilli Remaining microseconds without the milliseconds.
51 * @property int $dayzExcludeWeeks Total days remaining in the final week of the current instance (days % 7).
52 * @property int $daysExcludeWeeks alias of dayzExcludeWeeks
53 * @property-read float $totalYears Number of years equivalent to the interval.
54 * @property-read float $totalMonths Number of months equivalent to the interval.
55 * @property-read float $totalWeeks Number of weeks equivalent to the interval.
56 * @property-read float $totalDays Number of days equivalent to the interval.
57 * @property-read float $totalDayz Alias for totalDays.
58 * @property-read float $totalHours Number of hours equivalent to the interval.
59 * @property-read float $totalMinutes Number of minutes equivalent to the interval.
60 * @property-read float $totalSeconds Number of seconds equivalent to the interval.
61 * @property-read float $totalMilliseconds Number of milliseconds equivalent to the interval.
62 * @property-read float $totalMicroseconds Number of microseconds equivalent to the interval.
63 * @property-read string $locale locale of the current instance
64 *
65 * @method static CarbonInterval years($years = 1) Create instance specifying a number of years or modify the number of years if called on an instance.
66 * @method static CarbonInterval year($years = 1) Alias for years()
67 * @method static CarbonInterval months($months = 1) Create instance specifying a number of months or modify the number of months if called on an instance.
68 * @method static CarbonInterval month($months = 1) Alias for months()
69 * @method static CarbonInterval weeks($weeks = 1) Create instance specifying a number of weeks or modify the number of weeks if called on an instance.
70 * @method static CarbonInterval week($weeks = 1) Alias for weeks()
71 * @method static CarbonInterval days($days = 1) Create instance specifying a number of days or modify the number of days if called on an instance.
72 * @method static CarbonInterval dayz($days = 1) Alias for days()
73 * @method static CarbonInterval daysExcludeWeeks($days = 1) Create instance specifying a number of days or modify the number of days (keeping the current number of weeks) if called on an instance.
74 * @method static CarbonInterval dayzExcludeWeeks($days = 1) Alias for daysExcludeWeeks()
75 * @method static CarbonInterval day($days = 1) Alias for days()
76 * @method static CarbonInterval hours($hours = 1) Create instance specifying a number of hours or modify the number of hours if called on an instance.
77 * @method static CarbonInterval hour($hours = 1) Alias for hours()
78 * @method static CarbonInterval minutes($minutes = 1) Create instance specifying a number of minutes or modify the number of minutes if called on an instance.
79 * @method static CarbonInterval minute($minutes = 1) Alias for minutes()
80 * @method static CarbonInterval seconds($seconds = 1) Create instance specifying a number of seconds or modify the number of seconds if called on an instance.
81 * @method static CarbonInterval second($seconds = 1) Alias for seconds()
82 * @method static CarbonInterval milliseconds($milliseconds = 1) Create instance specifying a number of milliseconds or modify the number of milliseconds if called on an instance.
83 * @method static CarbonInterval millisecond($milliseconds = 1) Alias for milliseconds()
84 * @method static CarbonInterval microseconds($microseconds = 1) Create instance specifying a number of microseconds or modify the number of microseconds if called on an instance.
85 * @method static CarbonInterval microsecond($microseconds = 1) Alias for microseconds()
86 * @method $this addYears(int $years) Add given number of years to the current interval
87 * @method $this subYears(int $years) Subtract given number of years to the current interval
88 * @method $this addMonths(int $months) Add given number of months to the current interval
89 * @method $this subMonths(int $months) Subtract given number of months to the current interval
90 * @method $this addWeeks(int|float $weeks) Add given number of weeks to the current interval
91 * @method $this subWeeks(int|float $weeks) Subtract given number of weeks to the current interval
92 * @method $this addDays(int|float $days) Add given number of days to the current interval
93 * @method $this subDays(int|float $days) Subtract given number of days to the current interval
94 * @method $this addHours(int|float $hours) Add given number of hours to the current interval
95 * @method $this subHours(int|float $hours) Subtract given number of hours to the current interval
96 * @method $this addMinutes(int|float $minutes) Add given number of minutes to the current interval
97 * @method $this subMinutes(int|float $minutes) Subtract given number of minutes to the current interval
98 * @method $this addSeconds(int|float $seconds) Add given number of seconds to the current interval
99 * @method $this subSeconds(int|float $seconds) Subtract given number of seconds to the current interval
100 * @method $this addMilliseconds(int|float $milliseconds) Add given number of milliseconds to the current interval
101 * @method $this subMilliseconds(int|float $milliseconds) Subtract given number of milliseconds to the current interval
102 * @method $this addMicroseconds(int|float $microseconds) Add given number of microseconds to the current interval
103 * @method $this subMicroseconds(int|float $microseconds) Subtract given number of microseconds to the current interval
104 * @method $this roundYear(int|float $precision = 1, string $function = "round") Round the current instance year with given precision using the given function.
105 * @method $this roundYears(int|float $precision = 1, string $function = "round") Round the current instance year with given precision using the given function.
106 * @method $this floorYear(int|float $precision = 1) Truncate the current instance year with given precision.
107 * @method $this floorYears(int|float $precision = 1) Truncate the current instance year with given precision.
108 * @method $this ceilYear(int|float $precision = 1) Ceil the current instance year with given precision.
109 * @method $this ceilYears(int|float $precision = 1) Ceil the current instance year with given precision.
110 * @method $this roundMonth(int|float $precision = 1, string $function = "round") Round the current instance month with given precision using the given function.
111 * @method $this roundMonths(int|float $precision = 1, string $function = "round") Round the current instance month with given precision using the given function.
112 * @method $this floorMonth(int|float $precision = 1) Truncate the current instance month with given precision.
113 * @method $this floorMonths(int|float $precision = 1) Truncate the current instance month with given precision.
114 * @method $this ceilMonth(int|float $precision = 1) Ceil the current instance month with given precision.
115 * @method $this ceilMonths(int|float $precision = 1) Ceil the current instance month with given precision.
116 * @method $this roundWeek(int|float $precision = 1, string $function = "round") Round the current instance day with given precision using the given function.
117 * @method $this roundWeeks(int|float $precision = 1, string $function = "round") Round the current instance day with given precision using the given function.
118 * @method $this floorWeek(int|float $precision = 1) Truncate the current instance day with given precision.
119 * @method $this floorWeeks(int|float $precision = 1) Truncate the current instance day with given precision.
120 * @method $this ceilWeek(int|float $precision = 1) Ceil the current instance day with given precision.
121 * @method $this ceilWeeks(int|float $precision = 1) Ceil the current instance day with given precision.
122 * @method $this roundDay(int|float $precision = 1, string $function = "round") Round the current instance day with given precision using the given function.
123 * @method $this roundDays(int|float $precision = 1, string $function = "round") Round the current instance day with given precision using the given function.
124 * @method $this floorDay(int|float $precision = 1) Truncate the current instance day with given precision.
125 * @method $this floorDays(int|float $precision = 1) Truncate the current instance day with given precision.
126 * @method $this ceilDay(int|float $precision = 1) Ceil the current instance day with given precision.
127 * @method $this ceilDays(int|float $precision = 1) Ceil the current instance day with given precision.
128 * @method $this roundHour(int|float $precision = 1, string $function = "round") Round the current instance hour with given precision using the given function.
129 * @method $this roundHours(int|float $precision = 1, string $function = "round") Round the current instance hour with given precision using the given function.
130 * @method $this floorHour(int|float $precision = 1) Truncate the current instance hour with given precision.
131 * @method $this floorHours(int|float $precision = 1) Truncate the current instance hour with given precision.
132 * @method $this ceilHour(int|float $precision = 1) Ceil the current instance hour with given precision.
133 * @method $this ceilHours(int|float $precision = 1) Ceil the current instance hour with given precision.
134 * @method $this roundMinute(int|float $precision = 1, string $function = "round") Round the current instance minute with given precision using the given function.
135 * @method $this roundMinutes(int|float $precision = 1, string $function = "round") Round the current instance minute with given precision using the given function.
136 * @method $this floorMinute(int|float $precision = 1) Truncate the current instance minute with given precision.
137 * @method $this floorMinutes(int|float $precision = 1) Truncate the current instance minute with given precision.
138 * @method $this ceilMinute(int|float $precision = 1) Ceil the current instance minute with given precision.
139 * @method $this ceilMinutes(int|float $precision = 1) Ceil the current instance minute with given precision.
140 * @method $this roundSecond(int|float $precision = 1, string $function = "round") Round the current instance second with given precision using the given function.
141 * @method $this roundSeconds(int|float $precision = 1, string $function = "round") Round the current instance second with given precision using the given function.
142 * @method $this floorSecond(int|float $precision = 1) Truncate the current instance second with given precision.
143 * @method $this floorSeconds(int|float $precision = 1) Truncate the current instance second with given precision.
144 * @method $this ceilSecond(int|float $precision = 1) Ceil the current instance second with given precision.
145 * @method $this ceilSeconds(int|float $precision = 1) Ceil the current instance second with given precision.
146 * @method $this roundMillennium(int|float $precision = 1, string $function = "round") Round the current instance millennium with given precision using the given function.
147 * @method $this roundMillennia(int|float $precision = 1, string $function = "round") Round the current instance millennium with given precision using the given function.
148 * @method $this floorMillennium(int|float $precision = 1) Truncate the current instance millennium with given precision.
149 * @method $this floorMillennia(int|float $precision = 1) Truncate the current instance millennium with given precision.
150 * @method $this ceilMillennium(int|float $precision = 1) Ceil the current instance millennium with given precision.
151 * @method $this ceilMillennia(int|float $precision = 1) Ceil the current instance millennium with given precision.
152 * @method $this roundCentury(int|float $precision = 1, string $function = "round") Round the current instance century with given precision using the given function.
153 * @method $this roundCenturies(int|float $precision = 1, string $function = "round") Round the current instance century with given precision using the given function.
154 * @method $this floorCentury(int|float $precision = 1) Truncate the current instance century with given precision.
155 * @method $this floorCenturies(int|float $precision = 1) Truncate the current instance century with given precision.
156 * @method $this ceilCentury(int|float $precision = 1) Ceil the current instance century with given precision.
157 * @method $this ceilCenturies(int|float $precision = 1) Ceil the current instance century with given precision.
158 * @method $this roundDecade(int|float $precision = 1, string $function = "round") Round the current instance decade with given precision using the given function.
159 * @method $this roundDecades(int|float $precision = 1, string $function = "round") Round the current instance decade with given precision using the given function.
160 * @method $this floorDecade(int|float $precision = 1) Truncate the current instance decade with given precision.
161 * @method $this floorDecades(int|float $precision = 1) Truncate the current instance decade with given precision.
162 * @method $this ceilDecade(int|float $precision = 1) Ceil the current instance decade with given precision.
163 * @method $this ceilDecades(int|float $precision = 1) Ceil the current instance decade with given precision.
164 * @method $this roundQuarter(int|float $precision = 1, string $function = "round") Round the current instance quarter with given precision using the given function.
165 * @method $this roundQuarters(int|float $precision = 1, string $function = "round") Round the current instance quarter with given precision using the given function.
166 * @method $this floorQuarter(int|float $precision = 1) Truncate the current instance quarter with given precision.
167 * @method $this floorQuarters(int|float $precision = 1) Truncate the current instance quarter with given precision.
168 * @method $this ceilQuarter(int|float $precision = 1) Ceil the current instance quarter with given precision.
169 * @method $this ceilQuarters(int|float $precision = 1) Ceil the current instance quarter with given precision.
170 * @method $this roundMillisecond(int|float $precision = 1, string $function = "round") Round the current instance millisecond with given precision using the given function.
171 * @method $this roundMilliseconds(int|float $precision = 1, string $function = "round") Round the current instance millisecond with given precision using the given function.
172 * @method $this floorMillisecond(int|float $precision = 1) Truncate the current instance millisecond with given precision.
173 * @method $this floorMilliseconds(int|float $precision = 1) Truncate the current instance millisecond with given precision.
174 * @method $this ceilMillisecond(int|float $precision = 1) Ceil the current instance millisecond with given precision.
175 * @method $this ceilMilliseconds(int|float $precision = 1) Ceil the current instance millisecond with given precision.
176 * @method $this roundMicrosecond(int|float $precision = 1, string $function = "round") Round the current instance microsecond with given precision using the given function.
177 * @method $this roundMicroseconds(int|float $precision = 1, string $function = "round") Round the current instance microsecond with given precision using the given function.
178 * @method $this floorMicrosecond(int|float $precision = 1) Truncate the current instance microsecond with given precision.
179 * @method $this floorMicroseconds(int|float $precision = 1) Truncate the current instance microsecond with given precision.
180 * @method $this ceilMicrosecond(int|float $precision = 1) Ceil the current instance microsecond with given precision.
181 * @method $this ceilMicroseconds(int|float $precision = 1) Ceil the current instance microsecond with given precision.
182 */
183class CarbonInterval extends DateInterval implements CarbonConverterInterface
184{
185 use IntervalRounding;
186 use IntervalStep;
187 use Mixin {
188 Mixin::mixin as baseMixin;
189 }
190 use Options;
191
192 /**
193 * Interval spec period designators
194 */
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100195 public const PERIOD_PREFIX = 'P';
196 public const PERIOD_YEARS = 'Y';
197 public const PERIOD_MONTHS = 'M';
198 public const PERIOD_DAYS = 'D';
199 public const PERIOD_TIME_PREFIX = 'T';
200 public const PERIOD_HOURS = 'H';
201 public const PERIOD_MINUTES = 'M';
202 public const PERIOD_SECONDS = 'S';
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200203
204 /**
205 * A translator to ... er ... translate stuff
206 *
207 * @var \Symfony\Component\Translation\TranslatorInterface
208 */
209 protected static $translator;
210
211 /**
212 * @var array|null
213 */
214 protected static $cascadeFactors;
215
216 /**
217 * @var array
218 */
219 protected static $formats = [
220 'y' => 'y',
221 'Y' => 'y',
222 'o' => 'y',
223 'm' => 'm',
224 'n' => 'm',
225 'W' => 'weeks',
226 'd' => 'd',
227 'j' => 'd',
228 'z' => 'd',
229 'h' => 'h',
230 'g' => 'h',
231 'H' => 'h',
232 'G' => 'h',
233 'i' => 'i',
234 's' => 's',
235 'u' => 'micro',
236 'v' => 'milli',
237 ];
238
239 /**
240 * @var array|null
241 */
242 private static $flipCascadeFactors;
243
244 /**
245 * The registered macros.
246 *
247 * @var array
248 */
249 protected static $macros = [];
250
251 /**
252 * Timezone handler for settings() method.
253 *
254 * @var mixed
255 */
256 protected $tzName;
257
258 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100259 * Set the instance's timezone from a string or object.
260 *
261 * @param \DateTimeZone|string $tzName
262 *
263 * @return static
264 */
265 public function setTimezone($tzName)
266 {
267 $this->tzName = $tzName;
268
269 return $this;
270 }
271
272 /**
273 * @internal
274 *
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200275 * Set the instance's timezone from a string or object and add/subtract the offset difference.
276 *
277 * @param \DateTimeZone|string $tzName
278 *
279 * @return static
280 */
281 public function shiftTimezone($tzName)
282 {
283 $this->tzName = $tzName;
284
285 return $this;
286 }
287
288 /**
289 * Mapping of units and factors for cascading.
290 *
291 * Should only be modified by changing the factors or referenced constants.
292 *
293 * @return array
294 */
295 public static function getCascadeFactors()
296 {
297 return static::$cascadeFactors ?: [
298 'milliseconds' => [Carbon::MICROSECONDS_PER_MILLISECOND, 'microseconds'],
299 'seconds' => [Carbon::MILLISECONDS_PER_SECOND, 'milliseconds'],
300 'minutes' => [Carbon::SECONDS_PER_MINUTE, 'seconds'],
301 'hours' => [Carbon::MINUTES_PER_HOUR, 'minutes'],
302 'dayz' => [Carbon::HOURS_PER_DAY, 'hours'],
303 'weeks' => [Carbon::DAYS_PER_WEEK, 'dayz'],
304 'months' => [Carbon::WEEKS_PER_MONTH, 'weeks'],
305 'years' => [Carbon::MONTHS_PER_YEAR, 'months'],
306 ];
307 }
308
309 private static function standardizeUnit($unit)
310 {
311 $unit = rtrim($unit, 'sz').'s';
312
313 return $unit === 'days' ? 'dayz' : $unit;
314 }
315
316 private static function getFlipCascadeFactors()
317 {
318 if (!self::$flipCascadeFactors) {
319 self::$flipCascadeFactors = [];
320
321 foreach (static::getCascadeFactors() as $to => [$factor, $from]) {
322 self::$flipCascadeFactors[self::standardizeUnit($from)] = [self::standardizeUnit($to), $factor];
323 }
324 }
325
326 return self::$flipCascadeFactors;
327 }
328
329 /**
330 * Set default cascading factors for ->cascade() method.
331 *
332 * @param array $cascadeFactors
333 */
334 public static function setCascadeFactors(array $cascadeFactors)
335 {
336 self::$flipCascadeFactors = null;
337 static::$cascadeFactors = $cascadeFactors;
338 }
339
340 ///////////////////////////////////////////////////////////////////
341 //////////////////////////// CONSTRUCTORS /////////////////////////
342 ///////////////////////////////////////////////////////////////////
343
344 /**
345 * Create a new CarbonInterval instance.
346 *
347 * @param int|null $years
348 * @param int|null $months
349 * @param int|null $weeks
350 * @param int|null $days
351 * @param int|null $hours
352 * @param int|null $minutes
353 * @param int|null $seconds
354 * @param int|null $microseconds
355 *
356 * @throws Exception when the interval_spec (passed as $years) cannot be parsed as an interval.
357 */
358 public function __construct($years = 1, $months = null, $weeks = null, $days = null, $hours = null, $minutes = null, $seconds = null, $microseconds = null)
359 {
360 if ($years instanceof Closure) {
361 $this->step = $years;
362 $years = null;
363 }
364
365 if ($years instanceof DateInterval) {
366 parent::__construct(static::getDateIntervalSpec($years));
367 $this->f = $years->f;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100368 self::copyNegativeUnits($years, $this);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200369
370 return;
371 }
372
373 $spec = $years;
374
375 if (!\is_string($spec) || (float) $years || preg_match('/^[0-9.]/', $years)) {
376 $spec = static::PERIOD_PREFIX;
377
378 $spec .= $years > 0 ? $years.static::PERIOD_YEARS : '';
379 $spec .= $months > 0 ? $months.static::PERIOD_MONTHS : '';
380
381 $specDays = 0;
382 $specDays += $weeks > 0 ? $weeks * static::getDaysPerWeek() : 0;
383 $specDays += $days > 0 ? $days : 0;
384
385 $spec .= $specDays > 0 ? $specDays.static::PERIOD_DAYS : '';
386
387 if ($hours > 0 || $minutes > 0 || $seconds > 0) {
388 $spec .= static::PERIOD_TIME_PREFIX;
389 $spec .= $hours > 0 ? $hours.static::PERIOD_HOURS : '';
390 $spec .= $minutes > 0 ? $minutes.static::PERIOD_MINUTES : '';
391 $spec .= $seconds > 0 ? $seconds.static::PERIOD_SECONDS : '';
392 }
393
394 if ($spec === static::PERIOD_PREFIX) {
395 // Allow the zero interval.
396 $spec .= '0'.static::PERIOD_YEARS;
397 }
398 }
399
400 parent::__construct($spec);
401
402 if ($microseconds !== null) {
403 $this->f = $microseconds / Carbon::MICROSECONDS_PER_SECOND;
404 }
405 }
406
407 /**
408 * Returns the factor for a given source-to-target couple.
409 *
410 * @param string $source
411 * @param string $target
412 *
413 * @return int|null
414 */
415 public static function getFactor($source, $target)
416 {
417 $source = self::standardizeUnit($source);
418 $target = self::standardizeUnit($target);
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100419 $factors = self::getFlipCascadeFactors();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200420
421 if (isset($factors[$source])) {
422 [$to, $factor] = $factors[$source];
423
424 if ($to === $target) {
425 return $factor;
426 }
427
428 return $factor * static::getFactor($to, $target);
429 }
430
431 return null;
432 }
433
434 /**
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100435 * Returns the factor for a given source-to-target couple if set,
436 * else try to find the appropriate constant as the factor, such as Carbon::DAYS_PER_WEEK.
437 *
438 * @param string $source
439 * @param string $target
440 *
441 * @return int|null
442 */
443 public static function getFactorWithDefault($source, $target)
444 {
445 $factor = self::getFactor($source, $target);
446
447 if ($factor) {
448 return $factor;
449 }
450
451 static $defaults = [
452 'month' => ['year' => Carbon::MONTHS_PER_YEAR],
453 'week' => ['month' => Carbon::WEEKS_PER_MONTH],
454 'day' => ['week' => Carbon::DAYS_PER_WEEK],
455 'hour' => ['day' => Carbon::HOURS_PER_DAY],
456 'minute' => ['hour' => Carbon::MINUTES_PER_HOUR],
457 'second' => ['minute' => Carbon::SECONDS_PER_MINUTE],
458 'millisecond' => ['second' => Carbon::MILLISECONDS_PER_SECOND],
459 'microsecond' => ['millisecond' => Carbon::MICROSECONDS_PER_MILLISECOND],
460 ];
461
462 return $defaults[$source][$target] ?? null;
463 }
464
465 /**
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200466 * Returns current config for days per week.
467 *
468 * @return int
469 */
470 public static function getDaysPerWeek()
471 {
472 return static::getFactor('dayz', 'weeks') ?: Carbon::DAYS_PER_WEEK;
473 }
474
475 /**
476 * Returns current config for hours per day.
477 *
478 * @return int
479 */
480 public static function getHoursPerDay()
481 {
482 return static::getFactor('hours', 'dayz') ?: Carbon::HOURS_PER_DAY;
483 }
484
485 /**
486 * Returns current config for minutes per hour.
487 *
488 * @return int
489 */
490 public static function getMinutesPerHour()
491 {
492 return static::getFactor('minutes', 'hours') ?: Carbon::MINUTES_PER_HOUR;
493 }
494
495 /**
496 * Returns current config for seconds per minute.
497 *
498 * @return int
499 */
500 public static function getSecondsPerMinute()
501 {
502 return static::getFactor('seconds', 'minutes') ?: Carbon::SECONDS_PER_MINUTE;
503 }
504
505 /**
506 * Returns current config for microseconds per second.
507 *
508 * @return int
509 */
510 public static function getMillisecondsPerSecond()
511 {
512 return static::getFactor('milliseconds', 'seconds') ?: Carbon::MILLISECONDS_PER_SECOND;
513 }
514
515 /**
516 * Returns current config for microseconds per second.
517 *
518 * @return int
519 */
520 public static function getMicrosecondsPerMillisecond()
521 {
522 return static::getFactor('microseconds', 'milliseconds') ?: Carbon::MICROSECONDS_PER_MILLISECOND;
523 }
524
525 /**
526 * Create a new CarbonInterval instance from specific values.
527 * This is an alias for the constructor that allows better fluent
528 * syntax as it allows you to do CarbonInterval::create(1)->fn() rather than
529 * (new CarbonInterval(1))->fn().
530 *
531 * @param int $years
532 * @param int $months
533 * @param int $weeks
534 * @param int $days
535 * @param int $hours
536 * @param int $minutes
537 * @param int $seconds
538 * @param int $microseconds
539 *
540 * @throws Exception when the interval_spec (passed as $years) cannot be parsed as an interval.
541 *
542 * @return static
543 */
544 public static function create($years = 1, $months = null, $weeks = null, $days = null, $hours = null, $minutes = null, $seconds = null, $microseconds = null)
545 {
546 return new static($years, $months, $weeks, $days, $hours, $minutes, $seconds, $microseconds);
547 }
548
549 /**
550 * Parse a string into a new CarbonInterval object according to the specified format.
551 *
552 * @example
553 * ```
554 * echo Carboninterval::createFromFormat('H:i', '1:30');
555 * ```
556 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100557 * @param string $format Format of the $interval input string
558 * @param string|null $interval Input string to convert into an interval
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200559 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100560 * @throws \Carbon\Exceptions\ParseErrorException when the $interval cannot be parsed as an interval.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200561 *
562 * @return static
563 */
564 public static function createFromFormat(string $format, ?string $interval)
565 {
566 $instance = new static(0);
567 $length = mb_strlen($format);
568
569 if (preg_match('/s([,.])([uv])$/', $format, $match)) {
570 $interval = explode($match[1], $interval);
571 $index = \count($interval) - 1;
572 $interval[$index] = str_pad($interval[$index], $match[2] === 'v' ? 3 : 6, '0');
573 $interval = implode($match[1], $interval);
574 }
575
576 $interval = $interval ?? '';
577
578 for ($index = 0; $index < $length; $index++) {
579 $expected = mb_substr($format, $index, 1);
580 $nextCharacter = mb_substr($interval, 0, 1);
581 $unit = static::$formats[$expected] ?? null;
582
583 if ($unit) {
584 if (!preg_match('/^-?\d+/', $interval, $match)) {
585 throw new ParseErrorException('number', $nextCharacter);
586 }
587
588 $interval = mb_substr($interval, mb_strlen($match[0]));
589 $instance->$unit += (int) ($match[0]);
590
591 continue;
592 }
593
594 if ($nextCharacter !== $expected) {
595 throw new ParseErrorException(
596 "'$expected'",
597 $nextCharacter,
598 'Allowed substitutes for interval formats are '.implode(', ', array_keys(static::$formats))."\n".
599 'See https://php.net/manual/en/function.date.php for their meaning'
600 );
601 }
602
603 $interval = mb_substr($interval, 1);
604 }
605
606 if ($interval !== '') {
607 throw new ParseErrorException(
608 'end of string',
609 $interval
610 );
611 }
612
613 return $instance;
614 }
615
616 /**
617 * Get a copy of the instance.
618 *
619 * @return static
620 */
621 public function copy()
622 {
623 $date = new static(0);
624 $date->copyProperties($this);
625 $date->step = $this->step;
626
627 return $date;
628 }
629
630 /**
631 * Get a copy of the instance.
632 *
633 * @return static
634 */
635 public function clone()
636 {
637 return $this->copy();
638 }
639
640 /**
641 * Provide static helpers to create instances. Allows CarbonInterval::years(3).
642 *
643 * Note: This is done using the magic method to allow static and instance methods to
644 * have the same names.
645 *
646 * @param string $method magic method name called
647 * @param array $parameters parameters list
648 *
649 * @return static|null
650 */
651 public static function __callStatic($method, $parameters)
652 {
653 try {
654 $interval = new static(0);
655 $localStrictModeEnabled = $interval->localStrictModeEnabled;
656 $interval->localStrictModeEnabled = true;
657
658 $result = static::hasMacro($method)
659 ? static::bindMacroContext(null, function () use (&$method, &$parameters, &$interval) {
660 return $interval->callMacro($method, $parameters);
661 })
662 : $interval->$method(...$parameters);
663
664 $interval->localStrictModeEnabled = $localStrictModeEnabled;
665
666 return $result;
667 } catch (BadFluentSetterException $exception) {
668 if (Carbon::isStrictModeEnabled()) {
669 throw new BadFluentConstructorException($method, 0, $exception);
670 }
671
672 return null;
673 }
674 }
675
676 /**
677 * Return the current context from inside a macro callee or a new one if static.
678 *
679 * @return static
680 */
681 protected static function this()
682 {
683 return end(static::$macroContextStack) ?: new static(0);
684 }
685
686 /**
687 * Creates a CarbonInterval from string.
688 *
689 * Format:
690 *
691 * Suffix | Unit | Example | DateInterval expression
692 * -------|---------|---------|------------------------
693 * y | years | 1y | P1Y
694 * mo | months | 3mo | P3M
695 * w | weeks | 2w | P2W
696 * d | days | 28d | P28D
697 * h | hours | 4h | PT4H
698 * m | minutes | 12m | PT12M
699 * s | seconds | 59s | PT59S
700 *
701 * e. g. `1w 3d 4h 32m 23s` is converted to 10 days 4 hours 32 minutes and 23 seconds.
702 *
703 * Special cases:
704 * - An empty string will return a zero interval
705 * - Fractions are allowed for weeks, days, hours and minutes and will be converted
706 * and rounded to the next smaller value (caution: 0.5w = 4d)
707 *
708 * @param string $intervalDefinition
709 *
710 * @return static
711 */
712 public static function fromString($intervalDefinition)
713 {
714 if (empty($intervalDefinition)) {
715 return new static(0);
716 }
717
718 $years = 0;
719 $months = 0;
720 $weeks = 0;
721 $days = 0;
722 $hours = 0;
723 $minutes = 0;
724 $seconds = 0;
725 $milliseconds = 0;
726 $microseconds = 0;
727
728 $pattern = '/(\d+(?:\.\d+)?)\h*([^\d\h]*)/i';
729 preg_match_all($pattern, $intervalDefinition, $parts, PREG_SET_ORDER);
730
731 while ([$part, $value, $unit] = array_shift($parts)) {
732 $intValue = (int) $value;
733 $fraction = (float) $value - $intValue;
734
735 // Fix calculation precision
736 switch (round($fraction, 6)) {
737 case 1:
738 $fraction = 0;
739 $intValue++;
740
741 break;
742 case 0:
743 $fraction = 0;
744
745 break;
746 }
747
748 switch ($unit === 'µs' ? 'µs' : strtolower($unit)) {
749 case 'millennia':
750 case 'millennium':
751 $years += $intValue * CarbonInterface::YEARS_PER_MILLENNIUM;
752
753 break;
754
755 case 'century':
756 case 'centuries':
757 $years += $intValue * CarbonInterface::YEARS_PER_CENTURY;
758
759 break;
760
761 case 'decade':
762 case 'decades':
763 $years += $intValue * CarbonInterface::YEARS_PER_DECADE;
764
765 break;
766
767 case 'year':
768 case 'years':
769 case 'y':
770 $years += $intValue;
771
772 break;
773
774 case 'quarter':
775 case 'quarters':
776 $months += $intValue * CarbonInterface::MONTHS_PER_QUARTER;
777
778 break;
779
780 case 'month':
781 case 'months':
782 case 'mo':
783 $months += $intValue;
784
785 break;
786
787 case 'week':
788 case 'weeks':
789 case 'w':
790 $weeks += $intValue;
791
792 if ($fraction) {
793 $parts[] = [null, $fraction * static::getDaysPerWeek(), 'd'];
794 }
795
796 break;
797
798 case 'day':
799 case 'days':
800 case 'd':
801 $days += $intValue;
802
803 if ($fraction) {
804 $parts[] = [null, $fraction * static::getHoursPerDay(), 'h'];
805 }
806
807 break;
808
809 case 'hour':
810 case 'hours':
811 case 'h':
812 $hours += $intValue;
813
814 if ($fraction) {
815 $parts[] = [null, $fraction * static::getMinutesPerHour(), 'm'];
816 }
817
818 break;
819
820 case 'minute':
821 case 'minutes':
822 case 'm':
823 $minutes += $intValue;
824
825 if ($fraction) {
826 $parts[] = [null, $fraction * static::getSecondsPerMinute(), 's'];
827 }
828
829 break;
830
831 case 'second':
832 case 'seconds':
833 case 's':
834 $seconds += $intValue;
835
836 if ($fraction) {
837 $parts[] = [null, $fraction * static::getMillisecondsPerSecond(), 'ms'];
838 }
839
840 break;
841
842 case 'millisecond':
843 case 'milliseconds':
844 case 'milli':
845 case 'ms':
846 $milliseconds += $intValue;
847
848 if ($fraction) {
849 $microseconds += round($fraction * static::getMicrosecondsPerMillisecond());
850 }
851
852 break;
853
854 case 'microsecond':
855 case 'microseconds':
856 case 'micro':
857 case 'µs':
858 $microseconds += $intValue;
859
860 break;
861
862 default:
863 throw new InvalidIntervalException(
864 sprintf('Invalid part %s in definition %s', $part, $intervalDefinition)
865 );
866 }
867 }
868
869 return new static($years, $months, $weeks, $days, $hours, $minutes, $seconds, $milliseconds * Carbon::MICROSECONDS_PER_MILLISECOND + $microseconds);
870 }
871
872 /**
873 * Creates a CarbonInterval from string using a different locale.
874 *
875 * @param string $interval interval string in the given language (may also contain English).
876 * @param string|null $locale if locale is null or not specified, current global locale will be used instead.
877 *
878 * @return static
879 */
880 public static function parseFromLocale($interval, $locale = null)
881 {
882 return static::fromString(Carbon::translateTimeString($interval, $locale ?: static::getLocale(), 'en'));
883 }
884
885 private static function castIntervalToClass(DateInterval $interval, string $className)
886 {
887 $mainClass = DateInterval::class;
888
889 if (!is_a($className, $mainClass, true)) {
890 throw new InvalidCastException("$className is not a sub-class of $mainClass.");
891 }
892
893 $microseconds = $interval->f;
894 $instance = new $className(static::getDateIntervalSpec($interval));
895
896 if ($microseconds) {
897 $instance->f = $microseconds;
898 }
899
900 if ($interval instanceof self && is_a($className, self::class, true)) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100901 self::copyStep($interval, $instance);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200902 }
903
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +0100904 self::copyNegativeUnits($interval, $instance);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +0200905
906 return $instance;
907 }
908
909 private static function copyNegativeUnits(DateInterval $from, DateInterval $to): void
910 {
911 $to->invert = $from->invert;
912
913 foreach (['y', 'm', 'd', 'h', 'i', 's'] as $unit) {
914 if ($from->$unit < 0) {
915 $to->$unit *= -1;
916 }
917 }
918 }
919
920 private static function copyStep(self $from, self $to): void
921 {
922 $to->setStep($from->getStep());
923 }
924
925 /**
926 * Cast the current instance into the given class.
927 *
928 * @param string $className The $className::instance() method will be called to cast the current object.
929 *
930 * @return DateInterval
931 */
932 public function cast(string $className)
933 {
934 return self::castIntervalToClass($this, $className);
935 }
936
937 /**
938 * Create a CarbonInterval instance from a DateInterval one. Can not instance
939 * DateInterval objects created from DateTime::diff() as you can't externally
940 * set the $days field.
941 *
942 * @param DateInterval $interval
943 *
944 * @return static
945 */
946 public static function instance(DateInterval $interval)
947 {
948 return self::castIntervalToClass($interval, static::class);
949 }
950
951 /**
952 * Make a CarbonInterval instance from given variable if possible.
953 *
954 * Always return a new instance. Parse only strings and only these likely to be intervals (skip dates
955 * and recurrences). Throw an exception for invalid format, but otherwise return null.
956 *
957 * @param mixed|int|DateInterval|string|Closure|null $interval interval or number of the given $unit
958 * @param string|null $unit if specified, $interval must be an integer
959 *
960 * @return static|null
961 */
962 public static function make($interval, $unit = null)
963 {
964 if ($unit) {
965 $interval = "$interval ".Carbon::pluralUnit($unit);
966 }
967
968 if ($interval instanceof DateInterval) {
969 return static::instance($interval);
970 }
971
972 if ($interval instanceof Closure) {
973 return new static($interval);
974 }
975
976 if (!\is_string($interval)) {
977 return null;
978 }
979
980 return static::makeFromString($interval);
981 }
982
983 protected static function makeFromString(string $interval)
984 {
985 $interval = preg_replace('/\s+/', ' ', trim($interval));
986
987 if (preg_match('/^P[T0-9]/', $interval)) {
988 return new static($interval);
989 }
990
991 if (preg_match('/^(?:\h*\d+(?:\.\d+)?\h*[a-z]+)+$/i', $interval)) {
992 return static::fromString($interval);
993 }
994
995 /** @var static $interval */
996 $interval = static::createFromDateString($interval);
997
998 return !$interval || $interval->isEmpty() ? null : $interval;
999 }
1000
1001 protected function resolveInterval($interval)
1002 {
1003 if (!($interval instanceof self)) {
1004 return self::make($interval);
1005 }
1006
1007 return $interval;
1008 }
1009
1010 /**
1011 * Sets up a DateInterval from the relative parts of the string.
1012 *
1013 * @param string $time
1014 *
1015 * @return static
1016 *
1017 * @link https://php.net/manual/en/dateinterval.createfromdatestring.php
1018 */
1019 #[ReturnTypeWillChange]
1020 public static function createFromDateString($time)
1021 {
1022 $interval = @parent::createFromDateString(strtr($time, [
1023 ',' => ' ',
1024 ' and ' => ' ',
1025 ]));
1026
1027 if ($interval instanceof DateInterval) {
1028 $interval = static::instance($interval);
1029 }
1030
1031 return $interval;
1032 }
1033
1034 ///////////////////////////////////////////////////////////////////
1035 ///////////////////////// GETTERS AND SETTERS /////////////////////
1036 ///////////////////////////////////////////////////////////////////
1037
1038 /**
1039 * Get a part of the CarbonInterval object.
1040 *
1041 * @param string $name
1042 *
1043 * @throws UnknownGetterException
1044 *
1045 * @return int|float|string
1046 */
1047 public function get($name)
1048 {
1049 if (str_starts_with($name, 'total')) {
1050 return $this->total(substr($name, 5));
1051 }
1052
1053 switch ($name) {
1054 case 'years':
1055 return $this->y;
1056
1057 case 'months':
1058 return $this->m;
1059
1060 case 'dayz':
1061 return $this->d;
1062
1063 case 'hours':
1064 return $this->h;
1065
1066 case 'minutes':
1067 return $this->i;
1068
1069 case 'seconds':
1070 return $this->s;
1071
1072 case 'milli':
1073 case 'milliseconds':
1074 return (int) (round($this->f * Carbon::MICROSECONDS_PER_SECOND) / Carbon::MICROSECONDS_PER_MILLISECOND);
1075
1076 case 'micro':
1077 case 'microseconds':
1078 return (int) round($this->f * Carbon::MICROSECONDS_PER_SECOND);
1079
1080 case 'microExcludeMilli':
1081 return (int) round($this->f * Carbon::MICROSECONDS_PER_SECOND) % Carbon::MICROSECONDS_PER_MILLISECOND;
1082
1083 case 'weeks':
1084 return (int) ($this->d / static::getDaysPerWeek());
1085
1086 case 'daysExcludeWeeks':
1087 case 'dayzExcludeWeeks':
1088 return $this->d % static::getDaysPerWeek();
1089
1090 case 'locale':
1091 return $this->getTranslatorLocale();
1092
1093 default:
1094 throw new UnknownGetterException($name);
1095 }
1096 }
1097
1098 /**
1099 * Get a part of the CarbonInterval object.
1100 *
1101 * @param string $name
1102 *
1103 * @throws UnknownGetterException
1104 *
1105 * @return int|float|string
1106 */
1107 public function __get($name)
1108 {
1109 return $this->get($name);
1110 }
1111
1112 /**
1113 * Set a part of the CarbonInterval object.
1114 *
1115 * @param string|array $name
1116 * @param int $value
1117 *
1118 * @throws UnknownSetterException
1119 *
1120 * @return $this
1121 */
1122 public function set($name, $value = null)
1123 {
1124 $properties = \is_array($name) ? $name : [$name => $value];
1125
1126 foreach ($properties as $key => $value) {
1127 switch (Carbon::singularUnit(rtrim($key, 'z'))) {
1128 case 'year':
1129 $this->y = $value;
1130
1131 break;
1132
1133 case 'month':
1134 $this->m = $value;
1135
1136 break;
1137
1138 case 'week':
1139 $this->d = $value * static::getDaysPerWeek();
1140
1141 break;
1142
1143 case 'day':
1144 $this->d = $value;
1145
1146 break;
1147
1148 case 'daysexcludeweek':
1149 case 'dayzexcludeweek':
1150 $this->d = $this->weeks * static::getDaysPerWeek() + $value;
1151
1152 break;
1153
1154 case 'hour':
1155 $this->h = $value;
1156
1157 break;
1158
1159 case 'minute':
1160 $this->i = $value;
1161
1162 break;
1163
1164 case 'second':
1165 $this->s = $value;
1166
1167 break;
1168
1169 case 'milli':
1170 case 'millisecond':
1171 $this->microseconds = $value * Carbon::MICROSECONDS_PER_MILLISECOND + $this->microseconds % Carbon::MICROSECONDS_PER_MILLISECOND;
1172
1173 break;
1174
1175 case 'micro':
1176 case 'microsecond':
1177 $this->f = $value / Carbon::MICROSECONDS_PER_SECOND;
1178
1179 break;
1180
1181 default:
1182 if ($this->localStrictModeEnabled ?? Carbon::isStrictModeEnabled()) {
1183 throw new UnknownSetterException($key);
1184 }
1185
1186 $this->$key = $value;
1187 }
1188 }
1189
1190 return $this;
1191 }
1192
1193 /**
1194 * Set a part of the CarbonInterval object.
1195 *
1196 * @param string $name
1197 * @param int $value
1198 *
1199 * @throws UnknownSetterException
1200 */
1201 public function __set($name, $value)
1202 {
1203 $this->set($name, $value);
1204 }
1205
1206 /**
1207 * Allow setting of weeks and days to be cumulative.
1208 *
1209 * @param int $weeks Number of weeks to set
1210 * @param int $days Number of days to set
1211 *
1212 * @return static
1213 */
1214 public function weeksAndDays($weeks, $days)
1215 {
1216 $this->dayz = ($weeks * static::getDaysPerWeek()) + $days;
1217
1218 return $this;
1219 }
1220
1221 /**
1222 * Returns true if the interval is empty for each unit.
1223 *
1224 * @return bool
1225 */
1226 public function isEmpty()
1227 {
1228 return $this->years === 0 &&
1229 $this->months === 0 &&
1230 $this->dayz === 0 &&
1231 !$this->days &&
1232 $this->hours === 0 &&
1233 $this->minutes === 0 &&
1234 $this->seconds === 0 &&
1235 $this->microseconds === 0;
1236 }
1237
1238 /**
1239 * Register a custom macro.
1240 *
1241 * @example
1242 * ```
1243 * CarbonInterval::macro('twice', function () {
1244 * return $this->times(2);
1245 * });
1246 * echo CarbonInterval::hours(2)->twice();
1247 * ```
1248 *
1249 * @param string $name
1250 * @param object|callable $macro
1251 *
1252 * @return void
1253 */
1254 public static function macro($name, $macro)
1255 {
1256 static::$macros[$name] = $macro;
1257 }
1258
1259 /**
1260 * Register macros from a mixin object.
1261 *
1262 * @example
1263 * ```
1264 * CarbonInterval::mixin(new class {
1265 * public function daysToHours() {
1266 * return function () {
1267 * $this->hours += $this->days;
1268 * $this->days = 0;
1269 *
1270 * return $this;
1271 * };
1272 * }
1273 * public function hoursToDays() {
1274 * return function () {
1275 * $this->days += $this->hours;
1276 * $this->hours = 0;
1277 *
1278 * return $this;
1279 * };
1280 * }
1281 * });
1282 * echo CarbonInterval::hours(5)->hoursToDays() . "\n";
1283 * echo CarbonInterval::days(5)->daysToHours() . "\n";
1284 * ```
1285 *
1286 * @param object|string $mixin
1287 *
1288 * @throws ReflectionException
1289 *
1290 * @return void
1291 */
1292 public static function mixin($mixin)
1293 {
1294 static::baseMixin($mixin);
1295 }
1296
1297 /**
1298 * Check if macro is registered.
1299 *
1300 * @param string $name
1301 *
1302 * @return bool
1303 */
1304 public static function hasMacro($name)
1305 {
1306 return isset(static::$macros[$name]);
1307 }
1308
1309 /**
1310 * Call given macro.
1311 *
1312 * @param string $name
1313 * @param array $parameters
1314 *
1315 * @return mixed
1316 */
1317 protected function callMacro($name, $parameters)
1318 {
1319 $macro = static::$macros[$name];
1320
1321 if ($macro instanceof Closure) {
1322 $boundMacro = @$macro->bindTo($this, static::class) ?: @$macro->bindTo(null, static::class);
1323
1324 return ($boundMacro ?: $macro)(...$parameters);
1325 }
1326
1327 return $macro(...$parameters);
1328 }
1329
1330 /**
1331 * Allow fluent calls on the setters... CarbonInterval::years(3)->months(5)->day().
1332 *
1333 * Note: This is done using the magic method to allow static and instance methods to
1334 * have the same names.
1335 *
1336 * @param string $method magic method name called
1337 * @param array $parameters parameters list
1338 *
1339 * @throws BadFluentSetterException|Throwable
1340 *
1341 * @return static
1342 */
1343 public function __call($method, $parameters)
1344 {
1345 if (static::hasMacro($method)) {
1346 return static::bindMacroContext($this, function () use (&$method, &$parameters) {
1347 return $this->callMacro($method, $parameters);
1348 });
1349 }
1350
1351 $roundedValue = $this->callRoundMethod($method, $parameters);
1352
1353 if ($roundedValue !== null) {
1354 return $roundedValue;
1355 }
1356
1357 if (preg_match('/^(?<method>add|sub)(?<unit>[A-Z].*)$/', $method, $match)) {
1358 return $this->{$match['method']}($parameters[0], $match['unit']);
1359 }
1360
1361 try {
1362 $this->set($method, \count($parameters) === 0 ? 1 : $parameters[0]);
1363 } catch (UnknownSetterException $exception) {
1364 if ($this->localStrictModeEnabled ?? Carbon::isStrictModeEnabled()) {
1365 throw new BadFluentSetterException($method, 0, $exception);
1366 }
1367 }
1368
1369 return $this;
1370 }
1371
1372 protected function getForHumansInitialVariables($syntax, $short)
1373 {
1374 if (\is_array($syntax)) {
1375 return $syntax;
1376 }
1377
1378 if (\is_int($short)) {
1379 return [
1380 'parts' => $short,
1381 'short' => false,
1382 ];
1383 }
1384
1385 if (\is_bool($syntax)) {
1386 return [
1387 'short' => $syntax,
1388 'syntax' => CarbonInterface::DIFF_ABSOLUTE,
1389 ];
1390 }
1391
1392 return [];
1393 }
1394
1395 /**
1396 * @param mixed $syntax
1397 * @param mixed $short
1398 * @param mixed $parts
1399 * @param mixed $options
1400 *
1401 * @return array
1402 */
1403 protected function getForHumansParameters($syntax = null, $short = false, $parts = -1, $options = null)
1404 {
1405 $optionalSpace = ' ';
1406 $default = $this->getTranslationMessage('list.0') ?? $this->getTranslationMessage('list') ?? ' ';
1407 $join = $default === '' ? '' : ' ';
1408 $altNumbers = false;
1409 $aUnit = false;
1410 $minimumUnit = 's';
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001411 $skip = [];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001412 extract($this->getForHumansInitialVariables($syntax, $short));
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001413 $skip = array_filter((array) $skip, static function ($value) {
1414 return \is_string($value) && $value !== '';
1415 });
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001416
1417 if ($syntax === null) {
1418 $syntax = CarbonInterface::DIFF_ABSOLUTE;
1419 }
1420
1421 if ($parts === -1) {
1422 $parts = INF;
1423 }
1424
1425 if ($options === null) {
1426 $options = static::getHumanDiffOptions();
1427 }
1428
1429 if ($join === false) {
1430 $join = ' ';
1431 } elseif ($join === true) {
1432 $join = [
1433 $default,
1434 $this->getTranslationMessage('list.1') ?? $default,
1435 ];
1436 }
1437
1438 if ($altNumbers) {
1439 if ($altNumbers !== true) {
1440 $language = new Language($this->locale);
1441 $altNumbers = \in_array($language->getCode(), (array) $altNumbers);
1442 }
1443 }
1444
1445 if (\is_array($join)) {
1446 [$default, $last] = $join;
1447
1448 if ($default !== ' ') {
1449 $optionalSpace = '';
1450 }
1451
1452 $join = function ($list) use ($default, $last) {
1453 if (\count($list) < 2) {
1454 return implode('', $list);
1455 }
1456
1457 $end = array_pop($list);
1458
1459 return implode($default, $list).$last.$end;
1460 };
1461 }
1462
1463 if (\is_string($join)) {
1464 if ($join !== ' ') {
1465 $optionalSpace = '';
1466 }
1467
1468 $glue = $join;
1469 $join = function ($list) use ($glue) {
1470 return implode($glue, $list);
1471 };
1472 }
1473
1474 $interpolations = [
1475 ':optional-space' => $optionalSpace,
1476 ];
1477
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001478 return [$syntax, $short, $parts, $options, $join, $aUnit, $altNumbers, $interpolations, $minimumUnit, $skip];
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001479 }
1480
1481 protected static function getRoundingMethodFromOptions(int $options): ?string
1482 {
1483 if ($options & CarbonInterface::ROUND) {
1484 return 'round';
1485 }
1486
1487 if ($options & CarbonInterface::CEIL) {
1488 return 'ceil';
1489 }
1490
1491 if ($options & CarbonInterface::FLOOR) {
1492 return 'floor';
1493 }
1494
1495 return null;
1496 }
1497
1498 /**
1499 * Returns interval values as an array where key are the unit names and values the counts.
1500 *
1501 * @return int[]
1502 */
1503 public function toArray()
1504 {
1505 return [
1506 'years' => $this->years,
1507 'months' => $this->months,
1508 'weeks' => $this->weeks,
1509 'days' => $this->daysExcludeWeeks,
1510 'hours' => $this->hours,
1511 'minutes' => $this->minutes,
1512 'seconds' => $this->seconds,
1513 'microseconds' => $this->microseconds,
1514 ];
1515 }
1516
1517 /**
1518 * Returns interval non-zero values as an array where key are the unit names and values the counts.
1519 *
1520 * @return int[]
1521 */
1522 public function getNonZeroValues()
1523 {
1524 return array_filter($this->toArray(), 'intval');
1525 }
1526
1527 /**
1528 * Returns interval values as an array where key are the unit names and values the counts
1529 * from the biggest non-zero one the the smallest non-zero one.
1530 *
1531 * @return int[]
1532 */
1533 public function getValuesSequence()
1534 {
1535 $nonZeroValues = $this->getNonZeroValues();
1536
1537 if ($nonZeroValues === []) {
1538 return [];
1539 }
1540
1541 $keys = array_keys($nonZeroValues);
1542 $firstKey = $keys[0];
1543 $lastKey = $keys[\count($keys) - 1];
1544 $values = [];
1545 $record = false;
1546
1547 foreach ($this->toArray() as $unit => $count) {
1548 if ($unit === $firstKey) {
1549 $record = true;
1550 }
1551
1552 if ($record) {
1553 $values[$unit] = $count;
1554 }
1555
1556 if ($unit === $lastKey) {
1557 $record = false;
1558 }
1559 }
1560
1561 return $values;
1562 }
1563
1564 /**
1565 * Get the current interval in a human readable format in the current locale.
1566 *
1567 * @example
1568 * ```
1569 * echo CarbonInterval::fromString('4d 3h 40m')->forHumans() . "\n";
1570 * echo CarbonInterval::fromString('4d 3h 40m')->forHumans(['parts' => 2]) . "\n";
1571 * echo CarbonInterval::fromString('4d 3h 40m')->forHumans(['parts' => 3, 'join' => true]) . "\n";
1572 * echo CarbonInterval::fromString('4d 3h 40m')->forHumans(['short' => true]) . "\n";
1573 * echo CarbonInterval::fromString('1d 24h')->forHumans(['join' => ' or ']) . "\n";
1574 * echo CarbonInterval::fromString('1d 24h')->forHumans(['minimumUnit' => 'hour']) . "\n";
1575 * ```
1576 *
1577 * @param int|array $syntax if array passed, parameters will be extracted from it, the array may contains:
1578 * - 'syntax' entry (see below)
1579 * - 'short' entry (see below)
1580 * - 'parts' entry (see below)
1581 * - 'options' entry (see below)
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001582 * - 'skip' entry, list of units to skip (array of strings or a single string,
1583 * ` it can be the unit name (singular or plural) or its shortcut
1584 * ` (y, m, w, d, h, min, s, ms, µs).
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001585 * - 'aUnit' entry, prefer "an hour" over "1 hour" if true
1586 * - 'join' entry determines how to join multiple parts of the string
1587 * ` - if $join is a string, it's used as a joiner glue
1588 * ` - if $join is a callable/closure, it get the list of string and should return a string
1589 * ` - if $join is an array, the first item will be the default glue, and the second item
1590 * ` will be used instead of the glue for the last item
1591 * ` - if $join is true, it will be guessed from the locale ('list' translation file entry)
1592 * ` - if $join is missing, a space will be used as glue
1593 * - 'minimumUnit' entry determines the smallest unit of time to display can be long or
1594 * ` short form of the units, e.g. 'hour' or 'h' (default value: s)
1595 * if int passed, it add modifiers:
1596 * Possible values:
1597 * - CarbonInterface::DIFF_ABSOLUTE no modifiers
1598 * - CarbonInterface::DIFF_RELATIVE_TO_NOW add ago/from now modifier
1599 * - CarbonInterface::DIFF_RELATIVE_TO_OTHER add before/after modifier
1600 * Default value: CarbonInterface::DIFF_ABSOLUTE
1601 * @param bool $short displays short format of time units
1602 * @param int $parts maximum number of parts to display (default value: -1: no limits)
1603 * @param int $options human diff options
1604 *
1605 * @throws Exception
1606 *
1607 * @return string
1608 */
1609 public function forHumans($syntax = null, $short = false, $parts = -1, $options = null)
1610 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001611 [$syntax, $short, $parts, $options, $join, $aUnit, $altNumbers, $interpolations, $minimumUnit, $skip] = $this
1612 ->getForHumansParameters($syntax, $short, $parts, $options);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001613
1614 $interval = [];
1615
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001616 $syntax = (int) ($syntax ?? CarbonInterface::DIFF_ABSOLUTE);
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001617 $absolute = $syntax === CarbonInterface::DIFF_ABSOLUTE;
1618 $relativeToNow = $syntax === CarbonInterface::DIFF_RELATIVE_TO_NOW;
1619 $count = 1;
1620 $unit = $short ? 's' : 'second';
1621 $isFuture = $this->invert === 1;
1622 $transId = $relativeToNow ? ($isFuture ? 'from_now' : 'ago') : ($isFuture ? 'after' : 'before');
1623
1624 /** @var \Symfony\Component\Translation\Translator $translator */
1625 $translator = $this->getLocalTranslator();
1626
1627 $handleDeclensions = function ($unit, $count) use ($interpolations, $transId, $translator, $altNumbers, $absolute) {
1628 if (!$absolute) {
1629 // Some languages have special pluralization for past and future tense.
1630 $key = $unit.'_'.$transId;
1631 $result = $this->translate($key, $interpolations, $count, $translator, $altNumbers);
1632
1633 if ($result !== $key) {
1634 return $result;
1635 }
1636 }
1637
1638 $result = $this->translate($unit, $interpolations, $count, $translator, $altNumbers);
1639
1640 if ($result !== $unit) {
1641 return $result;
1642 }
1643
1644 return null;
1645 };
1646
1647 $intervalValues = $this;
1648 $method = static::getRoundingMethodFromOptions($options);
1649
1650 if ($method) {
1651 $previousCount = INF;
1652
1653 while (
1654 \count($intervalValues->getNonZeroValues()) > $parts &&
1655 ($count = \count($keys = array_keys($intervalValues->getValuesSequence()))) > 1
1656 ) {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001657 $index = min($count, $previousCount - 1) - 2;
1658
1659 if ($index < 0) {
1660 break;
1661 }
1662
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001663 $intervalValues = $this->copy()->roundUnit(
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001664 $keys[$index],
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001665 1,
1666 $method
1667 );
1668 $previousCount = $count;
1669 }
1670 }
1671
1672 $diffIntervalArray = [
1673 ['value' => $intervalValues->years, 'unit' => 'year', 'unitShort' => 'y'],
1674 ['value' => $intervalValues->months, 'unit' => 'month', 'unitShort' => 'm'],
1675 ['value' => $intervalValues->weeks, 'unit' => 'week', 'unitShort' => 'w'],
1676 ['value' => $intervalValues->daysExcludeWeeks, 'unit' => 'day', 'unitShort' => 'd'],
1677 ['value' => $intervalValues->hours, 'unit' => 'hour', 'unitShort' => 'h'],
1678 ['value' => $intervalValues->minutes, 'unit' => 'minute', 'unitShort' => 'min'],
1679 ['value' => $intervalValues->seconds, 'unit' => 'second', 'unitShort' => 's'],
1680 ['value' => $intervalValues->milliseconds, 'unit' => 'millisecond', 'unitShort' => 'ms'],
1681 ['value' => $intervalValues->microExcludeMilli, 'unit' => 'microsecond', 'unitShort' => 'µs'],
1682 ];
1683
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001684 if (!empty($skip)) {
1685 foreach ($diffIntervalArray as $index => &$unitData) {
1686 $nextIndex = $index + 1;
1687
1688 if ($unitData['value'] &&
1689 isset($diffIntervalArray[$nextIndex]) &&
1690 \count(array_intersect([$unitData['unit'], $unitData['unit'].'s', $unitData['unitShort']], $skip))
1691 ) {
1692 $diffIntervalArray[$nextIndex]['value'] += $unitData['value'] *
1693 self::getFactorWithDefault($diffIntervalArray[$nextIndex]['unit'], $unitData['unit']);
1694 $unitData['value'] = 0;
1695 }
1696 }
1697 }
1698
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001699 $transChoice = function ($short, $unitData) use ($absolute, $handleDeclensions, $translator, $aUnit, $altNumbers, $interpolations) {
1700 $count = $unitData['value'];
1701
1702 if ($short) {
1703 $result = $handleDeclensions($unitData['unitShort'], $count);
1704
1705 if ($result !== null) {
1706 return $result;
1707 }
1708 } elseif ($aUnit) {
1709 $result = $handleDeclensions('a_'.$unitData['unit'], $count);
1710
1711 if ($result !== null) {
1712 return $result;
1713 }
1714 }
1715
1716 if (!$absolute) {
1717 return $handleDeclensions($unitData['unit'], $count);
1718 }
1719
1720 return $this->translate($unitData['unit'], $interpolations, $count, $translator, $altNumbers);
1721 };
1722
1723 $fallbackUnit = ['second', 's'];
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001724
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001725 foreach ($diffIntervalArray as $diffIntervalData) {
1726 if ($diffIntervalData['value'] > 0) {
1727 $unit = $short ? $diffIntervalData['unitShort'] : $diffIntervalData['unit'];
1728 $count = $diffIntervalData['value'];
1729 $interval[] = $transChoice($short, $diffIntervalData);
1730 } elseif ($options & CarbonInterface::SEQUENTIAL_PARTS_ONLY && \count($interval) > 0) {
1731 break;
1732 }
1733
1734 // break the loop after we get the required number of parts in array
1735 if (\count($interval) >= $parts) {
1736 break;
1737 }
1738
1739 // break the loop after we have reached the minimum unit
1740 if (\in_array($minimumUnit, [$diffIntervalData['unit'], $diffIntervalData['unitShort']])) {
1741 $fallbackUnit = [$diffIntervalData['unit'], $diffIntervalData['unitShort']];
1742
1743 break;
1744 }
1745 }
1746
1747 if (\count($interval) === 0) {
1748 if ($relativeToNow && $options & CarbonInterface::JUST_NOW) {
1749 $key = 'diff_now';
1750 $translation = $this->translate($key, $interpolations, null, $translator);
1751
1752 if ($translation !== $key) {
1753 return $translation;
1754 }
1755 }
1756
1757 $count = $options & CarbonInterface::NO_ZERO_DIFF ? 1 : 0;
1758 $unit = $fallbackUnit[$short ? 1 : 0];
1759 $interval[] = $this->translate($unit, $interpolations, $count, $translator, $altNumbers);
1760 }
1761
1762 // join the interval parts by a space
1763 $time = $join($interval);
1764
1765 unset($diffIntervalArray, $interval);
1766
1767 if ($absolute) {
1768 return $time;
1769 }
1770
1771 $isFuture = $this->invert === 1;
1772
1773 $transId = $relativeToNow ? ($isFuture ? 'from_now' : 'ago') : ($isFuture ? 'after' : 'before');
1774
1775 if ($parts === 1) {
1776 if ($relativeToNow && $unit === 'day') {
1777 if ($count === 1 && $options & CarbonInterface::ONE_DAY_WORDS) {
1778 $key = $isFuture ? 'diff_tomorrow' : 'diff_yesterday';
1779 $translation = $this->translate($key, $interpolations, null, $translator);
1780
1781 if ($translation !== $key) {
1782 return $translation;
1783 }
1784 }
1785
1786 if ($count === 2 && $options & CarbonInterface::TWO_DAY_WORDS) {
1787 $key = $isFuture ? 'diff_after_tomorrow' : 'diff_before_yesterday';
1788 $translation = $this->translate($key, $interpolations, null, $translator);
1789
1790 if ($translation !== $key) {
1791 return $translation;
1792 }
1793 }
1794 }
1795
1796 $aTime = $aUnit ? $handleDeclensions('a_'.$unit, $count) : null;
1797
1798 $time = $aTime ?: $handleDeclensions($unit, $count) ?: $time;
1799 }
1800
1801 $time = [':time' => $time];
1802
1803 return $this->translate($transId, array_merge($time, $interpolations, $time), null, $translator);
1804 }
1805
1806 /**
1807 * Format the instance as a string using the forHumans() function.
1808 *
1809 * @throws Exception
1810 *
1811 * @return string
1812 */
1813 public function __toString()
1814 {
1815 $format = $this->localToStringFormat;
1816
1817 if ($format) {
1818 if ($format instanceof Closure) {
1819 return $format($this);
1820 }
1821
1822 return $this->format($format);
1823 }
1824
1825 return $this->forHumans();
1826 }
1827
1828 /**
1829 * Return native DateInterval PHP object matching the current instance.
1830 *
1831 * @example
1832 * ```
1833 * var_dump(CarbonInterval::hours(2)->toDateInterval());
1834 * ```
1835 *
1836 * @return DateInterval
1837 */
1838 public function toDateInterval()
1839 {
1840 return self::castIntervalToClass($this, DateInterval::class);
1841 }
1842
1843 /**
1844 * Convert the interval to a CarbonPeriod.
1845 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001846 * @param DateTimeInterface|string|int ...$params Start date, [end date or recurrences] and optional settings.
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001847 *
1848 * @return CarbonPeriod
1849 */
1850 public function toPeriod(...$params)
1851 {
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001852 if ($this->tzName) {
1853 $tz = \is_string($this->tzName) ? new DateTimeZone($this->tzName) : $this->tzName;
1854
1855 if ($tz instanceof DateTimeZone) {
1856 array_unshift($params, $tz);
1857 }
1858 }
1859
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001860 return CarbonPeriod::create($this, ...$params);
1861 }
1862
1863 /**
1864 * Invert the interval.
1865 *
1866 * @param bool|int $inverted if a parameter is passed, the passed value casted as 1 or 0 is used
1867 * as the new value of the ->invert property.
1868 *
1869 * @return $this
1870 */
1871 public function invert($inverted = null)
1872 {
1873 $this->invert = (\func_num_args() === 0 ? !$this->invert : $inverted) ? 1 : 0;
1874
1875 return $this;
1876 }
1877
1878 protected function solveNegativeInterval()
1879 {
1880 if (!$this->isEmpty() && $this->years <= 0 && $this->months <= 0 && $this->dayz <= 0 && $this->hours <= 0 && $this->minutes <= 0 && $this->seconds <= 0 && $this->microseconds <= 0) {
1881 $this->years *= -1;
1882 $this->months *= -1;
1883 $this->dayz *= -1;
1884 $this->hours *= -1;
1885 $this->minutes *= -1;
1886 $this->seconds *= -1;
1887 $this->microseconds *= -1;
1888 $this->invert();
1889 }
1890
1891 return $this;
1892 }
1893
1894 /**
1895 * Add the passed interval to the current instance.
1896 *
1897 * @param string|DateInterval $unit
1898 * @param int|float $value
1899 *
1900 * @return $this
1901 */
1902 public function add($unit, $value = 1)
1903 {
1904 if (is_numeric($unit)) {
1905 [$value, $unit] = [$unit, $value];
1906 }
1907
1908 if (\is_string($unit) && !preg_match('/^\s*\d/', $unit)) {
1909 $unit = "$value $unit";
1910 $value = 1;
1911 }
1912
1913 $interval = static::make($unit);
1914
1915 if (!$interval) {
1916 throw new InvalidIntervalException('This type of data cannot be added/subtracted.');
1917 }
1918
1919 if ($value !== 1) {
1920 $interval->times($value);
1921 }
1922
1923 $sign = ($this->invert === 1) !== ($interval->invert === 1) ? -1 : 1;
1924 $this->years += $interval->y * $sign;
1925 $this->months += $interval->m * $sign;
1926 $this->dayz += ($interval->days === false ? $interval->d : $interval->days) * $sign;
1927 $this->hours += $interval->h * $sign;
1928 $this->minutes += $interval->i * $sign;
1929 $this->seconds += $interval->s * $sign;
1930 $this->microseconds += $interval->microseconds * $sign;
1931
1932 $this->solveNegativeInterval();
1933
1934 return $this;
1935 }
1936
1937 /**
1938 * Subtract the passed interval to the current instance.
1939 *
1940 * @param string|DateInterval $unit
1941 * @param int|float $value
1942 *
1943 * @return $this
1944 */
1945 public function sub($unit, $value = 1)
1946 {
1947 if (is_numeric($unit)) {
1948 [$value, $unit] = [$unit, $value];
1949 }
1950
1951 return $this->add($unit, -(float) $value);
1952 }
1953
1954 /**
1955 * Subtract the passed interval to the current instance.
1956 *
1957 * @param string|DateInterval $unit
1958 * @param int|float $value
1959 *
1960 * @return $this
1961 */
1962 public function subtract($unit, $value = 1)
1963 {
1964 return $this->sub($unit, $value);
1965 }
1966
1967 /**
1968 * Add given parameters to the current interval.
1969 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01001970 * @param int $years
1971 * @param int $months
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001972 * @param int|float $weeks
1973 * @param int|float $days
1974 * @param int|float $hours
1975 * @param int|float $minutes
1976 * @param int|float $seconds
1977 * @param int|float $microseconds
1978 *
1979 * @return $this
1980 */
1981 public function plus(
1982 $years = 0,
1983 $months = 0,
1984 $weeks = 0,
1985 $days = 0,
1986 $hours = 0,
1987 $minutes = 0,
1988 $seconds = 0,
1989 $microseconds = 0
1990 ): self {
1991 return $this->add("
1992 $years years $months months $weeks weeks $days days
1993 $hours hours $minutes minutes $seconds seconds $microseconds microseconds
1994 ");
1995 }
1996
1997 /**
1998 * Add given parameters to the current interval.
1999 *
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01002000 * @param int $years
2001 * @param int $months
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002002 * @param int|float $weeks
2003 * @param int|float $days
2004 * @param int|float $hours
2005 * @param int|float $minutes
2006 * @param int|float $seconds
2007 * @param int|float $microseconds
2008 *
2009 * @return $this
2010 */
2011 public function minus(
2012 $years = 0,
2013 $months = 0,
2014 $weeks = 0,
2015 $days = 0,
2016 $hours = 0,
2017 $minutes = 0,
2018 $seconds = 0,
2019 $microseconds = 0
2020 ): self {
2021 return $this->sub("
2022 $years years $months months $weeks weeks $days days
2023 $hours hours $minutes minutes $seconds seconds $microseconds microseconds
2024 ");
2025 }
2026
2027 /**
2028 * Multiply current instance given number of times. times() is naive, it multiplies each unit
2029 * (so day can be greater than 31, hour can be greater than 23, etc.) and the result is rounded
2030 * separately for each unit.
2031 *
2032 * Use times() when you want a fast and approximated calculation that does not cascade units.
2033 *
2034 * For a precise and cascaded calculation,
2035 *
2036 * @see multiply()
2037 *
2038 * @param float|int $factor
2039 *
2040 * @return $this
2041 */
2042 public function times($factor)
2043 {
2044 if ($factor < 0) {
2045 $this->invert = $this->invert ? 0 : 1;
2046 $factor = -$factor;
2047 }
2048
2049 $this->years = (int) round($this->years * $factor);
2050 $this->months = (int) round($this->months * $factor);
2051 $this->dayz = (int) round($this->dayz * $factor);
2052 $this->hours = (int) round($this->hours * $factor);
2053 $this->minutes = (int) round($this->minutes * $factor);
2054 $this->seconds = (int) round($this->seconds * $factor);
2055 $this->microseconds = (int) round($this->microseconds * $factor);
2056
2057 return $this;
2058 }
2059
2060 /**
2061 * Divide current instance by a given divider. shares() is naive, it divides each unit separately
2062 * and the result is rounded for each unit. So 5 hours and 20 minutes shared by 3 becomes 2 hours
2063 * and 7 minutes.
2064 *
2065 * Use shares() when you want a fast and approximated calculation that does not cascade units.
2066 *
2067 * For a precise and cascaded calculation,
2068 *
2069 * @see divide()
2070 *
2071 * @param float|int $divider
2072 *
2073 * @return $this
2074 */
2075 public function shares($divider)
2076 {
2077 return $this->times(1 / $divider);
2078 }
2079
2080 protected function copyProperties(self $interval, $ignoreSign = false)
2081 {
2082 $this->years = $interval->years;
2083 $this->months = $interval->months;
2084 $this->dayz = $interval->dayz;
2085 $this->hours = $interval->hours;
2086 $this->minutes = $interval->minutes;
2087 $this->seconds = $interval->seconds;
2088 $this->microseconds = $interval->microseconds;
2089
2090 if (!$ignoreSign) {
2091 $this->invert = $interval->invert;
2092 }
2093
2094 return $this;
2095 }
2096
2097 /**
2098 * Multiply and cascade current instance by a given factor.
2099 *
2100 * @param float|int $factor
2101 *
2102 * @return $this
2103 */
2104 public function multiply($factor)
2105 {
2106 if ($factor < 0) {
2107 $this->invert = $this->invert ? 0 : 1;
2108 $factor = -$factor;
2109 }
2110
2111 $yearPart = (int) floor($this->years * $factor); // Split calculation to prevent imprecision
2112
2113 if ($yearPart) {
2114 $this->years -= $yearPart / $factor;
2115 }
2116
2117 return $this->copyProperties(
2118 static::create($yearPart)
2119 ->microseconds(abs($this->totalMicroseconds) * $factor)
2120 ->cascade(),
2121 true
2122 );
2123 }
2124
2125 /**
2126 * Divide and cascade current instance by a given divider.
2127 *
2128 * @param float|int $divider
2129 *
2130 * @return $this
2131 */
2132 public function divide($divider)
2133 {
2134 return $this->multiply(1 / $divider);
2135 }
2136
2137 /**
2138 * Get the interval_spec string of a date interval.
2139 *
2140 * @param DateInterval $interval
2141 *
2142 * @return string
2143 */
2144 public static function getDateIntervalSpec(DateInterval $interval)
2145 {
2146 $date = array_filter([
2147 static::PERIOD_YEARS => abs($interval->y),
2148 static::PERIOD_MONTHS => abs($interval->m),
2149 static::PERIOD_DAYS => abs($interval->d),
2150 ]);
2151
2152 $time = array_filter([
2153 static::PERIOD_HOURS => abs($interval->h),
2154 static::PERIOD_MINUTES => abs($interval->i),
2155 static::PERIOD_SECONDS => abs($interval->s),
2156 ]);
2157
2158 $specString = static::PERIOD_PREFIX;
2159
2160 foreach ($date as $key => $value) {
2161 $specString .= $value.$key;
2162 }
2163
2164 if (\count($time) > 0) {
2165 $specString .= static::PERIOD_TIME_PREFIX;
2166 foreach ($time as $key => $value) {
2167 $specString .= $value.$key;
2168 }
2169 }
2170
2171 return $specString === static::PERIOD_PREFIX ? 'PT0S' : $specString;
2172 }
2173
2174 /**
2175 * Get the interval_spec string.
2176 *
2177 * @return string
2178 */
2179 public function spec()
2180 {
2181 return static::getDateIntervalSpec($this);
2182 }
2183
2184 /**
2185 * Comparing 2 date intervals.
2186 *
2187 * @param DateInterval $first
2188 * @param DateInterval $second
2189 *
2190 * @return int
2191 */
2192 public static function compareDateIntervals(DateInterval $first, DateInterval $second)
2193 {
2194 $current = Carbon::now();
2195 $passed = $current->avoidMutation()->add($second);
2196 $current->add($first);
2197
2198 if ($current < $passed) {
2199 return -1;
2200 }
2201 if ($current > $passed) {
2202 return 1;
2203 }
2204
2205 return 0;
2206 }
2207
2208 /**
2209 * Comparing with passed interval.
2210 *
2211 * @param DateInterval $interval
2212 *
2213 * @return int
2214 */
2215 public function compare(DateInterval $interval)
2216 {
2217 return static::compareDateIntervals($this, $interval);
2218 }
2219
2220 private function invertCascade(array $values)
2221 {
2222 return $this->set(array_map(function ($value) {
2223 return -$value;
2224 }, $values))->doCascade(true)->invert();
2225 }
2226
2227 private function doCascade(bool $deep)
2228 {
2229 $originalData = $this->toArray();
2230 $originalData['milliseconds'] = (int) ($originalData['microseconds'] / static::getMicrosecondsPerMillisecond());
2231 $originalData['microseconds'] = $originalData['microseconds'] % static::getMicrosecondsPerMillisecond();
2232 $originalData['daysExcludeWeeks'] = $originalData['days'];
2233 unset($originalData['days']);
2234 $newData = $originalData;
2235
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01002236 foreach (self::getFlipCascadeFactors() as $source => [$target, $factor]) {
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002237 foreach (['source', 'target'] as $key) {
2238 if ($$key === 'dayz') {
2239 $$key = 'daysExcludeWeeks';
2240 }
2241 }
2242
2243 $value = $newData[$source];
2244 $modulo = ($factor + ($value % $factor)) % $factor;
2245 $newData[$source] = $modulo;
2246 $newData[$target] += ($value - $modulo) / $factor;
2247 }
2248
2249 $positive = null;
2250
2251 if (!$deep) {
2252 foreach ($newData as $value) {
2253 if ($value) {
2254 if ($positive === null) {
2255 $positive = ($value > 0);
2256
2257 continue;
2258 }
2259
2260 if (($value > 0) !== $positive) {
2261 return $this->invertCascade($originalData)
2262 ->solveNegativeInterval();
2263 }
2264 }
2265 }
2266 }
2267
2268 return $this->set($newData)
2269 ->solveNegativeInterval();
2270 }
2271
2272 /**
2273 * Convert overflowed values into bigger units.
2274 *
2275 * @return $this
2276 */
2277 public function cascade()
2278 {
2279 return $this->doCascade(false);
2280 }
2281
2282 public function hasNegativeValues(): bool
2283 {
2284 foreach ($this->toArray() as $value) {
2285 if ($value < 0) {
2286 return true;
2287 }
2288 }
2289
2290 return false;
2291 }
2292
2293 public function hasPositiveValues(): bool
2294 {
2295 foreach ($this->toArray() as $value) {
2296 if ($value > 0) {
2297 return true;
2298 }
2299 }
2300
2301 return false;
2302 }
2303
2304 /**
2305 * Get amount of given unit equivalent to the interval.
2306 *
2307 * @param string $unit
2308 *
2309 * @throws UnknownUnitException|UnitNotConfiguredException
2310 *
2311 * @return float
2312 */
2313 public function total($unit)
2314 {
2315 $realUnit = $unit = strtolower($unit);
2316
2317 if (\in_array($unit, ['days', 'weeks'])) {
2318 $realUnit = 'dayz';
2319 } elseif (!\in_array($unit, ['microseconds', 'milliseconds', 'seconds', 'minutes', 'hours', 'dayz', 'months', 'years'])) {
2320 throw new UnknownUnitException($unit);
2321 }
2322
2323 $result = 0;
2324 $cumulativeFactor = 0;
2325 $unitFound = false;
Matthias Andreas Benkard1ba53812022-12-27 17:32:58 +01002326 $factors = self::getFlipCascadeFactors();
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02002327 $daysPerWeek = static::getDaysPerWeek();
2328
2329 $values = [
2330 'years' => $this->years,
2331 'months' => $this->months,
2332 'weeks' => (int) ($this->d / $daysPerWeek),
2333 'dayz' => $this->d % $daysPerWeek,
2334 'hours' => $this->hours,
2335 'minutes' => $this->minutes,
2336 'seconds' => $this->seconds,
2337 'milliseconds' => (int) ($this->microseconds / Carbon::MICROSECONDS_PER_MILLISECOND),
2338 'microseconds' => $this->microseconds % Carbon::MICROSECONDS_PER_MILLISECOND,
2339 ];
2340
2341 if (isset($factors['dayz']) && $factors['dayz'][0] !== 'weeks') {
2342 $values['dayz'] += $values['weeks'] * $daysPerWeek;
2343 $values['weeks'] = 0;
2344 }
2345
2346 foreach ($factors as $source => [$target, $factor]) {
2347 if ($source === $realUnit) {
2348 $unitFound = true;
2349 $value = $values[$source];
2350 $result += $value;
2351 $cumulativeFactor = 1;
2352 }
2353
2354 if ($factor === false) {
2355 if ($unitFound) {
2356 break;
2357 }
2358
2359 $result = 0;
2360 $cumulativeFactor = 0;
2361
2362 continue;
2363 }
2364
2365 if ($target === $realUnit) {
2366 $unitFound = true;
2367 }
2368
2369 if ($cumulativeFactor) {
2370 $cumulativeFactor *= $factor;
2371 $result += $values[$target] * $cumulativeFactor;
2372
2373 continue;
2374 }
2375
2376 $value = $values[$source];
2377
2378 $result = ($result + $value) / $factor;
2379 }
2380
2381 if (isset($target) && !$cumulativeFactor) {
2382 $result += $values[$target];
2383 }
2384
2385 if (!$unitFound) {
2386 throw new UnitNotConfiguredException($unit);
2387 }
2388
2389 if ($this->invert) {
2390 $result *= -1;
2391 }
2392
2393 if ($unit === 'weeks') {
2394 return $result / $daysPerWeek;
2395 }
2396
2397 return $result;
2398 }
2399
2400 /**
2401 * Determines if the instance is equal to another
2402 *
2403 * @param CarbonInterval|DateInterval|mixed $interval
2404 *
2405 * @see equalTo()
2406 *
2407 * @return bool
2408 */
2409 public function eq($interval): bool
2410 {
2411 return $this->equalTo($interval);
2412 }
2413
2414 /**
2415 * Determines if the instance is equal to another
2416 *
2417 * @param CarbonInterval|DateInterval|mixed $interval
2418 *
2419 * @return bool
2420 */
2421 public function equalTo($interval): bool
2422 {
2423 $interval = $this->resolveInterval($interval);
2424
2425 return $interval !== null && $this->totalMicroseconds === $interval->totalMicroseconds;
2426 }
2427
2428 /**
2429 * Determines if the instance is not equal to another
2430 *
2431 * @param CarbonInterval|DateInterval|mixed $interval
2432 *
2433 * @see notEqualTo()
2434 *
2435 * @return bool
2436 */
2437 public function ne($interval): bool
2438 {
2439 return $this->notEqualTo($interval);
2440 }
2441
2442 /**
2443 * Determines if the instance is not equal to another
2444 *
2445 * @param CarbonInterval|DateInterval|mixed $interval
2446 *
2447 * @return bool
2448 */
2449 public function notEqualTo($interval): bool
2450 {
2451 return !$this->eq($interval);
2452 }
2453
2454 /**
2455 * Determines if the instance is greater (longer) than another
2456 *
2457 * @param CarbonInterval|DateInterval|mixed $interval
2458 *
2459 * @see greaterThan()
2460 *
2461 * @return bool
2462 */
2463 public function gt($interval): bool
2464 {
2465 return $this->greaterThan($interval);
2466 }
2467
2468 /**
2469 * Determines if the instance is greater (longer) than another
2470 *
2471 * @param CarbonInterval|DateInterval|mixed $interval
2472 *
2473 * @return bool
2474 */
2475 public function greaterThan($interval): bool
2476 {
2477 $interval = $this->resolveInterval($interval);
2478
2479 return $interval === null || $this->totalMicroseconds > $interval->totalMicroseconds;
2480 }
2481
2482 /**
2483 * Determines if the instance is greater (longer) than or equal to another
2484 *
2485 * @param CarbonInterval|DateInterval|mixed $interval
2486 *
2487 * @see greaterThanOrEqualTo()
2488 *
2489 * @return bool
2490 */
2491 public function gte($interval): bool
2492 {
2493 return $this->greaterThanOrEqualTo($interval);
2494 }
2495
2496 /**
2497 * Determines if the instance is greater (longer) than or equal to another
2498 *
2499 * @param CarbonInterval|DateInterval|mixed $interval
2500 *
2501 * @return bool
2502 */
2503 public function greaterThanOrEqualTo($interval): bool
2504 {
2505 return $this->greaterThan($interval) || $this->equalTo($interval);
2506 }
2507
2508 /**
2509 * Determines if the instance is less (shorter) than another
2510 *
2511 * @param CarbonInterval|DateInterval|mixed $interval
2512 *
2513 * @see lessThan()
2514 *
2515 * @return bool
2516 */
2517 public function lt($interval): bool
2518 {
2519 return $this->lessThan($interval);
2520 }
2521
2522 /**
2523 * Determines if the instance is less (shorter) than another
2524 *
2525 * @param CarbonInterval|DateInterval|mixed $interval
2526 *
2527 * @return bool
2528 */
2529 public function lessThan($interval): bool
2530 {
2531 $interval = $this->resolveInterval($interval);
2532
2533 return $interval !== null && $this->totalMicroseconds < $interval->totalMicroseconds;
2534 }
2535
2536 /**
2537 * Determines if the instance is less (shorter) than or equal to another
2538 *
2539 * @param CarbonInterval|DateInterval|mixed $interval
2540 *
2541 * @see lessThanOrEqualTo()
2542 *
2543 * @return bool
2544 */
2545 public function lte($interval): bool
2546 {
2547 return $this->lessThanOrEqualTo($interval);
2548 }
2549
2550 /**
2551 * Determines if the instance is less (shorter) than or equal to another
2552 *
2553 * @param CarbonInterval|DateInterval|mixed $interval
2554 *
2555 * @return bool
2556 */
2557 public function lessThanOrEqualTo($interval): bool
2558 {
2559 return $this->lessThan($interval) || $this->equalTo($interval);
2560 }
2561
2562 /**
2563 * Determines if the instance is between two others.
2564 *
2565 * The third argument allow you to specify if bounds are included or not (true by default)
2566 * but for when you including/excluding bounds may produce different results in your application,
2567 * we recommend to use the explicit methods ->betweenIncluded() or ->betweenExcluded() instead.
2568 *
2569 * @example
2570 * ```
2571 * CarbonInterval::hours(48)->between(CarbonInterval::day(), CarbonInterval::days(3)); // true
2572 * CarbonInterval::hours(48)->between(CarbonInterval::day(), CarbonInterval::hours(36)); // false
2573 * CarbonInterval::hours(48)->between(CarbonInterval::day(), CarbonInterval::days(2)); // true
2574 * CarbonInterval::hours(48)->between(CarbonInterval::day(), CarbonInterval::days(2), false); // false
2575 * ```
2576 *
2577 * @param CarbonInterval|DateInterval|mixed $interval1
2578 * @param CarbonInterval|DateInterval|mixed $interval2
2579 * @param bool $equal Indicates if an equal to comparison should be done
2580 *
2581 * @return bool
2582 */
2583 public function between($interval1, $interval2, $equal = true): bool
2584 {
2585 return $equal
2586 ? $this->greaterThanOrEqualTo($interval1) && $this->lessThanOrEqualTo($interval2)
2587 : $this->greaterThan($interval1) && $this->lessThan($interval2);
2588 }
2589
2590 /**
2591 * Determines if the instance is between two others, bounds excluded.
2592 *
2593 * @example
2594 * ```
2595 * CarbonInterval::hours(48)->betweenExcluded(CarbonInterval::day(), CarbonInterval::days(3)); // true
2596 * CarbonInterval::hours(48)->betweenExcluded(CarbonInterval::day(), CarbonInterval::hours(36)); // false
2597 * CarbonInterval::hours(48)->betweenExcluded(CarbonInterval::day(), CarbonInterval::days(2)); // true
2598 * ```
2599 *
2600 * @param CarbonInterval|DateInterval|mixed $interval1
2601 * @param CarbonInterval|DateInterval|mixed $interval2
2602 *
2603 * @return bool
2604 */
2605 public function betweenIncluded($interval1, $interval2): bool
2606 {
2607 return $this->between($interval1, $interval2, true);
2608 }
2609
2610 /**
2611 * Determines if the instance is between two others, bounds excluded.
2612 *
2613 * @example
2614 * ```
2615 * CarbonInterval::hours(48)->betweenExcluded(CarbonInterval::day(), CarbonInterval::days(3)); // true
2616 * CarbonInterval::hours(48)->betweenExcluded(CarbonInterval::day(), CarbonInterval::hours(36)); // false
2617 * CarbonInterval::hours(48)->betweenExcluded(CarbonInterval::day(), CarbonInterval::days(2)); // false
2618 * ```
2619 *
2620 * @param CarbonInterval|DateInterval|mixed $interval1
2621 * @param CarbonInterval|DateInterval|mixed $interval2
2622 *
2623 * @return bool
2624 */
2625 public function betweenExcluded($interval1, $interval2): bool
2626 {
2627 return $this->between($interval1, $interval2, false);
2628 }
2629
2630 /**
2631 * Determines if the instance is between two others
2632 *
2633 * @example
2634 * ```
2635 * CarbonInterval::hours(48)->isBetween(CarbonInterval::day(), CarbonInterval::days(3)); // true
2636 * CarbonInterval::hours(48)->isBetween(CarbonInterval::day(), CarbonInterval::hours(36)); // false
2637 * CarbonInterval::hours(48)->isBetween(CarbonInterval::day(), CarbonInterval::days(2)); // true
2638 * CarbonInterval::hours(48)->isBetween(CarbonInterval::day(), CarbonInterval::days(2), false); // false
2639 * ```
2640 *
2641 * @param CarbonInterval|DateInterval|mixed $interval1
2642 * @param CarbonInterval|DateInterval|mixed $interval2
2643 * @param bool $equal Indicates if an equal to comparison should be done
2644 *
2645 * @return bool
2646 */
2647 public function isBetween($interval1, $interval2, $equal = true): bool
2648 {
2649 return $this->between($interval1, $interval2, $equal);
2650 }
2651
2652 /**
2653 * Round the current instance at the given unit with given precision if specified and the given function.
2654 *
2655 * @param string $unit
2656 * @param float|int|string|DateInterval|null $precision
2657 * @param string $function
2658 *
2659 * @throws Exception
2660 *
2661 * @return $this
2662 */
2663 public function roundUnit($unit, $precision = 1, $function = 'round')
2664 {
2665 $base = CarbonImmutable::parse('2000-01-01 00:00:00', 'UTC')
2666 ->roundUnit($unit, $precision, $function);
2667 $next = $base->add($this);
2668 $inverted = $next < $base;
2669
2670 if ($inverted) {
2671 $next = $base->sub($this);
2672 }
2673
2674 $this->copyProperties(
2675 $next
2676 ->roundUnit($unit, $precision, $function)
2677 ->diffAsCarbonInterval($base)
2678 );
2679
2680 return $this->invert($inverted);
2681 }
2682
2683 /**
2684 * Truncate the current instance at the given unit with given precision if specified.
2685 *
2686 * @param string $unit
2687 * @param float|int|string|DateInterval|null $precision
2688 *
2689 * @throws Exception
2690 *
2691 * @return $this
2692 */
2693 public function floorUnit($unit, $precision = 1)
2694 {
2695 return $this->roundUnit($unit, $precision, 'floor');
2696 }
2697
2698 /**
2699 * Ceil the current instance at the given unit with given precision if specified.
2700 *
2701 * @param string $unit
2702 * @param float|int|string|DateInterval|null $precision
2703 *
2704 * @throws Exception
2705 *
2706 * @return $this
2707 */
2708 public function ceilUnit($unit, $precision = 1)
2709 {
2710 return $this->roundUnit($unit, $precision, 'ceil');
2711 }
2712
2713 /**
2714 * Round the current instance second with given precision if specified.
2715 *
2716 * @param float|int|string|DateInterval|null $precision
2717 * @param string $function
2718 *
2719 * @throws Exception
2720 *
2721 * @return $this
2722 */
2723 public function round($precision = 1, $function = 'round')
2724 {
2725 return $this->roundWith($precision, $function);
2726 }
2727
2728 /**
2729 * Round the current instance second with given precision if specified.
2730 *
2731 * @param float|int|string|DateInterval|null $precision
2732 *
2733 * @throws Exception
2734 *
2735 * @return $this
2736 */
2737 public function floor($precision = 1)
2738 {
2739 return $this->round($precision, 'floor');
2740 }
2741
2742 /**
2743 * Ceil the current instance second with given precision if specified.
2744 *
2745 * @param float|int|string|DateInterval|null $precision
2746 *
2747 * @throws Exception
2748 *
2749 * @return $this
2750 */
2751 public function ceil($precision = 1)
2752 {
2753 return $this->round($precision, 'ceil');
2754 }
2755}