Skip to content

Commit 3cf78c3

Browse files
authored
Merge pull request #2272 from GitoxideLabs/copilot/add-gix-date-baseline-tests
Expand gix-date baseline tests and implement missing Git date formats
2 parents 25099c8 + fad8219 commit 3cf78c3

File tree

14 files changed

+1471
-808
lines changed

14 files changed

+1471
-808
lines changed

gix-date/src/parse.rs

Lines changed: 0 additions & 439 deletions
This file was deleted.

gix-date/src/parse/function.rs

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
use std::{str::FromStr, time::SystemTime};
2+
3+
use jiff::{civil::Date, fmt::rfc2822, tz::TimeZone, Zoned};
4+
5+
use crate::parse::git::parse_git_date_format;
6+
use crate::parse::raw::parse_raw;
7+
use crate::{
8+
parse::{relative, Error},
9+
time::format::{DEFAULT, GITOXIDE, ISO8601, ISO8601_STRICT, SHORT},
10+
OffsetInSeconds, SecondsSinceUnixEpoch, Time,
11+
};
12+
13+
/// Parse `input` as any time that Git can parse when inputting a date.
14+
///
15+
/// ## Examples
16+
///
17+
/// ### 1. SHORT Format
18+
///
19+
/// * `2018-12-24`
20+
/// * `1970-01-01`
21+
/// * `1950-12-31`
22+
/// * `2024-12-31`
23+
///
24+
/// ### 2. RFC2822 Format
25+
///
26+
/// * `Thu, 18 Aug 2022 12:45:06 +0800`
27+
/// * `Mon Oct 27 10:30:00 2023 -0800`
28+
///
29+
/// ### 3. GIT_RFC2822 Format
30+
///
31+
/// * `Thu, 8 Aug 2022 12:45:06 +0800`
32+
/// * `Mon Oct 27 10:30:00 2023 -0800` (Note the single-digit day)
33+
///
34+
/// ### 4. ISO8601 Format
35+
///
36+
/// * `2022-08-17 22:04:58 +0200`
37+
/// * `1970-01-01 00:00:00 -0500`
38+
///
39+
/// ### 5. ISO8601_STRICT Format
40+
///
41+
/// * `2022-08-17T21:43:13+08:00`
42+
///
43+
/// ### 6. UNIX Timestamp (Seconds Since Epoch)
44+
///
45+
/// * `123456789`
46+
/// * `0` (January 1, 1970 UTC)
47+
/// * `-1000`
48+
/// * `1700000000`
49+
///
50+
/// ### 7. Commit Header Format
51+
///
52+
/// * `1745582210 +0200`
53+
/// * `1660874655 +0800`
54+
/// * `-1660874655 +0800`
55+
///
56+
/// See also the [`parse_header()`].
57+
///
58+
/// ### 8. GITOXIDE Format
59+
///
60+
/// * `Thu Sep 04 2022 10:45:06 -0400`
61+
/// * `Mon Oct 27 2023 10:30:00 +0000`
62+
///
63+
/// ### 9. DEFAULT Format
64+
///
65+
/// * `Thu Sep 4 10:45:06 2022 -0400`
66+
/// * `Mon Oct 27 10:30:00 2023 +0000`
67+
///
68+
/// ### 10. Relative Dates (e.g., "2 minutes ago", "1 hour from now")
69+
///
70+
/// These dates are parsed *relative to a `now` timestamp*. The examples depend entirely on the value of `now`.
71+
/// If `now` is October 27, 2023 at 10:00:00 UTC:
72+
/// * `2 minutes ago` (October 27, 2023 at 09:58:00 UTC)
73+
/// * `3 hours ago` (October 27, 2023 at 07:00:00 UTC)
74+
pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
75+
Ok(if let Ok(val) = Date::strptime(SHORT.0, input) {
76+
let val = val
77+
.to_zoned(TimeZone::UTC)
78+
.map_err(|_| Error::InvalidDateString { input: input.into() })?;
79+
Time::new(val.timestamp().as_second(), val.offset().seconds())
80+
} else if let Ok(val) = rfc2822_relaxed(input) {
81+
Time::new(val.timestamp().as_second(), val.offset().seconds())
82+
} else if let Ok(val) = strptime_relaxed(ISO8601.0, input) {
83+
Time::new(val.timestamp().as_second(), val.offset().seconds())
84+
} else if let Ok(val) = strptime_relaxed(ISO8601_STRICT.0, input) {
85+
Time::new(val.timestamp().as_second(), val.offset().seconds())
86+
} else if let Ok(val) = strptime_relaxed(GITOXIDE.0, input) {
87+
Time::new(val.timestamp().as_second(), val.offset().seconds())
88+
} else if let Ok(val) = strptime_relaxed(DEFAULT.0, input) {
89+
Time::new(val.timestamp().as_second(), val.offset().seconds())
90+
} else if let Ok(val) = SecondsSinceUnixEpoch::from_str(input) {
91+
Time::new(val, 0)
92+
} else if let Some(val) = parse_git_date_format(input) {
93+
val
94+
} else if let Some(val) = relative::parse(input, now).transpose()? {
95+
Time::new(val.timestamp().as_second(), val.offset().seconds())
96+
} else if let Some(val) = parse_raw(input) {
97+
// Format::Raw
98+
val
99+
} else {
100+
return Err(Error::InvalidDateString { input: input.into() });
101+
})
102+
}
103+
104+
/// Unlike [`parse()`] which handles all kinds of input, this function only parses the commit-header format
105+
/// like `1745582210 +0200`.
106+
///
107+
/// Note that failure to parse the time zone isn't fatal, instead it will default to `0`. To know if
108+
/// the time is wonky, serialize the return value to see if it matches the `input.`
109+
pub fn parse_header(input: &str) -> Option<Time> {
110+
pub enum Sign {
111+
Plus,
112+
Minus,
113+
}
114+
fn parse_offset(offset: &str) -> Option<OffsetInSeconds> {
115+
if (offset.len() != 5) && (offset.len() != 7) {
116+
return None;
117+
}
118+
let sign = match offset.get(..1)? {
119+
"-" => Some(Sign::Minus),
120+
"+" => Some(Sign::Plus),
121+
_ => None,
122+
}?;
123+
if offset.as_bytes().get(1).is_some_and(|b| !b.is_ascii_digit()) {
124+
return None;
125+
}
126+
let hours: i32 = offset.get(1..3)?.parse().ok()?;
127+
let minutes: i32 = offset.get(3..5)?.parse().ok()?;
128+
let offset_seconds: i32 = if offset.len() == 7 {
129+
offset.get(5..7)?.parse().ok()?
130+
} else {
131+
0
132+
};
133+
let mut offset_in_seconds = hours * 3600 + minutes * 60 + offset_seconds;
134+
if matches!(sign, Sign::Minus) {
135+
offset_in_seconds *= -1;
136+
}
137+
Some(offset_in_seconds)
138+
}
139+
140+
if input.contains(':') {
141+
return None;
142+
}
143+
let mut split = input.split_whitespace();
144+
let seconds = split.next()?;
145+
let seconds = match seconds.parse::<SecondsSinceUnixEpoch>() {
146+
Ok(s) => s,
147+
Err(_err) => {
148+
// Inefficient, but it's not the common case.
149+
let first_digits: String = seconds.chars().take_while(char::is_ascii_digit).collect();
150+
first_digits.parse().ok()?
151+
}
152+
};
153+
let offset = match split.next() {
154+
None => 0,
155+
Some(offset) => {
156+
if split.next().is_some() {
157+
0
158+
} else {
159+
parse_offset(offset).unwrap_or_default()
160+
}
161+
}
162+
};
163+
let time = Time { seconds, offset };
164+
Some(time)
165+
}
166+
167+
/// This is just like `Zoned::strptime`, but it allows parsing datetimes
168+
/// whose weekdays are inconsistent with the date. While the day-of-week
169+
/// still must be parsed, it is otherwise ignored. This seems to be
170+
/// consistent with how `git` behaves.
171+
fn strptime_relaxed(fmt: &str, input: &str) -> Result<Zoned, jiff::Error> {
172+
let mut tm = jiff::fmt::strtime::parse(fmt, input)?;
173+
tm.set_weekday(None);
174+
tm.to_zoned()
175+
}
176+
177+
/// This is just like strptime_relaxed, except for RFC 2822 parsing.
178+
/// Namely, it permits the weekday to be inconsistent with the date.
179+
fn rfc2822_relaxed(input: &str) -> Result<Zoned, jiff::Error> {
180+
static P: rfc2822::DateTimeParser = rfc2822::DateTimeParser::new().relaxed_weekday(true);
181+
P.parse_zoned(input)
182+
}

0 commit comments

Comments
 (0)