1<?php
2
3namespace Cron;
4
5use DateTime;
6use InvalidArgumentException;
7
8
9/**
10 * Day of week field.  Allows: * / , - ? L #
11 *
12 * Days of the week can be represented as a number 0-7 (0|7 = Sunday)
13 * or as a three letter string: SUN, MON, TUE, WED, THU, FRI, SAT.
14 *
15 * 'L' stands for "last". It allows you to specify constructs such as
16 * "the last Friday" of a given month.
17 *
18 * '#' is allowed for the day-of-week field, and must be followed by a
19 * number between one and five. It allows you to specify constructs such as
20 * "the second Friday" of a given month.
21 */
22class DayOfWeekField extends AbstractField
23{
24    public function isSatisfiedBy(DateTime $date, $value)
25    {
26        if ($value == '?') {
27            return true;
28        }
29
30        // Convert text day of the week values to integers
31        $value = $this->convertLiterals($value);
32
33        $currentYear = $date->format('Y');
34        $currentMonth = $date->format('m');
35        $lastDayOfMonth = $date->format('t');
36
37        // Find out if this is the last specific weekday of the month
38        if (strpos($value, 'L')) {
39            $weekday = str_replace('7', '0', substr($value, 0, strpos($value, 'L')));
40            $tdate = clone $date;
41            $tdate->setDate($currentYear, $currentMonth, $lastDayOfMonth);
42            while ($tdate->format('w') != $weekday) {
43                $tdateClone = new DateTime();
44                $tdate = $tdateClone
45                    ->setTimezone($tdate->getTimezone())
46                    ->setDate($currentYear, $currentMonth, --$lastDayOfMonth);
47            }
48
49            return $date->format('j') == $lastDayOfMonth;
50        }
51
52        // Handle # hash tokens
53        if (strpos($value, '#')) {
54            list($weekday, $nth) = explode('#', $value);
55
56            // 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601
57            if ($weekday === '0') {
58                $weekday = 7;
59            }
60
61            // Validate the hash fields
62            if ($weekday < 0 || $weekday > 7) {
63                throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given");
64            }
65            if ($nth > 5) {
66                throw new InvalidArgumentException('There are never more than 5 of a given weekday in a month');
67            }
68            // The current weekday must match the targeted weekday to proceed
69            if ($date->format('N') != $weekday) {
70                return false;
71            }
72
73            $tdate = clone $date;
74            $tdate->setDate($currentYear, $currentMonth, 1);
75            $dayCount = 0;
76            $currentDay = 1;
77            while ($currentDay < $lastDayOfMonth + 1) {
78                if ($tdate->format('N') == $weekday) {
79                    if (++$dayCount >= $nth) {
80                        break;
81                    }
82                }
83                $tdate->setDate($currentYear, $currentMonth, ++$currentDay);
84            }
85
86            return $date->format('j') == $currentDay;
87        }
88
89        // Handle day of the week values
90        if (strpos($value, '-')) {
91            $parts = explode('-', $value);
92            if ($parts[0] == '7') {
93                $parts[0] = '0';
94            } elseif ($parts[1] == '0') {
95                $parts[1] = '7';
96            }
97            $value = implode('-', $parts);
98        }
99
100        // Test to see which Sunday to use -- 0 == 7 == Sunday
101        $format = in_array(7, str_split($value)) ? 'N' : 'w';
102        $fieldValue = $date->format($format);
103
104        return $this->isSatisfied($fieldValue, $value);
105    }
106
107    public function increment(DateTime $date, $invert = false)
108    {
109        if ($invert) {
110            $date->modify('-1 day');
111            $date->setTime(23, 59, 0);
112        } else {
113            $date->modify('+1 day');
114            $date->setTime(0, 0, 0);
115        }
116
117        return $this;
118    }
119
120    public function validate($value)
121    {
122        $value = $this->convertLiterals($value);
123
124        foreach (explode(',', $value) as $expr) {
125            if (!preg_match('/^(\*|[0-7](L?|#[1-5]))([\/\,\-][0-7]+)*$/', $expr)) {
126                return false;
127            }
128        }
129
130        return true;
131    }
132
133    private function convertLiterals($string)
134    {
135        return str_ireplace(
136            array('SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'),
137            range(0, 6),
138            $string
139        );
140    }
141}
142