Skip to content

Commit f5c1a5e

Browse files
authored
Merge pull request #173 from yuankunzhang/date-time-builder
refactor: introduce a builder and simplify the date time composition
2 parents 4d9afba + 706ab45 commit f5c1a5e

File tree

3 files changed

+372
-326
lines changed

3 files changed

+372
-326
lines changed

src/items/builder.rs

Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
// For the full copyright and license information, please view the LICENSE
2+
// file that was distributed with this source code.
3+
4+
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeZone, Timelike};
5+
6+
use super::{date, relative, time, weekday};
7+
8+
/// The builder is used to construct a DateTime object from various components.
9+
/// The parser creates a `DateTimeBuilder` object with the parsed components,
10+
/// but without the baseline date and time. So you normally need to set the base
11+
/// date and time using the `set_base()` method before calling `build()`, or
12+
/// leave it unset to use the current date and time as the base.
13+
#[derive(Debug, Default)]
14+
pub struct DateTimeBuilder {
15+
base: Option<DateTime<FixedOffset>>,
16+
timestamp: Option<i32>,
17+
date: Option<date::Date>,
18+
time: Option<time::Time>,
19+
weekday: Option<weekday::Weekday>,
20+
timezone: Option<time::Offset>,
21+
relative: Vec<relative::Relative>,
22+
}
23+
24+
impl DateTimeBuilder {
25+
pub(super) fn new() -> Self {
26+
Self::default()
27+
}
28+
29+
/// Sets the base date and time for the builder. If not set, the current
30+
/// date and time will be used.
31+
pub(super) fn set_base(mut self, base: DateTime<FixedOffset>) -> Self {
32+
self.base = Some(base);
33+
self
34+
}
35+
36+
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+
}
43+
}
44+
45+
pub(super) fn set_year(mut self, year: u32) -> Result<Self, &'static str> {
46+
if let Some(date) = self.date.as_mut() {
47+
if date.year.is_some() {
48+
Err("year cannot appear more than once")
49+
} else {
50+
date.year = Some(year);
51+
Ok(self)
52+
}
53+
} else {
54+
self.date = Some(date::Date {
55+
day: 1,
56+
month: 1,
57+
year: Some(year),
58+
});
59+
Ok(self)
60+
}
61+
}
62+
63+
pub(super) fn set_date(mut self, date: date::Date) -> Result<Self, &'static str> {
64+
if self.date.is_some() || self.timestamp.is_some() {
65+
Err("date cannot appear more than once")
66+
} else {
67+
self.date = Some(date);
68+
Ok(self)
69+
}
70+
}
71+
72+
pub(super) fn set_time(mut self, time: time::Time) -> Result<Self, &'static str> {
73+
if self.time.is_some() || self.timestamp.is_some() {
74+
Err("time cannot appear more than once")
75+
} else if self.timezone.is_some() && time.offset.is_some() {
76+
Err("time offset and timezone are mutually exclusive")
77+
} else {
78+
self.time = Some(time);
79+
Ok(self)
80+
}
81+
}
82+
83+
pub(super) fn set_weekday(mut self, weekday: weekday::Weekday) -> Result<Self, &'static str> {
84+
if self.weekday.is_some() {
85+
Err("weekday cannot appear more than once")
86+
} else {
87+
self.weekday = Some(weekday);
88+
Ok(self)
89+
}
90+
}
91+
92+
pub(super) fn set_timezone(mut self, timezone: time::Offset) -> Result<Self, &'static str> {
93+
if self.timezone.is_some() {
94+
Err("timezone cannot appear more than once")
95+
} else if self.time.as_ref().and_then(|t| t.offset.as_ref()).is_some() {
96+
Err("time offset and timezone are mutually exclusive")
97+
} else {
98+
self.timezone = Some(timezone);
99+
Ok(self)
100+
}
101+
}
102+
103+
pub(super) fn push_relative(mut self, relative: relative::Relative) -> Self {
104+
self.relative.push(relative);
105+
self
106+
}
107+
108+
pub(super) fn build(self) -> Option<DateTime<FixedOffset>> {
109+
let base = self.base.unwrap_or_else(|| chrono::Local::now().into());
110+
let mut dt = new_date(
111+
base.year(),
112+
base.month(),
113+
base.day(),
114+
0,
115+
0,
116+
0,
117+
0,
118+
*base.offset(),
119+
)?;
120+
121+
if let Some(ts) = self.timestamp {
122+
dt = chrono::Utc
123+
.timestamp_opt(ts.into(), 0)
124+
.unwrap()
125+
.with_timezone(&dt.timezone());
126+
}
127+
128+
if let Some(date::Date { year, month, day }) = self.date {
129+
dt = new_date(
130+
year.map(|x| x as i32).unwrap_or(dt.year()),
131+
month,
132+
day,
133+
dt.hour(),
134+
dt.minute(),
135+
dt.second(),
136+
dt.nanosecond(),
137+
*dt.offset(),
138+
)?;
139+
}
140+
141+
if let Some(time::Time {
142+
hour,
143+
minute,
144+
second,
145+
ref offset,
146+
}) = self.time
147+
{
148+
let offset = offset
149+
.clone()
150+
.and_then(|o| chrono::FixedOffset::try_from(o).ok())
151+
.unwrap_or(*dt.offset());
152+
153+
dt = new_date(
154+
dt.year(),
155+
dt.month(),
156+
dt.day(),
157+
hour,
158+
minute,
159+
second as u32,
160+
(second.fract() * 10f64.powi(9)).round() as u32,
161+
offset,
162+
)?;
163+
}
164+
165+
if let Some(weekday::Weekday { offset, day }) = self.weekday {
166+
if self.time.is_none() {
167+
dt = new_date(dt.year(), dt.month(), dt.day(), 0, 0, 0, 0, *dt.offset())?;
168+
}
169+
170+
let mut offset = offset;
171+
let day = day.into();
172+
173+
// If the current day is not the target day, we need to adjust
174+
// the x value to ensure we find the correct day.
175+
//
176+
// Consider this:
177+
// Assuming today is Monday, next Friday is actually THIS Friday;
178+
// but next Monday is indeed NEXT Monday.
179+
if dt.weekday() != day && offset > 0 {
180+
offset -= 1;
181+
}
182+
183+
// Calculate the delta to the target day.
184+
//
185+
// Assuming today is Thursday, here are some examples:
186+
//
187+
// Example 1: last Thursday (x = -1, day = Thursday)
188+
// delta = (3 - 3) % 7 + (-1) * 7 = -7
189+
//
190+
// Example 2: last Monday (x = -1, day = Monday)
191+
// delta = (0 - 3) % 7 + (-1) * 7 = -3
192+
//
193+
// Example 3: next Monday (x = 1, day = Monday)
194+
// delta = (0 - 3) % 7 + (0) * 7 = 4
195+
// (Note that we have adjusted the x value above)
196+
//
197+
// Example 4: next Thursday (x = 1, day = Thursday)
198+
// delta = (3 - 3) % 7 + (1) * 7 = 7
199+
let delta = (day.num_days_from_monday() as i32
200+
- dt.weekday().num_days_from_monday() as i32)
201+
.rem_euclid(7)
202+
+ offset.checked_mul(7)?;
203+
204+
dt = if delta < 0 {
205+
dt.checked_sub_days(chrono::Days::new((-delta) as u64))?
206+
} else {
207+
dt.checked_add_days(chrono::Days::new(delta as u64))?
208+
}
209+
}
210+
211+
for rel in self.relative {
212+
if self.timestamp.is_none()
213+
&& self.date.is_none()
214+
&& self.time.is_none()
215+
&& self.weekday.is_none()
216+
{
217+
dt = base;
218+
}
219+
220+
match rel {
221+
relative::Relative::Years(x) => {
222+
dt = dt.with_year(dt.year() + x)?;
223+
}
224+
relative::Relative::Months(x) => {
225+
// *NOTE* This is done in this way to conform to
226+
// GNU behavior.
227+
let days = last_day_of_month(dt.year(), dt.month());
228+
if x >= 0 {
229+
dt += dt
230+
.date_naive()
231+
.checked_add_days(chrono::Days::new((days * x as u32) as u64))?
232+
.signed_duration_since(dt.date_naive());
233+
} else {
234+
dt += dt
235+
.date_naive()
236+
.checked_sub_days(chrono::Days::new((days * -x as u32) as u64))?
237+
.signed_duration_since(dt.date_naive());
238+
}
239+
}
240+
relative::Relative::Days(x) => dt += chrono::Duration::days(x.into()),
241+
relative::Relative::Hours(x) => dt += chrono::Duration::hours(x.into()),
242+
relative::Relative::Minutes(x) => {
243+
dt += chrono::Duration::try_minutes(x.into())?;
244+
}
245+
// Seconds are special because they can be given as a float
246+
relative::Relative::Seconds(x) => {
247+
dt += chrono::Duration::try_seconds(x as i64)?;
248+
}
249+
}
250+
}
251+
252+
if let Some(offset) = self.timezone {
253+
dt = with_timezone_restore(offset, dt)?;
254+
}
255+
256+
Some(dt)
257+
}
258+
}
259+
260+
#[allow(clippy::too_many_arguments)]
261+
fn new_date(
262+
year: i32,
263+
month: u32,
264+
day: u32,
265+
hour: u32,
266+
minute: u32,
267+
second: u32,
268+
nano: u32,
269+
offset: FixedOffset,
270+
) -> Option<DateTime<FixedOffset>> {
271+
let newdate = NaiveDate::from_ymd_opt(year, month, day)
272+
.and_then(|naive| naive.and_hms_nano_opt(hour, minute, second, nano))?;
273+
274+
Some(DateTime::<FixedOffset>::from_local(newdate, offset))
275+
}
276+
277+
/// Restores year, month, day, etc after applying the timezone
278+
/// returns None if timezone overflows the date
279+
fn with_timezone_restore(
280+
offset: time::Offset,
281+
at: DateTime<FixedOffset>,
282+
) -> Option<DateTime<FixedOffset>> {
283+
let offset: FixedOffset = chrono::FixedOffset::try_from(offset).ok()?;
284+
let copy = at;
285+
let x = at
286+
.with_timezone(&offset)
287+
.with_day(copy.day())?
288+
.with_month(copy.month())?
289+
.with_year(copy.year())?
290+
.with_hour(copy.hour())?
291+
.with_minute(copy.minute())?
292+
.with_second(copy.second())?
293+
.with_nanosecond(copy.nanosecond())?;
294+
Some(x)
295+
}
296+
297+
fn last_day_of_month(year: i32, month: u32) -> u32 {
298+
NaiveDate::from_ymd_opt(year, month + 1, 1)
299+
.unwrap_or(NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap())
300+
.pred_opt()
301+
.unwrap()
302+
.day()
303+
}

0 commit comments

Comments
 (0)