Completed
Pull Request — master (#105)
by ignace nyamagana
01:44
created

Duration::createFromTimer()   A

Complexity

Conditions 5
Paths 9

Size

Total Lines 28
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5.005

Importance

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