Completed
Push — master ( 602526...ecddbd )
by Bushlanov
02:15
created

WorkingTime::calculatingWorkingTime()   C

Complexity

Conditions 7
Paths 5

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 7.1929

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 34
ccs 16
cts 19
cp 0.8421
rs 6.7272
cc 7
eloc 20
nc 5
nop 2
crap 7.1929
1
<?php
2
/**
3
 * @copyright 2016, Aleksandr Bushlanov
4
 * @package gtvolk\WorkingTime
5
 * @link https://github.com/GT-Volk/
6
 * @license https://github.com/GT-Volk/working-time/blob/master/LICENSE.md
7
 */
8
9
namespace gtvolk\WorkingTime;
10
11
/**
12
 * Класс рассчитывает временнЫе интервалы, учитывая рабочие часы и выходные дни.
13
 *
14
 * @property \DateTime $dateTime
15
 * @property array $workingDays
16
 * @property array $weekends
17
 * @property array $holidays
18
 *
19
 * @since  1.0.3
20
 *
21
 * @author Aleksandr Bushlanov <[email protected]>
22
 */
23
class WorkingTime
24
{
25
    /**
26
     * @var \DateTime
27
     */
28
    public $dateTime;
29
30
    /**
31
     * @var array
32
     */
33
    public $workingDays;
34
35
    /**
36
     * @var array
37
     */
38
    public $weekends;
39
40
    /**
41
     * @var array
42
     */
43
    public $holidays;
44
45
    /**
46
     * WorkingTime constructor.
47
     *
48
     * @param array $workTimeConfig
49
     * @param string $dateTime
50
     */
51 14
    public function __construct(array $workTimeConfig, string $dateTime = 'now')
52
    {
53 14
        $this->dateTime = new \DateTime($dateTime);
54 14
        foreach ($workTimeConfig as $name => $value) {
55 14
            $this->$name = $value;
56
        }
57 14
    }
58
59
    /**
60
     * Фурмирует строку дня.
61
     * @param null|string $date
62
     * @param string $format
63
     * @return string
64
     */
65 11
    private function buildDataPartString(?string $date, string $format): string
66
    {
67 11
        if ($date === null) {
68 1
            $day = date($format, $this->dateTime->getTimestamp());
69
        } else {
70 11
            $day = date($format, strtotime($date));
71
        }
72
73 11
        return $day;
74
    }
75
76
    /**
77
     * Формирует дату из строки.
78
     *
79
     * @param string $date
80
     * @return \DateTime
81
     */
82 7
    private function buildDate(string $date = null): \DateTime
83
    {
84 7
        if ($date === null) {
85 1
            return new \DateTime($this->dateTime->format('Y-m-d H:i'));
86
        } else {
87 7
            return new \DateTime($date);
88
        }
89
    }
90
91
    /**
92
     * Проверяет является ли дата праздничным днём.
93
     *
94
     * @param string $date
95
     * @return bool
96
     */
97 11
    public function isHoliday(string $date = null): bool
98
    {
99 11
        if (empty($this->holidays)) {
100
            // Если не указаны праздничные дни, то день рабочий.
101
            return false;
102
        }
103
104 11
        $day = $this->buildDataPartString($date, 'm-d');
105 11
        return in_array($day, $this->holidays, false);
106
    }
107
108
    /**
109
     * Проверяет является ли дата выходным днём.
110
     *
111
     * @param string $date
112
     * @return bool
113
     */
114 11
    public function isWeekend(string $date = null): bool
115
    {
116 11
        if (empty($this->weekends)) {
117
            // Если не указаны выходные дни, то день рабочий.
118
            return false;
119
        }
120
121 11
        $day = $this->buildDataPartString($date, 'w');
122 11
        return in_array($day, $this->weekends, false);
123
    }
124
125
    /**
126
     * Проверяет евляется ли дата рабочим днём.
127
     *
128
     * Формат даты - "Y-m-d"
129
     * @param string $date
130
     * @return bool
131
     */
132 10
    public function isWorkingDate(string $date = null): bool
133
    {
134 10
        if ($this->isWeekend($date) || $this->isHoliday($date)) {
135
            // Выходной или праздничный день
136 5
            return false;
137
        } else {
138
            // Рабочий день
139 10
            return true;
140
        }
141
    }
142
143
    /**
144
     * Проверяет евляется ли время рабочим.
145
     *
146
     * @param string|null $time Формат времени - "H:i" или полный "Y-m-d H:i"
147
     * @return bool
148
     * @throws \InvalidArgumentException
149
     */
150 2
    public function isWorkingTime(string $time = null): bool
151
    {
152 2
        if ($time === null) {
153 1
            $dateTime = $this->dateTime;
154
        } else {
155 2
            if (self::validateDate($time, 'H:i')) {
156 1
                $dateTime = new \DateTime($this->dateTime->format('Y-m-d') . ' ' . $time);
157 2
            } elseif (self::validateDate($time, 'Y-m-d H:i')) {
158 1
                $dateTime = new \DateTime($time);
159
            } else {
160 1
                throw new \InvalidArgumentException("Date `{$time}` isn't a valid date. Dates should be formatted as Y-m-d or H:i, e.g. `2016-10-27` or `17:30`.");
161
            }
162
        }
163
164 1
        list($jobStart, $jobEnd) = explode('-', $this->workingDays[$dateTime->format('w')]);
165
166 1
        $dtStart = new \DateTime($dateTime->format('Y-m-d') . ' ' . $jobStart);
167 1
        $dtEnd = new \DateTime($dateTime->format('Y-m-d') . ' ' . $jobEnd);
168
169 1
        return $dateTime > $dtStart && $dateTime < $dtEnd;
170
    }
171
172
    /**
173
     * Возвращает следующий рабочий день.
174
     *
175
     * @param string|null $date Формат даты - "Y-m-d"
176
     * @return string
177
     */
178 6
    public function nextWorkingDay(string $date = null): string
179
    {
180 6
        if ($date === null) {
181 1
            $dateTime = new \DateTime($this->dateTime->format('Y-m-d'));
182
        } else {
183 6
            $dateTime = new \DateTime($date);
184
        }
185
186
        do {
187 6
            $dateTime->modify('+1 day');
188 6
        } while (!$this->isWorkingDate($dateTime->format('Y-m-d')));
189
190 6
        return $dateTime->format('Y-m-d');
191
    }
192
193
    /**
194
     * Возвращает ближайшее рабочее время. Либо null если текущее время уже рабочее.
195
     *
196
     * @param string|null $date
197
     * @return null|string
198
     */
199 7
    public function nextWorkingTime(string $date = null): ?string
200
    {
201 7
        $dateTime = $this->buildDate($date);
202 7
        $nextWorkingTime = null;
203
204
        // Если дня нет в конфиге считаем его выходным
205 7
        if (!array_key_exists($dateTime->format('w'), $this->workingDays)) {
206
            $nextWorkingDay = $this->nextWorkingDay($dateTime->format('Y-m-d'));
207
            $nWDateTime = new \DateTime($nextWorkingDay);
208
            $workTime = explode('-', $this->workingDays[$nWDateTime->format('w')]);
209
            return $nextWorkingDay . ' ' . $workTime[0];
210
        }
211
212 7
        list($jobStart, $jobEnd) = explode('-', $this->workingDays[$dateTime->format('w')]);
213
214 7
        if ($this->isWorkingDate($dateTime->format('Y-m-d'))) { // Если день рабочий проверяем время
215
216 7
            $dtStart = new \DateTime($dateTime->format('Y-m-d') . ' ' . $jobStart);
217 7
            $dtEnd = new \DateTime($dateTime->format('Y-m-d') . ' ' . $jobEnd);
218
219
            // Если начало дня еще не наступило (утро) возвращаем указанную дату + время
220 7
            if ($dateTime < $dtStart) {
221 4
                $nextWorkingTime = $dateTime->format('Y-m-d') . ' ' . $jobStart;
222 6
            } elseif ($dateTime >= $dtEnd) { // Если рабочий день уже закончился
223
                // Ищем следующий рабочий день и выводим его + время начало дня
224 7
                $nextWorkingTime = $this->nextWorkingDay($dateTime->format('Y-m-d')) . ' ' . $jobStart;
225
            }
226
        } else { // Если день не рабочий
227
228
            // Ищем следующий рабочий день и выводим его + время начало дня
229 1
            $nextWorkingTime = $this->nextWorkingDay($dateTime->format('Y-m-d')) . ' ' . $jobStart;
230
        }
231
232 7
        return $nextWorkingTime;
233
    }
234
235
    /**
236
     * Возвращает дату время начала следующего дня.
237
     *
238
     * @param string|null $date
239
     * @return string
240
     */
241 2
    public function nextWorkingDayStart(string $date = null): string
242
    {
243 2
        $nextWorkingDayDT = new \DateTime($this->nextWorkingDay($date));
244 2
        $day = $nextWorkingDayDT->format('w');
245 2
        $jobStart = explode('-', $this->workingDays[$day])[0];
246
247 2
        return $nextWorkingDayDT->format('Y-m-d') . ' ' . $jobStart;
248
    }
249
250
    /**
251
     * Возвращает дату время начала следующего дня.
252
     *
253
     * @param string|null $date
254
     * @return string
255
     */
256 1
    private function nextWorkingDayEnd(string $date = null): string
257
    {
258 1
        $nextWorkingDayDT = new \DateTime($this->nextWorkingDay($date));
259 1
        $day = $nextWorkingDayDT->format('w');
260 1
        $jobEnd = explode('-', $this->workingDays[$day])[1];
261
262 1
        return $nextWorkingDayDT->format('Y-m-d') . ' ' . $jobEnd;
263
    }
264
265
    /**
266
     * Возвращает длинну рабочего дня в минутах.
267
     *
268
     * @param string|null $date Формат даты - "Y-m-d"
269
     * @return int
270
     */
271 6
    public function getJobMinutesInDay(string $date = null): int
272
    {
273 6
        $day = $this->buildDataPartString($date, 'w');
274 6
        $nextWorkingTime = $this->nextWorkingTime($date ?? $this->dateTime->format('Y-m-d H:i'));
275
276 6
        list($jobStart, $jobEnd) = explode('-', $this->workingDays[$day]);
277
        // Считаем остаток рабочего времени
278 6
        if ($nextWorkingTime === null) {
279 5
            $jobStart = ($date === null ? date('H:i', $this->dateTime->getTimestamp()) : date('H:i', strtotime($date)));
280
        }
281
282 6
        $dtStart = new \DateTime($jobStart);
283 6
        $dtEnd = new \DateTime($jobEnd);
284 6
        $diff = $dtEnd->diff($dtStart);
285
286 6
        return ($diff->h * 60 + $diff->i);
287
    }
288
289
    /**
290
     * Прибавляет заданное количество минут к дате с учетом рабочего времени.
291
     *
292
     * @param int $minutes
293
     * @param string $date
294
     * @return \DateTime
295
     */
296 3
    private function modifyDate(int $minutes, string $date): \DateTime
297
    {
298 3
        $dateTime = new \DateTime($date);
299 3
        $jobMinutesInDay = $this->getJobMinutesInDay($date);
300
301
        // Если длинна дня больше чем время модификации
302 3
        if ($jobMinutesInDay > $minutes) {
303 3
            $dateTime->modify("+$minutes minutes");
304
        } else { // Если длинна дня меньше чем время модификации
305
            do {
306 1
                $dateTime->modify("+$jobMinutesInDay minutes");
307 1
                $minutes -= $jobMinutesInDay;
308 1
                $nextWorkingTime = $this->nextWorkingTime($dateTime->format('Y-m-d H:i'));
309 1
                $dateTime = new \DateTime($nextWorkingTime);
310 1
                $jobMinutesInNextDay = $this->getJobMinutesInDay($dateTime->format('Y-m-d H:i'));
311 1
                if ($jobMinutesInNextDay > $minutes) {
312 1
                    $dateTime->modify("+$minutes minutes");
313 1
                    $minutes = 0;
314
                }
315 1
            } while ($minutes > 0);
316
        }
317
318 3
        return $dateTime;
319
    }
320
321
    /**
322
     * Прибавляет заданное количество минут к дате с учетом рабочего времени.
323
     *
324
     * @param int $minutes
325
     * @param string $date
326
     * @return string
327
     */
328 3
    public function modify(int $minutes, string $date = null): string
329
    {
330 3
        $nextWorkingTime = $this->nextWorkingTime($date);
331
        // Если дата вне рабочего времени
332 3
        if ($nextWorkingTime !== null) {
333 3
            $dateTime = $this->modifyDate($minutes, $nextWorkingTime);
334
        } else { // если дата в пределах рабочего времени
335 2
            $dateTime = $this->modifyDate($minutes, $date ?? $this->dateTime->format('Y-m-d H:i'));
336
        }
337 3
        if ($date === null) {
338 1
            $this->dateTime->setTimestamp($dateTime->getTimestamp());
339
        }
340 3
        return $dateTime->format('Y-m-d H:i');
341
    }
342
343
    /**
344
     * Возвращает рабочее время в минутах в заданном временном интервале.
345
     *
346
     * @param string $startDate
347
     * @param string $endDate
348
     * @return int
349
     * @throws \InvalidArgumentException
350
     */
351 1
    public function calculatingWorkingTime(string $startDate, string $endDate): int
352
    {
353 1
        if (!self::validateDate($startDate) || !self::validateDate($endDate)) {
354
            throw new \InvalidArgumentException("Date isn't a valid date. Dates should be formatted as Y-m-d H:i:s, e.g. `2016-10-27 17:30:00`.");
355
        }
356
357 1
        $dtStart = $this->buildDate($startDate);
358 1
        $dtEnd = $this->buildDate($endDate);
359
360 1
        $diff = $dtEnd->diff($dtStart);
361 1
        $diffMinutes = $diff->d * 1440 + $diff->h * 60 + $diff->i;
362 1
        $jobMinutesInDay = $this->getJobMinutesInDay($dtStart->format('Y-m-d H:i'));
363
364
        // Если разница во времени больше длинны рабочего дня
365 1
        if ($diffMinutes > $jobMinutesInDay) {
366
367 1
            $nextWorkingDayStartDT = new \DateTime($this->nextWorkingDayStart($dtStart->format('Y-m-d')));
368 1
            $nextWorkingDayEndDT = new \DateTime($this->nextWorkingDayEnd($dtStart->format('Y-m-d')));
369
370
            // Дата находится в промежутке ДО наступления следующего рабочего времени
371 1
            if ($nextWorkingDayStartDT > $dtEnd) {
372
                return $jobMinutesInDay; // Возвращает остатки рабочего времени
373 1
            } elseif ($nextWorkingDayStartDT < $dtEnd && $dtEnd < $nextWorkingDayEndDT) {
374
                // Дата в промежутке следующего рабочего дня
375 1
                $nextDiff = $dtEnd->diff($nextWorkingDayStartDT);
376 1
                $nextDiffMinutes = $nextDiff->d * 1440 + $nextDiff->h * 60 + $nextDiff->i;
377 1
                return $nextDiffMinutes + $jobMinutesInDay;
378
            }
379
380
            return 1;
381
        } else {
382 1
            return $diffMinutes;
383
        }
384
    }
385
386
    /**
387
     * Проверяет является ли строка корректной датой.
388
     *
389
     * @param $date
390
     * @param string $format
391
     * @return bool
392
     */
393 4
    public static function validateDate(string $date, string $format = 'Y-m-d H:i:s'): bool
394
    {
395 4
        $vDate = \DateTime::createFromFormat($format, $date);
396 4
        return $vDate && $vDate->format($format) === $date;
397
    }
398
}
399