xref: /plugin/combo/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php (revision 37748cd8654635afbeca80942126742f0f4cc346)
1*37748cd8SNickeau<?php
2*37748cd8SNickeau
3*37748cd8SNickeaunamespace Cron;
4*37748cd8SNickeau
5*37748cd8SNickeauuse DateTime;
6*37748cd8SNickeau
7*37748cd8SNickeau/**
8*37748cd8SNickeau * Day of month field.  Allows: * , / - ? L W
9*37748cd8SNickeau *
10*37748cd8SNickeau * 'L' stands for "last" and specifies the last day of the month.
11*37748cd8SNickeau *
12*37748cd8SNickeau * The 'W' character is used to specify the weekday (Monday-Friday) nearest the
13*37748cd8SNickeau * given day. As an example, if you were to specify "15W" as the value for the
14*37748cd8SNickeau * day-of-month field, the meaning is: "the nearest weekday to the 15th of the
15*37748cd8SNickeau * month". So if the 15th is a Saturday, the trigger will fire on Friday the
16*37748cd8SNickeau * 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If
17*37748cd8SNickeau * the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you
18*37748cd8SNickeau * specify "1W" as the value for day-of-month, and the 1st is a Saturday, the
19*37748cd8SNickeau * trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary
20*37748cd8SNickeau * of a month's days. The 'W' character can only be specified when the
21*37748cd8SNickeau * day-of-month is a single day, not a range or list of days.
22*37748cd8SNickeau *
23*37748cd8SNickeau * @author Michael Dowling <mtdowling@gmail.com>
24*37748cd8SNickeau */
25*37748cd8SNickeauclass DayOfMonthField extends AbstractField
26*37748cd8SNickeau{
27*37748cd8SNickeau    /**
28*37748cd8SNickeau     * Get the nearest day of the week for a given day in a month
29*37748cd8SNickeau     *
30*37748cd8SNickeau     * @param int $currentYear  Current year
31*37748cd8SNickeau     * @param int $currentMonth Current month
32*37748cd8SNickeau     * @param int $targetDay    Target day of the month
33*37748cd8SNickeau     *
34*37748cd8SNickeau     * @return \DateTime Returns the nearest date
35*37748cd8SNickeau     */
36*37748cd8SNickeau    private static function getNearestWeekday($currentYear, $currentMonth, $targetDay)
37*37748cd8SNickeau    {
38*37748cd8SNickeau        $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT);
39*37748cd8SNickeau        $target = DateTime::createFromFormat('Y-m-d', "$currentYear-$currentMonth-$tday");
40*37748cd8SNickeau        $currentWeekday = (int) $target->format('N');
41*37748cd8SNickeau
42*37748cd8SNickeau        if ($currentWeekday < 6) {
43*37748cd8SNickeau            return $target;
44*37748cd8SNickeau        }
45*37748cd8SNickeau
46*37748cd8SNickeau        $lastDayOfMonth = $target->format('t');
47*37748cd8SNickeau
48*37748cd8SNickeau        foreach (array(-1, 1, -2, 2) as $i) {
49*37748cd8SNickeau            $adjusted = $targetDay + $i;
50*37748cd8SNickeau            if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) {
51*37748cd8SNickeau                $target->setDate($currentYear, $currentMonth, $adjusted);
52*37748cd8SNickeau                if ($target->format('N') < 6 && $target->format('m') == $currentMonth) {
53*37748cd8SNickeau                    return $target;
54*37748cd8SNickeau                }
55*37748cd8SNickeau            }
56*37748cd8SNickeau        }
57*37748cd8SNickeau    }
58*37748cd8SNickeau
59*37748cd8SNickeau    public function isSatisfiedBy(DateTime $date, $value)
60*37748cd8SNickeau    {
61*37748cd8SNickeau        // ? states that the field value is to be skipped
62*37748cd8SNickeau        if ($value == '?') {
63*37748cd8SNickeau            return true;
64*37748cd8SNickeau        }
65*37748cd8SNickeau
66*37748cd8SNickeau        $fieldValue = $date->format('d');
67*37748cd8SNickeau
68*37748cd8SNickeau        // Check to see if this is the last day of the month
69*37748cd8SNickeau        if ($value == 'L') {
70*37748cd8SNickeau            return $fieldValue == $date->format('t');
71*37748cd8SNickeau        }
72*37748cd8SNickeau
73*37748cd8SNickeau        // Check to see if this is the nearest weekday to a particular value
74*37748cd8SNickeau        if (strpos($value, 'W')) {
75*37748cd8SNickeau            // Parse the target day
76*37748cd8SNickeau            $targetDay = substr($value, 0, strpos($value, 'W'));
77*37748cd8SNickeau            // Find out if the current day is the nearest day of the week
78*37748cd8SNickeau            return $date->format('j') == self::getNearestWeekday(
79*37748cd8SNickeau                $date->format('Y'),
80*37748cd8SNickeau                $date->format('m'),
81*37748cd8SNickeau                $targetDay
82*37748cd8SNickeau            )->format('j');
83*37748cd8SNickeau        }
84*37748cd8SNickeau
85*37748cd8SNickeau        return $this->isSatisfied($date->format('d'), $value);
86*37748cd8SNickeau    }
87*37748cd8SNickeau
88*37748cd8SNickeau    public function increment(DateTime $date, $invert = false)
89*37748cd8SNickeau    {
90*37748cd8SNickeau        if ($invert) {
91*37748cd8SNickeau            $date->modify('previous day');
92*37748cd8SNickeau            $date->setTime(23, 59);
93*37748cd8SNickeau        } else {
94*37748cd8SNickeau            $date->modify('next day');
95*37748cd8SNickeau            $date->setTime(0, 0);
96*37748cd8SNickeau        }
97*37748cd8SNickeau
98*37748cd8SNickeau        return $this;
99*37748cd8SNickeau    }
100*37748cd8SNickeau
101*37748cd8SNickeau    /**
102*37748cd8SNickeau     * Validates that the value is valid for the Day of the Month field
103*37748cd8SNickeau     * Days of the month can contain values of 1-31, *, L, or ? by default. This can be augmented with lists via a ',',
104*37748cd8SNickeau     * ranges via a '-', or with a '[0-9]W' to specify the closest weekday.
105*37748cd8SNickeau     *
106*37748cd8SNickeau     * @param string $value
107*37748cd8SNickeau     * @return bool
108*37748cd8SNickeau     */
109*37748cd8SNickeau    public function validate($value)
110*37748cd8SNickeau    {
111*37748cd8SNickeau        // Allow wildcards and a single L
112*37748cd8SNickeau        if ($value === '?' || $value === '*' || $value === 'L') {
113*37748cd8SNickeau            return true;
114*37748cd8SNickeau        }
115*37748cd8SNickeau
116*37748cd8SNickeau        // If you only contain numbers and are within 1-31
117*37748cd8SNickeau        if ((bool) preg_match('/^\d{1,2}$/', $value) && ($value >= 1 && $value <= 31)) {
118*37748cd8SNickeau            return true;
119*37748cd8SNickeau        }
120*37748cd8SNickeau
121*37748cd8SNickeau        // If you have a -, we will deal with each of your chunks
122*37748cd8SNickeau        if ((bool) preg_match('/-/', $value)) {
123*37748cd8SNickeau            // We cannot have a range within a list or vice versa
124*37748cd8SNickeau            if ((bool) preg_match('/,/', $value)) {
125*37748cd8SNickeau                return false;
126*37748cd8SNickeau            }
127*37748cd8SNickeau
128*37748cd8SNickeau            $chunks = explode('-', $value);
129*37748cd8SNickeau            foreach ($chunks as $chunk) {
130*37748cd8SNickeau                if (!$this->validate($chunk)) {
131*37748cd8SNickeau                    return false;
132*37748cd8SNickeau                }
133*37748cd8SNickeau            }
134*37748cd8SNickeau
135*37748cd8SNickeau            return true;
136*37748cd8SNickeau        }
137*37748cd8SNickeau
138*37748cd8SNickeau        // If you have a comma, we will deal with each value
139*37748cd8SNickeau        if ((bool) preg_match('/,/', $value)) {
140*37748cd8SNickeau            // We cannot have a range within a list or vice versa
141*37748cd8SNickeau            if ((bool) preg_match('/-/', $value)) {
142*37748cd8SNickeau                return false;
143*37748cd8SNickeau            }
144*37748cd8SNickeau
145*37748cd8SNickeau            $chunks = explode(',', $value);
146*37748cd8SNickeau            foreach ($chunks as $chunk) {
147*37748cd8SNickeau                if (!$this->validate($chunk)) {
148*37748cd8SNickeau                    return false;
149*37748cd8SNickeau                }
150*37748cd8SNickeau            }
151*37748cd8SNickeau
152*37748cd8SNickeau            return true;
153*37748cd8SNickeau        }
154*37748cd8SNickeau
155*37748cd8SNickeau        // If you contain a /, we'll deal with it
156*37748cd8SNickeau        if ((bool) preg_match('/\//', $value)) {
157*37748cd8SNickeau            $chunks = explode('/', $value);
158*37748cd8SNickeau            foreach ($chunks as $chunk) {
159*37748cd8SNickeau                if (!$this->validate($chunk)) {
160*37748cd8SNickeau                    return false;
161*37748cd8SNickeau                }
162*37748cd8SNickeau            }
163*37748cd8SNickeau            return true;
164*37748cd8SNickeau        }
165*37748cd8SNickeau
166*37748cd8SNickeau        // If you end in W, make sure that it has a numeric in front of it
167*37748cd8SNickeau        if ((bool) preg_match('/^\d{1,2}W$/', $value)) {
168*37748cd8SNickeau            return true;
169*37748cd8SNickeau        }
170*37748cd8SNickeau
171*37748cd8SNickeau        return false;
172*37748cd8SNickeau    }
173*37748cd8SNickeau}
174