fix(absences) : demi-journée mono-jour décomptée 0,5 (et non 0)
Sur une demande d'un seul jour, le formulaire recopie la demi-journée de début sur la fin (même date), si bien que les deux bornes portaient une demi-journée et 0,5 était soustrait deux fois (1 - 0,5 - 0,5 = 0). Quand start et end tombent le même jour, les deux bornes se confondent : on ne soustrait désormais 0,5 qu'une seule fois. Comparaison par getTimestamp() pour rester compatible strict_comparison (=== sur deux DateTimeImmutable distincts teste l'identité d'instance, pas la valeur). Couverture complétée : mono-jour plein, week-end, férié, inversion de dates, demi-journée de fin seule, demi sur samedi en mode ouvrables. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,12 +54,20 @@ final readonly class AbsenceDayCalculator
|
|||||||
// A half-day only subtracts 0.5 when its boundary day is actually
|
// A half-day only subtracts 0.5 when its boundary day is actually
|
||||||
// counted (otherwise a half-day posted on a weekend/holiday would
|
// counted (otherwise a half-day posted on a weekend/holiday would
|
||||||
// wrongly under-count the absence).
|
// wrongly under-count the absence).
|
||||||
|
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)) {
|
if (null !== $startHalfDay && $this->isCountedDay($start, $workingDaysOnly)) {
|
||||||
$days -= 0.5;
|
$days -= 0.5;
|
||||||
}
|
}
|
||||||
if (null !== $endHalfDay && $this->isCountedDay($end, $workingDaysOnly)) {
|
if (null !== $endHalfDay && $this->isCountedDay($end, $workingDaysOnly)) {
|
||||||
$days -= 0.5;
|
$days -= 0.5;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return max(0.0, $days);
|
return max(0.0, $days);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
public function testSingleHalfDay(): void
|
||||||
{
|
{
|
||||||
self::assertSame(0.5, $this->calculator->countWorkingDays(
|
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
|
public function testHalfDayOnNonCountedStartIsIgnored(): void
|
||||||
{
|
{
|
||||||
// Sat 2026-06-06 → Mon 2026-06-08, jours ouvrés : Sat & Sun skipped, Mon = 1.
|
// Sat 2026-06-06 → Mon 2026-06-08, jours ouvrés : Sat & Sun skipped, Mon = 1.
|
||||||
|
|||||||
Reference in New Issue
Block a user