Completed
Pull Request — master (#105)
by ignace nyamagana
05:11
created

Duration::createFromTimeString()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 16
c 0
b 0
f 0
dl 0
loc 28
ccs 17
cts 17
cp 1
rs 9.4222
cc 5
nc 9
nop 1
crap 5
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>\+|-)?
56
        (((?<hour>\d+):)?(?<minute>\d+):)?
57
        ((?<second>\d+)(\.(?<fraction>\d{1,6}))?)
58
    $@x';
59
60
    /**
61
     * New instance.
62
     *
63
     * Returns a new instance from an Interval specification
64
     */
65 304
    public function __construct(string $interval_spec)
66
    {
67 304
        if (1 === preg_match(self::REGEXP_MICROSECONDS_INTERVAL_SPEC, $interval_spec, $matches)) {
68 3
            parent::__construct($matches['interval'].'S');
69 3
            $this->f = (float) str_pad($matches['fraction'], 6, '0') / 1e6;
70 3
            return;
71
        }
72
73 304
        if (1 === preg_match(self::REGEXP_MICROSECONDS_DATE_SPEC, $interval_spec, $matches)) {
74 3
            parent::__construct($matches['interval']);
75 3
            $this->f = (float) str_pad($matches['fraction'], 6, '0') / 1e6;
76 3
            return;
77
        }
78
79 301
        parent::__construct($interval_spec);
80 301
    }
81
82
    /**
83
     * Returns a continuous portion of time between two datepoints expressed as a DateInterval object.
84
     *
85
     * The duration can be
86
     * <ul>
87
     * <li>an Period object</li>
88
     * <li>a DateInterval object</li>
89
     * <li>an integer interpreted as the duration expressed in seconds.</li>
90
     * <li>a string parsable by DateInterval::createFromDateString</li>
91
     * </ul>
92
     *
93
     * @param mixed $duration a continuous portion of time
94
     *
95
     * @throws TypeError if the duration type is not a supported
96
     */
97 306
    public static function create($duration): self
