Completed
Push — master ( a41e82...12c914 )
by ignace nyamagana
23s queued 11s
created

Duration::createFromChronoString()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 10
cc 3
nc 3
nop 1
crap 3
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 308
    public function __construct(string $interval_spec)
73
    {
74 308
        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 308
        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 305
        parent::__construct($interval_spec);
87 305
    }
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
            $duration = $duration->getDateInterval();
108
        }
109
110 300
        if ($duration instanceof DateInterval) {
111 45
            $new = new self('PT0S');
112 45
            foreach ($duration as $name => $value) {
113 45
                if (property_exists($new, $name)) {
114 45
                    $new->$name = $value;
115
                }
116
            }
117
118 45
            return $new;
119
        }
120
121 255
        $seconds = filter_var($duration, FILTER_VALIDATE_FLOAT);
122 255
        if (false !== $seconds) {
123 54
            return self::createFromSeconds($seconds);
124
        }
125
126 201
        if (!is_string($duration) && !method_exists($duration, '__toString')) {
127 9
            throw new TypeError(sprintf('%s expects parameter 1 to be string, %s given', __METHOD__, gettype($duration)));
128
        }
129
130 192
        $duration = (string) $duration;
131
132 192
        if (1 === preg_match(self::REGEXP_CHRONO_FORMAT, $duration)) {
133 12
            return self::createFromChronoString($duration);
134
        }
135
136 180
        if (1 === preg_match(self::REGEXP_DATEINTERVAL_WORD_SPEC, $duration)) {
137 21
            if (1 === preg_match(self::REGEXP_DATEINTERVAL_SPEC, $duration)) {
138 3
                return new self($duration);
139
            }
140
141 18
            throw new Exception(sprintf('Unknown or bad format (%s)', $duration));
142
        }
143
144 159
        $instance = self::createFromDateString($duration);
145 159
        if (false !== $instance) {
146 159
            return $instance;
147
        }
148
149
        throw new Exception(sprintf('Unknown or bad format (%s)', $duration));
150
    }
151
152
    /**
153
     * Creates a new instance from a seconds.
154
     *
155
     * the second value will be overflow up to the hour time unit.
156
     */
157 54
    public static function createFromSeconds(float $seconds): self
158
    {
159 54
        $invert = 0 > $seconds;
160 54
        if ($invert) {
161 6
            $seconds = $seconds * -1;
162
        }
163
164 54
        $secondsInt = (int) $seconds;
165 54
        $fraction = (int) (($seconds - $secondsInt) * 1e6);
166 54
        $minute = intdiv($secondsInt, 60);
167 54
        $secondsInt = $secondsInt - ($minute * 60);
168 54
        $hour = intdiv($minute, 60);
169 54
        $minute = $minute - ($hour * 60);
170
171 54
        return self::createFromTimeUnits([
172 54
            'hour' => (string) $hour,
173 54
            'minute' => (string) $minute,
174 54
            'second' => (string) $secondsInt,
175 54
            'fraction' => (string) $fraction,
176 54
            'sign' => $invert ? '-' : '+',
177
        ]);
178
    }
179
180
    /**
181
     * Creates a new instance from a timer string representation.
182
     *
183
     * @throws Exception
184
     */
185 30
    public static function createFromChronoString(string $duration): self
186
    {
187 30
        if (1 !== preg_match(self::REGEXP_CHRONO_FORMAT, $duration, $units)) {
188 6
            throw new Exception(sprintf('Unknown or bad format (%s)', $duration));
189
        }
190
191 24
        if ('' === $units['hour']) {
192 12
            $units['hour'] = '0';
193
        }
194
195 24
        return self::createFromTimeUnits($units);
196
    }
197
198
    /**
199
     * Creates a new instance from a time string representation following RDBMS specification.
200
     *
201
     * @throws Exception
202
     */
203 18
    public static function createFromTimeString(string $duration): self
204
    {
205 18
        if (1 !== preg_match(self::REGEXP_TIME_FORMAT, $duration, $units)) {
206 3
            throw new Exception(sprintf('Unknown or bad format (%s)', $duration));
207
        }
208
209 15
        return self::createFromTimeUnits($units);
210
    }
211
212
    /**
213
     * Creates an instance from DateInterval units.
214
     *
215
     * @param array<string,string> $units
216
     */
217 93
    private static function createFromTimeUnits(array $units): self
