-
-
Notifications
You must be signed in to change notification settings - Fork 327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
gix-date: switch from time
to jiff
#1474
Changes from 1 commit
5a88413
1b9c30d
28ac657
66b9e07
9d35a0a
9fd1090
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,7 +14,7 @@ pub enum Error { | |
pub(crate) mod function { | ||
use std::{str::FromStr, time::SystemTime}; | ||
|
||
use time::{format_description::well_known, Date, OffsetDateTime}; | ||
use jiff::{civil::Date, fmt::rfc2822, tz::TimeZone, Zoned}; | ||
|
||
use crate::{ | ||
parse::{relative, Error}, | ||
|
@@ -32,27 +32,27 @@ pub(crate) mod function { | |
return Ok(Time::new(42, 1800)); | ||
} | ||
|
||
Ok(if let Ok(val) = Date::parse(input, SHORT.0) { | ||
let val = val.with_hms(0, 0, 0).expect("date is in range").assume_utc(); | ||
Time::new(val.unix_timestamp(), val.offset().whole_seconds()) | ||
} else if let Ok(val) = OffsetDateTime::parse(input, &well_known::Rfc2822) { | ||
Time::new(val.unix_timestamp(), val.offset().whole_seconds()) | ||
} else if let Ok(val) = OffsetDateTime::parse(input, ISO8601.0) { | ||
Time::new(val.unix_timestamp(), val.offset().whole_seconds()) | ||
} else if let Ok(val) = OffsetDateTime::parse(input, ISO8601_STRICT.0) { | ||
Time::new(val.unix_timestamp(), val.offset().whole_seconds()) | ||
} else if let Ok(val) = OffsetDateTime::parse(input, GITOXIDE.0) { | ||
Time::new(val.unix_timestamp(), val.offset().whole_seconds()) | ||
} else if let Ok(val) = OffsetDateTime::parse(input, DEFAULT.0) { | ||
Time::new(val.unix_timestamp(), val.offset().whole_seconds()) | ||
Ok(if let Ok(val) = Date::strptime(SHORT.0, input) { | ||
let val = val.to_zoned(TimeZone::UTC).expect("date is in range"); | ||
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) { | ||
// Format::Unix | ||
Time::new(val, 0) | ||
} else if let Some(val) = parse_raw(input) { | ||
// Format::Raw | ||
val | ||
} else if let Some(time) = relative::parse(input, now).transpose()? { | ||
Time::new(time.unix_timestamp(), time.offset().whole_seconds()) | ||
} else if let Some(val) = relative::parse(input, now).transpose()? { | ||
Time::new(val.timestamp().as_second(), val.offset().seconds()) | ||
} else { | ||
return Err(Error::InvalidDateString { input: input.into() }); | ||
}) | ||
|
@@ -83,52 +83,79 @@ pub(crate) mod function { | |
}; | ||
Some(time) | ||
} | ||
|
||
/// This is just like `Zoned::strptime`, but it allows parsing datetimes | ||
/// whose weekdays are inconsistent with the date. While the day-of-week | ||
/// still must be parsed, it is otherwise ignored. This seems to be | ||
/// consistent with how `git` behaves. | ||
fn strptime_relaxed(fmt: &str, input: &str) -> Result<Zoned, jiff::Error> { | ||
let mut tm = jiff::fmt::strtime::parse(fmt, input)?; | ||
tm.set_weekday(None); | ||
tm.to_zoned() | ||
} | ||
|
||
/// This is just like strptime_relaxed, except for RFC 2822 parsing. | ||
/// Namely, it permits the weekday to be inconsistent with the date. | ||
fn rfc2822_relaxed(input: &str) -> Result<Zoned, jiff::Error> { | ||
static P: rfc2822::DateTimeParser = rfc2822::DateTimeParser::new().relaxed_weekday(true); | ||
P.parse_zoned(input) | ||
} | ||
} | ||
|
||
mod relative { | ||
use std::{str::FromStr, time::SystemTime}; | ||
|
||
use time::{Duration, OffsetDateTime}; | ||
use jiff::{tz::TimeZone, Span, Timestamp, Zoned}; | ||
|
||
use crate::parse::Error; | ||
|
||
fn parse_inner(input: &str) -> Option<Duration> { | ||
fn parse_inner(input: &str) -> Option<Result<Span, Error>> { | ||
let mut split = input.split_whitespace(); | ||
let multiplier = i64::from_str(split.next()?).ok()?; | ||
let units = i64::from_str(split.next()?).ok()?; | ||
let period = split.next()?; | ||
if split.next()? != "ago" { | ||
return None; | ||
} | ||
duration(period, multiplier) | ||
span(period, units) | ||
} | ||
|
||
pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<OffsetDateTime, Error>> { | ||
parse_inner(input).map(|offset| { | ||
let offset = std::time::Duration::from_secs(offset.whole_seconds().try_into()?); | ||
pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<Zoned, Error>> { | ||
parse_inner(input).map(|result| { | ||
let span = result?; | ||
// This was an error case in a previous version of this code, where | ||
// it would fail when converting from a negative signed integer | ||
// to an unsigned integer. This preserves that failure case even | ||
// though the code below handles it okay. | ||
if span.is_negative() { | ||
return Err(Error::RelativeTimeConversion); | ||
} | ||
now.ok_or(Error::MissingCurrentTime).and_then(|now| { | ||
std::panic::catch_unwind(|| { | ||
now.checked_sub(offset) | ||
.expect("BUG: values can't be large enough to cause underflow") | ||
.into() | ||
}) | ||
.map_err(|_| Error::RelativeTimeConversion) | ||
let ts = Timestamp::try_from(now).map_err(|_| Error::RelativeTimeConversion)?; | ||
// N.B. This matches the behavior of this code when it was | ||
// written with `time`, but we might consider using the system | ||
// time zone here. If we did, then it would implement "1 day | ||
// ago" correctly, even when it crosses DST transitions. Since | ||
// we're in the UTC time zone here, which has no DST, 1 day is | ||
// in practice always 24 hours. ---AG | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd love to see this becoming more correct, and likely even more correct than Git itself. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I definitely don't have the context to be fully confident in such a change here. It depends on your test coverage I think. But the idea here is that you'd do |
||
let zdt = ts.to_zoned(TimeZone::UTC); | ||
zdt.checked_sub(span).map_err(|_| Error::RelativeTimeConversion) | ||
}) | ||
}) | ||
} | ||
|
||
fn duration(period: &str, multiplier: i64) -> Option<Duration> { | ||
fn span(period: &str, units: i64) -> Option<Result<Span, Error>> { | ||
let period = period.strip_suffix('s').unwrap_or(period); | ||
let seconds: i64 = match period { | ||
"second" => 1, | ||
"minute" => 60, | ||
"hour" => 60 * 60, | ||
"day" => 24 * 60 * 60, | ||
"week" => 7 * 24 * 60 * 60, | ||
let result = match period { | ||
"second" => Span::new().try_seconds(units), | ||
"minute" => Span::new().try_minutes(units), | ||
"hour" => Span::new().try_hours(units), | ||
"day" => Span::new().try_days(units), | ||
"week" => Span::new().try_weeks(units), | ||
// TODO months & years? YES | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this something that would easily be supported now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup. It would just be |
||
// Ignore values you don't know, assume seconds then (so does git) | ||
_ => return None, | ||
}; | ||
seconds.checked_mul(multiplier).map(Duration::seconds) | ||
Some(result.map_err(|_| Error::RelativeTimeConversion)) | ||
} | ||
|
||
#[cfg(test)] | ||
|
@@ -137,7 +164,7 @@ mod relative { | |
|
||
#[test] | ||
fn two_weeks_ago() { | ||
assert_eq!(parse_inner("2 weeks ago"), Some(Duration::weeks(2))); | ||
assert_eq!(parse_inner("2 weeks ago").unwrap().unwrap(), Span::new().weeks(2)); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I remember that some tests were indeed quite
time
dependent, while some code also tried to workaround panics intime
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I noticed that because I was intensely curious as to why there was a
catch_unwind
here deep inside datetime parsing. And yeah indeed, I believetime
's panic comes from itsFrom<SystemTime>
impl, which in turn callsOffsetDateTime
'sAdd
impl that specifically panics. TheAdd
impl panicking makes sense, since that's consistent with whatstd
does too. But theFrom
impl should probably be aTryFrom
impl. Since it's aFrom
impl,time
's behavior here is pretty much locked into its API. With that said, I think you probably could have avoided thecatch_unwind
here by first convertingnow
to atime::OffsetDateTime
and then doing your arithmetic, sincetime
exposes checked operations ontime::OffsetDateTime
. With Jiff, you can't mess this up because it doesn't provide aFrom<SystemTime>
impl. OnlyTryFrom<SystemTime>
.