*/ public static function days(int $now, \DateTimeZone $tz, int $count = self::MAX_DAYS): array { return self::periods($now, $tz, max(1, min(self::MAX_DAYS, $count)), 'today midnight', static fn(\DateTimeImmutable $d): \DateTimeImmutable => $d->modify('+1 day')->setTime(0, 0)); } /** @return list */ public static function months(int $now, \DateTimeZone $tz, int $count = self::MAX_MONTHS): array { return self::periods($now, $tz, max(1, min(self::MAX_MONTHS, $count)), 'first day of this month midnight', static fn(\DateTimeImmutable $d): \DateTimeImmutable => $d->modify('first day of next month')->setTime(0, 0)); } /** * @param callable(\DateTimeImmutable): \DateTimeImmutable $next the start of the period that follows the one starting at the argument * @return list */ private static function periods(int $now, \DateTimeZone $tz, int $count, string $anchor, callable $next): array { $start = (new \DateTimeImmutable('@' . $now))->setTimezone($tz)->modify($anchor); $end = $next($start); $out = []; for ($i = 0; $i < $count; $i++) { $out[] = ['start' => $start->getTimestamp(), 'end' => $end->getTimestamp()]; $end = $start; // The instant one second before this period began lies in the previous period, and that period's start is its anchor. // Stepping back by an elapsed second (not by wall-clock arithmetic) is what survives days of 23 or 25 hours and midnights // that do not exist: a wall-clock step from 01:00 on a day without a midnight lands on the same day again. $start = (new \DateTimeImmutable('@' . ($start->getTimestamp() - 1)))->setTimezone($tz)->modify($anchor); } return $out; } }