Skip to content

Commit e5f9894

Browse files
authored
Merge pull request #225 from yuankunzhang/result-based-error-handling
refactor: replace Option-based error handling with Result-based error handling
2 parents 74dd77d + 1f4ebd9 commit e5f9894

File tree

6 files changed

+115
-89
lines changed

6 files changed

+115
-89
lines changed

src/items/builder.rs

Lines changed: 23 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use jiff::{civil, Span, Zoned};
55

6-
use super::{date, epoch, relative, time, timezone, weekday, year};
6+
use super::{date, epoch, error, relative, time, timezone, weekday, year};
77

88
/// The builder is used to construct a DateTime object from various components.
99
/// The parser creates a `DateTimeBuilder` object with the parsed components,
@@ -148,16 +148,12 @@ impl DateTimeBuilder {
148148
self.set_time(time)
149149
}
150150

151-
pub(super) fn build(self) -> Option<Zoned> {
151+
pub(super) fn build(self) -> Result<Zoned, error::Error> {
152152
let base = self.base.unwrap_or(Zoned::now());
153153

154154
// If a timestamp is set, we use it to build the `Zoned` object.
155155
if let Some(ts) = self.timestamp {
156-
return Some(
157-
jiff::Timestamp::try_from(ts)
158-
.ok()?
159-
.to_zoned(base.offset().to_time_zone()),
160-
);
156+
return Ok(jiff::Timestamp::try_from(ts)?.to_zoned(base.offset().to_time_zone()));
161157
}
162158

163159
// If any of the following items are set, we truncate the time portion
@@ -170,30 +166,30 @@ impl DateTimeBuilder {
170166
{
171167
base
172168
} else {
173-
base.with().time(civil::time(0, 0, 0, 0)).build().ok()?
169+
base.with().time(civil::time(0, 0, 0, 0)).build()?
174170
};
175171

176172
if let Some(date) = self.date {
177173
let d: civil::Date = if date.year.is_some() {
178-
date.try_into().ok()?
174+
date.try_into()?
179175
} else {
180-
date.with_year(dt.date().year() as u16).try_into().ok()?
176+
date.with_year(dt.date().year() as u16).try_into()?
181177
};
182-
dt = dt.with().date(d).build().ok()?;
178+
dt = dt.with().date(d).build()?;
183179
}
184180

185181
if let Some(time) = self.time.clone() {
186182
if let Some(offset) = &time.offset {
187-
dt = dt.datetime().to_zoned(offset.try_into().ok()?).ok()?;
183+
dt = dt.datetime().to_zoned(offset.try_into()?)?;
188184
}
189185

190-
let t: civil::Time = time.try_into().ok()?;
191-
dt = dt.with().time(t).build().ok()?;
186+
let t: civil::Time = time.try_into()?;
187+
dt = dt.with().time(t).build()?;
192188
}
193189

194190
if let Some(weekday::Weekday { offset, day }) = self.weekday {
195191
if self.time.is_none() {
196-
dt = dt.with().time(civil::time(0, 0, 0, 0)).build().ok()?;
192+
dt = dt.with().time(civil::time(0, 0, 0, 0)).build()?;
197193
}
198194

199195
let mut offset = offset;
@@ -228,29 +224,27 @@ impl DateTimeBuilder {
228224
let delta = (day.since(civil::Weekday::Monday) as i32
229225
- dt.date().weekday().since(civil::Weekday::Monday) as i32)
230226
.rem_euclid(7)
231-
+ offset.checked_mul(7)?;
227+
+ offset.checked_mul(7).ok_or("multiplication overflow")?;
232228

233-
dt = dt.checked_add(Span::new().try_days(delta).ok()?).ok()?;
229+
dt = dt.checked_add(Span::new().try_days(delta)?)?;
234230
}
235231

236232
for rel in self.relative {
237-
dt = dt
238-
.checked_add::<Span>(if let relative::Relative::Months(x) = rel {
239-
// *NOTE* This is done in this way to conform to GNU behavior.
240-
let days = dt.date().last_of_month().day() as i32;
241-
Span::new().try_days(days.checked_mul(x)?).ok()?
242-
} else {
243-
rel.try_into().ok()?
244-
})
245-
.ok()?;
233+
dt = dt.checked_add::<Span>(if let relative::Relative::Months(x) = rel {
234+
// *NOTE* This is done in this way to conform to GNU behavior.
235+
let days = dt.date().last_of_month().day() as i32;
236+
Span::new().try_days(days.checked_mul(x).ok_or("multiplication overflow")?)?
237+
} else {
238+
rel.try_into()?
239+
})?;
246240
}
247241

248242
if let Some(offset) = self.timezone {
249243
let (offset, hour_adjustment) = offset.normalize();
250-
dt = dt.checked_add(Span::new().hours(hour_adjustment)).ok()?;
251-
dt = dt.datetime().to_zoned((&offset).try_into().ok()?).ok()?;
244+
dt = dt.checked_add(Span::new().hours(hour_adjustment))?;
245+
dt = dt.datetime().to_zoned((&offset).try_into()?)?;
252246
}
253247

254-
Some(dt)
248+
Ok(dt)
255249
}
256250
}

src/items/error.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use std::fmt;
2+
3+
use winnow::error::{ContextError, ErrMode};
4+
5+
#[derive(Debug)]
6+
pub(crate) enum Error {
7+
ParseError(String),
8+
}
9+
10+
impl std::error::Error for Error {}
11+
12+
impl fmt::Display for Error {
13+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
14+
match self {
15+
Error::ParseError(reason) => {
16+
write!(f, "{reason}")
17+
}
18+
}
19+
}
20+
}
21+
22+
impl From<&'static str> for Error {
23+
fn from(reason: &'static str) -> Self {
24+
Error::ParseError(reason.to_owned())
25+
}
26+
}
27+
28+
impl From<ErrMode<ContextError>> for Error {
29+
fn from(err: ErrMode<ContextError>) -> Self {
30+
Error::ParseError(err.to_string())
31+
}
32+
}
33+
34+
impl From<jiff::Error> for Error {
35+
fn from(err: jiff::Error) -> Self {
36+
Error::ParseError(err.to_string())
37+
}
38+
}

