*/ public static function forYear(int $year): array { $dates = []; // Easter-anchored [$em, $ed] = self::easter($year); $easter = Carbon::create($year, $em, $ed); $dates[] = $easter->copy()->subDays(2); // Good Friday $dates[] = $easter->copy()->addDay(); // Easter Monday // Floating Mondays $dates[] = self::firstMondayOf($year, 5); $dates[] = self::lastMondayOf($year, 5); $dates[] = self::lastMondayOf($year, 8); // Fixed dates with substitution $dates[] = self::substituteForward(Carbon::create($year, 1, 1), $dates); $christmas = self::substituteForward(Carbon::create($year, 12, 25), $dates); $dates[] = $christmas; $boxing = self::substituteForward(Carbon::create($year, 12, 26), $dates); $dates[] = $boxing; usort($dates, fn (CarbonInterface $a, CarbonInterface $b): int => $a->getTimestamp() <=> $b->getTimestamp()); return $dates; } /** * Is there a UK bank holiday in [$from, $from + $daysAhead - 1]? */ public static function holidayWithin(CarbonInterface $from, int $daysAhead): bool { $end = $from->copy()->addDays($daysAhead - 1); $years = array_unique([(int) $from->format('Y'), (int) $end->format('Y')]); foreach ($years as $year) { foreach (self::forYear($year) as $holiday) { if ($holiday->betweenIncluded($from, $end)) { return true; } } } return false; } /** * Anonymous Gregorian algorithm for Easter Sunday. * * @return array{0: int, 1: int} [month, day] */ private static function easter(int $year): array { $a = $year % 19; $b = intdiv($year, 100); $c = $year % 100; $d = intdiv($b, 4); $e = $b % 4; $f = intdiv($b + 8, 25); $g = intdiv($b - $f + 1, 3); $h = (19 * $a + $b - $d - $g + 15) % 30; $i = intdiv($c, 4); $k = $c % 4; $l = (32 + 2 * $e + 2 * $i - $h - $k) % 7; $m = intdiv($a + 11 * $h + 22 * $l, 451); $month = intdiv($h + $l - 7 * $m + 114, 31); $day = (($h + $l - 7 * $m + 114) % 31) + 1; return [$month, $day]; } private static function firstMondayOf(int $year, int $month): Carbon { $d = Carbon::create($year, $month, 1); while ($d->dayOfWeek !== Carbon::MONDAY) { $d->addDay(); } return $d; } private static function lastMondayOf(int $year, int $month): Carbon { $d = Carbon::create($year, $month, 1)->endOfMonth()->startOfDay(); while ($d->dayOfWeek !== Carbon::MONDAY) { $d->subDay(); } return $d; } /** * If $candidate falls on a weekend or collides with an already-claimed * date, return the next non-weekend non-claimed date. Christmas/Boxing * cascade is handled because we pass in the running list. * * @param array $taken */ private static function substituteForward(Carbon $candidate, array $taken): Carbon { $d = $candidate->copy(); while (true) { $isWeekend = in_array($d->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY], true); $isTaken = false; foreach ($taken as $t) { if ($t->isSameDay($d)) { $isTaken = true; break; } } if (! $isWeekend && ! $isTaken) { return $d; } $d->addDay(); } } }