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.0 |
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
|
|
|
public function __construct(array $workTimeConfig, string $dateTime = 'now') |
52
|
|
|
{ |
53
|
|
|
$this->dateTime = new \DateTime($dateTime); |
54
|
|
|
foreach ($workTimeConfig as $name => $value) { |
55
|
|
|
$this->$name = $value; |
56
|
|
|
} |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Фурмирует строку дня. |
61
|
|
|
* |
62
|
|
|
* @param string $date |
63
|
|
|
* @param string $format |
64
|
|
|
* @return string |
65
|
|
|
*/ |
66
|
|
|
private function buildDataPartString(string $date, string $format) : string |
67
|
|
|
{ |
68
|
|
|
if ($date === null) { |
69
|
|
|
$day = date($format, $this->dateTime->getTimestamp()); |
70
|
|
|
} else { |
71
|
|
|
$day = date($format, strtotime($date)); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
return $day; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Формирует дату из строки. |
79
|
|
|
* |
80
|
|
|
* @param string $date |
81
|
|
|
* @return \DateTime |
82
|
|
|
*/ |
83
|
|
|
private function buildDate(string $date) : \DateTime |
84
|
|
|
{ |
85
|
|
|
if ($date === null) { |
86
|
|
|
return new \DateTime($this->dateTime->format('Y-m-d H:i')); |
87
|
|
|
} else { |
88
|
|
|
return new \DateTime($date); |
89
|
|
|
} |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Проверяет является ли дата праздничным днём. |
94
|
|
|
* |
95
|
|
|
* @param string $date |
96
|
|
|
* @return bool |
97
|
|
|
*/ |
98
|
|
|
public function isHoliday(string $date = null) : bool |
99
|
|
|
{ |
100
|
|
|
$day = $this->buildDataPartString($date, 'm-d'); |
101
|
|
|
return in_array($day, $this->holidays, false); |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Проверяет является ли дата выходным днём. |
106
|
|
|
* |
107
|
|
|
* @param string $date |
108
|
|
|
* @return bool |
109
|
|
|
*/ |
110
|
|
|
public function isWeekend(string $date = null) : bool |
111
|
|
|
{ |
112
|
|
|
$day = $this->buildDataPartString($date, 'w'); |
113
|
|
|
return in_array($day, $this->weekends, false); |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* Проверяет евляется ли дата рабочим днём. |
118
|
|
|
* |
119
|
|
|
* Формат даты - "Y-m-d" |
120
|
|
|
* @param string $date |
121
|
|
|
* @return bool |
122
|
|
|
*/ |
123
|
|
|
public function isWorkingDate(string $date = null) : bool |
124
|
|
|
{ |
125
|
|
|
if ($date === null) { |
126
|
|
|
$unixTime = $this->dateTime->getTimestamp(); |
127
|
|
|
} else { |
128
|
|
|
$unixTime = strtotime($date); |
129
|
|
|
} |
130
|
|
|
|
131
|
|
|
$dayNumber = date('w', $unixTime); |
132
|
|
|
if (in_array($dayNumber, $this->weekends, false)) { |
133
|
|
|
// Это выходной день |
134
|
|
|
return false; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
$dayAndMonth = date('m-d', $unixTime); |
138
|
|
|
if (in_array($dayAndMonth, $this->holidays, false)) { |
139
|
|
|
// Это праздничный день |
140
|
|
|
return false; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
// Рабочий день |
144
|
|
|
return true; |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Проверяет евляется ли время рабочим. |
149
|
|
|
* |
150
|
|
|
* @param string|null $time Формат времени - "H:i" или полный "Y-m-d H:i" |
151
|
|
|
* @return bool |
152
|
|
|
* @throws \Exception |
153
|
|
|
*/ |
154
|
|
|
public function isWorkingTime(string $time = null) : bool |
155
|
|
|
{ |
156
|
|
|
if ($time === null) { |
157
|
|
|
$dateTime = $this->dateTime; |
158
|
|
|
} else { |
159
|
|
|
if (self::validateDate($time, 'H:i')) { |
160
|
|
|
$dateTime = new \DateTime($this->dateTime->format('Y-m-d') . ' ' . $time); |
161
|
|
|
} elseif (self::validateDate($time, 'Y-m-d H:i')) { |
162
|
|
|
$dateTime = new \DateTime($time); |
163
|
|
|
} else { |
164
|
|
|
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`."); |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
list($jobStart, $jobSEnd) = explode('-', $this->workingDays[$dateTime->format('w')]); |
169
|
|
|
|
170
|
|
|
$dtStart = new \DateTime($dateTime->format('Y-m-d') . ' ' . $jobStart); |
171
|
|
|
$dtEnd = new \DateTime($dateTime->format('Y-m-d') . ' ' . $jobSEnd); |
172
|
|
|
|
173
|
|
|
if ($dateTime > $dtStart && $dateTime < $dtEnd) { |
174
|
|
|
return true; |
175
|
|
|
} else { |
176
|
|
|
return false; |
177
|
|
|
} |
178
|
|
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* Возвращает следующий рабочий день. |
182
|
|
|
* |
183
|
|
|
* @param string|null $date Формат даты - "Y-m-d" |
184
|
|
|
* @return string |
185
|
|
|
*/ |
186
|
|
|
public function nextWorkingDay(string $date = null) : string |
187
|
|
|
{ |
188
|
|
|
if ($date === null) { |
189
|
|
|
$dateTime = new \DateTime($this->dateTime->format('Y-m-d')); |
190
|
|
|
} else { |
191
|
|
|
$dateTime = new \DateTime($date); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
do { |
195
|
|
|
$dateTime->modify('+1 day'); |
196
|
|
|
} while (!$this->isWorkingDate($dateTime->format('Y-m-d'))); |
197
|
|
|
|
198
|
|
|
return $dateTime->format('Y-m-d'); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
/** |
202
|
|
|
* Возвращает ближайшее рабочее время. Либо пустую строку если текущее время уже рабочее |
203
|
|
|
* |
204
|
|
|
* @param string|null $date |
205
|
|
|
* @return string |
206
|
|
|
*/ |
207
|
|
|
public function nextWorkingTime(string $date = null) : string |
208
|
|
|
{ |
209
|
|
|
$dateTime = $this->buildDate($date); |
210
|
|
|
|
211
|
|
|
$nextWorkingTime = ''; |
212
|
|
|
|
213
|
|
|
// Если дня нет в конфиге считаем его выходным |
214
|
|
|
if (!isset($this->workingDays[$dateTime->format('w')])) { |
215
|
|
|
$nextWorkingDay = $this->nextWorkingDay($dateTime->format('Y-m-d')); |
216
|
|
|
$nWDateTime = new \DateTime($nextWorkingDay); |
217
|
|
|
$workTime = explode('-', $this->workingDays[$nWDateTime->format('w')]); |
218
|
|
|
return $nextWorkingDay . ' ' . $workTime[0]; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
list($jobStart, $jobSEnd) = explode('-', $this->workingDays[$dateTime->format('w')]); |
222
|
|
|
|
223
|
|
|
if ($this->isWorkingDate($dateTime->format('Y-m-d'))) { // Если день рабочий проверяем время |
224
|
|
|
|
225
|
|
|
$dtStart = new \DateTime($dateTime->format('Y-m-d') . ' ' . $jobStart); |
226
|
|
|
$dtEnd = new \DateTime($dateTime->format('Y-m-d') . ' ' . $jobSEnd); |
227
|
|
|
|
228
|
|
|
// Если начало дня еще не наступило (утро) возвращаем указанную дату + время |
229
|
|
|
if ($dateTime < $dtStart) { |
230
|
|
|
$nextWorkingTime = $dateTime->format('Y-m-d') . ' ' . $jobStart; |
231
|
|
|
} elseif ($dateTime >= $dtEnd) { // Если рабочий день уже закончился |
232
|
|
|
// Ищем следующий рабочий день и выводим его + время начало дня |
233
|
|
|
$nextWorkingTime = $this->nextWorkingDay($dateTime->format('Y-m-d')) . ' ' . $jobStart; |
234
|
|
|
} |
235
|
|
|
} else { // Если день не рабочий |
236
|
|
|
|
237
|
|
|
// Ищем следующий рабочий день и выводим его + время начало дня |
238
|
|
|
$nextWorkingTime = $this->nextWorkingDay($dateTime->format('Y-m-d')) . ' ' . $jobStart; |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
return $nextWorkingTime; |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* Возвращает длинну рабочего дня в минутах. |
246
|
|
|
* |
247
|
|
|
* @param string|null $date Формат даты - "Y-m-d" |
248
|
|
|
* @return int |
249
|
|
|
*/ |
250
|
|
|
public function getJobMinutesInDay(string $date = null) : int |
251
|
|
|
{ |
252
|
|
|
$day = $this->buildDataPartString($date, 'w'); |
253
|
|
|
|
254
|
|
|
$nextWorkingTime = $this->nextWorkingTime($date ?? $this->dateTime->format('Y-m-d H:i')); |
255
|
|
|
|
256
|
|
|
list($jobStart, $jobSEnd) = explode('-', $this->workingDays[(int)$day]); |
257
|
|
|
// Считаем остаток рабочего времени |
258
|
|
|
if (empty($nextWorkingTime)) { |
259
|
|
|
$jobStart = ($date === null ? date('H:i', $this->dateTime->getTimestamp()) : date('H:i', strtotime($date))); |
260
|
|
|
} |
261
|
|
|
|
262
|
|
|
$dtStart = new \DateTime($jobStart); |
263
|
|
|
$dtEnd = new \DateTime($jobSEnd); |
264
|
|
|
$diff = $dtEnd->diff($dtStart); |
265
|
|
|
|
266
|
|
|
return ($diff->h * 60 + $diff->i); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Прибавляет заданное количество минут к дате с учетом рабочего времени. |
271
|
|
|
* |
272
|
|
|
* @param int $minutes |
273
|
|
|
* @param string $date |
274
|
|
|
* @return string |
275
|
|
|
*/ |
276
|
|
|
public function modify(int $minutes, string $date = null) : string |
277
|
|
|
{ |
278
|
|
|
$dateTime = $this->buildDate($date); |
279
|
|
|
|
280
|
|
|
$nextWorkingTime = $this->nextWorkingTime($dateTime->format('Y-m-d H:i')); |
281
|
|
|
|
282
|
|
|
// Если дата вне рабочего времени |
283
|
|
|
if (!empty($nextWorkingTime)) { |
284
|
|
|
$nextJobDT = new \DateTime($nextWorkingTime); |
285
|
|
|
$jobMinutesInDay = $this->getJobMinutesInDay($nextJobDT->format('Y-m-d')); |
286
|
|
|
|
287
|
|
|
// Если длинна дня больше чем время модификации |
288
|
|
View Code Duplication |
if ($jobMinutesInDay > $minutes) { |
|
|
|
|
289
|
|
|
$nextJobDT->modify("+$minutes minutes"); |
290
|
|
|
} else { // Если длинна дня меньше чем время модификации |
291
|
|
|
|
292
|
|
|
do { |
293
|
|
|
$nextJobDT->modify("+$jobMinutesInDay minutes"); |
294
|
|
|
$minutes -= $jobMinutesInDay; |
295
|
|
|
$nextWorkingTime = $this->nextWorkingTime($nextJobDT->format('Y-m-d H:i')); |
296
|
|
|
$nextJobDT = new \DateTime($nextWorkingTime); |
297
|
|
|
$jobMinutesInDay = $this->getJobMinutesInDay($nextJobDT->format('Y-m-d')); |
298
|
|
|
if ($jobMinutesInDay > $minutes) { |
299
|
|
|
$nextJobDT->modify("+$minutes minutes"); |
300
|
|
|
$minutes = 0; |
301
|
|
|
} |
302
|
|
|
} while ($minutes > 0); |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
if ($date === null) { |
306
|
|
|
$this->dateTime->setTimestamp($nextJobDT->getTimestamp()); |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
return $nextJobDT->format('Y-m-d H:i'); |
310
|
|
|
} else { // если дата в пределах рабочего времени |
311
|
|
|
|
312
|
|
|
$jobMinutesInDay = $this->getJobMinutesInDay($dateTime->format('Y-m-d')); |
313
|
|
|
|
314
|
|
View Code Duplication |
if ($jobMinutesInDay > $minutes) { |
|
|
|
|
315
|
|
|
$dateTime->modify("+$minutes minutes"); |
316
|
|
|
} else { // Если длинна дня меньше чем время модификации |
317
|
|
|
|
318
|
|
|
do { |
319
|
|
|
$dateTime->modify("+$jobMinutesInDay minutes"); |
320
|
|
|
$minutes -= $jobMinutesInDay; |
321
|
|
|
$nextWorkingTime = $this->nextWorkingTime($dateTime->format('Y-m-d H:i')); |
322
|
|
|
$dateTime = new \DateTime($nextWorkingTime); |
323
|
|
|
$jobMinutesInDay = $this->getJobMinutesInDay($dateTime->format('Y-m-d')); |
324
|
|
|
if ($jobMinutesInDay > $minutes) { |
325
|
|
|
$dateTime->modify("+$minutes minutes"); |
326
|
|
|
$minutes = 0; |
327
|
|
|
} |
328
|
|
|
} while ($minutes > 0); |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
if ($date === null) { |
332
|
|
|
$this->dateTime->setTimestamp($dateTime->getTimestamp()); |
333
|
|
|
} |
334
|
|
|
|
335
|
|
|
return $dateTime->format('Y-m-d H:i'); |
336
|
|
|
} |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Проверяет является ли строка корректной датой. |
341
|
|
|
* |
342
|
|
|
* @param $date |
343
|
|
|
* @param string $format |
344
|
|
|
* @return bool |
345
|
|
|
*/ |
346
|
|
|
public static function validateDate(string $date, string $format = 'Y-m-d H:i:s') : bool |
347
|
|
|
{ |
348
|
|
|
$vDate = \DateTime::createFromFormat($format, $date); |
349
|
|
|
return $vDate && $vDate->format($format) === $date; |
350
|
|
|
} |
351
|
|
|
} |
352
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.