$params Request parameters */ public static function fromRequest(array $params): self { $preset = (string) ($params['preset'] ?? ''); if ($preset === self::PRESET_LAST_QUARTER) { return self::lastQuarter(); } if ($preset === self::PRESET_NEXT_QUARTER) { return self::nextQuarter(); } if ($preset === self::PRESET_CUSTOM || (! empty($params['from']) && ! empty($params['to']))) { try { $from = dtHelper()->parseUserDateTime((string) $params['from'], 'start')->setToDbTimezone(); $to = dtHelper()->parseUserDateTime((string) $params['to'], 'end')->setToDbTimezone(); if ($from->lessThanOrEqualTo($to)) { return new self($from, $to, self::PRESET_CUSTOM); } } catch (\Exception $e) { // Unparseable custom range: fall through to the default preset. } } return self::thisQuarter(); } /** * The current quarter in the user's calendar. */ public static function thisQuarter(): self { $now = dtHelper()->userNow(); return new self( $now->startOfQuarter()->setToDbTimezone(), $now->endOfQuarter()->setToDbTimezone(), self::PRESET_THIS_QUARTER, ); } /** * The previous full quarter in the user's calendar. */ public static function lastQuarter(): self { $anchor = dtHelper()->userNow()->subQuarterNoOverflow(); return new self( $anchor->startOfQuarter()->setToDbTimezone(), $anchor->endOfQuarter()->setToDbTimezone(), self::PRESET_LAST_QUARTER, ); } /** * The next quarter in the user's calendar. */ public static function nextQuarter(): self { $anchor = dtHelper()->userNow()->addQuarterNoOverflow(); return new self( $anchor->startOfQuarter()->setToDbTimezone(), $anchor->endOfQuarter()->setToDbTimezone(), self::PRESET_NEXT_QUARTER, ); } /** * The equivalent preceding period, used for period-over-period deltas. For quarter presets * this is the previous quarter; for custom ranges the same number of days directly before. */ public function priorPeriod(): self { if ($this->preset !== self::PRESET_CUSTOM) { $anchor = $this->from->setToUserTimezone()->subQuarterNoOverflow(); return new self( $anchor->startOfQuarter()->setToDbTimezone(), $anchor->endOfQuarter()->setToDbTimezone(), self::PRESET_CUSTOM, ); } $lengthInDays = (int) $this->from->diffInDays($this->to) + 1; return new self( $this->from->subDays($lengthInDays), $this->to->subDays($lengthInDays), self::PRESET_CUSTOM, ); } /** * End of the "coming up" horizon: two full quarters beyond the period end. */ public function upcomingHorizon(): CarbonImmutable { return $this->to->setToUserTimezone()->addQuartersNoOverflow(2)->endOfQuarter()->setToDbTimezone(); } /** * Whether the given UTC datetime falls inside the period. */ public function contains(CarbonImmutable $dateTime): bool { return $dateTime->betweenIncluded($this->from, $this->to); } /** * Human readable label, e.g. "Q2 2026 · Apr 1 – Jun 30, 2026". */ public function label(): string { $userFrom = $this->from->setToUserTimezone(); $userTo = $this->to->setToUserTimezone(); $range = $userFrom->formatDateForUser().' – '.$userTo->formatDateForUser(); // A range spanning exactly one calendar quarter gets the quarter shorthand prefix. if ( $userFrom->equalTo($userFrom->startOfQuarter()) && $userTo->equalTo($userTo->endOfQuarter()) && $userFrom->isSameQuarter($userTo) ) { return 'Q'.$userFrom->quarter.' '.$userFrom->year.' · '.$range; } return $range; } /** * Query string carrying the period across filter swaps and drill-down links. */ public function toQueryString(): string { if ($this->preset !== self::PRESET_CUSTOM) { return http_build_query(['preset' => $this->preset]); } return http_build_query([ 'preset' => self::PRESET_CUSTOM, 'from' => $this->from->setToUserTimezone()->formatDateForUser(), 'to' => $this->to->setToUserTimezone()->formatDateForUser(), ]); } /** * Period start formatted for db comparisons (UTC, Y-m-d H:i:s). */ public function fromDbString(): string { return $this->from->format('Y-m-d H:i:s'); } /** * Period end formatted for db comparisons (UTC, Y-m-d H:i:s). */ public function toDbString(): string { return $this->to->format('Y-m-d H:i:s'); } }