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