Passed
Pull Request — master (#93)
by ignace nyamagana
03:06 queued 01:08
created

Datepoint::abuts()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
ccs 2
cts 2
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 DateTime;
18
use DateTimeImmutable;
19
use DateTimeInterface;
20
use DateTimeZone;
21
use function filter_var;
22
use function intdiv;
23
use function preg_match;
24
use function sprintf;
25
use function str_pad;
26
use const FILTER_VALIDATE_INT;
27
use const STR_PAD_LEFT;
28
29
/**
30
 * League Period Datepoint.
31
 *
32
 * @package League.period
33
 * @author  Ignace Nyamagana Butera <[email protected]>
34
 * @since   4.2.0
35
 */
36
final class Datepoint extends DateTimeImmutable
37
{
38
    private const ISO8601_FORMAT = 'Y-m-d\TH:i:s.u\Z';
39
40
    private const ISO8601_REGEXP = '/^
41
        (?<date>
42
            (?<year>(-?\d+))
43
            (-(?<month>1[0-2]|0[1-9]))?
44
            (-(?<day>3[01]|0[1-9]|[12][0-9]))?
45
        )
46
        (T(?<time>
47
            (?<hour>2[0-3]|[01][0-9])
48
            (:(?<minute>[0-5][0-9]))?
49
            (:(?<second>[0-5][0-9])(\.(?<micro>\d+))?)?
50
        ))?
51
        (?<utc>Z)?
52
     $/x';
53
54
    /**
55
     * @throws Exception
56
     */
57 33
    public static function fromIso8601(string $date, string $baseDate = null): self
58
    {
59
        //@todo This should be improve as it leads to bugs on edge cases.
60 33
        if (null !== $baseDate) {
61 24
            $baseLength = strlen($baseDate);
62 24
            $relativeLength = strlen($date);
63 24
            $diff = $baseLength <=> $relativeLength;
64 24
            if (-1 === $diff) {
65 3
                throw new Exception('The string format is not valid. Please review your submitted ISO8601 Interval format.');
66
            }
67
68 21
            if (1 === $diff) {
69 15
                $date = substr($baseDate, 0, - $relativeLength).$date;
70
            }
71
        }
72
73 33
        $newInstance = self::createFromFormat(self::ISO8601_FORMAT, self::formatIso8601($date));
74 30
        if (false === $newInstance) {
75 3
            throw new Exception(sprintf('The submitted interval string `%s` is not a valid ISO8601 interval date string.', $date));
76
        }
77
78 30
        return $newInstance;
79
    }
80
81
    /**
82
     * @throws Exception
83
     */
84 33
    private static function formatIso8601(string $date): string
85
    {
86 33
        if (1 !== preg_match(self::ISO8601_REGEXP, $date, $matches)) {
87 3
            throw new Exception(sprintf('The submitted interval string `%s` is not a valid ISO8601 interval date string.', $date));
88
        }
89
90 30
        foreach (['month', 'day'] as $part) {
91 30
            if (!isset($matches[$part]) || '' === $matches[$part]) {
92 24
                $matches[$part] = '01';
93
            }
94
        }
95
96 30
        if (!isset($matches['time']) || '' === $matches['time']) {
97 18
            $matches['time'] = '00:00:00';
98
        }
99
100 30
        foreach (['hour', 'minute', 'second'] as $part) {
101 30
            if (!isset($matches[$part]) || '' === $matches[$part]) {
102 27
                $matches[$part] = '00';
103
            }
104
        }
105 30
        if (!isset($matches['micro']) || '' === $matches['micro']) {
106 30
            $matches['micro'] = '000000';
107
        }
108 30
        $matches['utc'] = $matches['utc'] ?? '';
109
110 30
        return str_pad($matches['year'], 4, '0', STR_PAD_LEFT)
111 30
            .'-'.$matches['month']
112 30
            .'-'.$matches['day']
113 30
            .'T'.$matches['hour']
114 30
            .':'.$matches['minute']
115 30
            .':'.$matches['second']
116 30
            .'.'.$matches['micro']
117 30
            .'Z'
118
        ;
119
    }
120
121
122
    /**
123
     * Returns a position in time expressed as a DateTimeImmutable object.
124
     *
125
     * A datepoint can be
126
     * <ul>
127
     * <li>a DateTimeInterface object
128
     * <li>a integer interpreted as a timestamp
129
     * <li>a string parsable by DateTime::__construct
130
     * </ul>
131
     *
132
     * @param mixed $datepoint a position in time
133
     */
134 936
    public static function create($datepoint): self
135
    {
136 936
        if ($datepoint instanceof DateTimeInterface) {
137 459
            return new self($datepoint->format('Y-m-d H:i:s.u'), $datepoint->getTimezone());
138
        }
139
140 501
        if (false !== ($timestamp = filter_var($datepoint, FILTER_VALIDATE_INT))) {
141 15
            return new self('@'.$timestamp);
142
        }
143
144 486
        return new self($datepoint);
145
    }
146
147
    /**
148
     * @inheritDoc
149
     *
150
     * @param string       $format
151
     * @param string       $datetime
152
     * @param DateTimeZone $timezone
153
     *
154
     * @return static|false
155
     */
156 3
    public static function createFromFormat($format, $datetime, $timezone = null)
157
    {
158 3
        $datepoint = parent::createFromFormat($format, $datetime, $timezone);
159 3
        if (false !== $datepoint) {
160 3
            return self::create($datepoint);
161
        }
162
163 3
        return $datepoint;
164
    }
165
166
    /**
167
     * @inheritDoc
168
     *
169
     * @param DateTime $datetime
170
     *
171
     * @return static
172
     */
173 3
    public static function createFromMutable($datetime): self
174
    {
175 3
        return self::create(parent::createFromMutable($datetime));
176
    }
177
178
    /**************************************************
179
     * interval constructors
180
     **************************************************/
181
182
    /**
183
     * Returns a Period instance.
184
     *
185
     *  - the starting datepoint represents the beginning of the current datepoint second
186
     *  - the duration is equal to 1 second
187
     */
188 6
    public function getSecond(string $boundaryType = Period::INCLUDE_START_EXCLUDE_END): Period
189
    {
190 6
        $datepoint = $this->setTime(
191 6
            (int) $this->format('H'),
192 6
            (int) $this->format('i'),
193 6
            (int) $this->format('s')
194
        );
195
196 6
        return new Period($datepoint, $datepoint->add(new DateInterval('PT1S')), $boundaryType);
197
    }
198
199
    /**
200
     * Returns a Period instance.
201
     *
202
     *  - the starting datepoint represents the beginning of the current datepoint minute
203
     *  - the duration is equal to 1 minute
204
     */
205 6
    public function getMinute(string $boundaryType = Period::INCLUDE_START_EXCLUDE_END): Period
206
    {
207 6
        $datepoint = $this->setTime((int) $this->format('H'), (int) $this->format('i'), 0);
208
209 6
        return new Period($datepoint, $datepoint->add(new DateInterval('PT1M')), $boundaryType);
210
    }
211
212
    /**
213
     * Returns a Period instance.
214
     *
215
     *  - the starting datepoint represents the beginning of the current datepoint hour
216
     *  - the duration is equal to 1 hour
217
     */
218 9
    public function getHour(string $boundaryType = Period::INCLUDE_START_EXCLUDE_END): Period
219
    {
220 9
        $datepoint = $this->setTime((int) $this->format('H'), 0);
221
222 9
        return new Period($datepoint, $datepoint->add(new DateInterval('PT1H')), $boundaryType);
223
    }
224
225
    /**
226
     * Returns a Period instance.
227
     *
228
     *  - the starting datepoint represents the beginning of the current datepoint day
229
     *  - the duration is equal to 1 day
230
     */
231 57
    public function getDay(string $boundaryType = Period::INCLUDE_START_EXCLUDE_END): Period
232
    {
233 57
        $datepoint = $this->setTime(0, 0);
234
235 57
        return new Period($datepoint, $datepoint->add(new DateInterval('P1D')), $boundaryType);
236
    }
237
238
    /**
239
     * Returns a Period instance.
240
     *
241
     *  - the starting datepoint represents the beginning of the current datepoint iso week
242
     *  - the duration is equal to 7 days
243
     */
244 6
    public function getIsoWeek(string $boundaryType = Period::INCLUDE_START_EXCLUDE_END): Period
245
    {
246
        $startDate = $this
247 6
            ->setTime(0, 0)
248 6
            ->setISODate((int) $this->format('o'), (int) $this->format('W'), 1);
249
250 6
        return new Period($startDate, $startDate->add(new DateInterval('P7D')), $boundaryType);
251
    }
252
253
    /**
254
     * Returns a Period instance.
255
     *
256
     *  - the starting datepoint represents the beginning of the current datepoint month
257
     *  - the duration is equal to 1 month
258
     */
259 15
    public function getMonth(string $boundaryType = Period::INCLUDE_START_EXCLUDE_END): Period
260
    {
261
        $startDate = $this
262 15
            ->setTime(0, 0)
263 15
            ->setDate((int) $this->format('Y'), (int) $this->format('n'), 1);
264
265 15
        return new Period($startDate, $startDate->add(new DateInterval('P1M')), $boundaryType);
266
    }
267
268
    /**
269
     * Returns a Period instance.
270
     *
271
     *  - the starting datepoint represents the beginning of the current datepoint quarter
272
     *  - the duration is equal to 3 months
273
     */
274 6
    public function getQuarter(string $boundaryType = Period::INCLUDE_START_EXCLUDE_END): Period
275
    {
276
        $startDate = $this
277 6
            ->setTime(0, 0)
278 6
            ->setDate((int) $this->format('Y'), (intdiv((int) $this->format('n'), 3) * 3) + 1, 1);
279
280 6
        return new Period($startDate, $startDate->add(new DateInterval('P3M')), $boundaryType);
281
    }
282
283
    /**
284
     * Returns a Period instance.
285
     *
286
     *  - the starting datepoint represents the beginning of the current datepoint semester
287
     *  - the duration is equal to 6 months
288
     */
289 6
    public function getSemester(string $boundaryType = Period::INCLUDE_START_EXCLUDE_END): Period
290
    {
291
        $startDate = $this
292 6
            ->setTime(0, 0)
293 6
            ->setDate((int) $this->format('Y'), (intdiv((int) $this->format('n'), 6) * 6) + 1, 1);
294
295 6
        return new Period($startDate, $startDate->add(new DateInterval('P6M')), $boundaryType);
296
    }
297
298
    /**
299
     * Returns a Period instance.
300
     *
301
     *  - the starting datepoint represents the beginning of the current datepoint year
302
     *  - the duration is equal to 1 year
303
     */
304 15
    public function getYear(string $boundaryType = Period::INCLUDE_START_EXCLUDE_END): Period
305
    {
306 15
        $year = (int) $this->format('Y');
307 15
        $datepoint = $this->setTime(0, 0);
308
309 15
        return new Period($datepoint->setDate($year, 1, 1), $datepoint->setDate(++$year, 1, 1), $boundaryType);
310
    }
311
312
    /**
313
     * Returns a Period instance.
314
     *
315
     *  - the starting datepoint represents the beginning of the current datepoint iso year
316
     *  - the duration is equal to 1 iso year
317
     */
318 6
    public function getIsoYear(string $boundaryType = Period::INCLUDE_START_EXCLUDE_END): Period
319
    {
320 6
        $year = (int) $this->format('o');
321 6
        $datepoint = $this->setTime(0, 0);
322
323 6
        return new Period($datepoint->setISODate($year, 1, 1), $datepoint->setISODate(++$year, 1, 1), $boundaryType);
324
    }
325
326
    /**************************************************
327
     * relation methods
328
     **************************************************/
329
330
    /**
331
     * Tells whether the datepoint is before the interval.
332
     */
333 12
    public function isBefore(Period $interval): bool
334
    {
335 12
        return $interval->isAfter($this);
336
    }
337
338
    /**
339
     * Tell whether the datepoint borders on start the interval.
340
     */
341 3
    public function bordersOnStart(Period $interval): bool
342
    {
343 3
        return $this == $interval->getStartDate() && $interval->isStartExcluded();
344
    }
345
346
    /**
347
     * Tells whether the datepoint starts the interval.
348
     */
349 12
    public function isStarting(Period $interval): bool
350
    {
351 12
        return $interval->isStartedBy($this);
352
    }
353
354
    /**
355
     * Tells whether the datepoint is contained within the interval.
356
     */
357 27
    public function isDuring(Period $interval): bool
358
    {
359 27
        return $interval->contains($this);
360
    }
361
362
    /**
363
     * Tells whether the datepoint ends the interval.
364
     */
365 6
    public function isEnding(Period $interval): bool
366
    {
367 6
        return $interval->isEndedBy($this);
368
    }
369
370
    /**
371
     * Tells whether the datepoint borders on end the interval.
372
     */
373 3
    public function bordersOnEnd(Period $interval): bool
374
    {
375 3
        return $this == $interval->getEndDate() && $interval->isEndExcluded();
376
    }
377
378
    /**
379
     * Tells whether the datepoint abuts the interval.
380
     */
381 3
    public function abuts(Period $interval): bool
382
    {
383 3
        return $this->bordersOnEnd($interval) || $this->bordersOnStart($interval);
384
    }
385
386
    /**
387
     * Tells whether the datepoint is after the interval.
388
     */
389 18
    public function isAfter(Period $interval): bool
390
    {
391 18
        return $interval->isBefore($this);
392
    }
393
}
394