FastForwarder::exec()   B
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5

Importance

Changes 5
Bugs 1 Features 1
Metric Value
c 5
b 1
f 1
dl 0
loc 26
ccs 17
cts 17
cp 1
rs 8.439
cc 5
eloc 15
nc 6
nop 1
crap 5
1
<?php
2
3
namespace iansltx\BusinessDays;
4
5
use DateTimeInterface;
6
use DateTime;
7
use DateInterval;
8
9
/**
10
 * Class FastForwarder
11
 *
12
 * Provides an easy way to calculate dates that are a specified number of
13
 * "business bays" in the future relative to a supplied date. This is achieved
14
 * by first adding a series of filter callbacks that define what is NOT a
15
 * business day.
16
 *
17
 * Calculates a date X days after $start_date, where X was supplied in
18
 * static::createWithDays(); if the end date would land on a non-business
19
 * day, the first business day after that date is returned.
20
 *
21
 * A negative day count may be entered into the constructor to count in the
22
 * opposite direction (days in the past vs. in the future). Or just use
23
 * Rewinder.
24
 *
25
 * @package iansltx\BusinessDays
26
 */
27
class FastForwarder
28
{
29
    use SkipWhenTrait;
30
31
    protected $numDays = 0;
32
33
    /** @var DateInterval */
34
    protected $interval;
35
36
    /**
37
     * Static factory method for backward compatibility; see constructor for more details
38
     *
39
     * @param int|float $num_days
40
     * @param array $skip_when
41
     * @return static
42
     */
43 20
    public static function createWithDays($num_days, array $skip_when = [])
44
    {
45 20
        return new static($num_days, $skip_when);
46
    }
47
48
    /**
49
     * Creates an instance with a defined number of business days
50
     *
51
     * @param int|float $num_days the number of business days from the supplied date to
52
     *  the calculated date (non-integer values will be rounded up)
53
     * @param array $skip_when pre-defined filters to add to skipWhen without testing
54
     */
55 22
    public function __construct($num_days, array $skip_when = [])
56
    {
57 22
        $this->numDays = ceil(abs($num_days));
58 22
        $this->interval = new DateInterval('P1D');
59
        // invert iterator direction if negative day count
60 22
        $this->interval->invert = (int) ($num_days < 0);
61 22
        $this->replaceSkipWhen($skip_when);
62 22
    }
63
64
    /**
65
     * Iterates through dates in steps of $this->interval until $this->numDays
66
     * is zero. Then, if the current day is not a business day, iterate until
67
     * the current day is a business day. Return the result in the same format
68
     * as was supplied.
69
     *
70
     * @param DateTimeInterface $start_date
71
     * @return DateTimeInterface clone of $start_date with a modified timestamp
72
     */
73 20
    public function exec(DateTimeInterface $start_date)
74
    {
75 20
        $startTz = $start_date->getTimezone();
76 20
        $newDt = (new DateTime($start_date->format('c')))->setTimeZone($startTz);
77
78 20
        $daysLeft = $this->numDays;
79
80 20
        while ($daysLeft > 0) {
81 20
            if ($this->getSkippedBy($newDt) === false) {
82 20
                --$daysLeft;
83 10
            }
84
85 20
            $newDt->add($this->interval);
86 10
        }
87
88
        do { // if end date falls on a skipped day, keep skipping until a valid day is found
89 20
            $skipFilterName = $this->getSkippedBy($newDt);
90 20
            if ($skipFilterName !== false) {
91 12
                $newDt->add($this->interval);
92 6
            }
93 20
        } while ($skipFilterName !== false);
94
95
        /** @var DateTime|\DateTimeImmutable $clonedDt */
96 20
        $clonedDt = clone $start_date;
97 20
        return $clonedDt->setTimestamp($newDt->getTimestamp());
98
    }
99
}
100