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