Completed
Push — master ( 33e282...894cca )
by ignace nyamagana
03:49
created

Duration   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 188
Duplicated Lines 0 %

Test Coverage

Coverage 98.63%

Importance

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