use std::str::FromStr; use jiff::{Zoned, civil::Date, fmt::rfc2822, tz::TimeZone}; use crate::parse::git::parse_git_date_format; use crate::parse::raw::parse_raw; use crate::{ Error, OffsetInSeconds, SecondsSinceUnixEpoch, Time, parse::relative, time::format::{DEFAULT, GITOXIDE, ISO8601, ISO8601_STRICT, SHORT}, }; use gix_error::{Exn, ResultExt}; /// Parse `input` as any time that Git can parse when inputting a date. /// /// ## Examples /// /// ### 1. SHORT Format /// /// * `2018-12-24` /// * `1970-01-01` /// * `1950-12-31` /// * `2024-12-31` /// /// ### 2. RFC2822 Format /// /// * `Thu, 18 Aug 2022 12:45:06 +0800` /// * `Mon Oct 27 10:30:00 2023 -0800` /// /// ### 3. GIT_RFC2822 Format /// /// * `Thu, 8 Aug 2022 12:45:06 +0800` /// * `Mon Oct 27 10:30:00 2023 -0800` (Note the single-digit day) /// /// ### 4. ISO8601 Format /// /// * `2022-08-17 22:04:58 +0200` /// * `1970-01-01 00:00:00 -0500` /// /// ### 5. ISO8601_STRICT Format /// /// * `2022-08-17T21:43:13+08:00` /// /// ### 6. UNIX Timestamp (Seconds Since Epoch) /// /// * `123456789` /// * `0` (January 1, 1970 UTC) /// * `-1000` /// * `1700000000` /// /// ### 7. Commit Header Format /// /// * `1745582210 +0200` /// * `1660874655 +0800` /// * `-1660874655 +0800` /// /// A leading `@` may introduce either of the two forms above, as in `@1745582210 +0200` /// or `@1700000000`. /// /// See also the [`parse_header()`]. /// /// ### 8. GITOXIDE Format /// /// * `Thu Sep 04 2022 10:45:06 -0400` /// * `Mon Oct 27 2023 10:30:00 +0000` /// /// ### 9. DEFAULT Format /// /// * `Thu Sep 4 10:45:06 2022 -0400` /// * `Mon Oct 27 10:30:00 2023 +0000` /// /// ### 10. Relative Dates (e.g., "2 minutes ago") /// /// These dates are parsed relative to `now`, whose time zone controls calendar arithmetic. /// The examples depend entirely on the value of `now`. /// If `now` is October 27, 2023 at 10:00:00 UTC: /// * `2 minutes ago` (October 27, 2023 at 09:58:00 UTC) /// * `3 hours ago` (October 27, 2023 at 07:00:00 UTC) /// /// The forms understood are `now`, `today`, `yesterday`, and one or more ` ` pairs, /// as in `2 days 3 hours ago`. A count may be spelled out from `one` to `ten`, or be `last`, and /// any byte that is neither a digit nor a letter separates the parts, so `1.hour.ago` is the same /// as `1 hour ago`. The trailing `ago` is optional. /// /// ` ` pairs are applied in input order, the way Git applies them: `second` through `week` each /// subtract a fixed number of seconds, while `month` and `year` step down the respective calendar fields, /// leaving the day of the month alone. /// A day beyond the end of the shorter target month rolls over into the following month: one month before /// May 31st is May 1st, not April 30th. /// /// Note that there is no way to name a time in the future: Git has none either, so `1 hour from /// now` is an hour in the past to it, and to this function. pub fn parse(input: &str, now: Option) -> Result> { // Git accepts a leading `@` before a commit-header date: `match_object_header_date()` in // `date.c` takes ` ±HHMM`, while an offsetless `@` arrives at the same // result through the generic loop, which skips the `@` and reads the digits as an epoch. if let Some(rest) = input.strip_prefix('@') { if let Some(val) = parse_raw(rest) { return Ok(val); } if let Ok(seconds) = SecondsSinceUnixEpoch::from_str(rest) { return Ok(Time::new(seconds, 0)); } } Ok(if let Ok(val) = Date::strptime(SHORT.0, input) { let val = val .to_zoned(TimeZone::UTC) .or_raise(|| Error::new_with_input("Timezone conversion failed", input))?; Time::new(val.timestamp().as_second(), val.offset().seconds()) } else if let Ok(val) = rfc2822_relaxed(input) { Time::new(val.timestamp().as_second(), val.offset().seconds()) } else if let Ok(val) = strptime_relaxed(ISO8601.0, input) { Time::new(val.timestamp().as_second(), val.offset().seconds()) } else if let Ok(val) = strptime_relaxed(ISO8601_STRICT.0, input) { Time::new(val.timestamp().as_second(), val.offset().seconds()) } else if let Ok(val) = strptime_relaxed(GITOXIDE.0, input) { Time::new(val.timestamp().as_second(), val.offset().seconds()) } else if let Ok(val) = strptime_relaxed(DEFAULT.0, input) { Time::new(val.timestamp().as_second(), val.offset().seconds()) } else if let Ok(val) = SecondsSinceUnixEpoch::from_str(input) { Time::new(val, 0) } else if let Some(val) = parse_git_date_format(input) { val } else if let Some(val) = relative::parse(input, now).transpose()? { Time::new(val.timestamp().as_second(), val.offset().seconds()) } else if let Some(val) = parse_raw(input) { // Format::Raw val } else { return Err(Error::new_with_input("Unknown date format", input))?; }) } /// Unlike [`parse()`] which handles all kinds of input, this function only parses the commit-header format /// like `1745582210 +0200`. /// /// Note that failure to parse the time zone isn't fatal, instead it will default to `0`. To know if /// the time is wonky, serialize the return value to see if it matches the `input.` pub fn parse_header(input: &str) -> Option