AbstractProvider::compareDates()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 7
c 0
b 0
f 0
rs 10
cc 3
nc 3
nop 2
1
<?php declare(strict_types=1);
2
/**
3
 * This file is part of the Yasumi package.
4
 *
5
 * Copyright (c) 2015 - 2020 AzuyaLabs
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 *
10
 * @author Sacha Telgenhof <[email protected]>
11
 */
12
13
namespace Yasumi\Provider;
14
15
use ArrayIterator;
16
use Countable;
17
use InvalidArgumentException;
18
use IteratorAggregate;
19
use Yasumi\Exception\InvalidDateException;
20
use Yasumi\Exception\UnknownLocaleException;
21
use Yasumi\Filters\BetweenFilter;
22
use Yasumi\Filters\OnFilter;
23
use Yasumi\Holiday;
24
use Yasumi\ProviderInterface;
25
use Yasumi\SubstituteHoliday;
26
use Yasumi\TranslationsInterface;
27
use Yasumi\Yasumi;
28
29
/**
30
 * Class AbstractProvider.
31
 */
32
abstract class AbstractProvider implements ProviderInterface, Countable, IteratorAggregate
33
{
34
    /**
35
     * Code to identify the Holiday Provider. Typically this is the ISO3166 code corresponding to the respective
36
     * country or sub-region.
37
     */
38
    public const ID = 'US';
39
40
    /**
41
     * @var array list of the days of the week (the index of the weekdays) that are considered weekend days.
42
     *            This list only concerns those countries that deviate from the global common definition,
43
     *            where the weekend starts on Saturday and ends on Sunday (0 = Sunday, 1 = Monday, etc.).
44
     */
45
    public const WEEKEND_DATA = [
46
47
        // Thursday and Friday
48
        'AF' => [4, 5], // Afghanistan
49
50
        // Friday and Saturday
51
        'AE' => [5, 6], // United Arab Emirates
52
        'BH' => [5, 6], // Bahrain
53
        'DZ' => [5, 6], // Algeria
54
        'EG' => [5, 6], // Egypt
55
        'IL' => [5, 6], // Israel
56
        'IQ' => [5, 6], // Iraq
57
        'JO' => [5, 6], // Jordan
58
        'KW' => [5, 6], // Kuwait
59
        'LY' => [5, 6], // Libya
60
        'MA' => [5, 6], // Morocco
61
        'OM' => [5, 6], // Oman
62
        'QA' => [5, 6], // Qatar
63
        'SA' => [5, 6], // Saudi Arabia
64
        'SD' => [5, 6], // Sudan
65
        'SY' => [5, 6], // Syrian Arab Republic (Syria)
66
        'TN' => [5, 6], // Tunisia
67
        'YE' => [5, 6], // Yemen
68
69
        // Friday
70
        'IR' => [5], // Iran, Islamic Republic of
71
72
        // Sunday
73
        'IN' => [0], // India
74
    ];
75
76
    /**
77
     * @var int the object's current year
78
     */
79
    protected $year;
80
81
    /**
82
     * @var string the object's current timezone
83
     */
84
    protected $timezone;
85
86
    /**
87
     * @var string the object's current locale
88
     */
89
    protected $locale;
90
91
    /**
92
     * @var Holiday[] list of dates of the available holidays
93
     */
94
    private $holidays = [];
95
96
    /**
97
     * @var TranslationsInterface|null global translations
98
     */
99
    private $globalTranslations;
100
101
    /**
102
     * Creates a new holiday provider (i.e. country/state).
103
     *
104
     * @param int $year the year for which to provide holidays
105
     * @param string $locale |null the locale/language in which holidays need to be
106
     *                                                       represented
107
     * @param TranslationsInterface|null $globalTranslations global translations
108
     */
109
    public function __construct(
110
        int $year,
111
        ?string $locale = null,
112
        ?TranslationsInterface $globalTranslations = null
113
    ) {
114
        $this->clearHolidays();
115
116
        $this->year = $year ?: \getdate()['year'];
117
        $this->locale = $locale ?? 'en_US';
118
        $this->globalTranslations = $globalTranslations;
119
120
        $this->initialize();
121
    }
122
123
    /**
124
     * Clear all holidays.
125
     */
126
    protected function clearHolidays(): void
127
    {
128
        $this->holidays = [];
129
    }
130
131
    /**
132
     * Internal function to compare dates in order to sort them chronologically.
133
     *
134
     * @param \DateTimeInterface $dateA First date
135
     * @param \DateTimeInterface $dateB Second date
136
     *
137
     * @return int result where 0 means dates are equal, -1 the first date is before the second date, and 1 if the
138
     *             second date is after the first.
139
     */
140
    private static function compareDates(\DateTimeInterface $dateA, \DateTimeInterface $dateB): int
141
    {
142
        if ($dateA === $dateB) {
143
            return 0;
144
        }
145
146
        return $dateA < $dateB ? -1 : 1;
147
    }
148
149
    /**
150
     * Adds a holiday to the holidays providers (i.e. country/state) list of holidays.
151
     *
152
     * @param Holiday $holiday Holiday instance (representing a holiday) to be added to the internal list
153
     *                         of holidays of this country.
154
     */
155
    public function addHoliday(Holiday $holiday): void
156
    {
157
        if ($this->globalTranslations instanceof TranslationsInterface) {
158
            $holiday->mergeGlobalTranslations($this->globalTranslations);
159
        }
160
161
        $this->holidays[$holiday->getKey()] = $holiday;
162
        \uasort($this->holidays, [__CLASS__, 'compareDates']);
163
    }
164
165
166
    /**
167
     * Removes a holiday from the holidays providers (i.e. country/state) list of holidays.
168
     *
169
     * This function can be helpful in cases where an existing holiday provider class can be extended but some holidays
170
     * are not part of the original (extended) provider.
171
     *
172
     * @param string $key holiday key
173
     *
174
     * @return void
175
     */
176
    public function removeHoliday(string $key): void
177
    {
178
        unset($this->holidays[$key]);
179
    }
180
181
    /**
182
     * Determines whether a date represents a working day or not.
183
     *
184
     * A working day is defined as a day that is not a holiday nor falls in the weekend. The index of the weekdays of
185
     * the defined date is used for establishing this (0 = Sunday, 1 = Monday, etc.)
186
     *
187
     * @param \DateTimeInterface $date any date object that implements the DateTimeInterface (e.g. Yasumi\Holiday,
188
     *                                 \DateTime)
189
     *
190
     * @return bool true if date represents a working day, otherwise false
191
     * @throws InvalidDateException
192
     *
193
     */
194
    public function isWorkingDay(\DateTimeInterface $date): bool
195
    {
196
        return !$this->isHoliday($date) && !$this->isWeekendDay($date);
197
    }
198
199
    /**
200
     * Determines whether a date represents a holiday or not.
201
     *
202
     * @param \DateTimeInterface $date any date object that implements the DateTimeInterface (e.g. Yasumi\Holiday,
203
     *                                 \DateTime)
204
     *
205
     * @return bool true if date represents a holiday, otherwise false
206
     * @throws InvalidDateException
207
     *
208
     */
209
    public function isHoliday(\DateTimeInterface $date): bool
210
    {
211
        // Check if given date is a holiday or not
212
        if (\in_array($date->format('Y-m-d'), \array_values($this->getHolidayDates()), true)) {
213
            return true;
214
        }
215
216
        return false;
217
    }
218
219
    /**
220
     * Gets all of the holiday dates defined by this holiday provider (for the given year).
221
     *
222
     * @return array list of all holiday dates defined for the given year
223
     */
224
    public function getHolidayDates(): array
225
    {
226
        return \array_map(static function ($holiday) {
227
            return (string) $holiday;
228
        }, $this->holidays);
229
    }
230
231
    /**
232
     * Determines whether a date represents a weekend day or not.
233
     *
234
     * @param \DateTimeInterface $date any date object that implements the DateTimeInterface (e.g. Yasumi\Holiday,
235
     *                                 \DateTime)
236
     *
237
     * @return bool true if date represents a weekend day, otherwise false
238
     * @throws InvalidDateException
239
     *
240
     */
241
    public function isWeekendDay(\DateTimeInterface $date): bool
242
    {
243
        // If no data is defined for this Holiday Provider, the function falls back to the global weekend definition.
244
        if (\in_array(
245
            (int) $date->format('w'),
246
            self::WEEKEND_DATA[$this::ID] ?? [0, 6],
247
            true
248
        )
249
        ) {
250
            return true;
251
        }
252
253
        return false;
254
    }
255
256
    /**
257
     * On what date is the given holiday?
258
     *
259
     * @param string $key holiday key
260
     *
261
     * @return string the date of the requested holiday
262
     * @throws InvalidArgumentException when the given name is blank or empty.
263
     *
264
     */
265
    public function whenIs(string $key): string
266
    {
267
        $this->isHolidayNameNotEmpty($key); // Validate if key is not empty
0 ignored issues
show
Deprecated Code introduced by
The function Yasumi\Provider\Abstract...isHolidayNameNotEmpty() has been deprecated: deprecated in favor of isHolidayKeyNotEmpty() ( Ignorable by Annotation )

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

267
        /** @scrutinizer ignore-deprecated */ $this->isHolidayNameNotEmpty($key); // Validate if key is not empty

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
268
269
        return (string) $this->holidays[$key];
270
    }
271
272
    /**
273
     * Checks whether the given holiday is not empty.
274
     *
275
     * @param string $key key of the holiday to be checked.
276
     *
277
     * @return true upon success, otherwise an InvalidArgumentException is thrown
278
     * @throws InvalidArgumentException An InvalidArgumentException is thrown if the given holiday parameter is empty.
279
     */
280
    protected function isHolidayKeyNotEmpty(string $key): bool
281
    {
282
        if (empty($key)) {
283
            throw new InvalidArgumentException('Holiday key can not be blank.');
284
        }
285
286
        return true;
287
    }
288
289
    /**
290
     * Checks whether the given holiday is not empty.
291
     *
292
     * @param string $key key of the holiday to be checked.
293
     *
294
     * @return true upon success, otherwise an InvalidArgumentException is thrown
295
     * @throws InvalidArgumentException An InvalidArgumentException is thrown if the given holiday parameter is empty.
296
     * @deprecated deprecated in favor of isHolidayKeyNotEmpty()
297
     * @deprecated see isHolidayKeyNotEmpty()
298
     */
299
    protected function isHolidayNameNotEmpty(string $key): bool
300
    {
301
        return $this->isHolidayKeyNotEmpty($key);
302
    }
303
304
    /**
305
     * On what day of the week is the given holiday?
306
     *
307
     * This function returns the index number for the day of the week on which the given holiday falls.
308
     * The index number is an integer starting with 0 being Sunday, 1 = Monday, etc.
309
     *
310
     * @param string $key key of the holiday
311
     *
312
     * @return int the index of the weekdays of the requested holiday (0 = Sunday, 1 = Monday, etc.)
313
     * @throws InvalidArgumentException when the given name is blank or empty.
314
     *
315
     */
316
    public function whatWeekDayIs(string $key): int
317
    {
318
        $this->isHolidayNameNotEmpty($key); // Validate if key is not empty
0 ignored issues
show
Deprecated Code introduced by
The function Yasumi\Provider\Abstract...isHolidayNameNotEmpty() has been deprecated: deprecated in favor of isHolidayKeyNotEmpty() ( Ignorable by Annotation )

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

318
        /** @scrutinizer ignore-deprecated */ $this->isHolidayNameNotEmpty($key); // Validate if key is not empty

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
319
320
        return (int) $this->holidays[$key]->format('w');
321
    }
322
323
    /**
324
     * Returns the number of defined holidays (for the given country and the given year).
325
     * In case a holiday is substituted (e.g. observed), the holiday is only counted once.
326
     *
327
     * @return int number of holidays
328
     */
329
    public function count(): int
330
    {
331
        $names = \array_map(static function ($holiday) {
332
            if ($holiday instanceof SubstituteHoliday) {
333
                return $holiday->getSubstitutedHoliday()->getKey();
334
            }
335
336
            return $holiday->getKey();
337
        }, $this->getHolidays());
338
339
        return \count(\array_unique($names));
340
    }
341
342
    /**
343
     * Gets all of the holidays defined by this holiday provider (for the given year).
344
     *
345
     * @return Holiday[] list of all holidays defined for the given year
346
     */
347
    public function getHolidays(): array
348
    {
349
        return $this->holidays;
350
    }
351
352
    /**
353
     * Gets all of the holiday names defined by this holiday provider (for the given year).
354
     *
355
     * @return array list of all holiday names defined for the given year
356
     */
357
    public function getHolidayNames(): array
358
    {
359
        return \array_keys($this->holidays);
360
    }
361
362
    /**
363
     * Returns the current year set for this Holiday calendar.
364
     *
365
     * @return int the year set for this Holiday calendar
366
     */
367
    public function getYear(): int
368
    {
369
        return $this->year;
370
    }
371
372
    /**
373
     * Retrieves the next date (year) the given holiday is going to take place.
374
     *
375
     * @param string $key key of the holiday for which the next occurrence need to be retrieved.
376
     *
377
     * @return Holiday|null a Holiday instance for the given holiday
378
     *
379
     * @throws \ReflectionException
380
     * @throws UnknownLocaleException
381
     * @throws \RuntimeException
382
     * @throws InvalidArgumentException
383
     *
384
     * @covers AbstractProvider::anotherTime
385
     */
386
    public function next(string $key): ?Holiday
387
    {
388
        return $this->anotherTime($this->year + 1, $key);
389
    }
390
391
    /**
392
     * Determines the date of the given holiday for another year.
393
     *
394
     * @param int $year the year to get the holiday date for
395
     * @param string $key key of the holiday for which the date needs to be fetched
396
     *
397
     * @return Holiday|null a Holiday instance for the given holiday and year
398
     *
399
     * @throws \ReflectionException
400
     * @throws InvalidArgumentException when the given name is blank or empty.
401
     * @throws UnknownLocaleException
402
     * @throws \RuntimeException
403
     */
404
    private function anotherTime(int $year, string $key): ?Holiday
405
    {
406
        $this->isHolidayNameNotEmpty($key); // Validate if key is not empty
0 ignored issues
show
Deprecated Code introduced by
The function Yasumi\Provider\Abstract...isHolidayNameNotEmpty() has been deprecated: deprecated in favor of isHolidayKeyNotEmpty() ( Ignorable by Annotation )

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

406
        /** @scrutinizer ignore-deprecated */ $this->isHolidayNameNotEmpty($key); // Validate if key is not empty

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
407
408
        // Get calling class name
409
        $hReflectionClass = new \ReflectionClass(\get_class($this));
410
411
        return Yasumi::create($hReflectionClass->getShortName(), $year, $this->locale)->getHoliday($key);
412
    }
413
414
    /**
415
     * Retrieves the holiday object for the given holiday.
416
     *
417
     * @param string $key the name of the holiday.
418
     *
419
     * @return Holiday|null a Holiday instance for the given holiday
420
     * @throws InvalidArgumentException when the given name is blank or empty.
421
     *
422
     */
423
    public function getHoliday(string $key): ?Holiday
424
    {
425
        $this->isHolidayNameNotEmpty($key); // Validate if key is not empty
0 ignored issues
show
Deprecated Code introduced by
The function Yasumi\Provider\Abstract...isHolidayNameNotEmpty() has been deprecated: deprecated in favor of isHolidayKeyNotEmpty() ( Ignorable by Annotation )

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

425
        /** @scrutinizer ignore-deprecated */ $this->isHolidayNameNotEmpty($key); // Validate if key is not empty

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
426
427
        $holidays = $this->getHolidays();
428
429
        return $holidays[$key] ?? null;
430
    }
431
432
    /**
433
     * Retrieves the previous date (year) the given holiday took place.
434
     *
435
     * @param string $key key of the holiday for which the previous occurrence need to be retrieved.
436
     *
437
     * @return Holiday|null a Holiday instance for the given holiday
438
     *
439
     * @throws \ReflectionException
440
     * @throws UnknownLocaleException
441
     * @throws \RuntimeException
442
     * @throws InvalidArgumentException
443
     *
444
     * @covers AbstractProvider::anotherTime
445
     */
446
    public function previous(string $key): ?Holiday
447
    {
448
        return $this->anotherTime($this->year - 1, $key);
449
    }
450
451
    /**
452
     * Retrieves a list of all holidays between the given start and end date.
453
     *
454
     * Yasumi only calculates holidays for a single year, so a start date or end date beyond the given year will only
455
     * return holidays for the given year. For example, holidays calculated for the year 2016, will only return 2016
456
     * holidays if the provided period is for example 01/01/2012 - 31/12/2017.
457
     *
458
     * Please take care to use the appropriate timezone for the start and end date parameters. In case you use
459
     * different
460
     * timezone for these parameters versus the instantiated Holiday Provider, the outcome might be unexpected (but
461
     * correct).
462
     *
463
     * @param \DateTimeInterface $startDate Start date of the time frame to check against
464
     * @param \DateTimeInterface $endDate End date of the time frame to check against
465
     * @param bool $equals indicate whether the start and end dates should be included in the
466
     *                                       comparison
467
     *
468
     * @return BetweenFilter
469
     * @throws InvalidArgumentException An InvalidArgumentException is thrown if the start date is set after the end
470
     *                                  date.
471
     *
472
     */
473
    public function between(
474
        \DateTimeInterface $startDate,
475
        \DateTimeInterface $endDate,
476
        ?bool $equals = null
477
    ): BetweenFilter {
478
        if ($startDate > $endDate) {
479
            throw new InvalidArgumentException('Start date must be a date before the end date.');
480
        }
481
482
        return new BetweenFilter($this->getIterator(), $startDate, $endDate, $equals ?? true);
483
    }
484
485
    /**
486
     * Get an iterator for the holidays.
487
     *
488
     * @return ArrayIterator iterator for the holidays of this calendar
489
     */
490
    public function getIterator(): ArrayIterator
491
    {
492
        return new ArrayIterator($this->getHolidays());
493
    }
494
495
    /**
496
     * Retrieves a list of all holidays that happen on the given date.
497
     *
498
     * Yasumi only calculates holidays for a single year, so a date outside of the given year will not appear to
499
     * contain any holidays.
500
     *
501
     * Please take care to use the appropriate timezone for the date parameters. If there is a different timezone used
502
     * for these parameters versus the instantiated Holiday Provider, the outcome might be unexpected (but correct).
503
     *
504
     * @param \DateTimeInterface $date Date to check for holidays on.
505
     *
506
     * @return OnFilter
507
     */
508
    public function on(\DateTimeInterface $date): OnFilter
509
    {
510
        return new OnFilter($this->getIterator(), $date);
511
    }
512
}
513