[2026, false], 'leap' => [2024, true], 'century exception' => [1900, false], '400 years' => [2000, true], 'future century' => [2100, false]]; } #[DataProvider('monthLengths')] public function testMonthLength(int $year, int $month, int $expected): void { self::assertSame($expected, daysPerMonth($year, $month)); } public static function monthLengths(): array { return [[2026, 1, 31], [2026, 2, 28], [2024, 2, 29], [1900, 2, 28], [2000, 2, 29], [2026, 4, 30], [2026, 12, 31]]; } #[DataProvider('dayOffsets')] public function testDayArithmeticProducesValidCalendarDates(array $date, int $offset, array $expected): void { $actual = addDaysComplete(...[...$date, $offset]); self::assertSame($expected, $actual); self::assertTrue(checkdate($actual[1], $actual[2], $actual[0])); } public static function dayOffsets(): array { return [ 'unchanged' => [[2026, 10, 26], 0, [2026, 10, 26]], 'next day' => [[2026, 10, 26], 1, [2026, 10, 27]], 'year rollover' => [[2026, 12, 31], 1, [2027, 1, 1]], 'February non-leap' => [[2026, 2, 28], 1, [2026, 3, 1]], 'February leap' => [[2024, 2, 28], 1, [2024, 2, 29]], 'leap rollover' => [[2024, 2, 29], 1, [2024, 3, 1]], 'multiple months' => [[2026, 1, 31], 60, [2026, 4, 1]], 'previous day same month' => [[2026, 10, 26], -1, [2026, 10, 25]], 'previous year' => [[2026, 1, 1], -1, [2025, 12, 31]], 'backwards into February' => [[2026, 3, 1], -1, [2026, 2, 28]], 'backwards into leap February' => [[2024, 3, 1], -1, [2024, 2, 29]], 'backwards into 30-day month' => [[2026, 5, 1], -1, [2026, 4, 30]], 'backwards from short month' => [[2026, 4, 1], -1, [2026, 3, 31]], ]; } #[DataProvider('monthOffsets')] public function testMonthArithmetic(int $year, int $month, int $offset, array $expected): void { self::assertSame($expected, addMonths($year, $month, $offset)); } public static function monthOffsets(): array { return [[2026, 1, 0, [2026, 1]], [2026, 12, 1, [2027, 1]], [2026, 1, -1, [2025, 12]], [2026, 1, 25, [2028, 2]], [2026, 1, -13, [2024, 12]]]; } }