@@ -766,6 +766,71 @@ class DateTimeUtilsSuite extends SparkFunSuite with Matchers with SQLHelper {
766766 }
767767 }
768768
769+ test(" truncTimestamp with sub-hour zone offsets" ) {
770+ // Asia/Kolkata (+05:30) and Asia/Kathmandu (+05:45) are not aligned to HOUR in UTC.
771+ // The fast path applies the offset as part of its arithmetic, so HOUR/DAY truncation
772+ // produces the correct local-aligned result without needing the slow path.
773+ val kolkata = getZoneId(" Asia/Kolkata" )
774+ val ts = DateTimeUtils .stringToTimestamp(
775+ UTF8String .fromString(" 2024-01-15T09:42:17.123456+05:30" ), kolkata).get
776+ val expectedHour = DateTimeUtils .stringToTimestamp(
777+ UTF8String .fromString(" 2024-01-15T09:00:00+05:30" ), kolkata).get
778+ assert(DateTimeUtils .truncTimestamp(ts, DateTimeUtils .TRUNC_TO_HOUR , kolkata) === expectedHour)
779+ val expectedDay = DateTimeUtils .stringToTimestamp(
780+ UTF8String .fromString(" 2024-01-15T00:00:00+05:30" ), kolkata).get
781+ assert(DateTimeUtils .truncTimestamp(ts, DateTimeUtils .TRUNC_TO_DAY , kolkata) === expectedDay)
782+
783+ val kathmandu = getZoneId(" Asia/Kathmandu" )
784+ val ts2 = DateTimeUtils .stringToTimestamp(
785+ UTF8String .fromString(" 2024-01-15T09:42:17.123456+05:45" ), kathmandu).get
786+ val expectedHour2 = DateTimeUtils .stringToTimestamp(
787+ UTF8String .fromString(" 2024-01-15T09:00:00+05:45" ), kathmandu).get
788+ assert(DateTimeUtils .truncTimestamp(
789+ ts2, DateTimeUtils .TRUNC_TO_HOUR , kathmandu) === expectedHour2)
790+ }
791+
792+ test(" truncTimestamp across DST transitions" ) {
793+ val la = getZoneId(" America/Los_Angeles" )
794+ // Spring-forward in LA: 2024-03-10 02:00 PDT does not exist; 02:30 local maps to
795+ // 2024-03-10 03:30 PDT in wall-clock terms. Use an instant just after the transition
796+ // so HOUR/DAY truncation candidate falls into the pre-transition offset window.
797+ val postSpring = DateTimeUtils .stringToTimestamp(
798+ UTF8String .fromString(" 2024-03-10T03:30:00-07:00" ), la).get
799+ val expectedHour = DateTimeUtils .stringToTimestamp(
800+ UTF8String .fromString(" 2024-03-10T03:00:00-07:00" ), la).get
801+ assert(DateTimeUtils .truncTimestamp(postSpring, DateTimeUtils .TRUNC_TO_HOUR , la)
802+ === expectedHour)
803+ val expectedDay = DateTimeUtils .stringToTimestamp(
804+ UTF8String .fromString(" 2024-03-10T00:00:00-08:00" ), la).get
805+ assert(DateTimeUtils .truncTimestamp(postSpring, DateTimeUtils .TRUNC_TO_DAY , la)
806+ === expectedDay)
807+
808+ // Fall-back in LA: 2024-11-03 01:30 occurs twice. Truncation to HOUR/DAY should
809+ // produce the same wall-clock boundary as the slow path regardless.
810+ val postFall = DateTimeUtils .stringToTimestamp(
811+ UTF8String .fromString(" 2024-11-03T01:30:00-08:00" ), la).get
812+ val expectedHour2 = DateTimeUtils .stringToTimestamp(
813+ UTF8String .fromString(" 2024-11-03T01:00:00-08:00" ), la).get
814+ assert(DateTimeUtils .truncTimestamp(postFall, DateTimeUtils .TRUNC_TO_HOUR , la)
815+ === expectedHour2)
816+ val expectedDay2 = DateTimeUtils .stringToTimestamp(
817+ UTF8String .fromString(" 2024-11-03T00:00:00-07:00" ), la).get
818+ assert(DateTimeUtils .truncTimestamp(postFall, DateTimeUtils .TRUNC_TO_DAY , la)
819+ === expectedDay2)
820+ }
821+
822+ test(" SPARK-30766/30857: truncTimestamp before the epoch in HOUR/DAY" ) {
823+ val la = getZoneId(" America/Los_Angeles" )
824+ val ts1 = DateTimeUtils .stringToTimestamp(
825+ UTF8String .fromString(" 1960-02-11T00:01:02.123" ), la).get
826+ val expectedHour1 = DateTimeUtils .stringToTimestamp(
827+ UTF8String .fromString(" 1960-02-11T00:00:00" ), la).get
828+ assert(DateTimeUtils .truncTimestamp(ts1, DateTimeUtils .TRUNC_TO_HOUR , la) === expectedHour1)
829+ val expectedDay1 = DateTimeUtils .stringToTimestamp(
830+ UTF8String .fromString(" 1960-02-11T00:00:00" ), la).get
831+ assert(DateTimeUtils .truncTimestamp(ts1, DateTimeUtils .TRUNC_TO_DAY , la) === expectedDay1)
832+ }
833+
769834 test(" SPARK-51554: time truncation using timeTrunc" ) {
770835 // 01:02:03.400500600
771836 val input = localTimeToNanos(LocalTime .of(1 , 2 , 3 , 400500600 ))
0 commit comments