$validityTimes */ public function __construct(public Collection $validityTimes) {} /** Comprueba si el instante pertenece a la intersección efectiva del grupo. */ public function isValid(?CarbonInterface $at = null): bool { $at ??= now(); $startsAt = $this->effectiveStartsAt($at); $expiresAt = $this->effectiveExpiresAt($at); return $this->validityTimes->isNotEmpty() && ($startsAt === null || $startsAt->lessThanOrEqualTo($at)) && ($expiresAt === null || $expiresAt->greaterThan($at)); } /** Un grupo vence cuando termina su intersección efectiva. */ public function isExpired(?CarbonInterface $at = null): bool { $at ??= now(); $expiresAt = $this->effectiveExpiresAt($at); return $expiresAt !== null && $expiresAt->lessThanOrEqualTo($at); } /** * En un AND, la intersección comienza en el inicio más tardío. * Por ejemplo, fecha 00:00 + horario 12:00 comienza a las 12:00. */ public function effectiveStartsAt(?CarbonInterface $at = null): ?CarbonInterface { $at ??= now(); $anchor = $this->dateAnchor() ?? $at; return $this->validityTimes ->map(fn (ValidityTime $validityTime): ?CarbonInterface => $validityTime->startsAt($anchor)) ->filter() ->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp()) ->first(); } /** * En un AND, la intersección termina en el vencimiento más temprano. * Los horarios cuyo fin no supera al inicio se interpretan como nocturnos. */ public function effectiveExpiresAt(?CarbonInterface $at = null): ?CarbonInterface { $at ??= now(); $anchor = $this->dateAnchor() ?? $at; return $this->validityTimes ->map(function (ValidityTime $validityTime) use ($anchor): ?CarbonInterface { $startsAt = $validityTime->startsAt($anchor); $expiresAt = $validityTime->expiresAt($anchor); if ($startsAt !== null && $expiresAt !== null && $expiresAt->lessThanOrEqualTo($startsAt)) { return $expiresAt->addDay(); } return $expiresAt; }) ->filter() ->sortBy(fn (CarbonInterface $expiresAt): int => $expiresAt->getTimestamp()) ->first(); } /** * Usa la fecha de un fixed_window como ancla para convertir ventanas que * sólo contienen horas (time_window) en instantes concretos. */ private function dateAnchor(): ?CarbonInterface { return $this->validityTimes ->filter(fn (ValidityTime $validityTime): bool => $validityTime->type === ValidityTimeType::FixedWindow) ->map(fn (ValidityTime $validityTime): ?CarbonInterface => $validityTime->fixed_starts_at) ->filter() ->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp()) ->first(); } }