Skip to content

Commit e7fe6b4

Browse files
committed
Add named timezone support
1 parent e7dc97c commit e7fe6b4

File tree

7 files changed

+173
-17
lines changed

7 files changed

+173
-17
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ regex = "1.10.4"
1313
chrono = { version="0.4.38", default-features=false, features=["std", "alloc", "clock"] }
1414
winnow = "0.7.10"
1515
num-traits = "0.2.19"
16+
chrono-tz = { version = "0.10.4", features=["case-insensitive"] }
1617

1718
[dev-dependencies]
1819
rstest = "0.26"

fuzz/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/items/builder.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub struct DateTimeBuilder {
1818
time: Option<time::Time>,
1919
weekday: Option<weekday::Weekday>,
2020
timezone: Option<time::Offset>,
21-
conversion_timezone: Option<time::Offset>,
21+
conversion_timezone: Option<FixedOffset>,
2222
relative: Vec<relative::Relative>,
2323
}
2424

@@ -99,7 +99,10 @@ impl DateTimeBuilder {
9999
}
100100
}
101101

102-
pub(super) fn set_conversion_timezone(mut self, conversion_timezone: time::Offset) -> Result<Self, &'static str> {
102+
pub(super) fn set_conversion_timezone(
103+
mut self,
104+
conversion_timezone: FixedOffset,
105+
) -> Result<Self, &'static str> {
103106
if self.conversion_timezone.is_some() {
104107
Err("TZ= cannot appear more than once")
105108
} else {
@@ -277,13 +280,12 @@ impl DateTimeBuilder {
277280
}
278281

279282
if let Some(offset) = self.timezone {
280-
dt = with_timezone_restore(offset, dt)?;
283+
let offset = chrono::FixedOffset::try_from(offset).ok();
284+
dt = with_timezone_restore(offset?, dt)?;
281285
}
282286

283-
if let Some(mut offset) = self.conversion_timezone {
284-
// Reuse with_timezone_restore with a swapped offset
285-
offset.negative = !offset.negative;
286-
dt = with_timezone_restore(offset, dt)?;
287+
if let Some(conversion_timezone) = self.conversion_timezone {
288+
dt = with_timezone_restore(conversion_timezone, dt)?;
287289
}
288290

289291
Some(dt)
@@ -310,10 +312,9 @@ fn new_date(
310312
/// Restores year, month, day, etc after applying the timezone
311313
/// returns None if timezone overflows the date
312314
fn with_timezone_restore(
313-
offset: time::Offset,
315+
offset: FixedOffset,
314316
at: DateTime<FixedOffset>,
315317
) -> Option<DateTime<FixedOffset>> {
316-
let offset: FixedOffset = chrono::FixedOffset::try_from(offset).ok()?;
317318
let copy = at;
318319
let x = at
319320
.with_timezone(&offset)

src/items/conversion_timezone.rs

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

4-
use winnow::{ModalResult, Parser};
4+
use chrono::{FixedOffset, NaiveDate, TimeZone};
5+
use chrono_tz::Tz;
6+
use winnow::{combinator::peek, error::ErrMode, token::take_while, ModalResult, Parser};
57

6-
use super::time;
8+
use super::{primitive::ctx_err, time};
79

8-
pub(crate) fn parse(input: &mut &str) -> ModalResult<time::Offset> {
10+
pub(crate) fn parse(input: &mut &str) -> ModalResult<FixedOffset> {
911
let _ = "tz=\"".parse_next(input)?;
10-
let tz = time::timezone(input);
12+
let mut tz_name = take_while(1.., |character| character != '\"').parse_next(input)?;
1113
let _ = "\" ".parse_next(input)?;
12-
return tz
14+
15+
// Try and use the built in timezone system first before trying to use the
16+
// chrono_tz library which can handle named timezones
17+
if let Ok(mut offset) = peek(time::timezone).parse_next(&mut tz_name) {
18+
offset.negative = !offset.negative;
19+
offset
20+
.try_into()
21+
.map_err(|_| ErrMode::Cut(ctx_err("Invalid timezone")))
22+
} else {
23+
let conv: Tz = chrono_tz::Tz::from_str_insensitive(tz_name)
24+
.map_err(|_| ErrMode::Cut(ctx_err("Invalid timezone")))?;
25+
Ok(chrono::Offset::fix(
26+
&conv.offset_from_utc_date(&NaiveDate::default()),
27+
))
28+
}
1329
}

src/items/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@
3131

3232
// date and time items
3333
mod combined;
34+
mod conversion_timezone;
3435
mod date;
3536
mod epoch;
3637
mod relative;
3738
mod time;
3839
mod timezone;
3940
mod weekday;
4041
mod year;
41-
mod conversion_timezone;
4242

4343
// utility modules
4444
mod builder;
@@ -59,7 +59,7 @@ use crate::ParseDateTimeError;
5959

6060
#[derive(PartialEq, Debug)]
6161
pub(crate) enum Item {
62-
ConversionTimeZone(time::Offset),
62+
ConversionTimeZone(FixedOffset),
6363
Timestamp(f64),
6464
Year(u32),
6565
DateTime(combined::DateTime),

tests/date.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,29 @@ fn test_date_omitting_year(#[case] input: &str, #[case] year: u32, #[case] expec
9393
let now = DateTime::parse_from_rfc3339(&format!("{year}-06-01T00:00:00+00:00")).unwrap();
9494
check_relative(now, input, expected);
9595
}
96+
97+
#[rstest]
98+
#[case::convert_timezone_ahead(r#"TZ="UTC+1" 2022-12-14"#, 2022, "2022-12-14 01:00:00+00:00")]
99+
#[case::convert_timezone_behind(r#"TZ="UTC-1" 2022-12-14"#, 2022, "2022-12-13 23:00:00+00:00")]
100+
#[case::convert_timezone_ahead_minutes(
101+
r#"TZ="UTC+1:30" 2022-12-14"#,
102+
2022,
103+
"2022-12-14 01:30:00+00:00"
104+
)]
105+
#[case::convert_timezone_behind_minutes(
106+
r#"TZ="UTC-1:30" 2022-12-14"#,
107+
2022,
108+
"2022-12-13 22:30:00+00:00"
109+
)]
110+
#[case::convert_named_timezone(
111+
r#"TZ="America/New_York" 2022-12-14 12:00"#,
112+
2022,
113+
"2022-12-14 17:00:00+00:00"
114+
)]
115+
fn test_conversion_date(#[case] input: &str, #[case] year: u32, #[case] expected: &str) {
116+
use chrono::DateTime;
117+
use common::check_relative;
118+
119+
let now = DateTime::parse_from_rfc3339(&format!("{year}-06-01T00:00:00+00:00")).unwrap();
120+
check_relative(now, input, expected);
121+
}

0 commit comments

Comments
 (0)