blob: 03d3fe697ae4e85bfd0fe897be9d8e7842a4ad8f [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\CarbonInterface;
14use Carbon\Exceptions\UnknownUnitException;
15
16/**
17 * Trait Rounding.
18 *
19 * Round, ceil, floor units.
20 *
21 * Depends on the following methods:
22 *
23 * @method static copy()
24 * @method static startOfWeek(int $weekStartsAt = null)
25 */
26trait Rounding
27{
28 use IntervalRounding;
29
30 /**
31 * Round the current instance at the given unit with given precision if specified and the given function.
32 *
33 * @param string $unit
34 * @param float|int $precision
35 * @param string $function
36 *
37 * @return CarbonInterface
38 */
39 public function roundUnit($unit, $precision = 1, $function = 'round')
40 {
41 $metaUnits = [
42 // @call roundUnit
43 'millennium' => [static::YEARS_PER_MILLENNIUM, 'year'],
44 // @call roundUnit
45 'century' => [static::YEARS_PER_CENTURY, 'year'],
46 // @call roundUnit
47 'decade' => [static::YEARS_PER_DECADE, 'year'],
48 // @call roundUnit
49 'quarter' => [static::MONTHS_PER_QUARTER, 'month'],
50 // @call roundUnit
51 'millisecond' => [1000, 'microsecond'],
52 ];
53 $normalizedUnit = static::singularUnit($unit);
54 $ranges = array_merge(static::getRangesByUnit(), [
55 // @call roundUnit
56 'microsecond' => [0, 999999],
57 ]);
58 $factor = 1;
59 $initialMonth = $this->month;
60
61 if ($normalizedUnit === 'week') {
62 $normalizedUnit = 'day';
63 $precision *= static::DAYS_PER_WEEK;
64 }
65
66 if (isset($metaUnits[$normalizedUnit])) {
67 [$factor, $normalizedUnit] = $metaUnits[$normalizedUnit];
68 }
69
70 $precision *= $factor;
71
72 if (!isset($ranges[$normalizedUnit])) {
73 throw new UnknownUnitException($unit);
74 }
75
76 $found = false;
77 $fraction = 0;
78 $arguments = null;
79 $factor = $this->year < 0 ? -1 : 1;
80 $changes = [];
81
82 foreach ($ranges as $unit => [$minimum, $maximum]) {
83 if ($normalizedUnit === $unit) {
84 $arguments = [$this->$unit, $minimum];
85 $fraction = $precision - floor($precision);
86 $found = true;
87
88 continue;
89 }
90
91 if ($found) {
92 $delta = $maximum + 1 - $minimum;
93 $factor /= $delta;
94 $fraction *= $delta;
95 $arguments[0] += $this->$unit * $factor;
96 $changes[$unit] = round(
97 $minimum + ($fraction ? $fraction * $function(($this->$unit - $minimum) / $fraction) : 0)
98 );
99
100 // Cannot use modulo as it lose double precision
101 while ($changes[$unit] >= $delta) {
102 $changes[$unit] -= $delta;
103 }
104
105 $fraction -= floor($fraction);
106 }
107 }
108
109 [$value, $minimum] = $arguments;
110 $normalizedValue = floor($function(($value - $minimum) / $precision) * $precision + $minimum);
111
112 /** @var CarbonInterface $result */
113 $result = $this->$normalizedUnit($normalizedValue);
114
115 foreach ($changes as $unit => $value) {
116 $result = $result->$unit($value);
117 }
118
119 return $normalizedUnit === 'month' && $precision <= 1 && abs($result->month - $initialMonth) === 2
120 // Re-run the change in case an overflow occurred
121 ? $result->$normalizedUnit($normalizedValue)
122 : $result;
123 }
124
125 /**
126 * Truncate the current instance at the given unit with given precision if specified.
127 *
128 * @param string $unit
129 * @param float|int $precision
130 *
131 * @return CarbonInterface
132 */
133 public function floorUnit($unit, $precision = 1)
134 {
135 return $this->roundUnit($unit, $precision, 'floor');
136 }
137
138 /**
139 * Ceil the current instance at the given unit with given precision if specified.
140 *
141 * @param string $unit
142 * @param float|int $precision
143 *
144 * @return CarbonInterface
145 */
146 public function ceilUnit($unit, $precision = 1)
147 {
148 return $this->roundUnit($unit, $precision, 'ceil');
149 }
150
151 /**
152 * Round the current instance second with given precision if specified.
153 *
154 * @param float|int|string|\DateInterval|null $precision
155 * @param string $function
156 *
157 * @return CarbonInterface
158 */
159 public function round($precision = 1, $function = 'round')
160 {
161 return $this->roundWith($precision, $function);
162 }
163
164 /**
165 * Round the current instance second with given precision if specified.
166 *
167 * @param float|int|string|\DateInterval|null $precision
168 *
169 * @return CarbonInterface
170 */
171 public function floor($precision = 1)
172 {
173 return $this->round($precision, 'floor');
174 }
175
176 /**
177 * Ceil the current instance second with given precision if specified.
178 *
179 * @param float|int|string|\DateInterval|null $precision
180 *
181 * @return CarbonInterface
182 */
183 public function ceil($precision = 1)
184 {
185 return $this->round($precision, 'ceil');
186 }
187
188 /**
189 * Round the current instance week.
190 *
191 * @param int $weekStartsAt optional start allow you to specify the day of week to use to start the week
192 *
193 * @return CarbonInterface
194 */
195 public function roundWeek($weekStartsAt = null)
196 {
197 return $this->closest(
198 $this->avoidMutation()->floorWeek($weekStartsAt),
199 $this->avoidMutation()->ceilWeek($weekStartsAt)
200 );
201 }
202
203 /**
204 * Truncate the current instance week.
205 *
206 * @param int $weekStartsAt optional start allow you to specify the day of week to use to start the week
207 *
208 * @return CarbonInterface
209 */
210 public function floorWeek($weekStartsAt = null)
211 {
212 return $this->startOfWeek($weekStartsAt);
213 }
214
215 /**
216 * Ceil the current instance week.
217 *
218 * @param int $weekStartsAt optional start allow you to specify the day of week to use to start the week
219 *
220 * @return CarbonInterface
221 */
222 public function ceilWeek($weekStartsAt = null)
223 {
224 if ($this->isMutable()) {
225 $startOfWeek = $this->avoidMutation()->startOfWeek($weekStartsAt);
226
227 return $startOfWeek != $this ?
228 $this->startOfWeek($weekStartsAt)->addWeek() :
229 $this;
230 }
231
232 $startOfWeek = $this->startOfWeek($weekStartsAt);
233
234 return $startOfWeek != $this ?
235 $startOfWeek->addWeek() :
236 $this->avoidMutation();
237 }
238}