Skip to content

Commit 3082d40

Browse files
committed
feat: Parse relative months and years
This extends `gix_date::parse::relative::span` to recognize times with "months" and "years", as in "3 months ago" and "2 years ago". The actual change here is very simple, because it is directly facilitated by GitoxideLabs#1474. The units "seconds", "minutes", "hours", "days", and "weeks" continue to be recognized (as before). Relative times like "1 year, 2 months ago" remain unrecognized. For background, see 43b6c06 (GitoxideLabs#498), c5c6bf6, GitoxideLabs#1474 (comment), and GitoxideLabs#1696 (comment). Note that this change does not fix issue GitoxideLabs#1696, which continues to apply to the intepretation of "days" and "weeks", and now also applies in the same way to the interpretation of "months" and "years". (It continues not to apply to the interpretation of "seconds", "minutes", and "hours".) The tests are updated to cover "months" and "years", as well as to exercise a wider range of relative times, including showing which units (e.g. "days") are affected by GitoxideLabs#1696. A few sub-cases, of those adjusted or added here, test strings from a related test in Git, to allow comparison. But most are not related to cases there. As before, the tests pass on CI, or when the time zone is otherwise UTC, or with local time zones that never have DST adjustments. The sub-cases are reordered to be monotone increasing in the magnitude of the relative intervals tested (and thus monotone decreasing in the associated absolute timestamps), and the original input is kept zipped into the `actual` and `expected` arrays being compared. This way, if the assertion fails (such as due to GitoxideLabs#1696), it is clear and readable which sub-cases failed and exactly how, as well as what all the sub-cases are and what each sub-case tests. Reordering the sub-cases and preserving the inputs they parse in this way avoids the disadvantages of GitoxideLabs#1697 while keeping, and expanding on, its benefits. Failure, where it occurs, now looks like: --- STDERR: gix-date::date time::parse::relative::various --- thread 'time::parse::relative::various' panicked at gix-date\tests\time\parse.rs:252:9: assertion failed: `(left == right)`: relative times differ Diff < left / right > : [ ( "5 seconds ago", 2024-11-24T23:51:49Z, ), ( "5 minutes ago", 2024-11-24T23:46:54Z, ), ( "5 hours ago", 2024-11-24T18:51:54Z, ), ( "5 days ago", 2024-11-19T23:51:54Z, ), ( "3 weeks ago", 2024-11-03T23:51:54Z, ), ( "21 days ago", 2024-11-03T23:51:54Z, ), ( "504 hours ago", 2024-11-03T23:51:54Z, ), ( "30240 minutes ago", 2024-11-03T23:51:54Z, ), ( "2 months ago", < 2024-09-24T23:51:54Z, > 2024-09-24T22:51:54Z, ), ( "1460 hours ago", 2024-09-25T03:51:54Z, ), ( "87600 minutes ago", 2024-09-25T03:51:54Z, ), ( "14 weeks ago", < 2024-08-18T23:51:54Z, > 2024-08-18T22:51:54Z, ), ( "98 days ago", < 2024-08-18T23:51:54Z, > 2024-08-18T22:51:54Z, ), ( "2352 hours ago", 2024-08-18T23:51:54Z, ), ( "141120 minutes ago", 2024-08-18T23:51:54Z, ), ( "5 months ago", < 2024-06-24T23:51:54Z, > 2024-06-24T22:51:54Z, ), ( "3650 hours ago", 2024-06-25T21:51:54Z, ), ( "219000 minutes ago", 2024-06-25T21:51:54Z, ), ( "26 weeks ago", < 2024-05-26T23:51:54Z, > 2024-05-26T22:51:54Z, ), ( "182 days ago", < 2024-05-26T23:51:54Z, > 2024-05-26T22:51:54Z, ), ( "4368 hours ago", 2024-05-26T23:51:54Z, ), ( "262080 minutes ago", 2024-05-26T23:51:54Z, ), ( "8 months ago", < 2024-03-24T23:51:54Z, > 2024-03-24T22:51:54Z, ), ( "5840 hours ago", 2024-03-26T15:51:54Z, ), ( "350400 minutes ago", 2024-03-26T15:51:54Z, ), ( "38 weeks ago", 2024-03-03T23:51:54Z, ), ( "266 days ago", 2024-03-03T23:51:54Z, ), ( "6384 hours ago", 2024-03-03T23:51:54Z, ), ( "383040 minutes ago", 2024-03-03T23:51:54Z, ), ( "11 months ago", 2023-12-24T23:51:54Z, ), ( "8030 hours ago", 2023-12-26T09:51:54Z, ), ( "481800 minutes ago", 2023-12-26T09:51:54Z, ), ( "14 months ago", < 2023-09-24T23:51:54Z, > 2023-09-24T22:51:54Z, ), ( "21 months ago", 2023-02-24T23:51:54Z, ), ( "2 years ago", 2022-11-24T23:51:54Z, ), ( "20 years ago", 2004-11-24T23:51:54Z, ), ( "630720000 seconds ago", 2004-11-29T23:51:54Z, ), ]
1 parent 39227a9 commit 3082d40

File tree

2 files changed

+55
-18
lines changed

2 files changed

+55
-18
lines changed

