Skip to content

Commit 564d21d

Browse files
committed
fix: timestamp cannot be combined with other items
1 parent 893899e commit 564d21d

File tree

3 files changed

+77
-22
lines changed

3 files changed

+77
-22
lines changed

src/items/builder.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,11 @@ impl DateTimeBuilder {
3333
self
3434
}
3535

36+
/// Timestamp value is exclusive to other date/time components. Caller of
37+
/// the builder must ensure that it is not combined with other items.
3638
pub(super) fn set_timestamp(mut self, ts: i32) -> Result<Self, &'static str> {
37-
if self.timestamp.is_some() {
38-
Err("timestamp cannot appear more than once")
39-
} else {
40-
self.timestamp = Some(ts);
41-
Ok(self)
42-
}
39+
self.timestamp = Some(ts);
40+
Ok(self)
4341
}
4442

4543
pub(super) fn set_year(mut self, year: u32) -> Result<Self, &'static str> {

src/items/epoch.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ use winnow::{combinator::preceded, ModalResult, Parser};
55

66
use super::primitive::{dec_int, s};
77

8+
/// Parse a timestamp in the form of `@1234567890`.
9+
///
10+
/// Grammar:
11+
///
12+
/// ```ebnf
13+
/// timestamp = "@" dec_int ;
14+
/// ```
815
pub fn parse(input: &mut &str) -> ModalResult<i32> {
916
s(preceded("@", dec_int)).parse_next(input)
1017
}

src/items/mod.rs

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ use builder::DateTimeBuilder;
4545
use chrono::{DateTime, FixedOffset};
4646
use primitive::space;
4747
use winnow::{
48-
combinator::{alt, trace},
48+
combinator::{alt, eof, terminated, trace},
4949
error::{AddContext, ContextError, ErrMode, StrContext, StrContextValue},
5050
stream::Stream,
5151
ModalResult, Parser,
@@ -65,39 +65,49 @@ pub enum Item {
6565
TimeZone(time::Offset),
6666
}
6767

68+
fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode<ContextError> {
69+
ErrMode::Cut(ContextError::new()).add_context(
70+
input,
71+
&input.checkpoint(),
72+
StrContext::Expected(StrContextValue::Description(reason)),
73+
)
74+
}
75+
6876
/// Parse an item.
69-
/// TODO: timestamp values are exclusive with other items. See
70-
/// https://github.com/uutils/parse_datetime/issues/156
71-
pub fn parse_one(input: &mut &str) -> ModalResult<Item> {
77+
///
78+
/// Grammar:
79+
///
80+
/// ```ebnf
81+
/// item = combined | date | time | relative | weekday | timezone | year ;
82+
/// ```
83+
fn parse_item(input: &mut &str) -> ModalResult<Item> {
7284
trace(
73-
"parse_one",
85+
"parse_item",
7486
alt((
7587
combined::parse.map(Item::DateTime),
7688
date::parse.map(Item::Date),
7789
time::parse.map(Item::Time),
7890
relative::parse.map(Item::Relative),
7991
weekday::parse.map(Item::Weekday),
80-
epoch::parse.map(Item::Timestamp),
8192
timezone::parse.map(Item::TimeZone),
8293
date::year.map(Item::Year),
8394
)),
8495
)
8596
.parse_next(input)
8697
}
8798

88-
fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode<ContextError> {
89-
ErrMode::Cut(ContextError::new()).add_context(
90-
input,
91-
&input.checkpoint(),
92-
StrContext::Expected(StrContextValue::Description(reason)),
93-
)
94-
}
95-
96-
pub fn parse(input: &mut &str) -> ModalResult<DateTimeBuilder> {
99+
/// Parse a sequence of items.
100+
///
101+
/// Grammar:
102+
///
103+
/// ```ebnf
104+
/// items = item, { space, item } ;
105+
/// ```
106+
fn parse_items(input: &mut &str) -> ModalResult<DateTimeBuilder> {
97107
let mut builder = DateTimeBuilder::new();
98108

99109
loop {
100-
match parse_one.parse_next(input) {
110+
match parse_item.parse_next(input) {
101111
Ok(item) => match item {
102112
Item::Timestamp(ts) => {
103113
builder = builder
@@ -147,6 +157,38 @@ pub fn parse(input: &mut &str) -> ModalResult<DateTimeBuilder> {
147157
Ok(builder)
148158
}
149159

160+
/// Parse a timestamp.
161+
///
162+
/// From the GNU docs:
163+
///
164+
/// (Timestamp) Such a number cannot be combined with any other date item, as it
165+
/// specifies a complete timestamp.
166+
fn parse_timestamp(input: &mut &str) -> ModalResult<DateTimeBuilder> {
167+
trace(
168+
"parse_timestamp",
169+
terminated(epoch::parse.map(Item::Timestamp), eof),
170+
)
171+
.verify_map(|ts: Item| {
172+
if let Item::Timestamp(ts) = ts {
173+
DateTimeBuilder::new().set_timestamp(ts).ok()
174+
} else {
175+
None
176+
}
177+
})
178+
.parse_next(input)
179+
}
180+
181+
/// Parse a date and time string.
182+
///
183+
/// Grammar:
184+
///
185+
/// ```ebnf
186+
/// date_time = timestamp | items ;
187+
/// ```
188+
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTimeBuilder> {
189+
trace("parse", alt((parse_timestamp, parse_items))).parse_next(input)
190+
}
191+
150192
pub(crate) fn at_date(
151193
builder: DateTimeBuilder,
152194
base: DateTime<FixedOffset>,
@@ -287,6 +329,14 @@ mod tests {
287329
let result = parse(&mut "2025-05-19 abcdef");
288330
assert!(result.is_err());
289331
assert!(result.unwrap_err().to_string().contains("unexpected input"));
332+
333+
let result = parse(&mut "@1690466034 2025-05-19");
334+
assert!(result.is_err());
335+
assert!(result.unwrap_err().to_string().contains("unexpected input"));
336+
337+
let result = parse(&mut "2025-05-19 @1690466034");
338+
assert!(result.is_err());
339+
assert!(result.unwrap_err().to_string().contains("unexpected input"));
290340
}
291341

292342
#[test]

0 commit comments

Comments
 (0)