Skip to content

Commit 6d7ead4

Browse files
authored
Merge branch 'main' into TZ
2 parents e7fe6b4 + 5ca5586 commit 6d7ead4

File tree

9 files changed

+619
-449
lines changed

9 files changed

+619
-449
lines changed

src/items/builder.rs

Lines changed: 113 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,21 @@
33

44
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeZone, Timelike};
55

6-
use super::{date, relative, time, weekday};
6+
use super::{date, 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,
1010
/// but without the baseline date and time. So you normally need to set the base
1111
/// date and time using the `set_base()` method before calling `build()`, or
1212
/// leave it unset to use the current date and time as the base.
1313
#[derive(Debug, Default)]
14-
pub struct DateTimeBuilder {
14+
pub(crate) struct DateTimeBuilder {
1515
base: Option<DateTime<FixedOffset>>,
1616
timestamp: Option<f64>,
1717
date: Option<date::Date>,
1818
time: Option<time::Time>,
1919
weekday: Option<weekday::Weekday>,
20-
timezone: Option<time::Offset>,
20+
timezone: Option<timezone::Offset>,
2121
conversion_timezone: Option<FixedOffset>,
2222
relative: Vec<relative::Relative>,
2323
}
@@ -34,69 +34,70 @@ impl DateTimeBuilder {
3434
self
3535
}
3636

37-
/// Timestamp value is exclusive to other date/time components. Caller of
38-
/// the builder must ensure that it is not combined with other items.
37+
/// Sets a timestamp value. Timestamp values are exclusive to other date/time
38+
/// items (date, time, weekday, timezone, relative adjustments).
3939
pub(super) fn set_timestamp(mut self, ts: f64) -> Result<Self, &'static str> {
40+
if self.timestamp.is_some() {
41+
return Err("timestamp cannot appear more than once");
42+
} else if self.date.is_some()
43+
|| self.time.is_some()
44+
|| self.weekday.is_some()
45+
|| self.timezone.is_some()
46+
|| !self.relative.is_empty()
47+
{
48+
return Err("timestamp cannot be combined with other date/time items");
49+
}
50+
4051
self.timestamp = Some(ts);
4152
Ok(self)
4253
}
4354

44-
pub(super) fn set_year(mut self, year: u32) -> Result<Self, &'static str> {
45-
if let Some(date) = self.date.as_mut() {
46-
if date.year.is_some() {
47-
Err("year cannot appear more than once")
48-
} else {
49-
date.year = Some(year);
50-
Ok(self)
51-
}
52-
} else {
53-
self.date = Some(date::Date {
54-
day: 1,
55-
month: 1,
56-
year: Some(year),
57-
});
58-
Ok(self)
59-
}
60-
}
61-
6255
pub(super) fn set_date(mut self, date: date::Date) -> Result<Self, &'static str> {
63-
if self.date.is_some() || self.timestamp.is_some() {
64-
Err("date cannot appear more than once")
65-
} else {
66-
self.date = Some(date);
67-
Ok(self)
56+
if self.timestamp.is_some() {
57+
return Err("timestamp cannot be combined with other date/time items");
58+
} else if self.date.is_some() {
59+
return Err("date cannot appear more than once");
6860
}
61+
62+
self.date = Some(date);
63+
Ok(self)
6964
}
7065

7166
pub(super) fn set_time(mut self, time: time::Time) -> Result<Self, &'static str> {
72-
if self.time.is_some() || self.timestamp.is_some() {
73-
Err("time cannot appear more than once")
67+
if self.timestamp.is_some() {
68+
return Err("timestamp cannot be combined with other date/time items");
69+
} else if self.time.is_some() {
70+
return Err("time cannot appear more than once");
7471
} else if self.timezone.is_some() && time.offset.is_some() {
75-
Err("time offset and timezone are mutually exclusive")
76-
} else {
77-
self.time = Some(time);
78-
Ok(self)
72+
return Err("time offset and timezone are mutually exclusive");
7973
}
74+
75+
self.time = Some(time);
76+
Ok(self)
8077
}
8178

