tz = $tz; $this->doors = max( 1, $doors ); $this->start = new \DateTimeImmutable( $start . ' 00:00:00', $tz ); } public function doors(): int { return $this->doors; } public function dateOf( int $n ): \DateTimeImmutable { if ( ! $this->valid( $n ) ) { throw new \OutOfRangeException( 'Door out of range.' ); } // Calendar-day arithmetic in the local zone keeps 00:00 across DST changes. return $this->start->modify( '+' . ( $n - 1 ) . ' days' ); } /** * First moment after the last door's day. */ public function end(): \DateTimeImmutable { return $this->start->modify( '+' . $this->doors . ' days' ); } /** * @return string locked|today|past|ended|invalid */ public function status( int $n, \DateTimeImmutable $now ): string { if ( ! $this->valid( $n ) ) { return 'invalid'; } $now = $now->setTimezone( $this->tz ); if ( $now >= $this->end() ) { return 'ended'; } $opens = $this->dateOf( $n ); if ( $now < $opens ) { return 'locked'; } return $now < $opens->modify( '+1 day' ) ? 'today' : 'past'; } /** * @return array Door number => status. */ public function statuses( \DateTimeImmutable $now ): array { $all = []; for ( $n = 1; $n <= $this->doors; $n++ ) { $all[ $n ] = $this->status( $n, $now ); } return $all; } /** * Whether door $n may be opened now, and whether it still carries its coupon. * * @param string $missedPolicy open|locked|open_no_coupon — anything else is treated as locked. * @return array{allowed:bool, coupon:bool, reason:string} */ public function decide( int $n, \DateTimeImmutable $now, string $missedPolicy ): array { $status = $this->status( $n, $now ); switch ( $status ) { case 'today': return self::verdict( true, true, 'today' ); case 'past': if ( 'open' === $missedPolicy ) { return self::verdict( true, true, 'past' ); } if ( 'open_no_coupon' === $missedPolicy ) { return self::verdict( true, false, 'past' ); } return self::verdict( false, false, 'missed' ); default: return self::verdict( false, false, $status ); } } public function nextUnlock( \DateTimeImmutable $now ): ?\DateTimeImmutable { $now = $now->setTimezone( $this->tz ); for ( $n = 1; $n <= $this->doors; $n++ ) { $opens = $this->dateOf( $n ); if ( $opens > $now ) { return $opens; } } return null; } private function valid( int $n ): bool { return $n >= 1 && $n <= $this->doors; } private static function verdict( bool $allowed, bool $coupon, string $reason ): array { return [ 'allowed' => $allowed, 'coupon' => $coupon, 'reason' => $reason, ]; } }