Yasumi::create()   B
last analyzed

Complexity

Conditions 10
Paths 20

Size

Total Lines 35
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 15
c 4
b 0
f 0
dl 0
loc 35
rs 7.6666
cc 10
nc 20
nop 3

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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;
14
15
use FilesystemIterator;
16
use InvalidArgumentException;
17
use RecursiveDirectoryIterator;
18
use RecursiveIteratorIterator;
19
use ReflectionClass;
20
use RuntimeException;
21
use Yasumi\Exception\InvalidDateException;
22
use Yasumi\Exception\InvalidYearException;
23
use Yasumi\Exception\ProviderNotFoundException;
24
use Yasumi\Exception\UnknownLocaleException;
25
use Yasumi\Provider\AbstractProvider;
26
27
/**
28
 * Class Yasumi.
29
 */
30
class Yasumi
31
{
32
    /**
33
     * Default locale.
34
     */
35
    public const DEFAULT_LOCALE = 'en_US';
36
37
    /**
38
     * @var array list of all defined locales
39
     */
40
    private static $locales = [];
41
42
    /**
43
     * Global translations.
44
     *
45
     * @var Translations
46
     */
47
    private static $globalTranslations;
48
49
    /**
50
     * Provider class to be ignored (Abstract, trait, other)
51
     *
52
     * @var array
53
     */
54
    private static $ignoredProvider = [
55
        'AbstractProvider.php',
56
        'CommonHolidays.php',
57
        'ChristianHolidays.php',
58
    ];
59
60
    /**
61
     * Determines the next working day based on a given start date.
62
     *
63
     * The next working day based on a given start date excludes any holidays and weekends that may be defined
64
     * by this Holiday Provider. The workingDays parameter can be used how far ahead (in days) the next working day
65
     * must be searched for.
66
     *
67
     * @param string $class Holiday Provider name
68
     * @param \DateTimeInterface $startDate Start date, defaults to today
69
     * @param int $workingDays Number of days to look ahead for the (first) next working day
70
     *
71
     * @return \DateTimeInterface
72
     *
73
     * @throws \ReflectionException
74
     * @throws UnknownLocaleException
75
     * @throws RuntimeException
76
     * @throws InvalidArgumentException
77
     * @throws \Exception
78
     * @throws InvalidDateException
79
     *
80
     * @TODO we should accept a timezone so we can accept int/string for $startDate
81
     */
82
    public static function nextWorkingDay(
83
        string $class,
84
        \DateTimeInterface $startDate,
85
        int $workingDays = 1
86
    ): \DateTimeInterface {
87
88
        // Setup start date, if its an instance of \DateTime, clone to prevent modification to original
89
        $date = $startDate instanceof \DateTime ? clone $startDate : $startDate;
90
91
        $provider = null;
92
93
        while ($workingDays > 0) {
94
            $date = $date->add(new \DateInterval('P1D'));
95
            if (!$provider instanceof ProviderInterface || $provider->getYear() !== (int) $date->format('Y')) {
0 ignored issues
show
Bug introduced by
The method getYear() does not exist on Yasumi\ProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yasumi\ProviderInterface. ( Ignorable by Annotation )

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

95
            if (!$provider instanceof ProviderInterface || $provider->/** @scrutinizer ignore-call */ getYear() !== (int) $date->format('Y')) {
Loading history...
96
                $provider = self::create($class, (int) $date->format('Y'));
97
            }
98
            if ($provider->isWorkingDay($date)) {
0 ignored issues
show
Bug introduced by
The method isWorkingDay() does not exist on Yasumi\ProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to Yasumi\ProviderInterface. ( Ignorable by Annotation )

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

98
            if ($provider->/** @scrutinizer ignore-call */ isWorkingDay($date)) {
Loading history...
99
                $workingDays--;
100
            }
101
        }
102
103
        return $date;
104
    }
105
106
    /**
107
     * Create a new holiday provider instance.
108
     *
109
     * A new holiday provider instance can be created using this function. You can use one of the providers included
110
     * already with Yasumi, or your own provider by giving the name of your class in the first parameter. Your provider
111
     * class needs to implement the 'ProviderInterface' class.
112
     *
113
     * @param string $class holiday provider name
114
     * @param int $year year for which the country provider needs to be created. Year needs to be a valid integer
115
     *                       between 1000 and 9999.
116
     * @param string $locale The locale to use. If empty we'll use the default locale (en_US)
117
     *
118
     * @return AbstractProvider An instance of class $class is created and returned
119
     *
120
     * @throws RuntimeException          If no such holiday provider is found
121
     * @throws InvalidYearException      if the year parameter is not between 1000 and 9999
122
     * @throws UnknownLocaleException    if the locale parameter is invalid
123
     * @throws ProviderNotFoundException if the holiday provider for the given country does not exist
124
     * @throws \ReflectionException
125
     */
126
    public static function create(string $class, int $year = 0, string $locale = self::DEFAULT_LOCALE): AbstractProvider
127
    {
128
        // Find and return holiday provider instance
129
        $providerClass = \sprintf('Yasumi\Provider\%s', \str_replace('/', '\\', $class));
130
131
        if (\class_exists($class) && (new ReflectionClass($class))->implementsInterface(ProviderInterface::class)) {
132
            $providerClass = $class;
133
        }
134
135
        if ('AbstractProvider' === $class || !\class_exists($providerClass)) {
136
            throw new ProviderNotFoundException(\sprintf('Unable to find holiday provider "%s".', $class));
137
        }
138
139
        // Assert year input
140
        if ($year < 1000 || $year > 9999) {
141
            throw new InvalidYearException(\sprintf('Year needs to be between 1000 and 9999 (%d given).', $year));
142
        }
143
144
        // Load internal locales variable
145
        if (empty(self::$locales)) {
146
            self::$locales = self::getAvailableLocales();
147
        }
148
149
        // Load internal translations variable
150
        if (null === self::$globalTranslations) {
151
            self::$globalTranslations = new Translations(self::$locales);
152
            self::$globalTranslations->loadTranslations(__DIR__ . '/data/translations');
153
        }
154
155
        // Assert locale input
156
        if (!\in_array($locale, self::$locales, true)) {
157
            throw new UnknownLocaleException(\sprintf('Locale "%s" is not a valid locale.', $locale));
158
        }
159
160
        return new $providerClass($year, $locale, self::$globalTranslations);
161
    }
162
163
    /**
164
     * Returns a list of available locales.
165
     *
166
     * @return array list of available locales
167
     */
168
    public static function getAvailableLocales(): array
169
    {
170
        return require __DIR__ . '/data/locales.php';
171
    }
172
173
    /**
174
     * Create a new holiday provider instance.
175
     *
176
     * A new holiday provider instance can be created using this function. You can use one of the providers included
177
     * already with Yasumi, or your own provider by giving the 'const ID', corresponding to the ISO3166-2 Code, set in
178
     * your class in the first parameter. Your provider class needs to implement the 'ProviderInterface' class.
179
     *
180
     * @param string $isoCode ISO3166-2 Coded region, holiday provider will be searched for
181
     * @param int $year year for which the country provider needs to be created. Year needs to be a valid
182
     *                          integer between 1000 and 9999.
183
     * @param string $locale The locale to use. If empty we'll use the default locale (en_US)
184
     *
185
     * @return AbstractProvider An instance of class $class is created and returned
186
     *
187
     * @throws RuntimeException          If no such holiday provider is found
188
     * @throws InvalidArgumentException  if the year parameter is not between 1000 and 9999
189
     * @throws UnknownLocaleException    if the locale parameter is invalid
190
     * @throws ProviderNotFoundException if the holiday provider for the given ISO3166-2 code does not exist
191
     * @throws \ReflectionException
192
     */
193
    public static function createByISO3166_2(
194
        string $isoCode,
195
        int $year = 0,
196
        string $locale = self::DEFAULT_LOCALE
197
    ): AbstractProvider {
198
        $availableProviders = self::getProviders();
199
200
        if (false === isset($availableProviders[$isoCode])) {
201
            throw new ProviderNotFoundException(\sprintf(
202
                'Unable to find holiday provider by ISO3166-2 "%s".',
203
                $isoCode
204
            ));
205
        }
206
207
        return self::create($availableProviders[$isoCode], $year, $locale);
208
    }
209
210
    /**
211
     * Returns a list of available holiday providers.
212
     *
213
     * @return array list of available holiday providers
214
     *
215
     * @throws \ReflectionException
216
     */
217
    public static function getProviders(): array
218
    {
219
        // Basic static cache
220
        static $providers;
221
        if (!empty($providers)) {
222
            return $providers;
223
        }
224
225
        $providers = [];
226
        $filesIterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
227
            __DIR__ . DIRECTORY_SEPARATOR . 'Provider',
228
            FilesystemIterator::SKIP_DOTS
229
        ), RecursiveIteratorIterator::SELF_FIRST);
230
231
        foreach ($filesIterator as $file) {
232
            if ($file->isDir() || 'php' !== $file->getExtension() || \in_array(
233
                $file->getBasename('.php'),
234
                self::$ignoredProvider,
235
                true
236
            )) {
237
                continue;
238
            }
239
240
            $quotedDs = \preg_quote(DIRECTORY_SEPARATOR, '');
241
            $provider = \preg_replace("#^.+{$quotedDs}Provider{$quotedDs}(.+)\\.php$#", '$1', $file->getPathName());
242
243
            $class = new ReflectionClass(\sprintf('Yasumi\Provider\%s', \str_replace('/', '\\', $provider)));
244
245
            $key = 'ID';
246
            if ($class->isSubclassOf(AbstractProvider::class) && $class->hasConstant($key)) {
247
                $providers[\strtoupper($class->getConstant($key))] = $provider;
248
            }
249
        }
250
251
        return $providers;
252
    }
253
254
    /**
255
     * Determines the previous working day based on a given start date.
256
     *
257
     * The previous working day based on a given start date excludes any holidays and weekends that may be defined
258
     * by this Holiday Provider. The workingDays parameter can be used how far back (in days) the previous working day
259
     * must be searched for.
260
     *
261
     * @param string $class Holiday Provider name
262
     * @param \DateTimeInterface $startDate Start date, defaults to today
263
     * @param int $workingDays Number of days to look back for the (first) previous working day
264
     *
265
     * @return \DateTimeInterface
266
     *
267
     * @throws \ReflectionException
268
     * @throws UnknownLocaleException
269
     * @throws RuntimeException
270
     * @throws InvalidArgumentException
271
     * @throws \Exception
272
     * @throws InvalidDateException
273
     *
274
     * @TODO we should accept a timezone so we can accept int/string for $startDate
275
     */
276
    public static function prevWorkingDay(
277
        string $class,
278
        \DateTimeInterface $startDate,
279
        int $workingDays = 1
280
    ): \DateTimeInterface {
281
282
        // Setup start date, if its an instance of \DateTime, clone to prevent modification to original
283
        $date = $startDate instanceof \DateTime ? clone $startDate : $startDate;
284
285
        $provider = null;
286
287
        while ($workingDays > 0) {
288
            $date = $date->sub(new \DateInterval('P1D'));
289
            if (!$provider instanceof ProviderInterface || $provider->getYear() !== (int) $date->format('Y')) {
290
                $provider = self::create($class, (int) $date->format('Y'));
291
            }
292
            if ($provider->isWorkingDay($date)) {
293
                $workingDays--;
294
            }
295
        }
296
297
        return $date;
298
    }
299
}
300