format( 'Y-m-d' ) !== $start_date || $end->format( 'Y-m-d' ) !== $end_date ) { throw new InvalidArgumentException( 'Dates must be real dates in YYYY-MM-DD format.' ); } if ( $end < $start ) { throw new InvalidArgumentException( 'end_date is before start_date.' ); } if ( $start->diff( $end )->days > self::MAX_DAYS ) { throw new InvalidArgumentException( 'The date range is too long.' ); } return new self( $start, $end->setTime( 23, 59, 59 ) ); } /** * Number of days in the period. */ public function days(): int { return (int) $this->start->diff( $this->end->setTime( 0, 0 ) )->days + 1; } /** * Whether the period is one or more whole calendar months. */ public function is_whole_months(): bool { return '01' === $this->start->format( 'd' ) && $this->end->format( 'Y-m-d' ) === $this->end->modify( 'last day of this month' )->format( 'Y-m-d' ); } /** * The comparison period. * * Previous period: the same number of whole months before when the period is * whole months (so October compares with September), otherwise the same number * of days immediately before. Previous year: the same dates a year earlier. * * @param string $mode previous_period or previous_year. */ public function comparison( string $mode ): self { if ( 'previous_year' === $mode ) { $end = self::year_earlier( $this->end ); // A period ending on the last day of a month ends on that month's last day a year earlier (29 Feb → 28 Feb). if ( $this->end->format( 'Y-m-d' ) === $this->end->modify( 'last day of this month' )->format( 'Y-m-d' ) ) { $end = $end->modify( 'last day of this month' ); } return new self( self::year_earlier( $this->start ), $end->setTime( 23, 59, 59 ) ); } if ( $this->is_whole_months() ) { $months = ( (int) $this->end->format( 'Y' ) - (int) $this->start->format( 'Y' ) ) * 12 + (int) $this->end->format( 'm' ) - (int) $this->start->format( 'm' ) + 1; $start = $this->start->modify( "-{$months} months" ); $end = $this->start->modify( '-1 day' ); return new self( $start, $end->setTime( 23, 59, 59 ) ); } $days = $this->days(); return new self( $this->start->modify( "-{$days} days" ), $this->start->modify( '-1 day' )->setTime( 23, 59, 59 ) ); } /** * The same calendar date a year earlier, clamped to the month's last day. * * @param DateTimeImmutable $date Date. */ private static function year_earlier( DateTimeImmutable $date ): DateTimeImmutable { $year = (int) $date->format( 'Y' ) - 1; $month = (int) $date->format( 'm' ); $day = min( (int) $date->format( 'd' ), (int) $date->setDate( $year, $month, 1 )->format( 't' ) ); return $date->setDate( $year, $month, $day ); } /** * Analytics REST "after" parameter (store time). */ public function after(): string { return $this->start->format( 'Y-m-d\TH:i:s' ); } /** * Analytics REST "before" parameter (store time). */ public function before(): string { return $this->end->format( 'Y-m-d\TH:i:s' ); } /** * Interval for a chart series: day up to ~2 months, week up to ~6 months, then month. */ public function chart_interval(): string { $days = $this->days(); if ( $days <= 62 ) { return 'day'; } return $days <= 190 ? 'week' : 'month'; } /** * Plain description for tool results. * * @return array{start_date: string, end_date: string, days: int} */ public function to_array(): array { return array( 'start_date' => $this->start->format( 'Y-m-d' ), 'end_date' => $this->end->format( 'Y-m-d' ), 'days' => $this->days(), ); } }