8279
pub(super) fn set_weekday(mut self, weekday: weekday::Weekday) -> Result<Self, &'static str> {
83-
if self.weekday.is_some() {
84-
Err("weekday cannot appear more than once")
85-
} else {
86-
self.weekday = Some(weekday);
87-
Ok(self)
80+
if self.timestamp.is_some() {
81+
return Err("timestamp cannot be combined with other date/time items");
82+
} else if self.weekday.is_some() {
83+
return Err("weekday cannot appear more than once");
8884
}
85+
86+
self.weekday = Some(weekday);
87+
Ok(self)
8988
}
9089

91-
pub(super) fn set_timezone(mut self, timezone: time::Offset) -> Result<Self, &'static str> {
92-
if self.timezone.is_some() {
93-
Err("timezone cannot appear more than once")
90+
pub(super) fn set_timezone(mut self, timezone: timezone::Offset) -> Result<Self, &'static str> {
91+
if self.timestamp.is_some() {
92+
return Err("timestamp cannot be combined with other date/time items");
93+
} else if self.timezone.is_some() {
94+
return Err("timezone cannot appear more than once");
9495
} else if self.time.as_ref().and_then(|t| t.offset.as_ref()).is_some() {
95-
Err("time offset and timezone are mutually exclusive")
96-
} else {
97-
self.timezone = Some(timezone);
98-
Ok(self)
96+
return Err("time offset and timezone are mutually exclusive");
9997
}
98+
99+
self.timezone = Some(timezone);
100+
Ok(self)
100101
}
101102

102103
pub(super) fn set_conversion_timezone(
@@ -111,14 +112,77 @@ impl DateTimeBuilder {
111112
}
112113
}
113114

114-
pub(super) fn push_relative(mut self, relative: relative::Relative) -> Self {
115+
pub(super) fn push_relative(
116+
mut self,
117+
relative: relative::Relative,
118+
) -> Result<Self, &'static str> {
119+
if self.timestamp.is_some() {
120+
return Err("timestamp cannot be combined with other date/time items");
121+
}
115122
self.relative.push(relative);
116-
self
123+
Ok(self)
124+
}
125+
126+
/// Sets a pure number that can be interpreted as either a year or time
127+
/// depending on the current state of the builder.
128+
///
129+
/// If a date is already set but lacks a year, the number is interpreted as
130+
/// a year. Otherwise, it's interpreted as a time in HHMM, HMM, HH, or H
131+
/// format.
132+
pub(super) fn set_pure(mut self, pure: String) -> Result<Self, &'static str> {
133+
if self.timestamp.is_some() {
134+
return Err("timestamp cannot be combined with other date/time items");
135+
}
136+
137+
if let Some(date) = self.date.as_mut() {
138+
if date.year.is_none() {
139+
date.year = Some(year::year_from_str(&pure)?);
140+
return Ok(self);
141+
}
142+
}
143+
144+
let (mut hour_str, mut minute_str) = match pure.len() {
145+
1..=2 => (pure.as_str(), "0"),
146+
3..=4 => pure.split_at(pure.len() - 2),
147+
_ => {
148+
return Err("pure number must be 1-4 digits when interpreted as time");
149+
}
150+
};
151+
152+
let hour = time::hour24(&mut hour_str).map_err(|_| "invalid hour in pure number")?;
153+
let minute = time::minute(&mut minute_str).map_err(|_| "invalid minute in pure number")?;
154+
155+
let time = time::Time {
156+
hour,
157+
minute,
158+
..Default::default()
159+
};
160+
self.set_time(time)
161+
}
162+
163+
fn build_from_timestamp(ts: f64, tz: &FixedOffset) -> Option<DateTime<FixedOffset>> {
164+
// TODO: How to make the fract -> nanosecond conversion more precise?
165+
// Maybe considering using the
166+
// [rust_decimal](https://crates.io/crates/rust_decimal) crate?
167+
match chrono::Utc.timestamp_opt(ts as i64, (ts.fract() * 10f64.powi(9)).round() as u32) {
168+
chrono::MappedLocalTime::Single(t) => Some(t.with_timezone(tz)),
169+
chrono::MappedLocalTime::Ambiguous(earliest, _latest) => {
170+
// TODO: When there is a fold in the local time, which value
171+
// do we choose? For now, we use the earliest one.
172+
Some(earliest.with_timezone(tz))
173+
}
174+
chrono::MappedLocalTime::None => None, // Invalid timestamp
175+
}
117176
}
118177

