Skip to content

Commit 063e30d

Browse files
authored
Integrate mll-lab/holidays
1 parent cbaf32c commit 063e30d

File tree

9 files changed

+380
-16
lines changed

9 files changed

+380
-16
lines changed

.github/workflows/format.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545

4646
- run: composer install --no-interaction --no-progress --no-suggest
4747

48-
- run: vendor/bin/php-cs-fixer fix
48+
- run: vendor/bin/php-cs-fixer fix --using-cache=no
4949

5050
- uses: stefanzweifel/git-auto-commit-action@v4
5151
with:

.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
/.build
2+
/.idea
23
/vendor
34
/composer.lock
4-
/.idea
5-
/.php-cs-fixer.cache
6-
/.phpunit.result.cache

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ See [GitHub releases](https://github.com/mll-lab/php-utils/releases).
99

1010
## Unreleased
1111

12+
## v1.11.0
13+
14+
### Added
15+
16+
- Integrate `mll-lab/holidays`
17+
1218
## v1.10.0
1319

1420
### Added

Makefile

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,35 @@ help: ## Displays this list of targets with descriptions
77

88
.PHONY: coverage
99
coverage: vendor ## Collects coverage from running unit tests with phpunit
10-
mkdir -p .build/phpunit
10+
mkdir --parents .build/phpunit
1111
vendor/bin/phpunit --dump-xdebug-filter=.build/phpunit/xdebug-filter.php
1212
vendor/bin/phpunit --coverage-text --prepend=.build/phpunit/xdebug-filter.php
1313

1414
.PHONY: fix
15-
fix: vendor
15+
fix: rector php-cs-fixer
16+
17+
.PHONY: rector
18+
rector: vendor
1619
vendor/bin/rector process
17-
vendor/bin/php-cs-fixer fix
20+
21+
.PHONY: php-cs-fixer
22+
php-cs-fixer:
23+
mkdir --parents .build/php-cs-fixer
24+
vendor/bin/php-cs-fixer fix --cache-file=.build/php-cs-fixer/cache
1825

1926
.PHONY: infection
2027
infection: vendor ## Runs mutation tests with infection
21-
mkdir -p .build/infection
28+
mkdir --parents .build/infection
2229
vendor/bin/infection --ignore-msi-with-no-mutations --min-covered-msi=60 --min-msi=60
2330

2431
.PHONY: stan
2532
stan: vendor ## Runs a static analysis with phpstan
26-
mkdir -p .build/phpstan
33+
mkdir --parents .build/phpstan
2734
vendor/bin/phpstan analyse --configuration=phpstan.neon
2835

2936
.PHONY: test
3037
test: vendor ## Runs auto-review, unit, and integration tests with phpunit
31-
mkdir -p .build/phpunit
38+
mkdir --parents .build/phpunit
3239
vendor/bin/phpunit --cache-result-file=.build/phpunit/result.cache
3340

3441
vendor: composer.json

composer.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,25 @@
1616
},
1717
"require": {
1818
"php": "^7.4 || ^8",
19+
"ext-calendar": "*",
1920
"illuminate/support": "^8.73 || ^9 || ^10",
2021
"mll-lab/microplate": "^6",
2122
"mll-lab/str_putcsv": "^1",
23+
"nesbot/carbon": "^2.62.1",
2224
"thecodingmachine/safe": "^1 || ^2"
2325
},
2426
"require-dev": {
2527
"ergebnis/composer-normalize": "^2",
2628
"infection/infection": "^0.26 || ^0.27",
2729
"jangregor/phpstan-prophecy": "^1",
2830
"mll-lab/php-cs-fixer-config": "^5",
29-
"nesbot/carbon": "^2.62.1",
3031
"phpstan/extension-installer": "^1",
3132
"phpstan/phpstan": "^1",
3233
"phpstan/phpstan-deprecation-rules": "^1",
3334
"phpstan/phpstan-phpunit": "^1",
3435
"phpstan/phpstan-strict-rules": "^1",
3536
"phpunit/phpunit": "^9 || ^10",
3637
"rector/rector": "^0.17",
37-
"symfony/var-dumper": "^5 || ^6",
3838
"thecodingmachine/phpstan-safe-rule": "^1.2"
3939
},
4040
"autoload": {
@@ -45,10 +45,7 @@
4545
"autoload-dev": {
4646
"psr-4": {
4747
"MLL\\Utils\\Tests\\": "tests/"
48-
},
49-
"files": [
50-
"vendor/symfony/var-dumper/Resources/functions/dump.php"
51-
]
48+
}
5249
},
5350
"config": {
5451
"allow-plugins": {

src/BavarianHolidays.php

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace MLL\Utils;
4+
5+
use Carbon\Carbon;
6+
7+
/**
8+
* Some definitions:
9+
* Holiday: special occasions on a mostly fixed date where there is no work
10+
* Weekend Day: Saturday and Sunday
11+
* Business Day: any day that is neither a Holiday nor a Weekend Day.
12+
*/
13+
class BavarianHolidays
14+
{
15+
public const HOLIDAYS_STATIC = [
16+
'01.01' => 'Neujahrstag',
17+
'06.01' => 'Heilige Drei Könige',
18+
'01.05' => 'Tag der Arbeit',
19+
'15.08' => 'Maria Himmelfahrt',
20+
'03.10' => 'Tag der Deutschen Einheit',
21+
'01.11' => 'Allerheiligen',
22+
'24.12' => 'Heilig Abend',
23+
'25.12' => 'Erster Weihnachtstag',
24+
'26.12' => 'Zweiter Weihnachtstag',
25+
'31.12' => 'Sylvester',
26+
];
27+
28+
public const SAMSTAG = 'Samstag';
29+
public const SONNTAG = 'Sonntag';
30+
public const KARFREITAG = 'Karfreitag';
31+
public const OSTERSONNTAG = 'Ostersonntag';
32+
public const OSTERMONTAG = 'Ostermontag';
33+
public const CHRISTI_HIMMELFAHRT = 'Christi Himmelfahrt';
34+
public const PFINGSTSONNTAG = 'Pfingstsonntag';
35+
public const PFINGSTMONTAG = 'Pfingstmontag';
36+
public const FRONLEICHNAM = 'Fronleichnam';
37+
public const REFORMATIONSTAG_500_JAHRE_REFORMATION = 'Reformationstag (500 Jahre Reformation)';
38+
39+
/**
40+
* Optionally allows users to define extra holidays for a given year.
41+
*
42+
* The returned array is expected to be a map from the day of the year
43+
* (format with @see self::dayOfTheYear()) to holiday names.
44+
*
45+
* @example ['23.02' => 'Day of the Tentacle']
46+
*
47+
* @var (callable(int): array<string, string>)|null
48+
*/
49+
public static $loadUserDefinedHolidays;
50+
51+
/** Checks if given date is a business day. */
52+
public static function isBusinessDay(Carbon $date): bool
53+
{
54+
return ! self::isHoliday($date)
55+
&& ! $date->isWeekend();
56+
}
57+
58+
/** Checks if given date is a holiday. */
59+
public static function isHoliday(Carbon $date): bool
60+
{
61+
return is_string(self::nameHoliday($date));
62+
}
63+
64+
/**
65+
* Returns the name of the holiday if the date happens to land on one.
66+
* Saturday and Sunday are not evaluated as holiday.
67+
*/
68+
public static function nameHoliday(Carbon $date): ?string
69+
{
70+
$holidayMap = self::buildHolidayMap($date);
71+
72+
return $holidayMap[self::dayOfTheYear($date)] ?? null;
73+
}
74+
75+
/** Returns a new carbon instance with the given number of business days added. */
76+
public static function addBusinessDays(Carbon $date, int $days): Carbon
77+
{
78+
return DateModification::addDays(
79+
$date,
80+
$days,
81+
fn (Carbon $date): bool => self::isBusinessDay($date)
82+
);
83+
}
84+
85+
/** Returns a new carbon instance with the given number of business days subtracted. */
86+
public static function subBusinessDays(Carbon $date, int $days): Carbon
87+
{
88+
return DateModification::subDays(
89+
$date,
90+
$days,
91+
fn (Carbon $date): bool => self::isBusinessDay($date)
92+
);
93+
}
94+
95+
/**
96+
* Returns a map from day/month to named holidays.
97+
*
98+
* @return array<string, string>
99+
*/
100+
protected static function buildHolidayMap(Carbon $date): array
101+
{
102+
$holidays = self::HOLIDAYS_STATIC;
103+
104+
$year = $date->year;
105+
106+
// dynamic holidays
107+
// easter_days avoids issues with timezones and is not limited to UNIX timestamps, see https://github.com/briannesbitt/Carbon/pull/1052#issuecomment-381178494
108+
$easter = Carbon::createMidnightDate($year, 3, 21)
109+
->addDays(easter_days($year));
110+
$holidays[self::dateFromEaster($easter, -2)] = self::KARFREITAG;
111+
$holidays[self::dateFromEaster($easter, 0)] = self::OSTERSONNTAG;
112+
$holidays[self::dateFromEaster($easter, 1)] = self::OSTERMONTAG;
113+
$holidays[self::dateFromEaster($easter, 39)] = self::CHRISTI_HIMMELFAHRT;
114+
$holidays[self::dateFromEaster($easter, 49)] = self::PFINGSTSONNTAG;
115+
$holidays[self::dateFromEaster($easter, 50)] = self::PFINGSTMONTAG;
116+
$holidays[self::dateFromEaster($easter, 60)] = self::FRONLEICHNAM;
117+
118+
// exceptional holidays
119+
if ($year === 2017) {
120+
$holidays['31.10'] = self::REFORMATIONSTAG_500_JAHRE_REFORMATION;
121+
}
122+
123+
// user-defined holidays
124+
if (isset(self::$loadUserDefinedHolidays)) {
125+
$holidays = array_merge(
126+
$holidays,
127+
(self::$loadUserDefinedHolidays)($year)
128+
);
129+
}
130+
131+
return $holidays;
132+
}
133+
134+
protected static function dateFromEaster(Carbon $easter, int $daysAway): string
135+
{
136+
$date = $easter->clone()->addDays($daysAway);
137+
138+
return self::dayOfTheYear($date);
139+
}
140+
141+
public static function dayOfTheYear(Carbon $date): string
142+
{
143+
return $date->format('d.m');
144+
}
145+
}

src/DateModification.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace MLL\Utils;
4+
5+
use Carbon\Carbon;
6+
7+
class DateModification
8+
{
9+
/** @param callable(Carbon): bool $shouldCount should the given date be added? */
10+
public static function addDays(Carbon $date, int $days, callable $shouldCount): Carbon
11+
{
12+
// Make sure we do not mutate the original date
13+
$copy = $date->clone();
14+
15+
while ($days > 0) {
16+
$copy->addDay();
17+
if ($shouldCount($copy)) {
18+
--$days;
19+
}
20+
}
21+
22+
return $copy;
23+
}
24+
25+
/** @param callable(Carbon): bool $shouldCount should the given date be subtracted? */
26+
public static function subDays(Carbon $date, int $days, callable $shouldCount): Carbon
27+
{
28+
// Make sure we do not mutate the original date
29+
$copy = $date->clone();
30+
31+
while ($days > 0) {
32+
$copy->subDay();
33+
if ($shouldCount($copy)) {
34+
--$days;
35+
}
36+
}
37+
38+
return $copy;
39+
}
40+
}

0 commit comments

Comments
 (0)