218
    {
219 93
        $units = $units + ['hour' => '0', 'minute' => '0', 'second' => '0', 'fraction' => '0', 'sign' => '+'];
220
221 93
        $units['fraction'] = str_pad($units['fraction'] ?? '000000', 6, '0');
222
223 93
        $expression = $units['hour'].' hours '.
224 93
            $units['minute'].' minutes '.
225 93
            $units['second'].' seconds '.$units['fraction'].' microseconds';
226
227
        /** @var Duration $instance */
228 93
        $instance = self::createFromDateString($expression);
229 93
        if ('-' === $units['sign']) {
230 18
            $instance->invert = 1;
231
        }
232
233 93
        return $instance;
234
    }
235
236
    /**
237
     * @inheritDoc
238
     *
239
     * @param mixed $duration a date with relative parts
240
     *
241
     * @return self|false
242
     */
243 258
    public static function createFromDateString($duration)
244
    {
245 258
        $duration = parent::createFromDateString($duration);
246 257
        if (false === $duration) {
247
            return false;
248
        }
249
250 257
        $new = new self('PT0S');
251 257
        foreach ($duration as $name => $value) {
252 257
            $new->$name = $value;
253
        }
254
255 257
        return $new;
256
    }
257
258
    /**
259
     * DEPRECATION WARNING! This method will be removed in the next major point release.
260
     *
261
     * @deprecated deprecated since version 4.5
262
     * @see ::format
263
     *
264
     * Returns the ISO8601 interval string representation.
265
     *
266
     * Microseconds fractions are included
267
     */
268 93
    public function __toString(): string
269
    {
270 93
        $date = 'P';
271 93
        foreach (['Y' => 'y', 'M' => 'm', 'D' => 'd'] as $key => $value) {
272 93
            if (0 !== $this->$value) {
273 24
                $date .= '%'.$value.$key;
274
            }
275
        }
276
277 93
        $time = 'T';
278 93
        foreach (['H' => 'h', 'M' => 'i'] as $key => $value) {
279 93
            if (0 !== $this->$value) {
280 54
                $time .= '%'.$value.$key;
281
            }
282
        }
283
284 93
        if (0.0 !== $this->f) {
0 ignored issues
show
introduced by
The condition 0.0 !== $this->f is always true.
Loading history...
285 30
            $second = $this->s + $this->f;
286 30
            if (0 > $this->s) {
287 3
                $second = $this->s - $this->f;
288
            }
289
290 30
            $second = rtrim(sprintf('%f', $second), '0');
291
292 30
            return $this->format($date.$time).$second.'S';
293
        }
294
295 63
        if (0 !== $this->s) {
296 15
            $time .= '%sS';
297
298 15
            return $this->format($date.$time);
299
        }
300
301 48
        if ('T' !== $time) {
302 21
            return $this->format($date.$time);
303
        }
304
305 27
        if ('P' !== $date) {
306 24
            return $this->format($date);
307
        }
308
309 3
        return 'PT0S';
310
    }
311
312
    /**
313
     * DEPRECATION WARNING! This method will be removed in the next major point release.
314
     *
315
     * @deprecated deprecated since version 4.6
316
     * @see ::adjustedTo
317
     *
318
     * Returns a new instance with recalculate time and date segments to remove carry over points.
319
     *
320
     * This method MUST retain the state of the current instance, and return
321
     * an instance that contains the time and date segments recalculate to remove
322
     * carry over points.
323
     *
324
     * @param mixed $reference_date a reference datepoint {@see \League\Period\Datepoint::create}
325
     */
326 24
    public function withoutCarryOver($reference_date): self
327
    {
328 24
        return $this->adjustedTo($reference_date);
329
    }
330
331
    /**
332
     * Returns a new instance with recalculate properties according to a given datepoint.
333
     *
334
     * This method MUST retain the state of the current instance, and return
335
     * an instance that contains the time and date segments recalculate to remove
336
     * carry over points.
337
     *
338
     * @param mixed $reference_date a reference datepoint {@see \League\Period\Datepoint::create}
339
     */
340 24
    public function adjustedTo($reference_date): self
341
    {
342 24
        if (!$reference_date instanceof DateTimeImmutable) {
343 24
            $reference_date = Datepoint::create($reference_date);
344
        }
345
346 24
        return self::create($reference_date->diff($reference_date->add($this)));
347
    }
348
}
349