119178
pub(super) fn build(self) -> Option<DateTime<FixedOffset>> {
120179
let base = self.base.unwrap_or_else(|| chrono::Local::now().into());
121180

181+
// If a timestamp is set, we use it to build the DateTime object.
182+
if let Some(ts) = self.timestamp {
183+
return Self::build_from_timestamp(ts, base.offset());
184+
}
185+
122186
// If any of the following items are set, we truncate the time portion
123187
// of the base date to zero; otherwise, we use the base date as is.
124188
let mut dt = if self.timestamp.is_none()
@@ -141,27 +205,6 @@ impl DateTimeBuilder {
141205
)?
142206
};
143207

144-
if let Some(ts) = self.timestamp {
145-
// TODO: How to make the fract -> nanosecond conversion more precise?
146-
// Maybe considering using the
147-
// [rust_decimal](https://crates.io/crates/rust_decimal) crate?
148-
match chrono::Utc.timestamp_opt(ts as i64, (ts.fract() * 10f64.powi(9)).round() as u32)
149-
{
150-
chrono::MappedLocalTime::Single(t) => {
151-
// If the timestamp is valid, we can use it directly.
152-
dt = t.with_timezone(&dt.timezone());
153-
}
154-
chrono::MappedLocalTime::Ambiguous(earliest, _latest) => {
155-
// TODO: When there is a fold in the local time, which value
156-
// do we choose? For now, we use the earliest one.
157-
dt = earliest.with_timezone(&dt.timezone());
158-
}
159-
chrono::MappedLocalTime::None => {
160-
return None; // Invalid timestamp
161-
}
162-
}
163-
}
164-
165208
if let Some(date::Date { year, month, day }) = self.date {
166209
dt = new_date(
167210
year.map(|x| x as i32).unwrap_or(dt.year()),
@@ -292,7 +335,7 @@ impl DateTimeBuilder {
292335
}
293336
}
294337

295-
#[allow(clippy::too_many_arguments)]
338+
#[allow(clippy::too_many_arguments, deprecated)]
296339
fn new_date(
297340
year: i32,
298341
month: u32,

src/items/combined.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,12 @@ use winnow::{
2121

2222
use crate::items::space;
2323

24-
use super::{
25-
date::{self, Date},
26-
primitive::s,
27-
time::{self, Time},
28-
};
24+
use super::{date, primitive::s, time};
2925

3026
#[derive(PartialEq, Debug, Clone, Default)]
3127
pub(crate) struct DateTime {
32-
pub(crate) date: Date,
33-
pub(crate) time: Time,
28+
pub(crate) date: date::Date,
29+
pub(crate) time: time::Time,
3430
}
3531

3632
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {

src/items/epoch.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
// For the full copyright and license information, please view the LICENSE
22
// file that was distributed with this source code.
33

4+
//! From the GNU docs:
5+
//!
6+
//! > If you precede a number with ‘@’, it represents an internal timestamp as
7+
//! > a count of seconds. The number can contain an internal decimal point
8+
//! > (either ‘.’ or ‘,’); any excess precision not supported by the internal
9+
//! > representation is truncated toward minus infinity. Such a number cannot
10+
//! > be combined with any other date item, as it specifies a complete
11+
//! > timestamp.
12+
//! >
13+
//! > On most hosts, these counts ignore the presence of leap seconds. For
14+
//! > example, on most hosts ‘@1483228799’ represents 2016-12-31 23:59:59 UTC,
15+
//! > ‘@1483228800’ represents 2017-01-01 00:00:00 UTC, and there is no way to
16+
//! > represent the intervening leap second 2016-12-31 23:59:60 UTC.
17+
418
use winnow::{combinator::preceded, ModalResult, Parser};
519

620
use super::primitive::{float, s};

0 commit comments

Comments
 (0)