28 Feb, 29 Feb -> * 28 Feb) and a range that ends on the last day of a month keeps ending on the * last day of the earlier month, so "last 30 days" never compares against a * period that quietly spills into another month. */ if ( ! defined( 'ABSPATH' ) ) { exit; } class AI_O_Optimiser_Period_Compare { const MODES = array( 'prev', 'mom', 'yoy' ); /** * Normalise a requested mode. * * @param mixed $mode Requested mode. * @return string */ public static function mode( $mode ) { $mode = is_string( $mode ) ? strtolower( $mode ) : ''; return in_array( $mode, self::MODES, true ) ? $mode : 'prev'; } /** * Comparison dates for a range. * * @param string $mode prev, mom or yoy. * @param string $start Range start, Y-m-d. * @param string $end Range end, Y-m-d. * @return array{start:string,end:string}|null Null when the dates are not valid. */ public static function dates( $mode, $start, $end ) { $s = self::parse( $start ); $e = self::parse( $end ); if ( null === $s || null === $e || $s > $e ) { return null; } switch ( self::mode( $mode ) ) { case 'mom': return array( 'start' => self::shift_months( $s, 1 ), 'end' => self::shift_end( $e, 1 ) ); case 'yoy': return array( 'start' => self::shift_months( $s, 12 ), 'end' => self::shift_end( $e, 12 ) ); default: $days = (int) round( ( $e - $s ) / 86400 ) + 1; return array( 'start' => gmdate( 'Y-m-d', $s - $days * 86400 ), 'end' => gmdate( 'Y-m-d', $s - 86400 ) ); } } /** * Whether stored history reaches back to the comparison period. * * A count of zero from a period the site was not tracking yet is not "no * visits", so the dashboard must not show it as a drop to nothing. * * @param string $comparison_start Y-m-d. * @param string $first_day Earliest day with stored data, Y-m-d, or '' when none. * @return bool */ public static function history_covers( $comparison_start, $first_day ) { if ( '' === (string) $first_day || null === self::parse( $comparison_start ) || null === self::parse( $first_day ) ) { return false; } return self::parse( $first_day ) <= self::parse( $comparison_start ); } /** * Label shown next to a comparison. * * @param string $mode prev, mom or yoy. * @param int $days Range length in days (prev only). * @return string */ public static function label( $mode, $days = 0 ) { switch ( self::mode( $mode ) ) { case 'mom': return __( 'same dates last month', 'ai-o-optimiser' ); case 'yoy': return __( 'same dates last year', 'ai-o-optimiser' ); default: /* translators: %d: number of days. */ return $days > 1 ? sprintf( __( 'previous %d days', 'ai-o-optimiser' ), $days ) : __( 'previous day', 'ai-o-optimiser' ); } } /** * Change between two counts, with the states a reader needs to tell apart. * * @param int|float|null $current Current value. * @param int|float|null $previous Comparison value; null when it could not be read. * @return array{state:string,change:float|int|null,percent:float|null} */ public static function delta( $current, $previous ) { if ( null === $previous || null === $current ) { return array( 'state' => 'missing', 'change' => null, 'percent' => null ); } $change = $current - $previous; if ( 0 == $previous ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- Compares int and float counts. return 0 == $current ? array( 'state' => 'flat', 'change' => 0, 'percent' => null ) : array( 'state' => 'new', 'change' => $change, 'percent' => null ); } return array( 'state' => 0 == $change ? 'flat' : 'ok', // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- Compares int and float counts. 'change' => $change, 'percent' => round( $change / $previous * 100, 1 ), ); } /** * @param string $ymd Y-m-d. * @return int|null UTC midnight timestamp. */ private static function parse( $ymd ) { if ( ! is_string( $ymd ) || ! preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $ymd, $m ) || ! checkdate( (int) $m[2], (int) $m[3], (int) $m[1] ) ) { return null; } return gmmktime( 0, 0, 0, (int) $m[2], (int) $m[3], (int) $m[1] ); } private static function shift_months( $ts, $months ) { $index = (int) gmdate( 'Y', $ts ) * 12 + (int) gmdate( 'n', $ts ) - 1 - $months; $year = intdiv( $index, 12 ); $month = $index % 12 + 1; $day = min( (int) gmdate( 'j', $ts ), (int) gmdate( 't', gmmktime( 0, 0, 0, $month, 1, $year ) ) ); return sprintf( '%04d-%02d-%02d', $year, $month, $day ); } private static function shift_end( $ts, $months ) { if ( (int) gmdate( 'j', $ts ) === (int) gmdate( 't', $ts ) ) { $index = (int) gmdate( 'Y', $ts ) * 12 + (int) gmdate( 'n', $ts ) - 1 - $months; $year = intdiv( $index, 12 ); $month = $index % 12 + 1; return sprintf( '%04d-%02d-%02d', $year, $month, (int) gmdate( 't', gmmktime( 0, 0, 0, $month, 1, $year ) ) ); } return self::shift_months( $ts, $months ); } }