Skip to content

Commit ff67b9e

Browse files
committed
WIP: Add function to convert to chrono::DateTime
1 parent bf96790 commit ff67b9e

File tree

9 files changed

+247
-49
lines changed

9 files changed

+247
-49
lines changed

Cargo.lock

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ readme = "README.md"
99

1010
[dependencies]
1111
chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] }
12+
num-traits = "0.2.19"
1213
winnow = "0.5.34"
14+
15+
[dev-dependencies]
16+
anyhow = "1.0.86"

src/items/combined.rs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
//! > ISO 8601 fractional minutes and hours are not supported. Typically, hosts
1515
//! > support nanosecond timestamp resolution; excess precision is silently discarded.
1616
17+
use winnow::ascii::dec_uint;
18+
use winnow::token::take;
1719
use winnow::{combinator::alt, seq, PResult, Parser};
1820

21+
use crate::items::combined;
1922
use crate::items::space;
2023

2124
use super::{
@@ -24,13 +27,17 @@ use super::{
2427
time::{self, Time},
2528
};
2629

27-
#[derive(PartialEq, Debug, Clone)]
30+
#[derive(PartialEq, Debug, Clone, Default)]
2831
pub struct DateTime {
29-
date: Date,
30-
time: Time,
32+
pub(crate) date: Date,
33+
pub(crate) time: Time,
3134
}
3235

3336
pub fn parse(input: &mut &str) -> PResult<DateTime> {
37+
alt((parse_basic, parse_8digits)).parse_next(input)
38+
}
39+
40+
fn parse_basic(input: &mut &str) -> PResult<DateTime> {
3441
seq!(DateTime {
3542
date: date::iso,
3643
// Note: the `T` is lowercased by the main parse function
@@ -40,6 +47,31 @@ pub fn parse(input: &mut &str) -> PResult<DateTime> {
4047
.parse_next(input)
4148
}
4249

50+
fn parse_8digits(input: &mut &str) -> PResult<DateTime> {
51+
s((
52+
take(2usize).and_then(dec_uint),
53+
take(2usize).and_then(dec_uint),
54+
take(2usize).and_then(dec_uint),
55+
take(2usize).and_then(dec_uint),
56+
))
57+
.map(
58+
|(hour, minute, day, month): (u32, u32, u32, u32)| combined::DateTime {
59+
date: date::Date {
60+
day,
61+
month,
62+
year: None,
63+
},
64+
time: time::Time {
65+
hour,
66+
minute,
67+
second: 0.0,
68+
offset: None,
69+
},
70+
},
71+
)
72+
.parse_next(input)
73+
}
74+
4375
#[cfg(test)]
4476
mod tests {
4577
use super::{parse, DateTime};
@@ -68,6 +100,7 @@ mod tests {
68100
"2022-10-10 10:10:55",
69101
"2022-10-10 (A comment!) t 10:10:55",
70102
"2022-10-10 (A comment!) 10:10:55",
103+
"oct 10 10:10:55 2022",
71104
] {
72105
let old_s = s.to_owned();
73106
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}")

src/items/date.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ use winnow::{
3737
use super::s;
3838
use crate::ParseDateTimeError;
3939

40-
#[derive(PartialEq, Eq, Clone, Debug)]
40+
#[derive(PartialEq, Eq, Clone, Debug, Default)]
4141
pub struct Date {
4242
pub day: u32,
4343
pub month: u32,
@@ -96,7 +96,7 @@ fn literal2(input: &mut &str) -> PResult<Date> {
9696
.parse_next(input)
9797
}
9898

99-
fn year(input: &mut &str) -> PResult<u32> {
99+
pub fn year(input: &mut &str) -> PResult<u32> {
100100
s(alt((
101101
take(4usize).try_map(|x: &str| x.parse()),
102102
take(3usize).try_map(|x: &str| x.parse()),

src/items/mod.rs

Lines changed: 153 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,30 +28,51 @@
2828
2929
mod combined;
3030
mod date;
31+
mod number;
3132
mod ordinal;
3233
mod relative;
3334
mod time;
3435
mod time_zone;
3536
mod weekday;
36-
mod number {}
37+
mod epoch {
38+
use winnow::{ascii::dec_uint, combinator::preceded, PResult, Parser};
39+
40+
use super::s;
41+
pub fn parse(input: &mut &str) -> PResult<u32> {
42+
s(preceded("@", dec_uint)).parse_next(input)
43+
}
44+
}
45+
mod timezone {
46+
use super::s;
47+
use winnow::{PResult, Parser as _};
48+
49+
pub(crate) fn parse(input: &mut &str) -> PResult<String> {
50+
s("gmt").parse_next(input).map(|x| x.to_string())
51+
}
52+
}
53+
54+
use chrono::{DateTime, Datelike, Local, TimeZone, Timelike, Utc};
3755

3856
use winnow::{
3957
ascii::multispace0,
4058
combinator::{alt, delimited, not, peek, preceded, repeat, separated, terminated},
41-
error::ParserError,
59+
error::{ParseError, ParserError},
4260
stream::AsChar,
4361
token::{none_of, take_while},
4462
PResult, Parser,
4563
};
4664

4765
#[derive(PartialEq, Debug)]
4866
pub enum Item {
67+
Timestamp(u32),
68+
Year(u32),
4969
DateTime(combined::DateTime),
5070
Date(date::Date),
5171
Time(time::Time),
5272
Weekday(weekday::Weekday),
5373
Relative(relative::Relative),
54-
TimeZone(()),
74+
Unknown(String),
75+
TimeZone(String),
5576
}
5677

5778
/// Allow spaces and comments before a parser
@@ -115,32 +136,118 @@ where
115136
.parse_next(input)
116137
}
117138

118-
/// Parse an item
139+
// Parse an item
119140
pub fn parse_one(input: &mut &str) -> PResult<Item> {
120141
alt((
121142
combined::parse.map(Item::DateTime),
122143
date::parse.map(Item::Date),
123144
time::parse.map(Item::Time),
124145
relative::parse.map(Item::Relative),
125146
weekday::parse.map(Item::Weekday),
126-
// time_zone::parse.map(Item::TimeZone),
147+
epoch::parse.map(Item::Timestamp),
148+
date::year.map(Item::Year),
149+
timezone::parse.map(Item::TimeZone),
127150
))
128151
.parse_next(input)
129152
}
130153

131-
pub fn parse(input: &mut &str) -> Option<Vec<Item>> {
132-
terminated(repeat(0.., parse_one), space).parse(input).ok()
154+
pub fn parse<'a>(
155+
input: &'a mut &str,
156+
) -> Result<Vec<Item>, ParseError<&'a str, winnow::error::ContextError>> {
157+
terminated(repeat(0.., parse_one), space).parse(input)
158+
}
159+
160+
pub fn to_date(date: Vec<Item>) -> DateTime<Utc> {
161+
let mut d = chrono::Utc::now();
162+
eprintln!("now {d:?}");
163+
for item in date {
164+
match item {
165+
Item::Unknown(_) => {}
166+
Item::Timestamp(ts) => d = chrono::Utc.timestamp_opt(ts.into(), 0).unwrap(),
167+
Item::Date(date::Date { day, month, year }) => {
168+
d = d.with_day(day).unwrap_or(d);
169+
d = d.with_month(month).unwrap_or(d);
170+
d = year
171+
// converts i32 to u32 safely
172+
.and_then(|year: u32| <u32 as TryInto<i32>>::try_into(year).ok())
173+
.and_then(|year| d.with_year(year))
174+
.unwrap_or(d);
175+
}
176+
Item::DateTime(combined::DateTime {
177+
date: date::Date { day, month, year },
178+
time:
179+
time::Time {
180+
hour,
181+
minute,
182+
second,
183+
offset: _,
184+
},
185+
..
186+
}) => {
187+
d = d.with_day(day).unwrap_or(d);
188+
d = d.with_month(month).unwrap_or(d);
189+
d = year
190+
// converts i32 to u32 safely
191+
.and_then(|year: u32| <u32 as TryInto<i32>>::try_into(year).ok())
192+
.and_then(|year| d.with_year(year))
193+
.unwrap_or(d);
194+
d = d.with_hour(hour).unwrap_or(d);
195+
d = d.with_minute(minute).unwrap_or(d);
196+
d = d.with_second(second as u32).unwrap_or(d);
197+
}
198+
Item::Year(year) => d = d.with_year(year as i32).unwrap_or(d),
199+
Item::Time(time::Time {
200+
hour,
201+
minute,
202+
second,
203+
offset: _,
204+
}) => {
205+
d = d.with_hour(hour).unwrap_or(d);
206+
d = d.with_minute(minute).unwrap_or(d);
207+
d = d.with_second(second as u32).unwrap_or(d);
208+
// TODO, use the timezone
209+
}
210+
Item::Weekday(weekday::Weekday { .. }) => {
211+
todo!();
212+
}
213+
Item::Relative(relative::Relative::Years(x)) => d = d.with_year(x).unwrap_or(d),
214+
Item::Relative(relative::Relative::Months(x)) => {
215+
d = d.with_month(x as u32).unwrap_or(d)
216+
}
217+
Item::Relative(relative::Relative::Days(x)) => d += chrono::Duration::days(x.into()),
218+
Item::Relative(relative::Relative::Hours(x)) => d += chrono::Duration::hours(x.into()),
219+
Item::Relative(relative::Relative::Minutes(x)) => {
220+
d += chrono::Duration::minutes(x.into());
221+
}
222+
// Seconds are special because they can be given as a float
223+
Item::Relative(relative::Relative::Seconds(x)) => {
224+
d += chrono::Duration::seconds(x as i64);
225+
}
226+
Item::TimeZone(s) => {}
227+
}
228+
}
229+
d
133230
}
134231

135232
#[cfg(test)]
136233
mod tests {
137-
use super::{date::Date, parse, time::Time, Item};
234+
use super::{date::Date, parse, time::Time, to_date, Item};
235+
236+
fn test_eq_fmt(fmt: &str, input: &str) -> String {
237+
let input = input.to_ascii_lowercase();
238+
parse(&mut input.as_str())
239+
.map(to_date)
240+
.map_err(|e| eprintln!("TEST FAILED AT:\n{}", anyhow::format_err!("{e}")))
241+
.expect("parsing failed during tests")
242+
.format(fmt)
243+
.to_string()
244+
}
138245

139246
#[test]
140247
fn date_and_time() {
141248
assert_eq!(
142249
parse(&mut " 10:10 2022-12-12 "),
143-
Some(vec![
250+
Ok(vec![
144251
Item::Time(Time {
145252
hour: 10,
146253
minute: 10,
@@ -153,6 +260,42 @@ mod tests {
153260
year: Some(2022)
154261
})
155262
])
156-
)
263+
);
264+
265+
// TODO: fix these issues
266+
267+
// format, expected output, input
268+
assert_eq!("2024-01-02", test_eq_fmt("%Y-%m-%d", "2024-01-02"));
269+
270+
// https://github.com/uutils/coreutils/issues/6662
271+
assert_eq!("2005-01-02", test_eq_fmt("%Y-%m-%d", "2005-01-01 +1 day"));
272+
273+
// https://github.com/uutils/coreutils/issues/6644
274+
assert_eq!("Jul 16", test_eq_fmt("%b %d", "Jul 16"));
275+
assert_eq!("0718061449", test_eq_fmt("%m%d%H%M%S", "Jul 18 06:14:49"));
276+
assert_eq!(
277+
"07182024061449",
278+
test_eq_fmt("%m%d%Y%H%M%S", "Jul 18, 2024 06:14:49")
279+
);
280+
//assert_eq!("07182024061449", test_eq_fmt("%m%d%Y%H%M%S", "Jul 18 -2024 06:14:49"));
281+
assert_eq!(
282+
"07182024061449",
283+
test_eq_fmt("%m%d%Y%H%M%S", "Jul 18 06:14:49 2024")
284+
);
285+
//assert_eq!("07182024061449", test_eq_fmt("%m%d%Y%H%M%S", "Jul 18 06:14:49 2024 GMT"));
286+
287+
// https://github.com/uutils/coreutils/issues/5177
288+
assert_eq!(
289+
"2023-07-27T13:53:54+00:00",
290+
test_eq_fmt("%+", "@1690466034")
291+
);
292+
293+
// https://github.com/uutils/coreutils/issues/6398
294+
assert_eq!("1111 1111 00", test_eq_fmt("%m%d %H%M %S", "11111111"));
295+
296+
assert_eq!(
297+
"2024-07-17 06:14:49 UTC",
298+
test_eq_fmt("%Y-%m-%d %H:%M:%S %Z", "Jul 17 06:14:49 2024 GMT"),
299+
);
157300
}
158301
}

0 commit comments

Comments
 (0)