@@ -28,7 +28,7 @@ use arrow::datatypes::{ArrowTimestampType, DataType, TimeUnit};
2828use arrow_buffer:: ArrowNativeType ;
2929use chrono:: LocalResult :: Single ;
3030use chrono:: format:: { Parsed , StrftimeItems , parse} ;
31- use chrono:: { DateTime , MappedLocalTime , TimeDelta , TimeZone , Utc } ;
31+ use chrono:: { DateTime , MappedLocalTime , Offset , TimeDelta , TimeZone , Utc } ;
3232use datafusion_common:: cast:: as_generic_string_array;
3333use datafusion_common:: {
3434 DataFusionError , Result , ScalarValue , exec_datafusion_err, exec_err,
@@ -40,6 +40,59 @@ use std::ops::Add;
4040/// Error message if nanosecond conversion request beyond supported interval
4141const ERR_NANOSECONDS_NOT_SUPPORTED : & str = "The dates that can be represented as nanoseconds have to be between 1677-09-21T00:12:44.0 and 2262-04-11T23:47:16.854775804" ;
4242
43+ /// This function converts a timestamp with a timezone to a timestamp without a timezone.
44+ /// The display value of the adjusted timestamp remain the same, but the underlying timestamp
45+ /// representation is adjusted according to the relative timezone offset to UTC.
46+ ///
47+ /// This function uses chrono to handle daylight saving time changes.
48+ ///
49+ /// For example,
50+ ///
51+ /// ```text
52+ /// '2019-03-31T01:00:00Z'::timestamp at time zone 'Europe/Brussels'
53+ /// ```
54+ ///
55+ /// is displayed as follows in datafusion-cli:
56+ ///
57+ /// ```text
58+ /// 2019-03-31T01:00:00+01:00
59+ /// ```
60+ ///
61+ /// and is represented in DataFusion as:
62+ ///
63+ /// ```text
64+ /// TimestampNanosecond(Some(1_553_990_400_000_000_000), Some("Europe/Brussels"))
65+ /// ```
66+ ///
67+ /// To strip off the timezone while keeping the display value the same, we need to
68+ /// adjust the underlying timestamp with the timezone offset value using `adjust_to_local_time()`
69+ ///
70+ /// ```text
71+ /// adjust_to_local_time(1_553_990_400_000_000_000, "Europe/Brussels") --> 1_553_994_000_000_000_000
72+ /// ```
73+ ///
74+ /// The difference between `1_553_990_400_000_000_000` and `1_553_994_000_000_000_000` is
75+ /// `3600_000_000_000` ns, which corresponds to 1 hour. This matches with the timezone
76+ /// offset for "Europe/Brussels" for this date.
77+ ///
78+ /// Note that the offset varies with daylight savings time (DST), which makes this tricky! For
79+ /// example, timezone "Europe/Brussels" has a 2-hour offset during DST and a 1-hour offset
80+ /// when DST ends.
81+ ///
82+ /// Consequently, DataFusion can represent the timestamp in local time (with no offset or
83+ /// timezone information) as
84+ ///
85+ /// ```text
86+ /// TimestampNanosecond(Some(1_553_994_000_000_000_000), None)
87+ /// ```
88+ ///
89+ /// which is displayed as follows in datafusion-cli:
90+ ///
91+ /// ```text
92+ /// 2019-03-31T01:00:00
93+ /// ```
94+ ///
95+ /// See `test_adjust_to_local_time()` for example
4396pub fn adjust_to_local_time < T : ArrowTimestampType > ( ts : i64 , tz : Tz ) -> Result < i64 > {
4497 fn convert_timestamp < F > ( ts : i64 , converter : F ) -> Result < DateTime < Utc > >
4598 where
@@ -67,21 +120,10 @@ pub fn adjust_to_local_time<T: ArrowTimestampType>(ts: i64, tz: Tz) -> Result<i6
67120 TimeUnit :: Second => convert_timestamp ( ts, |ts| Utc . timestamp_opt ( ts, 0 ) ) ?,
68121 } ;
69122
70- // Get the timezone offset for this datetime
71- let tz_offset = tz. offset_from_utc_datetime ( & date_time. naive_utc ( ) ) ;
72- // Convert offset to seconds - offset is formatted like "+01:00" or "-05:00"
73- let offset_str = format ! ( "{tz_offset}" ) ;
74- let offset_seconds: i64 = if let Some ( stripped) = offset_str. strip_prefix ( '-' ) {
75- let parts: Vec < & str > = stripped. split ( ':' ) . collect ( ) ;
76- let hours: i64 = parts. first ( ) . and_then ( |s| s. parse ( ) . ok ( ) ) . unwrap_or ( 0 ) ;
77- let mins: i64 = parts. get ( 1 ) . and_then ( |s| s. parse ( ) . ok ( ) ) . unwrap_or ( 0 ) ;
78- -( ( hours * 3600 ) + ( mins * 60 ) )
79- } else {
80- let parts: Vec < & str > = offset_str. split ( ':' ) . collect ( ) ;
81- let hours: i64 = parts. first ( ) . and_then ( |s| s. parse ( ) . ok ( ) ) . unwrap_or ( 0 ) ;
82- let mins: i64 = parts. get ( 1 ) . and_then ( |s| s. parse ( ) . ok ( ) ) . unwrap_or ( 0 ) ;
83- ( hours * 3600 ) + ( mins * 60 )
84- } ;
123+ let offset_seconds: i64 = tz
124+ . offset_from_utc_datetime ( & date_time. naive_utc ( ) )
125+ . fix ( )
126+ . local_minus_utc ( ) as i64 ;
85127
86128 let adjusted_date_time = date_time. add (
87129 TimeDelta :: try_seconds ( offset_seconds)
0 commit comments