98
    {
99 306
        if ($duration instanceof Period) {
100 12
            $duration = $duration->getDateInterval();
101
        }
102
103 306
        if ($duration instanceof DateInterval) {
104 45
            $new = new self('PT0S');
105 45
            foreach ($duration as $name => $value) {
106 45
                if (property_exists($new, $name)) {
107 45
                    $new->$name = $value;
108
                }
109
            }
110
111 45
            return $new;
112
        }
113
114 261
        $seconds = filter_var($duration, FILTER_VALIDATE_FLOAT);
115 261
        if (false !== $seconds) {
116 60
            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::createFromTimeString($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 60
    public static function createFromSeconds(float $seconds): self
146
    {
147 60
        $invert = 0 > $seconds;
148 60
        if ($invert) {
149 9
            $seconds = $seconds * -1;
150
        }
151
152 60
        $secondsInt = (int) $seconds;
153 60
        $fraction = ($seconds - $secondsInt);
154 60
        $fraction = (int) ($fraction * 1e6);
155
156 60
        $expression = $secondsInt.' seconds '.$fraction.' microseconds';
157
158
        /** @var Duration $instance */
159 60
        $instance = self::createFromDateString($expression);
160 60
        if ($invert) {
161 9
            $instance->invert = 1;
162
        }
163
164 60
        return $instance;
165
    }
166
167
    /**
168
     * Creates a new instance from a timer string representation.
169
     *
170
     * @throws Exception
171
     */
172 33
    public static function createFromTimeString(string $duration): self
173
    {
174 33
        if (1 !== preg_match(self::REGEXP_CHRONO_FORMAT, $duration, $matches)) {
175 3
            throw new Exception(sprintf('Unknown or bad format (%s)', $duration));
176
        }
177
178 30
        $matches['hour'] = $matches['hour'] ?? '0';
179 30
        if ('' === $matches['hour']) {
180 18
            $matches['hour'] = '0';
181
        }
182
183 30
        $matches['minute'] = $matches['minute'] ?? '0';
184 30
        if ('' === $matches['minute']) {
185 6
            $matches['minute'] = '0';
186
        }
187
188 30
        $matches['fraction'] = str_pad($matches['fraction'] ?? '0000000', 6, '0');
189 30
        $expression = $matches['hour'].' hours '.
190 30
            $matches['minute'].' minutes '.
191 30
            $matches['second'].' seconds '.$matches['fraction'].' microseconds';
192
193
        /** @var Duration $instance */
194 30
        $instance = self::createFromDateString($expression);
195 30
        if ('-' === $matches['sign']) {
196 9
            $instance->invert = 1;
197
        }
198
199 30
        return $instance;
200
    }
201
202
    /**
203
     * @inheritDoc
204
     *
205
     * @param mixed $duration a date with relative parts
206
     *
207
     * @return self|false
208
     */
209 255
    public static function createFromDateString($duration)
210
    {
211 255
        $duration = parent::createFromDateString($duration);
212 253
        if (false === $duration) {
213
            return false;
214
        }
215
216 253
        $new = new self('PT0S');
217 253
        foreach ($duration as $name => $value) {
218 253
            $new->$name = $value;
219
        }
220
221 253
        return $new;
222
    }
223
224
    /**
225
     * DEPRECATION WARNING! This method will be removed in the next major point release.
226
     *
227
     * @deprecated deprecated since version 4.5
228
     * @see ::format
229
     *
230
     * Returns the ISO8601 interval string representation.
231
     *
232
     * Microseconds fractions are included
233
     */
234 90
    public function __toString(): string
235
    {
236 90
        $date = 'P';
237 90
        foreach (['Y' => 'y', 'M' => 'm', 'D' => 'd'] as $key => $value) {
238 90
            if (0 !== $this->$value) {
239 24
                $date .= '%'.$value.$key;
240
            }
241
        }
242
243 90
        $time = 'T';
244 90
        foreach (['H' => 'h', 'M' => 'i'] as $key => $value) {
245 90
            if (0 !== $this->$value) {
246 42
                $time .= '%'.$value.$key;
247
            }
248
        }
249
250 90
        if (0.0 !== $this->f) {
0 ignored issues
show
introduced by
The condition 0.0 !== $this->f is always true.
Loading history...
251 30
            $second = $this->s + $this->f;
252 30
            if (0 > $this->s) {
253 3
                $second = $this->s - $this->f;
254
            }
255
256 30
            $second = rtrim(sprintf('%f', $second), '0');
257
258 30
            return $this->format($date.$time).$second.'S';
259
        }
260
261 60
        if (0 !== $this->s) {
262 18
            $time .= '%sS';
263
264 18
            return $this->format($date.$time);
265
        }
266
267 42
        if ('T' !== $time) {
268 15
            return $this->format($date.$time);
269
        }
270
271 27
        if ('P' !== $date) {
272 24
            return $this->format($date);
273
        }
274
275 3
        return 'PT0S';
276
    }
277
278
    /**
279
     * DEPRECATION WARNING! This method will be removed in the next major point release.
280
     *
281
     * @deprecated deprecated since version 4.6
282
     * @see ::adjustedTo
283
     *
284
     * Returns a new instance with recalculate time and date segments to remove carry over points.
285
     *
286
     * This method MUST retain the state of the current instance, and return
287
     * an instance that contains the time and date segments recalculate to remove
288
     * carry over points.
289
     *
290
     * @param mixed $reference_date a reference datepoint {@see \League\Period\Datepoint::create}
291
     */
292 24
    public function withoutCarryOver($reference_date): self
293
    {
294 24
        return $this->adjustedTo($reference_date);
295
    }
296
297
    /**
298
     * Returns a new instance with recalculate properties according to a given datepoint.
299
     *
300
     * This method MUST retain the state of the current instance, and return
301
     * an instance that contains the time and date segments recalculate to remove
302
     * carry over points.
303
     *
304
     * @param mixed $reference_date a reference datepoint {@see \League\Period\Datepoint::create}
305
     */
306 24
    public function adjustedTo($reference_date): self
307
    {
308 24
        if (!$reference_date instanceof DateTimeImmutable) {
309 24
            $reference_date = Datepoint::create($reference_date);
310
        }
311
312 24
        return self::create($reference_date->diff($reference_date->add($this)));
313
    }
314
}
315