Completed
Pull Request — master (#104)
by Jérôme
04:53
created

Duration::adjustedTo()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 2
rs 10
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_DATEINTERVAL_SPEC = '@^P
40
        (?:(?:\d+Y)?(?:\d+M)?(?:\d+D)?)?
41
        (?:T(?:\d+H)?(?:\d+M)?(?:\d+S)?)?
42
    $@x';
43
44
    private const REGEXP_MICROSECONDS_INTERVAL_SPEC = '@^(?<interval>.*)(\.|,)(?<fraction>\d{1,6})S$@';
45
46
    private const REGEXP_MICROSECONDS_DATE_SPEC = '@^(?<interval>.*)(\.)(?<fraction>\d{1,6})$@';
47
48
    private const REGEXP_CHRONO_FORMAT = '@^
49
        (?<sign>\+|-)?
50
        (((?<hour>\d+):)?(?<minute>\d+):)?
51
        ((?<second>\d+)(\.(?<fraction>\d{1,6}))?)
52
    $@x';
53
54
    /**
55
     * New instance.
56
     *
57
     * Returns a new instance from an Interval specification
58
     */
59 285
    public function __construct(string $interval_spec)
60
    {
61 285
        if (1 === preg_match(self::REGEXP_MICROSECONDS_INTERVAL_SPEC, $interval_spec, $matches)) {
62 3
            parent::__construct($matches['interval'].'S');
63 3
            $this->f = (float) str_pad($matches['fraction'], 6, '0') / 1e6;
64 3
            return;
65
        }
66
67 285
        if (1 === preg_match(self::REGEXP_MICROSECONDS_DATE_SPEC, $interval_spec, $matches)) {
68 3
            parent::__construct($matches['interval']);
69 3
            $this->f = (float) str_pad($matches['fraction'], 6, '0') / 1e6;
70 3
            return;
71
        }
72
73 282
        parent::__construct($interval_spec);
74 276
    }
75
76
    /**
77
     * Returns a continuous portion of time between two datepoints expressed as a DateInterval object.
78
     *
79
     * The duration can be
80
     * <ul>
81
     * <li>an Period object</li>
82
     * <li>a DateInterval object</li>
83
     * <li>an integer interpreted as the duration expressed in seconds.</li>
84
     * <li>a string parsable by DateInterval::createFromDateString</li>
85
     * </ul>
86
     *
87
     * @param mixed $duration a continuous portion of time
88
     *
89
     * @throws TypeError if the duration type is not a supported
90
     */
91 288
    public static function create($duration): self
92
    {
93 288
        if ($duration instanceof Period) {
94 12
            $duration = $duration->getDateInterval();
95
        }
96
97 288
        if ($duration instanceof DateInterval) {
98 45
            $new = new self('PT0S');
99 45
            foreach ($duration as $name => $value) {
100 45
                if (property_exists($new, $name)) {
101 45
                    $new->$name = $value;
102
                }
103
            }
104
105 45
            return $new;
106
        }
107
108 243
        if (false !== ($second = filter_var($duration, FILTER_VALIDATE_INT))) {
109 57
            return new self('PT'.$second.'S');
110
        }
111
112 186
        if (!is_string($duration) && !method_exists($duration, '__toString')) {
113 9
            throw new TypeError(sprintf('%s expects parameter 1 to be string, %s given', __METHOD__, gettype($duration)));
114
        }
115
116 177
        $duration = (string) $duration;
117
118 177
        if (1 === preg_match(self::REGEXP_DATEINTERVAL_SPEC, $duration)) {
119 3
            return new self($duration);
120
        }
121
122 174
        if (1 !== preg_match(self::REGEXP_CHRONO_FORMAT, $duration, $matches)) {
123 159
            $new = self::createFromDateString($duration);
124 159
            if ($new !== false) {
0 ignored issues
show
introduced by
The condition $new !== false is always true.
Loading history...
125 159
                return $new;
126
            }
127
128
            throw new Exception(sprintf('Unknown or bad format (%s)', $duration));
129
        }
130
131 15
        $matches['hour'] = $matches['hour'] ?? '0';
132 15
        if ('' === $matches['hour']) {
133 9
            $matches['hour'] = '0';
134
        }
135
136 15
        $matches['minute'] = $matches['minute'] ?? '0';
137 15
        if ('' === $matches['minute']) {
138 3
            $matches['minute'] = '0';
139
        }
140
141 15
        $matches['fraction'] = str_pad($matches['fraction'] ?? '0000000', 6, '0');
142 15
        $expression = $matches['hour'].' hours '.
143 15
            $matches['minute'].' minutes '.
144 15
            $matches['second'].' seconds '.$matches['fraction'].' microseconds';
145
146 15
        $instance = self::createFromDateString($expression);
147 15
        if (false === $instance) {
0 ignored issues
show
introduced by
The condition false === $instance is always false.
Loading history...
148
            throw new Exception(sprintf('Unknown or bad format (%s)', $expression));
149
        }
150
151 15
        if ('-' === $matches['sign']) {
152 6
            $instance->invert = 1;
153
        }
154
155 15
        return $instance;
156
    }
157
158
    /**
159
     * @inheritDoc
160
     *
161
     * @param mixed $duration a date with relative parts
162
     *
163
     * @return static|false
164
     */
165 177
    public static function createFromDateString($duration): self
166
    {
167 177
        $duration = parent::createFromDateString($duration);
168 177
        if (false === $duration) {
169
            return $duration;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $duration returns the type DateInterval which includes types incompatible with the type-hinted return League\Period\Duration.
Loading history...
170
        }
171
172 177
        $new = new self('PT0S');
173 177
        foreach ($duration as $name => $value) {
174 177
            $new->$name = $value;
175
        }
176
177 177
        return $new;
178
    }
179
180
    /**
181
     * DEPRECATION WARNING! This method will be removed in the next major point release.
182
     *
183
     * @deprecated deprecated since version 4.5
184
     * @see ::format
185
     *
186
     * Returns the ISO8601 interval string representation.
187
     *
188
     * Microseconds fractions are included
189
     */
190 72
    public function __toString(): string
191
    {
192 72
        $date = 'P';
193 72
        foreach (['Y' => 'y', 'M' => 'm', 'D' => 'd'] as $key => $value) {
194 72
            if (0 !== $this->$value) {
195 24
                $date .= '%'.$value.$key;
196
            }
197
        }
198
199 72
        $time = 'T';
200 72
        foreach (['H' => 'h', 'M' => 'i'] as $key => $value) {
201 72
            if (0 !== $this->$value) {
202 30
                $time .= '%'.$value.$key;
203
            }
204
        }
205
206 72
        if (0.0 !== $this->f) {
0 ignored issues
show
introduced by
The condition 0.0 !== $this->f is always true.
Loading history...
207 21
            $second = $this->s + $this->f;
208 21
            if (0 > $this->s) {
209 3
                $second = $this->s - $this->f;
210
            }
211
212 21
            $second = rtrim(sprintf('%f', $second), '0');
213
214 21
            return $this->format($date.$time).$second.'S';
215
        }
216
217 51
        if (0 !== $this->s) {
218 9
            $time .= '%sS';
219
220 9
            return $this->format($date.$time);
221
        }
222
223 42
        if ('T' !== $time) {
224 15
            return $this->format($date.$time);
225
        }
226
227 27
        if ('P' !== $date) {
228 24
            return $this->format($date);
229
        }
230
231 3
        return 'PT0S';
232
    }
233
234
    /**
235
     * DEPRECATION WARNING! This method will be removed in the next major point release.
236
     *
237
     * @deprecated deprecated since version 4.6
238
     * @see ::adjustedTo
239
     *
240
     * Returns a new instance with recalculate time and date segments to remove carry over points.
241
     *
242
     * This method MUST retain the state of the current instance, and return
243
     * an instance that contains the time and date segments recalculate to remove
244
     * carry over points.
245
     *
246
     * @param mixed $reference_date a reference datepoint {@see \League\Period\Datepoint::create}
247
     */
248 24
    public function withoutCarryOver($reference_date): self
249
    {
250 24
        return $this->adjustedTo($reference_date);
251
    }
252
253
    /**
254
     * Returns a new instance with recalculate properties according to a given datepoint.
255
     *
256
     * This method MUST retain the state of the current instance, and return
257
     * an instance that contains the time and date segments recalculate to remove
258
     * carry over points.
259
     *
260
     * @param mixed $reference_date a reference datepoint {@see \League\Period\Datepoint::create}
261
     */
262 24
    public function adjustedTo($reference_date): self
263
    {
264 24
        if (!$reference_date instanceof DateTimeImmutable) {
265 24
            $reference_date = Datepoint::create($reference_date);
266
        }
267
268 24
        return self::create($reference_date->diff($reference_date->add($this)));
269
    }
270
}
271