aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim/src/datetime.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 15:37:21 +0900
committernsfisis <nsfisis@gmail.com>2026-06-26 00:20:05 +0900
commit3a883e6912e642a1bcfe68336007eae018207308 (patch)
tree2750519e1bbaaf8caa493bcf1eb15efb143280d9 /crates/shirabe-php-shim/src/datetime.rs
parentd4cdccb8de8758bd46a12283f8df90e020327b99 (diff)
downloadphp-shirabe-3a883e6912e642a1bcfe68336007eae018207308.tar.gz
php-shirabe-3a883e6912e642a1bcfe68336007eae018207308.tar.zst
php-shirabe-3a883e6912e642a1bcfe68336007eae018207308.zip
test: port 35 auth/installer/io/zip/bitbucket tests; implement date_create
Port auth_helper (14), library_installer (8), console_io (7), zip_downloader (3), git_bitbucket_driver (3) tests. Implement date_create/strtotime for the ISO8601/ RFC3339/relative formats Composer uses (unknown input -> None, no silent guess). Fix production bugs: Question::is_assoc list-vs-assoc, auth_helper gitlab-domains list handling, LibraryInstaller RefCell double-borrow, ZipArchive::extract_to ErrorException propagation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim/src/datetime.rs')
-rw-r--r--crates/shirabe-php-shim/src/datetime.rs99
1 files changed, 89 insertions, 10 deletions
diff --git a/crates/shirabe-php-shim/src/datetime.rs b/crates/shirabe-php-shim/src/datetime.rs
index e41028e..f44bb7b 100644
--- a/crates/shirabe-php-shim/src/datetime.rs
+++ b/crates/shirabe-php-shim/src/datetime.rs
@@ -1,11 +1,55 @@
static DEFAULT_TIMEZONE: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
-pub fn date_create<Tz: chrono::TimeZone>(s: &str) -> chrono::ParseResult<chrono::DateTime<Tz>> {
- // TODO(phase-d): PHP `date_create` accepts the full strtotime() grammar (RFC2822, ISO8601,
- // VCS-specific and relative formats), which requires a dedicated date parser not available
- // here. The generic `Tz` also has no constructor from a parsed `FixedOffset`/`Utc` value.
- let _ = s;
- todo!()
+/// 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 `@<unixtime>`. 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.
+fn parse_to_fixed(s: &str) -> Option<chrono::DateTime<chrono::FixedOffset>> {
+ let s = s.trim();
+ if s.is_empty() {
+ return None;
+ }
+
+ // `@<unixtime>` (optionally with a fractional part) denotes a Unix timestamp in UTC.
+ if let Some(rest) = s.strip_prefix('@') {
+ let secs = if let Some((whole, _frac)) = rest.split_once('.') {
+ whole.parse::<i64>().ok()?
+ } else {
+ rest.parse::<i64>().ok()?
+ };
+ let utc = chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0)?;
+ return Some(utc.fixed_offset());
+ }
+
+ // RFC3339 / ISO8601 with an explicit offset or trailing `Z`.
+ if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(s) {
+ return Some(dt);
+ }
+
+ // `Y-m-d H:i:s` and `Y-m-d`, both interpreted as UTC.
+ let naive = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S")
+ .ok()
+ .or_else(|| {
+ chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d")
+ .ok()
+ .and_then(|d| d.and_hms_opt(0, 0, 0))
+ })?;
+ let utc = chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(naive, chrono::Utc);
+ Some(utc.fixed_offset())
+}
+
+pub fn date_create<Tz: chrono::TimeZone>(s: &str) -> chrono::ParseResult<chrono::DateTime<Tz>>
+where
+ chrono::DateTime<Tz>: From<chrono::DateTime<chrono::FixedOffset>>,
+{
+ match parse_to_fixed(s) {
+ Some(dt) => Ok(dt.into()),
+ // PHP `date_create` returns `false` here; the closest faithful signal under chrono's
+ // `ParseResult` is a parse error, produced by replaying a deliberately invalid parse.
+ None => Err(chrono::DateTime::parse_from_rfc3339("").unwrap_err()),
+ }
}
/// PHP: \DATE_RFC3339 ("Y-m-d\TH:i:sP").
@@ -26,10 +70,45 @@ pub fn date_format_to_strftime(format: &str) -> &'static str {
}
}
-pub fn strtotime(_time: &str) -> Option<i64> {
- // TODO(phase-d): requires the full strtotime() grammar (absolute, relative and compound
- // expressions); a partial parser would silently mis-handle unsupported inputs.
- todo!()
+/// Subset of strtotime() covering what Composer passes: `now`, the absolute formats handled by
+/// `parse_to_fixed`, and simple single-unit relative offsets such as `-8 days` / `+3 hours`.
+///
+/// Returns `None` for anything else, matching PHP `strtotime()` returning `false`, rather than
+/// silently mis-handling an unsupported expression.
+pub fn strtotime(time: &str) -> Option<i64> {
+ let trimmed = time.trim();
+
+ if trimmed.eq_ignore_ascii_case("now") {
+ return Some(self::time());
+ }
+
+ if let Some(dt) = parse_to_fixed(trimmed) {
+ return Some(dt.timestamp());
+ }
+
+ parse_relative(trimmed).map(|delta| self::time() + delta)
+}
+
+/// Parse a single-unit relative offset like `-8 days`, `+3hours`, `1 week`, returning the signed
+/// number of seconds. Whitespace between the count and unit is optional, as PHP accepts both.
+fn parse_relative(s: &str) -> Option<i64> {
+ let s = s.trim();
+ let split = s
+ .find(|c: char| c.is_ascii_alphabetic())
+ .filter(|&i| i > 0)?;
+ let (count_part, unit_part) = s.split_at(split);
+ let count: i64 = count_part.trim().parse().ok()?;
+
+ let unit = unit_part.trim().to_ascii_lowercase();
+ let per_unit = match unit.as_str() {
+ "sec" | "secs" | "second" | "seconds" => 1,
+ "min" | "mins" | "minute" | "minutes" => 60,
+ "hour" | "hours" => 60 * 60,
+ "day" | "days" => 24 * 60 * 60,
+ "week" | "weeks" => 7 * 24 * 60 * 60,
+ _ => return None,
+ };
+ Some(count * per_unit)
}
pub fn time() -> i64 {