Skip to content

Commit de57e97

Browse files
committed
feat: Support parsing IANA timezones
Signed-off-by: Alex Qyoun-ae <4062971+MazterQyou@users.noreply.github.com>
1 parent 64899e4 commit de57e97

1 file changed

Lines changed: 69 additions & 0 deletions

File tree

arrow/src/compute/kernels/cast_utils.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@ pub fn string_to_timestamp_nanos(s: &str) -> Result<i64> {
101101
return Ok(ts);
102102
}
103103

104+
// Try to parse a trailing IANA timezone name, e.g.
105+
// `2026-06-15 00:00:00 America/Los_Angeles`, which PostgreSQL accepts.
106+
if let Some(ts) = parse_timestamp_with_named_tz(s) {
107+
return Ok(ts);
108+
}
109+
104110
// Support timestamps without an explicit timezone offset, again
105111
// to be compatible with what Apache Spark SQL does.
106112

@@ -223,6 +229,38 @@ fn parse_timestamp_with_manual_offset(s: &str) -> Option<i64> {
223229
None
224230
}
225231

232+
/// Try to parse a timestamp with a trailing IANA timezone name, such as
233+
/// `2026-06-15 00:00:00 America/Los_Angeles`. The timezone name is the
234+
/// whitespace-separated token after the time component and is resolved
235+
/// against `chrono-tz`. The wall-clock time is interpreted in that
236+
/// timezone and converted to a UTC nanosecond timestamp.
237+
///
238+
/// Returns `None` if the `chrono-tz` feature is disabled, no trailing
239+
/// name is present, the name does not resolve, or the datetime part does
240+
/// not parse.
241+
#[cfg(feature = "chrono-tz")]
242+
fn parse_timestamp_with_named_tz(s: &str) -> Option<i64> {
243+
let (datetime_part, tz_name) = s.rsplit_once(' ')?;
244+
let tz: chrono_tz::Tz = tz_name.parse().ok()?;
245+
for fmt in &[
246+
"%Y-%m-%dT%H:%M:%S%.f",
247+
"%Y-%m-%dT%H:%M:%S",
248+
"%Y-%m-%d %H:%M:%S%.f",
249+
"%Y-%m-%d %H:%M:%S",
250+
] {
251+
if let Ok(naive) = NaiveDateTime::parse_from_str(datetime_part, fmt) {
252+
let dt = tz.from_local_datetime(&naive).single()?;
253+
return Some(dt.timestamp_nanos());
254+
}
255+
}
256+
None
257+
}
258+
259+
#[cfg(not(feature = "chrono-tz"))]
260+
fn parse_timestamp_with_named_tz(_s: &str) -> Option<i64> {
261+
None
262+
}
263+
226264
/// Converts the naive datetime (which has no specific timezone) to a
227265
/// nanosecond epoch timestamp relative to UTC.
228266
fn naive_datetime_to_timestamp(s: &str, datetime: NaiveDateTime) -> Result<i64> {
@@ -559,6 +597,37 @@ mod tests {
559597
Ok(())
560598
}
561599

600+
#[cfg(feature = "chrono-tz")]
601+
#[test]
602+
fn string_to_timestamp_named_timezone() -> Result<()> {
603+
// A trailing IANA timezone name is resolved and the wall-clock time
604+
// converted to a UTC instant. `America/Los_Angeles` is UTC-7 (PDT)
605+
// in June, so 13:42:29 local is 20:42:29 UTC. PostgreSQL accepts this
606+
// trailing IANA tz name.
607+
const UTC: i64 = 1_781_556_149_000_000_000; // 2026-06-15T20:42:29Z
608+
609+
assert_eq!(
610+
UTC,
611+
parse_timestamp("2026-06-15 13:42:29 America/Los_Angeles")?
612+
);
613+
// `T` separator is also accepted.
614+
assert_eq!(
615+
UTC,
616+
parse_timestamp("2026-06-15T13:42:29 America/Los_Angeles")?
617+
);
618+
// Fractional seconds with a named timezone.
619+
assert_eq!(
620+
UTC + 190_855_000,
621+
parse_timestamp("2026-06-15 13:42:29.190855 America/Los_Angeles")?
622+
);
623+
// UTC named timezone leaves the wall-clock time unchanged.
624+
assert_eq!(
625+
1_781_530_949_000_000_000, // 2026-06-15T13:42:29Z
626+
parse_timestamp("2026-06-15 13:42:29 UTC")?
627+
);
628+
Ok(())
629+
}
630+
562631
/// Interprets a naive_datetime (with no explicit timezone offset)
563632
/// using the local timezone and returns the timestamp in UTC (0
564633
/// offset)

0 commit comments

Comments
 (0)