Completed
Push — master ( 9337bf...d3d2ca )
by ignace nyamagana
04:04
created

Duration   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 196
Duplicated Lines 0 %

Test Coverage

Coverage 98.68%

Importance

Changes 0
Metric Value
eloc 72
dl 0
loc 196
ccs 75
cts 76
cp 0.9868
rs 10
c 0
b 0
f 0
wmc 29

6 Methods

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