gix-date/src/parse.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ mod relative {
153153
"hour" => Span::new().try_hours(units),
154154
"day" => Span::new().try_days(units),
155155
"week" => Span::new().try_weeks(units),
156-
// TODO months & years? YES
156+
"month" => Span::new().try_months(units),
157+
"year" => Span::new().try_years(units),
157158
// Ignore values you don't know, assume seconds then (so does git)
158159
_ => return None,
159160
};

gix-date/tests/time/parse.rs

+53-17
Original file line numberDiff line numberDiff line change
@@ -181,39 +181,75 @@ mod relative {
181181
fn various() {
182182
let now = SystemTime::now();
183183

184+
// For comparison, a few are the same as in: https://github.com/git/git/blob/master/t/t0006-date.sh
184185
let cases = [
185-
("2 weeks ago", 2.weeks()),
186+
("5 seconds ago", 5.seconds()),
187+
("5 minutes ago", 5.minutes()),
188+
("5 hours ago", 5.hours()),
189+
("5 days ago", 5.days()),
190+
("3 weeks ago", 3.weeks()),
191+
("21 days ago", 21.days()), // 3 weeks
192+
("504 hours ago", 504.hours()), // 3 weeks
193+
("30240 minutes ago", 30_240.minutes()), // 3 weeks
194+
("2 months ago", 2.months()),
195+
("1460 hours ago", 1460.hours()), // 2 months
196+
("87600 minutes ago", 87_600.minutes()), // 2 months
186197
("14 weeks ago", 14.weeks()),
187-
("26 weeks ago", 26.weeks()),
188-
("38 weeks ago", 38.weeks()),
189-
("50 weeks ago", 50.weeks()),
190-
("20160 minutes ago", 20_160.minutes()), // 2 weeks
198+
("98 days ago", 98.days()), // 14 weeks
199+
("2352 hours ago", 2352.hours()), // 14 weeks
191200
("141120 minutes ago", 141_120.minutes()), // 14 weeks
201+
("5 months ago", 5.months()),
202+
("3650 hours ago", 3650.hours()), // 5 months
203+
("219000 minutes ago", 219_000.minutes()), // 5 months
204+
("26 weeks ago", 26.weeks()),
205+
("182 days ago", 182.days()), // 26 weeks
206+
("4368 hours ago", 4368.hours()), // 26 weeks
192207
("262080 minutes ago", 262_080.minutes()), // 26 weeks
208+
("8 months ago", 8.months()),
209+
("5840 hours ago", 5840.hours()), // 8 months
210+
("350400 minutes ago", 350_400.minutes()), // 8 months
211+
("38 weeks ago", 38.weeks()),
212+
("266 days ago", 266.days()), // 38 weeks
213+
("6384 hours ago", 6384.hours()), // 38 weeks
193214
("383040 minutes ago", 383_040.minutes()), // 38 weeks
194-
("504000 minutes ago", 504_000.minutes()), // 50 weeks
215+
("11 months ago", 11.months()),
216+
("8030 hours ago", 8030.hours()), // 11 months
217+
("481800 minutes ago", 481_800.minutes()), // 11 months
218+
("14 months ago", 14.months()), // "1 year, 2 months ago" not yet supported.
219+
("21 months ago", 21.months()), // "1 year, 9 months ago" not yet supported.
220+
("2 years ago", 2.years()),
221+
("20 years ago", 20.years()),
222+
("630720000 seconds ago", 630_720_000.seconds()), // 20 years
195223
];
196224

197-
let times = cases.map(|(input, _)| gix_date::parse(input, Some(now)).unwrap());
198-
199-
assert_eq!(times.map(|_| Sign::Plus), times.map(|time| time.sign));
200-
assert_eq!(times.map(|_| 0), times.map(|time| time.offset));
225+
let with_times = cases.map(|(input, _)| {
226+
let time = gix_date::parse(input, Some(now)).expect("relative time string should parse to a Time");
227+
(input, time)
228+
});
229+
assert_eq!(with_times.map(|_| Sign::Plus), with_times.map(|(_, time)| time.sign));
230+
assert_eq!(with_times.map(|_| 0), with_times.map(|(_, time)| time.offset));
201231

202-
let expected = cases.map(|(_, span)| {
203-
Zoned::try_from(now)
204-
.unwrap()
232+
let with_expected = cases.map(|(input, span)| {
233+
let expected = Zoned::try_from(now)
234+
.expect("test needs to convert current time to a timestamp")
205235
// account for the loss of precision when creating `Time` with seconds
206236
.round(
207237
jiff::ZonedRound::new()
208238
.smallest(jiff::Unit::Second)
209239
.mode(jiff::RoundMode::Trunc),
210240
)
211-
.unwrap()
241+
.expect("test needs to truncate current timestamp to seconds")
212242
.saturating_sub(span)
213-
.timestamp()
243+
.timestamp();
244+
245+
(input, expected)
246+
});
247+
let with_actual = with_times.map(|(input, time)| {
248+
let actual = jiff::Timestamp::from_second(time.seconds)
249+
.expect("seconds obtained from a Time should convert to Timestamp");
250+
(input, actual)
214251
});
215-
let actual = times.map(|time| jiff::Timestamp::from_second(time.seconds).unwrap());
216-
assert_eq!(actual, expected, "relative times differ");
252+
assert_eq!(with_actual, with_expected, "relative times differ");
217253
}
218254
}
219255

0 commit comments

Comments
 (0)