Completed
Push — master ( 3a2539...9d10f7 )
by ignace nyamagana
14:04 queued 35s
created

Duration::create()   B

Complexity

Conditions 9
Paths 14

Size

Total Lines 40
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 9

Importance

Changes 0
Metric Value
cc 9
eloc 23
nc 14
nop 1
dl 0
loc 40
ccs 16
cts 16
cp 1
crap 9
rs 8.0555
c 0
b 0
f 0
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_INT;
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_MICROSECONDS_INTERVAL_SPEC = '@^(?<interval>.*)(\.|,)(?<fraction>\d{1,6})S$@';
40
41
    private const REGEXP_MICROSECONDS_DATE_SPEC = '@^(?<interval>.*)(\.)(?<fraction>\d{1,6})$@';
42
43
    private const REGEXP_CHRONO_FORMAT = '@^
44
        (
45
            ((?<hour>\d+):)?
46
            (?<minute>\d+):
47
        )?
48
        (
49
            (?<second>\d+)
50
            (\.(?<fraction>\d{1,6}))?
51
        )
52 237
    $@x';
53
54 237
    /**
55 12
     * Returns a continuous portion of time between two datepoints expressed as a DateInterval object.
56
     *
57
     * The duration can be
58 237
     * <ul>
59 30
     * <li>an Period object</li>
60 30
     * <li>a DateInterval object</li>
61 30
     * <li>an integer interpreted as the duration expressed in seconds.</li>
62 30
     * <li>a string parsable by DateInterval::createFromDateString</li>
63
     * </ul>
64
     *
65
     * @param mixed $duration a continuous portion of time
66 30
     */
67
    public static function create($duration): self
68
    {
69 207
        if ($duration instanceof Period) {
70 57
            $duration = $duration->getDateInterval();
71
        }
72
73 150
        if ($duration instanceof DateInterval) {
74
            $new = new self('PT0S');
75
            foreach ($duration as $name => $value) {
76
                if (property_exists($new, $name)) {
77
                    $new->$name = $value;
78
                }
79
            }
80
81 153
            return $new;
82
        }
83 153
84 144
        if (false !== ($second = filter_var($duration, FILTER_VALIDATE_INT))) {
85 144
            return new self('PT'.$second.'S');
86 144
        }
87
88
        if (!is_string($duration) && !method_exists($duration, '__toString')) {
89 144
            throw new TypeError(sprintf('%s expects parameter 1 to be string, %s given', __METHOD__, gettype($duration)));
90
        }
91
92
        $duration = (string) $duration;
93
        if (1 === preg_match(self::REGEXP_CHRONO_FORMAT, $duration, $matches)) {
94
            $matches['hour'] = $matches['hour'] ?? '0';
95
            $matches['minute'] = $matches['minute'] ?? '0';
96
            $matches['fraction'] = str_pad($matches['fraction'] ?? '0000000', 6, '0');
97 237
98
            return self::createFromDateString(
99 237
                $matches['hour'].' hours '.
100 3
                $matches['minute'].' minutes '.
101 3
                $matches['second'].' seconds '.
102 3
                $matches['fraction'].' microseconds'
103
            );
104
        }
105 237
106 3
        return self::createFromDateString($duration);
107 3
    }
108 3
109
    /**
110
     * @inheritdoc
111 234
     *
112 228
     * @param mixed $duration a date with relative parts
113
     */
114
    public static function createFromDateString($duration): self
115
    {
116
        $duration = parent::createFromDateString($duration);
117
        $new = new self('PT0S');
118
        foreach ($duration as $name => $value) {
119 39
            $new->$name = $value;
120
        }
121 39
122
        return $new;
123
    }
124
125
    /**
126
     * New instance.
127 39
     *
128
     * Returns a new instance from an Interval specification
129 39
     */
130 39
    public function __construct(string $interval_spec)
131 39
    {
132 32
        if (1 === preg_match(self::REGEXP_MICROSECONDS_INTERVAL_SPEC, $interval_spec, $matches)) {
133
            parent::__construct($matches['interval'].'S');
134
            $this->f = (float) str_pad($matches['fraction'], 6, '0') / 1e6;
135
            return;
136 39
        }
137 39
138 39
        if (1 === preg_match(self::REGEXP_MICROSECONDS_DATE_SPEC, $interval_spec, $matches)) {
139 30
            parent::__construct($matches['interval']);
140
            $this->f = (float) str_pad($matches['fraction'], 6, '0') / 1e6;
141
            return;
142
        }
143 39
144 9
        parent::__construct($interval_spec);
145
    }
146 9
147
    /**
148
     * Returns the ISO8601 interval string representation.
149 30
     *
150 3
     * Microseconds fractions are included
151
     */
152 3
    public function __toString(): string
153
    {
154
        return $this->toString($this);
155 27
    }
156 9
157
    /**
158
     * Generates the ISO8601 interval string representation.
159 21
     */
160 18
    private function toString(DateInterval $interval): string
161
    {
162
        $date = 'P';
163 3
        foreach (['Y' => $interval->y, 'M' => $interval->m, 'D' => $interval->d] as $key => $value) {
164
            if (0 !== $value) {
165
                $date .= $value.$key;
166
            }
167
        }
168
169
        $time = 'T';
170
        foreach (['H' => $interval->h, 'M' => $interval->i] as $key => $value) {
171
            if (0 !== $value) {
172
                $time .= $value.$key;
173
            }
174
        }
175
176
        if (0.0 !== $interval->f) {
0 ignored issues
show
introduced by
The condition 0.0 !== $interval->f is always true.
Loading history...
177 12
            $time .= rtrim(sprintf('%f', $interval->s + $interval->f), '0').'S';
178
179 12
            return $date.$time;
180 12
        }
181
182
        if (0 !== $interval->s) {
183 12
            $time .= $interval->s.'S';
184 12
185 3
            return $date.$time;
186
        }
187
188 9
        if ('T' !== $time) {
189
            return $date.$time;
190
        }
191
192
        if ('P' !== $date) {
193
            return $date;
194
        }
195
196
        return 'PT0S';
197
    }
198
199
    /**
200
     * Returns a new instance with recalculate time and date segments to remove carry over points.
201
     *
202
     * This method MUST retain the state of the current instance, and return
203
     * an instance that contains the time and date segments recalculate to remove
204
     * carry over points.
205
     *
206
     * @param mixed $datepoint a Reference datepoint
207
     *                         by default will use the epoch time
208
     *                         accepts the same input as {@see Duration::create}
209
     */
210
    public function withoutCarryOver($datepoint = 0): self
211
    {
212
        if (!$datepoint instanceof DateTimeImmutable) {
213
            $datepoint = Datepoint::create($datepoint);
214
        }
215
216
        $duration = $datepoint->diff($datepoint->add($this));
217
        if ($this->toString($duration) === $this->toString($this)) {
218
            return $this;
219
        }
220
221
        return self::create($duration);
222
    }
223
}
224