From e969955943e0dace8ecf5ca2b45c311f7446ab7d Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 11 Jun 2026 11:36:45 +0200 Subject: [PATCH] feat(night-contingent) : service partage NightHoursCalculator Co-Authored-By: Claude Sonnet 4.6 --- .../WorkHours/NightHoursCalculator.php | 102 ++++++++++++++++++ .../WorkHours/NightHoursCalculatorTest.php | 70 ++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 src/Service/WorkHours/NightHoursCalculator.php create mode 100644 tests/Service/WorkHours/NightHoursCalculatorTest.php diff --git a/src/Service/WorkHours/NightHoursCalculator.php b/src/Service/WorkHours/NightHoursCalculator.php new file mode 100644 index 0000000..a32104c --- /dev/null +++ b/src/Service/WorkHours/NightHoursCalculator.php @@ -0,0 +1,102 @@ +6h). + * + * Fenetres en minutes depuis 00:00 : [0,360] (00:00-06:00) et [1260,1440] + * (21:00-24:00). On projette sur J+1 pour les shifts qui traversent minuit. + * Source de verite unique partagee par les ecrans Heures et les exports. + */ +final class NightHoursCalculator +{ + /** + * Minutes de nuit d'un WorkHour. Conducteurs : champ manuel nightHoursMinutes. + * Non-conducteurs : somme calculee depuis les plages matin/apres-midi/soir. + */ + public function nightMinutesForWorkHour(WorkHour $workHour, bool $isDriver): int + { + if ($isDriver) { + return $workHour->getNightHoursMinutes() ?? 0; + } + + return $this->nightMinutesFromRanges($workHour); + } + + public function nightMinutesFromRanges(WorkHour $workHour): int + { + $ranges = [ + [$workHour->getMorningFrom(), $workHour->getMorningTo()], + [$workHour->getAfternoonFrom(), $workHour->getAfternoonTo()], + [$workHour->getEveningFrom(), $workHour->getEveningTo()], + ]; + + $total = 0; + foreach ($ranges as [$from, $to]) { + $total += $this->nightIntervalMinutes($from, $to); + } + + return $total; + } + + public function nightIntervalMinutes(?string $from, ?string $to): int + { + $interval = $this->resolveInterval($from, $to); + if (null === $interval) { + return 0; + } + + [$start, $end] = $interval; + $windows = [[0, 360], [1260, 1440]]; + $total = 0; + + for ($dayOffset = 0; $dayOffset <= 1; ++$dayOffset) { + $shift = $dayOffset * 1440; + foreach ($windows as [$windowStart, $windowEnd]) { + $total += $this->overlap($start, $end, $windowStart + $shift, $windowEnd + $shift); + } + } + + return $total; + } + + /** + * @return null|array{int, int} + */ + private function resolveInterval(?string $from, ?string $to): ?array + { + $fromMinutes = $this->toMinutes($from); + $toMinutes = $this->toMinutes($to); + if (null === $fromMinutes || null === $toMinutes) { + return null; + } + + $end = $toMinutes <= $fromMinutes ? $toMinutes + 1440 : $toMinutes; + + return [$fromMinutes, $end]; + } + + private function toMinutes(?string $time): ?int + { + if (null === $time || '' === $time) { + return null; + } + + [$hours, $minutes] = array_map('intval', explode(':', $time)); + + return ($hours * 60) + $minutes; + } + + private function overlap(int $startA, int $endA, int $startB, int $endB): int + { + $start = max($startA, $startB); + $end = min($endA, $endB); + + return max(0, $end - $start); + } +} diff --git a/tests/Service/WorkHours/NightHoursCalculatorTest.php b/tests/Service/WorkHours/NightHoursCalculatorTest.php new file mode 100644 index 0000000..ea0e554 --- /dev/null +++ b/tests/Service/WorkHours/NightHoursCalculatorTest.php @@ -0,0 +1,70 @@ +nightIntervalMinutes(null, null)); + self::assertSame(0, $calc->nightIntervalMinutes('08:00', null)); + } + + public function testPureDayRangeHasNoNight(): void + { + $calc = new NightHoursCalculator(); + // 08:00 -> 17:00 : entierement hors fenetres nuit (00:00-06:00, 21:00-24:00). + self::assertSame(0, $calc->nightIntervalMinutes('08:00', '17:00')); + } + + public function testEveningWindowCounts(): void + { + $calc = new NightHoursCalculator(); + // 21:00 -> 24:00 = 180 min de nuit. + self::assertSame(180, $calc->nightIntervalMinutes('21:00', '00:00')); + } + + public function testShiftCrossingMidnightCountsBothWindows(): void + { + $calc = new NightHoursCalculator(); + // 21:00 -> 05:00 : 21-24 (180) + 00-05 (300) = 480 min. + self::assertSame(480, $calc->nightIntervalMinutes('21:00', '05:00')); + } + + public function testNightMinutesForWorkHourDriverUsesManualField(): void + { + $calc = new NightHoursCalculator(); + $wh = new WorkHour(); + $wh->setWorkDate(new DateTimeImmutable('2026-01-15')) + ->setDayHoursMinutes(300) + ->setNightHoursMinutes(250) + ->setMorningFrom('08:00')->setMorningTo('12:00') + ; + + // Driver -> champ manuel nightHoursMinutes, plages ignorees. + self::assertSame(250, $calc->nightMinutesForWorkHour($wh, true)); + } + + public function testNightMinutesForWorkHourNonDriverSumsRanges(): void + { + $calc = new NightHoursCalculator(); + $wh = new WorkHour(); + $wh->setWorkDate(new DateTimeImmutable('2026-01-15')) + ->setMorningFrom('22:00')->setMorningTo('00:00') // 120 min nuit + ->setEveningFrom('04:00')->setEveningTo('06:00') // 120 min nuit + ; + + self::assertSame(240, $calc->nightMinutesForWorkHour($wh, false)); + } +}