Duration::withoutCarryOver()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 3
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
/**
4
 * League.Period (https://period.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Period;
15
16
use DateInterval;
17
use DateTimeImmutable;
18
use TypeError;
19
use function filter_var;
20
use function gettype;
21
use function is_string;
22
use function method_exists;
23
use function preg_match;
24
use function property_exists;
25
use function rtrim;
26
use function sprintf;
27
use function str_pad;
28
use const FILTER_VALIDATE_FLOAT;
29
30
/**
31
 * League Period Duration.
32
 *
33
 * @package League.period
34
 * @author  Ignace Nyamagana Butera <[email protected]>
35
 * @since   4.2.0
36
 */
37
final class Duration extends DateInterval
38
{
39
    private const REGEXP_DATEINTERVAL_WORD_SPEC = '/^P\S*$/';
40
41
    private const REGEXP_DATEINTERVAL_SPEC = '@^P
42
        (?!$)                             # making sure there something after the interval delimiter
43
        (?:(\d+Y)?(\d+M)?(\d+W)?(\d+D)?)? # day, week, month, year part
44
        (?:T                              # interval time delimiter
45
            (?!$)                         # making sure there something after the interval time delimiter
46
            (?:\d+H)?(?:\d+M)?(?:\d+S)?   # hour, minute, second part
47
        )?
48
    $@x';
49
50
    private const REGEXP_MICROSECONDS_INTERVAL_SPEC = '@^(?<interval>.*)(\.|,)(?<fraction>\d{1,6})S$@';
51
52
    private const REGEXP_MICROSECONDS_DATE_SPEC = '@^(?<interval>.*)(\.)(?<fraction>\d{1,6})$@';
53
54
    private const REGEXP_CHRONO_FORMAT = '@^
55
        (?<sign>\+|-)?                  # optional sign
56
        ((?<hour>\d+):)?                # optional hour
57
        ((?<minute>\d+):)(?<second>\d+) # required minute and second
58
        (\.(?<fraction>\d{1,6}))?       # optional fraction
59
    $@x';
60
61
    private const REGEXP_TIME_FORMAT = '@^
62
        (?<sign>\+|-)?                               # optional sign
63
        (?<hour>\d+)(:(?<minute>\d+))                # required hour and minute
64
        (:(?<second>\d+)(\.(?<fraction>\d{1,6}))?)?  # optional second and fraction
65
    $@x';
66
67
    /**
68
     * New instance.
69
     *
70
     * Returns a new instance from an Interval specification
71
     */
72 307
    public function __construct(string $interval_spec)
73
    {
74 307
        if (1 === preg_match(self::REGEXP_MICROSECONDS_INTERVAL_SPEC, $interval_spec, $matches)) {
75 3
            parent::__construct($matches['interval'].'S');
76 3
            $this->f = (float) str_pad($matches['fraction'], 6, '0') / 1e6;
77 3
            return;
78
        }
79
80 307
        if (1 === preg_match(self::REGEXP_MICROSECONDS_DATE_SPEC, $interval_spec, $matches)) {
81 3
            parent::__construct($matches['interval']);
82 3
            $this->f = (float) str_pad($matches['fraction'], 6, '0') / 1e6;
83 3
            return;
84
        }
85
86 304
        parent::__construct($interval_spec);
87 304
    }
88
89
    /**
90
     * Returns a continuous portion of time between two datepoints expressed as a DateInterval object.
91
     *
92
     * The duration can be
93
     * <ul>
94
     * <li>an Period object</li>
95
     * <li>a DateInterval object</li>
96
     * <li>an integer interpreted as the duration expressed in seconds.</li>
97
     * <li>a string parsable by DateInterval::createFromDateString</li>
98
     * </ul>
99
     *
100
     * @param mixed $duration a continuous portion of time
101
     *
102
     * @throws TypeError if the duration type is not a supported
103
     */
104 300
    public static function create($duration): self
105
    {
106 300
        if ($duration instanceof Period) {
107 12
            return self::createFromDateInterval($duration->getDateInterval());
108
        }
109
110 288
        if ($duration instanceof DateInterval) {
111 33
            return self::createFromDateInterval($duration);
112
        }
113
114 255
        $seconds = filter_var($duration, FILTER_VALIDATE_FLOAT);
115 255
        if (false !== $seconds) {
116 54
            return self::createFromSeconds($seconds);
117
        }
118
119 201
        if (!is_string($duration) && !method_exists($duration, '__toString')) {
120 9
            throw new TypeError(sprintf('%s expects parameter 1 to be string, %s given', __METHOD__, gettype($duration)));
121
        }
122
123 192
        $duration = (string) $duration;
124
125 192
        if (1 === preg_match(self::REGEXP_CHRONO_FORMAT, $duration)) {
126 12
            return self::createFromChronoString($duration);
127
        }
128
129 180
        if (1 === preg_match(self::REGEXP_DATEINTERVAL_WORD_SPEC, $duration)) {
130 21
            if (1 === preg_match(self::REGEXP_DATEINTERVAL_SPEC, $duration)) {
131 3
                return new self($duration);
132
            }
133
134 18
            throw new Exception(sprintf('Unknown or bad format (%s)', $duration));
135
        }
136
137 159
        $instance = self::createFromDateString($duration);
138 159
        if (false !== $instance) {
139 159
            return $instance;
140
        }
141
142
        throw new Exception(sprintf('Unknown or bad format (%s)', $duration));
143
    }
144
145
    /**
146
     * Creates a new instance from a DateInterval object.
147
     *
148
     * the second value will be overflow up to the hour time unit.
149
     */
150 45
    public static function createFromDateInterval(DateInterval $duration): self
151
    {
152 45
        $new = new self('PT0S');
153 45
        foreach ($duration as $name => $value) {
154 45
            if (property_exists($new, $name)) {
155 45
                $new->$name = $value;
156
            }
157
        }
158
159 45
        return $new;
160
    }
161
162
    /**
163
     * Creates a new instance from a seconds.
164
     *
165
     * the second value will be overflow up to the hour time unit.
166
     */
167 54
    public static function createFromSeconds(float $seconds): self
168
    {
169 54
        $invert = 0 > $seconds;
170 54
        if ($invert) {
171 6
            $seconds = $seconds * -1;
172
        }
173
174 54
        $secondsInt = (int) $seconds;
175 54
        $fraction = (int) (($seconds - $secondsInt) * 1e6);
176 54
        $minute = intdiv($secondsInt, 60);
177 54
        $secondsInt = $secondsInt - ($minute * 60);
178 54
        $hour = intdiv($minute, 60);
179 54
        $minute = $minute - ($hour * 60);
180
181 54
        return self::createFromTimeUnits([
182 54
            'hour' => (string) $hour,
183 54
            'minute' => (string) $minute,
184 54
            'second' => (string) $secondsInt,
185 54
            'fraction' => (string) $fraction,
186 54
            'sign' => $invert ? '-' : '+',
187
        ]);
188
    }
189
190
    /**
191
     * Creates a new instance from a timer string representation.
192
     *
193
     * @throws Exception
194
     */
195 30
    public static function createFromChronoString(string $duration): self
196
    {
197 30
        if (1 !== preg_match(self::REGEXP_CHRONO_FORMAT, $duration, $units)) {
198 6
            throw new Exception(sprintf('Unknown or bad format (%s)', $duration));
199
        }
200
201 24
        if ('' === $units['hour']) {
202 12
            $units['hour'] = '0';
203
        }
204
205 24
        return self::createFromTimeUnits($units);
206
    }
207
208
    /**
209
     * Creates a new instance from a time string representation following RDBMS specification.
210
     *
211
     * @throws Exception
212
     */
213 18
    public static function createFromTimeString(string $duration): self
214
    {
215 18
        if (1 !== preg_match(self::REGEXP_TIME_FORMAT, $duration, $units)) {
216 3
            throw new Exception(sprintf('Unknown or bad format (%s)', $duration));
217
        }
218
219 15
        return self::createFromTimeUnits($units);
220
    }
221
222
    /**
223
     * Creates an instance from DateInterval units.
224
     *
225
     * @param array<string,string> $units
226
     */
227 93
    private static function createFromTimeUnits(array $units): self
228
    {
229 93
        $units = $units + ['hour' => '0', 'minute' => '0', 'second' => '0', 'fraction' => '0', 'sign' => '+'];
230
231 93
        $units['fraction'] = str_pad($units['fraction'] ?? '000000', 6, '0');
232
233 93
        $expression = $units['hour'].' hours '
234 93
            .$units['minute'].' minutes '
235 93
            .$units['second'].' seconds '
236 93
            .$units['fraction'].' microseconds';
237
238
        /** @var Duration $instance */
239 93
        $instance = self::createFromDateString($expression);
240 93
        if ('-' === $units['sign']) {
241 18
            $instance->invert = 1;
242
        }
243
244 93
        return $instance;
245
    }
246
247
    /**
248
     * @inheritDoc
249
     *
250
     * @param mixed $duration a date with relative parts
251
     *
252
     * @return self|false
253
     */
254 258
    public static function createFromDateString($duration)
255
    {
256 258
        $duration = parent::createFromDateString($duration);
257 256
        if (false === $duration) {
258
            return false;
259
        }
260
261 256
        $new = new self('PT0S');
262 256
        foreach ($duration as $name => $value) {
263 256
            $new->$name = $value;
264
        }
265
266 256
        return $new;
267
    }
268
269
    /**
270
     * DEPRECATION WARNING! This method will be removed in the next major point release.
271
     *
272
     * @deprecated deprecated since version 4.5
273
     * @see ::format
274
     *
275
     * Returns the ISO8601 interval string representation.
276
     *
277
     * Microseconds fractions are included
278
     */
279 93
    public function __toString(): string
280
    {
281 93
        $date = 'P';
282 93
        foreach (['Y' => 'y', 'M' => 'm', 'D' => 'd'] as $key => $value) {
283 93
            if (0 !== $this->$value) {
284 24
                $date .= '%'.$value.$key;
285
            }
286
        }
287
288 93
        $time = 'T';
289 93
        foreach (['H' => 'h', 'M' => 'i'] as $key => $value) {
290 93
            if (0 !== $this->$value) {
291 54
                $time .= '%'.$value.$key;
292
            }
293
        }
294
295 93
        if (0.0 !== $this->f) {
0 ignored issues
show
introduced by
The condition 0.0 !== $this->f is always true.
Loading history...
296 30
            $second = $this->s + $this->f;
297 30
            if (0 > $this->s) {
298 3
                $second = $this->s - $this->f;
299
            }
300
301 30
            $second = rtrim(sprintf('%f', $second), '0');
302
303 30
            return $this->format($date.$time).$second.'S';
304
        }
305
306 63
        if (0 !== $this->s) {
307 15
            $time .= '%sS';
308
309 15
            return $this->format($date.$time);
310
        }
311
312 48
        if ('T' !== $time) {
313 21
            return $this->format($date.$time);
314
        }
315
316 27
        if ('P' !== $date) {
317 24
            return $this->format($date);
318
        }
319
320 3
        return 'PT0S';
321
    }
322
323
    /**
324
     * DEPRECATION WARNING! This method will be removed in the next major point release.
325
     *
326
     * @deprecated deprecated since version 4.6
327
     * @see ::adjustedTo
328
     *
329
     * Returns a new instance with recalculate time and date segments to remove carry over points.
330
     *
331
     * This method MUST retain the state of the current instance, and return
332
     * an instance that contains the time and date segments recalculate to remove
333
     * carry over points.
334
     *
335
     * @param mixed $reference_date a reference datepoint {@see \League\Period\Datepoint::create}
336
     */
337 24
    public function withoutCarryOver($reference_date): self
338
    {
339 24
        return $this->adjustedTo($reference_date);
340
    }
341
342
    /**
343
     * Returns a new instance with recalculate properties according to a given datepoint.
344
     *
345
     * This method MUST retain the state of the current instance, and return
346
     * an instance that contains the time and date segments recalculate to remove
347
     * carry over points.
348
     *
349
     * @param mixed $reference_date a reference datepoint {@see \League\Period\Datepoint::create}
350
     */
351 24
    public function adjustedTo($reference_date): self
352
    {
353 24
        if (!$reference_date instanceof DateTimeImmutable) {
354 24
            $reference_date = Datepoint::create($reference_date);
355
        }
356
357 24
        return self::create($reference_date->diff($reference_date->add($this)));
358
    }
359
}
360