Total Complexity | 71 |
Total Lines | 442 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 1 |
Complex classes like TimeInterval often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use TimeInterval, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
27 | class TimeInterval implements IComparable |
||
28 | { |
||
29 | use ComparableWithPhpOperators; |
||
30 | |||
31 | const MILLENNIUM = 'millennium'; |
||
32 | const CENTURY = 'century'; |
||
33 | const DECADE = 'decade'; |
||
34 | const YEAR = 'year'; |
||
35 | const MONTH = 'month'; |
||
36 | const WEEK = 'week'; |
||
37 | const DAY = 'day'; |
||
38 | const HOUR = 'hour'; |
||
39 | const MINUTE = 'minute'; |
||
40 | const SECOND = 'second'; |
||
41 | const MILLISECOND = 'millisecond'; |
||
42 | const MICROSECOND = 'microsecond'; |
||
43 | |||
44 | private const PRECISION = 7; |
||
45 | private const UNIT_ISO_ABBR = [ |
||
46 | self::YEAR => 'Y', |
||
47 | self::MONTH => 'M', |
||
48 | self::WEEK => 'W', |
||
49 | self::DAY => 'D', |
||
50 | self::HOUR => 'H', |
||
51 | self::MINUTE => 'M', |
||
52 | self::SECOND => 'S', |
||
53 | ]; |
||
54 | private const TIME_UNIT_HASH = [ |
||
55 | self::HOUR => true, |
||
56 | self::MINUTE => true, |
||
57 | self::SECOND => true, |
||
58 | self::MILLISECOND => true, |
||
59 | self::MICROSECOND => true, |
||
60 | ]; |
||
61 | |||
62 | // NOTE: the order of fields is important so that comparison with operators <, ==, and > works |
||
63 | /** @var int */ |
||
64 | private $mon; |
||
65 | /** @var int */ |
||
66 | private $day; |
||
67 | /** @var int|float */ |
||
68 | private $sec; |
||
69 | |||
70 | |||
71 | /** |
||
72 | * @param number[] $parts map of units (any of {@link TimeInterval} constants) to the corresponding quantity |
||
73 | * @return TimeInterval |
||
74 | */ |
||
75 | public static function fromParts(array $parts): TimeInterval |
||
76 | { |
||
77 | $mon = 0; |
||
78 | $day = 0; |
||
79 | $sec = 0; |
||
80 | |||
81 | // fractional quantities might lead to some units to be added repetitively |
||
82 | $queue = []; |
||
83 | foreach ($parts as $unit => $quantity) { |
||
84 | $queue[] = [$unit, $quantity]; |
||
85 | } |
||
86 | |||
87 | /** @noinspection PhpAssignmentInConditionInspection it's a pity explicit parentheses do not suffice */ |
||
88 | for (; ($pair = current($queue)) !== false; next($queue)) { |
||
89 | [$unit, $quantity] = $pair; |
||
90 | $intQuantity = (int)round($quantity, self::PRECISION); |
||
91 | $fracQuantity = $quantity - $intQuantity; |
||
92 | |||
93 | switch ($unit) { |
||
94 | case self::MILLENNIUM: |
||
95 | $mon += $intQuantity * 12 * 1000; |
||
96 | if ($fracQuantity) { |
||
97 | $queue[] = [self::YEAR, $fracQuantity * 1000]; |
||
98 | } |
||
99 | break; |
||
100 | case self::CENTURY: |
||
101 | $mon += $intQuantity * 12 * 100; |
||
102 | if ($fracQuantity) { |
||
103 | $queue[] = [self::YEAR, $fracQuantity * 100]; |
||
104 | } |
||
105 | break; |
||
106 | case self::DECADE: |
||
107 | $mon += $intQuantity * 12 * 10; |
||
108 | if ($fracQuantity) { |
||
109 | $queue[] = [self::YEAR, $fracQuantity * 10]; |
||
110 | } |
||
111 | break; |
||
112 | case self::YEAR: |
||
113 | $mon += $intQuantity * 12; |
||
114 | if ($fracQuantity) { |
||
115 | $queue[] = [self::MONTH, $fracQuantity * 12]; |
||
116 | } |
||
117 | break; |
||
118 | case self::MONTH: |
||
119 | $mon += $intQuantity; |
||
120 | if ($fracQuantity) { |
||
121 | $queue[] = [self::DAY, $fracQuantity * 30]; |
||
122 | } |
||
123 | break; |
||
124 | case self::WEEK: |
||
125 | $day += $intQuantity * 7; |
||
126 | if ($fracQuantity) { |
||
127 | $queue[] = [self::DAY, $fracQuantity * 7]; |
||
128 | } |
||
129 | break; |
||
130 | case self::DAY: |
||
131 | $day += $intQuantity; |
||
132 | if ($fracQuantity) { |
||
133 | $queue[] = [self::SECOND, $fracQuantity * 24 * 60 * 60]; |
||
134 | } |
||
135 | break; |
||
136 | case self::HOUR: |
||
137 | $sec += $intQuantity * 60 * 60; |
||
138 | if ($fracQuantity) { |
||
139 | $queue[] = [self::SECOND, $fracQuantity * 60 * 60]; |
||
140 | } |
||
141 | break; |
||
142 | case self::MINUTE: |
||
143 | $sec += $intQuantity * 60; |
||
144 | if ($fracQuantity) { |
||
145 | $queue[] = [self::SECOND, $fracQuantity * 60]; |
||
146 | } |
||
147 | break; |
||
148 | case self::SECOND: |
||
149 | $sec += round($quantity, self::PRECISION); |
||
150 | break; |
||
151 | case self::MILLISECOND: |
||
152 | $sec += round($quantity / 1000, self::PRECISION); |
||
153 | break; |
||
154 | case self::MICROSECOND: |
||
155 | $sec += round($quantity / 100000, self::PRECISION); |
||
156 | break; |
||
157 | default: |
||
158 | throw new \InvalidArgumentException("Undefined unit: '$unit'"); |
||
159 | } |
||
160 | } |
||
161 | return new TimeInterval($mon, $day, $sec); |
||
162 | } |
||
163 | |||
164 | /** |
||
165 | * Creates a time interval from the PHP's standard {@link \DateInterval}. |
||
166 | * |
||
167 | * @param \DateInterval $dateInterval |
||
168 | * @return TimeInterval |
||
169 | */ |
||
170 | public static function fromDateInterval(\DateInterval $dateInterval): TimeInterval |
||
171 | { |
||
172 | $sgn = ($dateInterval->invert ? -1 : 1); |
||
173 | return self::fromParts([ |
||
174 | self::YEAR => $sgn * $dateInterval->y, |
||
175 | self::MONTH => $sgn * $dateInterval->m, |
||
176 | self::DAY => $sgn * $dateInterval->d, |
||
177 | self::HOUR => $sgn * $dateInterval->h, |
||
178 | self::MINUTE => $sgn * $dateInterval->i, |
||
179 | self::SECOND => $sgn * $dateInterval->s, |
||
180 | ]); |
||
181 | } |
||
182 | |||
183 | /** |
||
184 | * Creates a time interval from a string specification. |
||
185 | * |
||
186 | * Several formats are supported: |
||
187 | * - ISO 8601 (e.g., `'P4DT5H'`, `'P1.5Y'`, `'P1,5Y'`, `'P0001-02-03'`, or `'P0001-02-03T04:05:06.7'`); |
||
188 | * - SQL (e.g., `'200-10'` for 200 years and 10 months, or `'1 12:59:10'`); |
||
189 | * - PostgreSQL (e.g., `'1 year 4.5 months 8 sec'`, `'@ 3 days 04:05:06'`, or just `'2'` for 2 seconds). |
||
190 | * |
||
191 | * @see https://www.postgresql.org/docs/11/datatype-datetime.html#DATATYPE-INTERVAL-INPUT |
||
192 | * |
||
193 | * @param string $str interval specification in the ISO 8601, SQL, or PostgreSQL format |
||
194 | * @return TimeInterval |
||
195 | */ |
||
196 | public static function fromString(string $str): TimeInterval |
||
197 | { |
||
198 | if ($str[0] == 'P') { |
||
199 | // ISO format |
||
200 | $timeDelimPos = strpos($str, 'T'); |
||
201 | if (!$timeDelimPos) { |
||
202 | $parts = self::parseIsoDateStr(substr($str, 1)); |
||
203 | } elseif ($timeDelimPos == 1) { |
||
204 | $parts = self::parseTimeStr($str, 2); |
||
205 | } else { |
||
206 | $parts = self::parseIsoDateStr(substr($str, 1, $timeDelimPos - 1)) + |
||
207 | self::parseTimeStr($str, $timeDelimPos + 1); |
||
208 | } |
||
209 | } elseif ($str[0] == '@') { |
||
210 | // verbose PostgreSQL format |
||
211 | $parts = self::parsePostgresqlStr($str, 1); |
||
212 | } elseif (preg_match('~^(?:(-)?(\d+)-(\d+))?\s*(-?\d+)??\s*(-?\d+(?::\d+(?::\d+(?:\.\d+)?)?)?)?$~', $str, $m)) { |
||
213 | // sql format |
||
214 | $parts = (isset($m[5]) ? self::parseTimeStr($m[5], 0, false) : []); |
||
215 | if (!empty($m[2]) || !empty($m[3])) { |
||
216 | $sgn = $m[1] . '1'; |
||
217 | $parts[self::YEAR] = $sgn * $m[2]; |
||
218 | $parts[self::MONTH] = $sgn * $m[3]; |
||
219 | } |
||
220 | if (!empty($m[4])) { |
||
221 | $parts[self::DAY] = (int)$m[4]; |
||
222 | } |
||
223 | } else { |
||
224 | // PostgreSQL format |
||
225 | $parts = self::parsePostgresqlStr($str); |
||
226 | } |
||
227 | |||
228 | return self::fromParts($parts); |
||
229 | } |
||
230 | |||
231 | private static function parseIsoDateStr(string $str): array |
||
232 | { |
||
233 | if (preg_match('~^(-?\d+)-(-?\d+)-(-?\d+)$~', $str, $m)) { |
||
234 | return [ |
||
235 | self::YEAR => (int)$m[1], |
||
236 | self::MONTH => (int)$m[2], |
||
237 | self::DAY => (int)$m[3], |
||
238 | ]; |
||
239 | } else { |
||
240 | $parts = []; |
||
241 | preg_match_all('~(-?\d+(?:\.\d+)?)([YMDW])~', $str, $matches, PREG_SET_ORDER); |
||
242 | static $units = ['Y' => self::YEAR, 'M' => self::MONTH, 'D' => self::DAY, 'W' => self::WEEK]; |
||
243 | foreach ($matches as $m) { |
||
244 | $parts[$units[$m[2]]] = (float)$m[1]; |
||
245 | } |
||
246 | return $parts; |
||
247 | } |
||
248 | } |
||
249 | |||
250 | private static function parseTimeStr(string $str, int $offset = 0, bool $separateMinuteSigns = true): array |
||
251 | { |
||
252 | $timeRe = '~^ |
||
253 | ( -? \d+ (?: \.\d+ )? ) |
||
254 | (?: : ( -? \d+ (?:\.\d+)? ) )? |
||
255 | (?: : ( -? \d+ (?:\.\d+)? ) )? |
||
256 | $~x'; |
||
257 | if (preg_match($timeRe, substr($str, $offset), $m)) { |
||
258 | if (isset($m[2])) { |
||
259 | $sgn = ($separateMinuteSigns || $m[1] >= 0 ? 1 : -1); |
||
260 | return [ |
||
261 | self::HOUR => (float)$m[1], |
||
262 | self::MINUTE => $sgn * $m[2], |
||
263 | self::SECOND => $sgn * (isset($m[3]) ? (float)$m[3] : 0), |
||
264 | ]; |
||
265 | } else { |
||
266 | return [ |
||
267 | self::SECOND => (float)$m[1], |
||
268 | ]; |
||
269 | } |
||
270 | } else { |
||
271 | $parts = []; |
||
272 | preg_match_all('~(-?\d+(?:\.\d+)?)([HMS])~', $str, $matches, PREG_SET_ORDER, $offset); |
||
273 | static $units = ['H' => self::HOUR, 'M' => self::MINUTE, 'S' => self::SECOND]; |
||
274 | foreach ($matches as $m) { |
||
275 | $parts[$units[$m[2]]] = (float)$m[1]; |
||
276 | } |
||
277 | return $parts; |
||
278 | } |
||
279 | } |
||
280 | |||
281 | private static function parsePostgresqlStr(string $str, int $offset = 0): array |
||
336 | } |
||
337 | |||
338 | private static function parseQuantityUnitPairs(string $str, int $offset, array $units): array |
||
339 | { |
||
340 | $result = []; |
||
341 | // OPT: the regular expression might be cached |
||
342 | $re = '~(-?\d+(?:\.\d+)?)\s*(' . implode('|', array_map('preg_quote', array_keys($units))) . ')\b~i'; |
||
343 | preg_match_all($re, $str, $matches, PREG_SET_ORDER, $offset); |
||
344 | $unitsLower = array_change_key_case($units, CASE_LOWER); // OPT: keys $units might be required to be lower-case |
||
345 | foreach ($matches as $m) { |
||
346 | $unit = $unitsLower[strtolower($m[2])]; |
||
347 | $result[$unit] = (float)$m[1]; |
||
348 | } |
||
349 | return $result; |
||
350 | } |
||
351 | |||
352 | private function __construct(int $mon, int $day, $sec) |
||
357 | } |
||
358 | |||
359 | /** |
||
360 | * @return number[] map: unit => quantity, the sum of which equals to the represented interval; |
||
361 | * the output will consist of number of years, months, days, hours, minutes (all of which will |
||
362 | * always be non-zero integers) and seconds (which may be fractional, and will be zero iff the |
||
363 | * interval is zero); |
||
364 | * the order of parts is guaranteed to be as mentioned in the previous sentence, i.e., from years |
||
365 | * to seconds |
||
366 | */ |
||
367 | public function toParts(): array |
||
368 | { |
||
369 | $result = []; |
||
370 | $yr = (int)($this->mon / 12); |
||
371 | $mon = $this->mon % 12; |
||
372 | if ($yr != 0) { |
||
373 | $result[self::YEAR] = $yr; |
||
374 | } |
||
375 | if ($mon != 0) { |
||
376 | $result[self::MONTH] = $mon; |
||
377 | } |
||
378 | if ($this->day != 0) { |
||
379 | $result[self::DAY] = $this->day; |
||
380 | } |
||
381 | $hr = (int)($this->sec / (60 * 60)); |
||
382 | $sec = $this->sec - $hr * 60 * 60; |
||
383 | $min = (int)($sec / 60); |
||
384 | $sec -= $min * 60; |
||
385 | if ($hr != 0) { |
||
386 | $result[self::HOUR] = $hr; |
||
387 | } |
||
388 | if ($min != 0) { |
||
389 | $result[self::MINUTE] = $min; |
||
390 | } |
||
391 | if ($sec != 0 || !$result) { |
||
392 | $result[self::SECOND] = $sec; |
||
393 | } |
||
394 | return $result; |
||
395 | } |
||
396 | |||
397 | public function toIsoString(): string |
||
398 | { |
||
399 | $str = ''; |
||
400 | $inDatePart = true; |
||
401 | foreach ($this->toParts() as $unit => $quantity) { |
||
402 | if ($inDatePart && isset(self::TIME_UNIT_HASH[$unit])) { |
||
403 | $str .= 'T'; |
||
404 | $inDatePart = false; |
||
405 | } |
||
406 | $str .= $quantity . self::UNIT_ISO_ABBR[$unit]; |
||
407 | } |
||
408 | return ($str ? "P$str" : 'PT0S'); |
||
409 | } |
||
410 | |||
411 | /** |
||
412 | * Adds a time interval to this interval and returns the result as a new time interval object. |
||
413 | * |
||
414 | * @param TimeInterval $addend |
||
415 | * @return TimeInterval |
||
416 | */ |
||
417 | public function add(TimeInterval $addend): TimeInterval |
||
418 | { |
||
419 | return new TimeInterval( |
||
420 | $this->mon + $addend->mon, |
||
421 | $this->day + $addend->day, |
||
422 | $this->sec + $addend->sec |
||
423 | ); |
||
424 | } |
||
425 | |||
426 | /** |
||
427 | * Subtracts a time interval from this interval and returns the result as a new time interval object. |
||
428 | * |
||
429 | * @param TimeInterval $subtrahend |
||
430 | * @return TimeInterval |
||
431 | */ |
||
432 | public function subtract(TimeInterval $subtrahend): TimeInterval |
||
433 | { |
||
434 | return new TimeInterval( |
||
435 | $this->mon - $subtrahend->mon, |
||
436 | $this->day - $subtrahend->day, |
||
437 | $this->sec - $subtrahend->sec |
||
438 | ); |
||
439 | } |
||
440 | |||
441 | /** |
||
442 | * Multiplies this time interval with a scalar and returns the result as a new time interval object. |
||
443 | * |
||
444 | * @param number $multiplier |
||
445 | * @return TimeInterval |
||
446 | */ |
||
447 | public function multiply($multiplier): TimeInterval |
||
448 | { |
||
449 | return new TimeInterval($multiplier * $this->mon, $multiplier * $this->day, $multiplier * $this->sec); |
||
450 | } |
||
451 | |||
452 | /** |
||
453 | * Divides this time interval with a scalar and returns the result as a new time interval object. |
||
454 | * |
||
455 | * @param number $divisor |
||
456 | * @return TimeInterval |
||
457 | */ |
||
458 | public function divide($divisor): TimeInterval |
||
461 | } |
||
462 | |||
463 | /** |
||
464 | * @return TimeInterval time interval negative to this one |
||
465 | */ |
||
466 | public function negate(): TimeInterval |
||
469 | } |
||
470 | } |
||
471 |