src/items/mod.rs

Lines changed: 36 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ mod builder;
4444
mod ordinal;
4545
mod primitive;
4646

47-
use builder::DateTimeBuilder;
47+
pub(crate) mod error;
48+
4849
use jiff::Zoned;
4950
use primitive::space;
5051
use winnow::{
@@ -54,7 +55,8 @@ use winnow::{
5455
ModalResult, Parser,
5556
};
5657

57-
use crate::ParseDateTimeError;
58+
use builder::DateTimeBuilder;
59+
use error::Error;
5860

5961
#[derive(PartialEq, Debug)]
6062
enum Item {
@@ -69,40 +71,23 @@ enum Item {
6971
}
7072

7173
/// Parse a date and time string based on a specific date.
72-
pub(crate) fn parse_at_date<S: AsRef<str> + Clone>(
73-
base: Zoned,
74-
input: S,
75-
) -> Result<Zoned, ParseDateTimeError> {
74+
pub(crate) fn parse_at_date<S: AsRef<str> + Clone>(base: Zoned, input: S) -> Result<Zoned, Error> {
7675
let input = input.as_ref().to_ascii_lowercase();
7776
match parse(&mut input.as_str()) {
78-
Ok(builder) => at_date(builder, base),
79-
Err(_) => Err(ParseDateTimeError::InvalidInput),
77+
Ok(builder) => builder.set_base(base).build(),
78+
Err(e) => Err(e.into()),
8079
}
8180
}
8281

8382
/// Parse a date and time string based on the current local time.
84-
pub(crate) fn parse_at_local<S: AsRef<str> + Clone>(input: S) -> Result<Zoned, ParseDateTimeError> {
83+
pub(crate) fn parse_at_local<S: AsRef<str> + Clone>(input: S) -> Result<Zoned, Error> {
8584
let input = input.as_ref().to_ascii_lowercase();
8685
match parse(&mut input.as_str()) {
87-
Ok(builder) => at_local(builder),
88-
Err(_) => Err(ParseDateTimeError::InvalidInput),
86+
Ok(builder) => builder.build(),
87+
Err(e) => Err(e.into()),
8988
}
9089
}
9190

92-
/// Build a `Zoned` object from a `DateTimeBuilder` and a base `Zoned` object.
93-
fn at_date(builder: DateTimeBuilder, base: Zoned) -> Result<Zoned, ParseDateTimeError> {
94-
builder
95-
.set_base(base)
96-
.build()
97-
.ok_or(ParseDateTimeError::InvalidInput)
98-
}
99-
100-
/// Build a `Zoned` object from a `DateTimeBuilder` and a default `Zoned` object
101-
/// (the current time in the local timezone).
102-
fn at_local(builder: DateTimeBuilder) -> Result<Zoned, ParseDateTimeError> {
103-
builder.build().ok_or(ParseDateTimeError::InvalidInput)
104-
}
105-
10691
/// Parse a date and time string.
10792
///
10893
/// Grammar:
@@ -310,10 +295,14 @@ fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode<ContextError>
310295
mod tests {
311296
use jiff::{civil::DateTime, tz::TimeZone, ToSpan, Zoned};
312297

313-
use super::{at_date, parse, DateTimeBuilder};
298+
use super::{parse, DateTimeBuilder};
299+
300+
fn at_date(builder: DateTimeBuilder, base: Zoned) -> Zoned {
301+
builder.set_base(base).build().unwrap()
302+
}
314303

315304
fn at_utc(builder: DateTimeBuilder) -> Zoned {
316-
at_date(builder, Zoned::now().with_time_zone(TimeZone::UTC)).unwrap()
305+
at_date(builder, Zoned::now().with_time_zone(TimeZone::UTC))
317306
}
318307

319308
fn test_eq_fmt(fmt: &str, input: &str) -> String {
@@ -479,43 +468,40 @@ mod tests {
479468
.unwrap();
480469

481470
assert_eq!(
482-
at_date(parse(&mut "last wed").unwrap(), now.clone()).unwrap(),
471+
at_date(parse(&mut "last wed").unwrap(), now.clone()),
483472
now.checked_sub(7.days()).unwrap()
484473
);
474+
assert_eq!(at_date(parse(&mut "this wed").unwrap(), now.clone()), now);
485475
assert_eq!(
486-
at_date(parse(&mut "this wed").unwrap(), now.clone()).unwrap(),
487-
now
488-
);
489-
assert_eq!(
490-
at_date(parse(&mut "next wed").unwrap(), now.clone()).unwrap(),
476+
at_date(parse(&mut "next wed").unwrap(), now.clone()),
491477
now.checked_add(7.days()).unwrap()
492478
);
493479
assert_eq!(
494-
at_date(parse(&mut "last thu").unwrap(), now.clone()).unwrap(),
480+
at_date(parse(&mut "last thu").unwrap(), now.clone()),
495481
now.checked_sub(6.days()).unwrap()
496482
);
497483
assert_eq!(
498-
at_date(parse(&mut "this thu").unwrap(), now.clone()).unwrap(),
484+
at_date(parse(&mut "this thu").unwrap(), now.clone()),
499485
now.checked_add(1.days()).unwrap()
500486
);
501487
assert_eq!(
502-
at_date(parse(&mut "next thu").unwrap(), now.clone()).unwrap(),
488+
at_date(parse(&mut "next thu").unwrap(), now.clone()),
503489
now.checked_add(1.days()).unwrap()
504490
);
505491
assert_eq!(
506-
at_date(parse(&mut "1 wed").unwrap(), now.clone()).unwrap(),
492+
at_date(parse(&mut "1 wed").unwrap(), now.clone()),
507493
now.checked_add(7.days()).unwrap()
508494
);
509495
assert_eq!(
510-
at_date(parse(&mut "1 thu").unwrap(), now.clone()).unwrap(),
496+
at_date(parse(&mut "1 thu").unwrap(), now.clone()),
511497
now.checked_add(1.days()).unwrap()
512498
);
513499
assert_eq!(
514-
at_date(parse(&mut "2 wed").unwrap(), now.clone()).unwrap(),
500+
at_date(parse(&mut "2 wed").unwrap(), now.clone()),
515501
now.checked_add(14.days()).unwrap()
516502
);
517503
assert_eq!(
518-
at_date(parse(&mut "2 thu").unwrap(), now.clone()).unwrap(),
504+
at_date(parse(&mut "2 thu").unwrap(), now.clone()),
519505
now.checked_add(8.days()).unwrap()
520506
);
521507
}
@@ -524,30 +510,30 @@ mod tests {
524510
fn relative_date_time() {
525511
let now = Zoned::now().with_time_zone(TimeZone::UTC);
526512

527-
let result = at_date(parse(&mut "2 days ago").unwrap(), now.clone()).unwrap();
513+
let result = at_date(parse(&mut "2 days ago").unwrap(), now.clone());
528514
assert_eq!(result, now.checked_sub(2.days()).unwrap());
529515
assert_eq!(result.hour(), now.hour());
530516
assert_eq!(result.minute(), now.minute());
531517
assert_eq!(result.second(), now.second());
532518

533-
let result = at_date(parse(&mut "2 days 3 days ago").unwrap(), now.clone()).unwrap();
519+
let result = at_date(parse(&mut "2 days 3 days ago").unwrap(), now.clone());
534520
assert_eq!(result, now.checked_sub(1.days()).unwrap());
535521
assert_eq!(result.hour(), now.hour());
536522
assert_eq!(result.minute(), now.minute());
537523
assert_eq!(result.second(), now.second());
538524

539-
let result = at_date(parse(&mut "2025-01-01 2 days ago").unwrap(), now.clone()).unwrap();
525+
let result = at_date(parse(&mut "2025-01-01 2 days ago").unwrap(), now.clone());
540526
assert_eq!(result.hour(), 0);
541527
assert_eq!(result.minute(), 0);
542528
assert_eq!(result.second(), 0);
543529

544-
let result = at_date(parse(&mut "3 weeks").unwrap(), now.clone()).unwrap();
530+
let result = at_date(parse(&mut "3 weeks").unwrap(), now.clone());
545531
assert_eq!(result, now.checked_add(21.days()).unwrap());
546532
assert_eq!(result.hour(), now.hour());
547533
assert_eq!(result.minute(), now.minute());
548534
assert_eq!(result.second(), now.second());
549535

550-
let result = at_date(parse(&mut "2025-01-01 3 weeks").unwrap(), now).unwrap();
536+
let result = at_date(parse(&mut "2025-01-01 3 weeks").unwrap(), now);
551537
assert_eq!(result.hour(), 0);
552538
assert_eq!(result.minute(), 0);
553539
assert_eq!(result.second(), 0);
@@ -558,23 +544,23 @@ mod tests {
558544
let now = Zoned::now().with_time_zone(TimeZone::UTC);
559545

560546
// Pure number as year.
561-
let result = at_date(parse(&mut "jul 18 12:30 2025").unwrap(), now.clone()).unwrap();
547+
let result = at_date(parse(&mut "jul 18 12:30 2025").unwrap(), now.clone());
562548
assert_eq!(result.year(), 2025);
563549

564550
// Pure number as time.
565-
let result = at_date(parse(&mut "1230").unwrap(), now.clone()).unwrap();
551+
let result = at_date(parse(&mut "1230").unwrap(), now.clone());
566552
assert_eq!(result.hour(), 12);
567553
assert_eq!(result.minute(), 30);
568554

569-
let result = at_date(parse(&mut "123").unwrap(), now.clone()).unwrap();
555+
let result = at_date(parse(&mut "123").unwrap(), now.clone());
570556
assert_eq!(result.hour(), 1);
571557
assert_eq!(result.minute(), 23);
572558

573-
let result = at_date(parse(&mut "12").unwrap(), now.clone()).unwrap();
559+
let result = at_date(parse(&mut "12").unwrap(), now.clone());
574560
assert_eq!(result.hour(), 12);
575561
assert_eq!(result.minute(), 0);
576562

577-
let result = at_date(parse(&mut "1").unwrap(), now.clone()).unwrap();
563+
let result = at_date(parse(&mut "1").unwrap(), now.clone());
578564
assert_eq!(result.hour(), 1);
579565
assert_eq!(result.minute(), 0);
580566
}

src/items/pure.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
//! Parse a pure number string.
55
//!
6-
//! The GNU docs say:
6+
//! From the GNU docs:
77
//!
88
//! > The precise interpretation of a pure decimal number depends on the
99
//! > context in the date string.
@@ -22,12 +22,16 @@
2222
//! > number in the date string, but no relative item, then the number
2323
//! > overrides the year.
2424
25-
use winnow::{stream::AsChar, token::take_while, ModalResult, Parser};
25+
use winnow::{ModalResult, Parser};
2626

27-
use super::primitive::s;
27+
use super::primitive::{dec_uint_str, s};
2828

29+
/// Parse a pure number string and return it as an owned `String`. We return a
30+
/// `String` here because the interpretation of the number depends on the
31+
/// parsing context in which it appears. The interpretation is deferred to the
32+
/// result building phase.
2933
pub(super) fn parse(input: &mut &str) -> ModalResult<String> {
30-
s(take_while(1.., AsChar::is_dec_digit))
34+
s(dec_uint_str)
3135
.map(|s: &str| s.to_owned())
3236
.parse_next(input)
3337
}

0 commit comments

Comments
 (0)