WorkingDateTime::setExceptionDates()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
eloc 2
c 1
b 0
f 1
dl 0
loc 5
rs 10
cc 1
nc 1
nop 1
1
<?php
2
/**
3
 * PHP 8.4
4
 * Created by PhpStorm.
5
 *
6
 * @author    : Oleh Boiko <[email protected]> | <https://mackrais.com>
7
 * @license   MIT License
8
 * @copyright Copyright (c) 2016 - 2025, MackRais
9
 */
10
11
declare(strict_types=1);
12
13
namespace MackRais\DateTime;
14
15
use MackRais\DateTime\Exception\MaxAttemptsException;
16
17
final class WorkingDateTime
18
{
19
    private int $maxAttempts;
20
    private int $dayHourStart;
21
    private int $dayMinutesStart;
22
    private int $dayHourEnd;
23
    private int $dayMinutesEnd;
24
25
    /**
26
     * Gets name weekends day (days of the week).
27
     *
28
     * For example two days ['Sunday','Saturday']
29
     *
30
     * List all days ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
31
     *
32
     * @var array
33
     */
34
    private array $weekends = [];
35
36
    /**
37
     * Exception dates.
38
     *
39
     * Use two formats:
40
     *
41
     * MM-DD - month and day
42
     * YYYY-MM-DD - full data
43
     *
44
     * For example Each year for the new year is weekend
45
     * [ '01-01' ]
46
     *
47
     * Weekend once a time (for example Easter) because in next year it`s new date
48
     * [ '2018-04-08' ]
49
     *
50
     * @var array
51
     */
52
    private array $exceptionDates = [];
53
    private int $years = 0;
54
    private int $months = 0;
55
    private int $days = 0;
56
    private int $hours = 0;
57
    private int $minutes = 0;
58
    private int $seconds = 0;
59
    private string $dateFrom;
60
    private bool $reverse = false;
61
62
    /**
63
     * WorkingDateTime constructor.
64
     */
65
    public function __construct()
66
    {
67
        $this->dateFrom = date('Y-m-d H:i:s');
68
        $this->dayHourStart = 6;
69
        $this->dayMinutesStart = 0;
70
        $this->dayHourEnd = 23;
71
        $this->dayMinutesEnd = 0;
72
        $this->maxAttempts = PHP_INT_MAX;
73
    }
74
75
    public function calculate(): \DateTime
76
    {
77
        $datetime = new \DateTime($this->dateFrom);
78
        $endOfDay = $this->getEndOfDay($datetime);
79
        $startOfDay = $this->getStartOfDay($datetime);
80
81
        $interval = $this->generateIntervalString();
82
83
        if ($this->reverse) {
84
            return $this->calculateReverse($datetime, $startOfDay, $endOfDay, $interval);
85
        }
86
87
        return $this->calculateForward($datetime, $startOfDay, $endOfDay, $interval);
88
    }
89
90
    public function setDateFrom(string $date): self
91
    {
92
        $this->dateFrom = $date;
93
94
        return $this;
95
    }
96
97
    public function setStartHourWorkingDay(int $hour): self
98
    {
99
        $this->dayHourStart = $hour;
100
101
        return $this;
102
    }
103
104
    public function setStartMinuteWorkingDay(int $minute): self
105
    {
106
        $this->dayMinutesStart = $minute;
107
108
        return $this;
109
    }
110
111
    public function setEndHourWorkingDay(int $hour): self
112
    {
113
        $this->dayHourEnd = $hour;
114
115
        return $this;
116
    }
117
118
    public function setEndMinuteWorkingDay(int $minute): self
119
    {
120
        $this->dayMinutesEnd = $minute;
121
122
        return $this;
123
    }
124
125
    public function setYears(int $years): self
126
    {
127
        $this->years = $years;
128
129
        return $this;
130
    }
131
132
    public function setMonths(int $months): self
133
    {
134
        $this->months = $months;
135
136
        return $this;
137
    }
138
139
    public function setDays(int $days): self
140
    {
141
        $this->days = $days;
142
143
        return $this;
144
    }
145
146
    public function setHours(int $hours): self
147
    {
148
        $this->hours = $hours;
149
150
        return $this;
151
    }
152
153
    public function setMinutes(int $minutes): self
154
    {
155
        $this->minutes = $minutes;
156
157
        return $this;
158
    }
159
160
    public function setSeconds(int $seconds): self
161
    {
162
        $this->seconds = $seconds;
163
164
        return $this;
165
    }
166
167
    public function asReverse(): self
168
    {
169
        $this->reverse = true;
170
171
        return $this;
172
    }
173
174
    public function setWeekends(array $days): self
175
    {
176
        $this->weekends = $days;
177
178
        return $this;
179
    }
180
181
    public function setExceptionDates(array $dates): self
182
    {
183
        $this->exceptionDates = $dates;
184
185
        return $this;
186
    }
187
188
    /**
189
     * Calculates in reverse mode.
190
     */
191
    private function calculateReverse(\DateTime $datetime, \DateTime $startOfDay, \DateTime $endOfDay, string $interval): \DateTime
192
    {
193
        $datetime->sub(new \DateInterval($interval));
194
195
        if ($datetime < $startOfDay) {
196
            return $this->adjustReverseTime($datetime, $startOfDay, $endOfDay);
197
        }
198
199
        return $datetime;
200
    }
201
202
    /**
203
     * Calculates in forward mode.
204
     */
205
    private function calculateForward(\DateTime $datetime, \DateTime $startOfDay, \DateTime $endOfDay, string $interval): \DateTime
0 ignored issues
show
Unused Code introduced by
The parameter $startOfDay is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

205
    private function calculateForward(\DateTime $datetime, /** @scrutinizer ignore-unused */ \DateTime $startOfDay, \DateTime $endOfDay, string $interval): \DateTime

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
206
    {
207
        $datetime->add(new \DateInterval($interval));
208
209
        if ($datetime > $endOfDay) {
210
            return $this->adjustForwardTime($datetime, $endOfDay);
211
        }
212
213
        return $datetime;
214
    }
215
216
    /**
217
     * Adjusts time when moving to the previous working day in reverse mode.
218
     */
219
    private function adjustReverseTime(\DateTime $datetime, \DateTime $startOfDay, \DateTime $endOfDay): \DateTime
220
    {
221
        $seconds = $startOfDay->getTimestamp() - $datetime->getTimestamp();
222
        $attempts = 0;
223
224
        while ($attempts <= $this->maxAttempts) {
225
            $endOfDay->sub(new \DateInterval('PT24H')); // Перехід на попередній день
226
            $prevDay = $this->setEndOfDay($endOfDay);
227
228
            if ($this->isNonWorkingDay($prevDay)) {
229
                continue;
230
            }
231
232
            $tmpDate = $this->setStartOfDay(clone $prevDay);
233
            $prevDay->sub(new \DateInterval('PT'.abs($seconds).'S'));
234
235
            if ($prevDay < $tmpDate) {
236
                $seconds = $tmpDate->getTimestamp() - $prevDay->getTimestamp();
237
                $endOfDay = $this->setEndOfDay(clone $tmpDate);
238
            } else {
239
                return $endOfDay;
240
            }
241
            $attempts++;
242
        }
243
244
        throw new MaxAttemptsException('Unable to adjust time');
245
    }
246
247
    /**
248
     * Adjusts time when carrying over to the next working day.
249
     */
250
    private function adjustForwardTime(\DateTime $datetime, \DateTime $endOfDay): \DateTime
251
    {
252
        $seconds = $datetime->getTimestamp() - $endOfDay->getTimestamp();
253
        $attempts = 0;
254
255
        while ($attempts <= $this->maxAttempts) {
256
            $endOfDay->add(new \DateInterval('PT24H'));
257
            $nextDay = $this->setStartOfDay($endOfDay);
258
259
            if ($this->isNonWorkingDay($nextDay)) {
260
                continue;
261
            }
262
263
            $tmpDate = $this->setEndOfDay(clone $nextDay);
264
            $nextDay->add(new \DateInterval('PT'.abs($seconds).'S'));
265
266
            if ($nextDay > $tmpDate) {
267
                $seconds = $nextDay->getTimestamp() - $tmpDate->getTimestamp();
268
                $endOfDay = $this->setStartOfDay(clone $tmpDate);
269
            } else {
270
                return $endOfDay;
271
            }
272
            $attempts++;
273
        }
274
275
        throw new MaxAttemptsException('Unable to adjust time');
276
    }
277
278
    private function getStartOfDay(\DateTime $date): \DateTime
279
    {
280
        return (clone $date)->setTime($this->dayHourStart, $this->dayMinutesStart);
281
    }
282
283
    private function getEndOfDay(\DateTime $date): \DateTime
284
    {
285
        return (clone $date)->setTime($this->dayHourEnd, $this->dayMinutesEnd);
286
    }
287
288
    private function setEndOfDay(\DateTime $date): \DateTime
289
    {
290
        return $date->setTime($this->dayHourEnd, $this->dayMinutesEnd);
291
    }
292
293
    private function setStartOfDay(\DateTime $date): \DateTime
294
    {
295
        return $date->setTime($this->dayHourStart, $this->dayMinutesStart);
296
    }
297
298
    private function isNonWorkingDay(\DateTime $date): bool
299
    {
300
        return in_array($date->format('l'), $this->weekends, true) || $this->isExceptionDate($date);
301
    }
302
303
    private function isExceptionDate(\DateTime $dateTime): bool
304
    {
305
        if (!empty($this->exceptionDates)) {
306
            $continue = false;
307
            foreach ($this->exceptionDates as $eDate) {
308
                $eMonthAndDay = \DateTime::createFromFormat('m-d', $eDate);
309
                $eFullDate = \DateTime::createFromFormat('Y-m-d', $eDate);
310
                if (!empty($eMonthAndDay) && $dateTime->format('md') === $eMonthAndDay->format('md')) {
311
                    $continue = true;
312
                }
313
                if (!empty($eFullDate) && $dateTime->format('Ymd') === $eFullDate->format('Ymd')) {
314
                    $continue = true;
315
                }
316
                if ($continue) {
317
                    break;
318
                }
319
            }
320
321
            return $continue;
322
        }
323
324
        return false;
325
    }
326
327
    private function generateIntervalString(): string
328
    {
329
        return "P{$this->years}Y{$this->months}M{$this->days}DT{$this->hours}H{$this->minutes}M{$this->seconds}S";
330
    }
331
}
332