$s, 'e' => self::add_days( $month, -1 ), 'ps' => $ps, 'pe' => self::add_days( $s, -1 ), ); } /** * The last full calendar quarter, compared with the quarter before. * * @param string|null $today Override for "today" (Y-m-d). * @return array{s:string,e:string,ps:string,pe:string} */ public static function last_full_quarter( $today = null ) { $end = self::end_date( $today ); $month = (int) substr( $end, 5, 2 ); $start = sprintf( '%s-%02d-01', substr( $end, 0, 4 ), (int) ( floor( ( $month - 1 ) / 3 ) * 3 + 1 ) ); $s = self::add_months( $start, -3 ); $ps = self::add_months( $s, -3 ); return array( 's' => $s, 'e' => self::add_days( $start, -1 ), 'ps' => $ps, 'pe' => self::add_days( $s, -1 ), ); } /** * The last N settled days, compared with the N days before. * * @param int $days Number of days. * @param string|null $today Override for "today". * @return array{s:string,e:string,ps:string,pe:string} */ public static function last_days( $days, $today = null ) { $e = self::end_date( $today ); $s = self::add_days( $e, -( $days - 1 ) ); $pe = self::add_days( $s, -1 ); return array( 's' => $s, 'e' => $e, 'ps' => self::add_days( $pe, -( $days - 1 ) ), 'pe' => $pe, ); } /** * Swaps the comparison range for the same dates one year earlier. * * @param array $period Period with s and e. * @return array */ public static function versus_last_year( array $period ) { $period['ps'] = self::shift_year( $period['s'], -1 ); $period['pe'] = self::shift_year( $period['e'], -1 ); return $period; } /** * The six-month trend range ending on a date. * * @param string $end End date. * @return array{s:string,e:string} */ public static function trend( $end ) { return array( 's' => self::add_months( substr( $end, 0, 8 ) . '01', -5 ), 'e' => $end, ); } /** * Adds days to a date. * * @param string $date Y-m-d. * @param int $days Days to add (negative to subtract). * @return string */ public static function add_days( $date, $days ) { return self::date( $date )->modify( ( $days >= 0 ? '+' : '' ) . (int) $days . ' days' )->format( 'Y-m-d' ); } /** * Number of calendar days in a validated inclusive range. * * @param string $start Start date. * @param string $end End date. * @return int */ public static function inclusive_days( $start, $end ) { return (int) self::date( $start )->diff( self::date( $end ) )->days + 1; } /** * Adds months to the first day of a month. * * @param string $first_of_month Y-m-01. * @param int $months Months to add. * @return string */ public static function add_months( $first_of_month, $months ) { return self::date( substr( $first_of_month, 0, 8 ) . '01' )->modify( ( $months >= 0 ? '+' : '' ) . (int) $months . ' months' )->format( 'Y-m-d' ); } /** * Moves a date by whole years, turning 29 February into 28 February. * * @param string $date Y-m-d. * @param int $years Years to add. * @return string */ public static function shift_year( $date, $years ) { $y = (int) substr( $date, 0, 4 ) + $years; $m = (int) substr( $date, 5, 2 ); $d = (int) substr( $date, 8, 2 ); if ( 2 === $m && 29 === $d && ! checkdate( 2, 29, $y ) ) { $d = 28; } return sprintf( '%04d-%02d-%02d', $y, $m, $d ); } /** * Parses a date in UTC (dates are calendar days, not instants). * * @param string $date Y-m-d. * @return \DateTimeImmutable */ private static function date( $date ) { return new \DateTimeImmutable( $date . ' 00:00:00', new \DateTimeZone( 'UTC' ) ); } }