diff --git a/src/Service/AbsenceDayCalculator.php b/src/Service/AbsenceDayCalculator.php index 2a4d667..d30a598 100644 --- a/src/Service/AbsenceDayCalculator.php +++ b/src/Service/AbsenceDayCalculator.php @@ -54,11 +54,19 @@ final readonly class AbsenceDayCalculator // A half-day only subtracts 0.5 when its boundary day is actually // counted (otherwise a half-day posted on a weekend/holiday would // wrongly under-count the absence). - if (null !== $startHalfDay && $this->isCountedDay($start, $workingDaysOnly)) { - $days -= 0.5; - } - if (null !== $endHalfDay && $this->isCountedDay($end, $workingDaysOnly)) { - $days -= 0.5; + if ($start->getTimestamp() === $end->getTimestamp()) { + // Single-day request: both boundaries collapse onto the same day, + // so a half-day must subtract 0.5 once, never twice. + if ((null !== $startHalfDay || null !== $endHalfDay) && $this->isCountedDay($start, $workingDaysOnly)) { + $days -= 0.5; + } + } else { + if (null !== $startHalfDay && $this->isCountedDay($start, $workingDaysOnly)) { + $days -= 0.5; + } + if (null !== $endHalfDay && $this->isCountedDay($end, $workingDaysOnly)) { + $days -= 0.5; + } } return max(0.0, $days); diff --git a/tests/Unit/Service/AbsenceDayCalculatorTest.php b/tests/Unit/Service/AbsenceDayCalculatorTest.php index 0eecdcd..33b96a3 100644 --- a/tests/Unit/Service/AbsenceDayCalculatorTest.php +++ b/tests/Unit/Service/AbsenceDayCalculatorTest.php @@ -68,6 +68,15 @@ class AbsenceDayCalculatorTest extends TestCase )); } + public function testSingleFullDayCountsOne(): void + { + // Most common request: a single full working day = 1.0 (no half-day). + self::assertSame(1.0, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-01'), + new DateTimeImmutable('2026-06-01'), + )); + } + public function testSingleHalfDay(): void { self::assertSame(0.5, $this->calculator->countWorkingDays( @@ -77,6 +86,73 @@ class AbsenceDayCalculatorTest extends TestCase )); } + public function testHalfDayEndOnlySubtractsHalf(): void + { + // Mirror of testHalfDayStartSubtractsHalf: end half-day only, on a + // counted multi-day range => 5 - 0.5 = 4.5. + self::assertSame(4.5, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-01'), + new DateTimeImmutable('2026-06-05'), + null, + HalfDay::Morning, + )); + } + + public function testEndBeforeStartCountsZero(): void + { + // Inverted range guard (end < start) => 0, never negative. + self::assertSame(0.0, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-05'), + new DateTimeImmutable('2026-06-01'), + )); + } + + public function testSingleDayOnWeekendCountsZero(): void + { + // Saturday, jours ouvrés: not counted, even with a half-day => 0. + self::assertSame(0.0, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-06'), + new DateTimeImmutable('2026-06-06'), + HalfDay::Morning, + HalfDay::Morning, + )); + } + + public function testSingleDayOnHolidayCountsZero(): void + { + // Fri 2026-05-08 (Victoire 1945): public holiday => 0. + self::assertSame(0.0, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-05-08'), + new DateTimeImmutable('2026-05-08'), + )); + } + + public function testHalfDayOnSaturdayCountedInOpenDays(): void + { + // Fri + Sat in "ouvrables" mode = 2; the end half-day sits on Saturday, + // which IS counted here => 2 - 0.5 = 1.5. + self::assertSame(1.5, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-05'), + new DateTimeImmutable('2026-06-06'), + null, + HalfDay::Morning, + false, + )); + } + + public function testSingleDayWithBothHalfDaysIsStillHalf(): void + { + // Real-world single-day request: the form mirrors the start half-day + // onto the end (same day), so both boundaries carry a half-day. It must + // still count as 0.5, not 0 (the two boundaries collapse onto one day). + self::assertSame(0.5, $this->calculator->countWorkingDays( + new DateTimeImmutable('2026-06-01'), + new DateTimeImmutable('2026-06-01'), + HalfDay::Morning, + HalfDay::Morning, + )); + } + public function testHalfDayOnNonCountedStartIsIgnored(): void { // Sat 2026-06-06 → Mon 2026-06-08, jours ouvrés : Sat & Sun skipped, Mon = 1.