From aad468e8b75ffc3e87ea6dfa22c53a8299fc08da Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 16 Aug 2026 00:29:03 +0900 Subject: feat(php-shim): render human-facing timestamps in the local timezone Split date() into date_utc() and date_local(), the latter resolving the system's local timezone through the tzfile crate ($TZ, then /etc/localtime, falling back to UTC when neither is readable). The timestamps Composer renders for humans -- the GitHub OAuth token note, the GitHub API rate limit reset time, the Perforce client spec fields and the "today" check of the show command -- now go through date_local(). PHP resolves its default timezone from the date.timezone ini setting, which Shirabe does not read, so date_default_timezone_get/set have no input left to model and are dropped from the shim and its callers. The resulting difference is recorded in docs/known-incompatibilities.md. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe-php-shim/src/datetime.rs | 58 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 30 deletions(-) (limited to 'crates/shirabe-php-shim/src/datetime.rs') diff --git a/crates/shirabe-php-shim/src/datetime.rs b/crates/shirabe-php-shim/src/datetime.rs index fd0348ff..28ec52af 100644 --- a/crates/shirabe-php-shim/src/datetime.rs +++ b/crates/shirabe-php-shim/src/datetime.rs @@ -1,11 +1,8 @@ -static DEFAULT_TIMEZONE: std::sync::Mutex> = std::sync::Mutex::new(None); - /// Parse the subset of the strtotime()/date_create() grammar that Composer actually emits. /// /// Supported: ISO8601/RFC3339 (`2023-01-15T12:34:56Z`, `...+00:00`), `Y-m-d H:i:s`, `Y-m-d`, -/// and `@`. Inputs without an explicit offset are interpreted as UTC, matching the -/// default timezone this shim assumes elsewhere. Anything else returns `None` rather than -/// guessing, mirroring PHP returning `false` on unrecognized input. +/// and `@`. Inputs without an explicit offset are interpreted as UTC. Anything else +/// returns `None` rather than guessing, mirroring PHP returning `false` on unrecognized input. fn parse_to_fixed(s: &str) -> Option> { let s = s.trim(); if s.is_empty() { @@ -126,34 +123,35 @@ pub fn microtime() -> f64 { duration.as_secs_f64() } -// PHP defaults to "UTC" when no default timezone has been configured. -pub fn date_default_timezone_get() -> String { - DEFAULT_TIMEZONE - .lock() - .unwrap() - .clone() - .unwrap_or_else(|| "UTC".to_string()) -} - -pub fn date_default_timezone_set(tz: &str) -> bool { - *DEFAULT_TIMEZONE.lock().unwrap() = Some(tz.to_string()); - true +/// PHP: `date()`, rendering in UTC. +pub fn date_utc(format: &str, timestamp: Option) -> String { + let timestamp = timestamp.unwrap_or_else(time); + let dt = chrono::DateTime::::from_timestamp(timestamp, 0) + .expect("date() timestamp out of range"); + dt.format(date_format_to_strftime(format)).to_string() } -pub fn date(format: &str, timestamp: Option) -> String { +/// PHP: `date()`, rendering in the system's local timezone. +pub fn date_local(format: &str, timestamp: Option) -> String { let timestamp = timestamp.unwrap_or_else(time); - // TODO(php-semantics): model the system default timezone. PHP `date()` renders in the default - // timezone (usually the system's local zone); without a timezone database only "UTC" can be - // resolved here, so on a non-UTC machine this diverges whenever the local date differs from - // the UTC date (e.g. daily 00:00-09:00 JST). Fixing this needs a timezone database (a new - // crate). Any named zone is rejected loudly rather than silently rendered in the wrong zone. - let tz = date_default_timezone_get(); - if tz != "UTC" { - panic!( - "date() with non-UTC default timezone {tz:?} is not supported (no timezone database)" - ); - } let dt = chrono::DateTime::::from_timestamp(timestamp, 0) .expect("date() timestamp out of range"); - dt.format(date_format_to_strftime(format)).to_string() + let tz = local_timezone(); + dt.with_timezone(&&tz) + .format(date_format_to_strftime(format)) + .to_string() +} + +/// The zone `$TZ` names, or the one `/etc/localtime` describes. Falls back to UTC when no tz +/// database is readable, as PHP does when `date.timezone` is unset. +fn local_timezone() -> tzfile::Tz { + #[cfg(unix)] + { + tzfile::Tz::local().unwrap_or_else(|_| tzfile::Tz::from(chrono::Utc)) + } + #[cfg(not(unix))] + { + // TODO(windows): `tzfile::Tz::local()` is Unix-only. + todo!() + } } -- cgit v1.3.1-4-g156e