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

Duration::createFromTimeUnits()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 2

Importance

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