Skip to content

Commit 6003dd4

Browse files
Chong Gaores-life
authored andcommitted
Preserve terminal ORC timezone offsets
Signed-off-by: Chong Gao <chongg@nvidia.com>
1 parent 21d9a65 commit 6003dd4

4 files changed

Lines changed: 87 additions & 10 deletions

File tree

src/main/java/com/nvidia/spark/rapids/jni/GpuTimeZoneDB.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -532,17 +532,38 @@ private static int getOrc2015YearBaseOffsetMillis(String timezoneId, OrcTimezone
532532
}
533533

534534
private static ColumnVector getTransitionsForUtilTZ(OrcTimezoneInfo info) {
535-
try (HostColumnVector hcv = HostColumnVector.fromLongs(info.transitions)) {
535+
long[] transitions = info.transitions;
536+
if (needsTerminalOffsetSentinel(info)) {
537+
transitions = Arrays.copyOf(transitions, transitions.length + 1);
538+
transitions[transitions.length - 1] = Long.MAX_VALUE;
539+
}
540+
try (HostColumnVector hcv = HostColumnVector.fromLongs(transitions)) {
536541
return hcv.copyToDevice();
537542
}
538543
}
539544

540545
private static ColumnVector getOffsetsForUtilTZ(OrcTimezoneInfo info) {
541-
try (HostColumnVector hcv = HostColumnVector.fromInts(info.offsets)) {
546+
int[] offsets = info.offsets;
547+
if (needsTerminalOffsetSentinel(info)) {
548+
offsets = Arrays.copyOf(offsets, offsets.length + 1);
549+
offsets[offsets.length - 1] = offsets[offsets.length - 2];
550+
}
551+
try (HostColumnVector hcv = HostColumnVector.fromInts(offsets)) {
542552
return hcv.copyToDevice();
543553
}
544554
}
545555

556+
private static boolean needsTerminalOffsetSentinel(OrcTimezoneInfo info) {
557+
// Native lookup normally falls back to rawOffset beyond the last historical
558+
// transition. Some JDK TimeZone implementations instead retain the final
559+
// wall offset indefinitely. Keep every timestamp_us lookup inside the table
560+
// with a Long.MAX_VALUE-millisecond sentinel when those offsets differ.
561+
return info.dstRule == null
562+
&& info.offsets != null
563+
&& info.offsets.length > 0
564+
&& info.offsets[info.offsets.length - 1] != info.rawOffset;
565+
}
566+
546567
private static Table getTableForUtilTZ(OrcTimezoneInfo info) {
547568
if (info.transitions == null) {
548569
return null;

src/main/java/com/nvidia/spark/rapids/jni/OrcDstRuleExtractor.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ static final class DstRule {
135135
* {@code rules}
136136
* @param rules {@link ZoneRules} for the zone
137137
* @return the recurring DST rule, or {@code null} if the zone has no DST
138-
* ({@code rules.isFixedOffset()} or {@code !tz.useDaylightTime()})
139138
* @throws IllegalStateException if the zone reports DST but neither
140139
* extraction path produces a usable rule — for example, an unsupported
141140
* {@link ZoneRules#getTransitionRules()} count (not 0 and not 2), a
@@ -149,7 +148,7 @@ static DstRule extractDstRule(String timezoneId, TimeZone tz, ZoneRules rules) {
149148
// TimeZone.getTimeZone(zoneId) silently returns GMT for such ids on most
150149
// JVMs, which would leave `tz` describing a different zone than `rules`.
151150
// Mirrors the guard in OrcTimezoneInfo.buildRuntimeOrcTimezoneInfo.
152-
if (rules.isFixedOffset() || !tz.useDaylightTime()) {
151+
if (rules.isFixedOffset()) {
153152
return null;
154153
}
155154
// Sanity-check that tz and rules describe the same zone. Both Path A and
@@ -173,6 +172,12 @@ static DstRule extractDstRule(String timezoneId, TimeZone tz, ZoneRules rules) {
173172
if (rule != null) {
174173
return rule;
175174
}
175+
// Some JDK timezone implementations report useDaylightTime() == false even
176+
// though getOffset() still applies a recurring DST rule in future years.
177+
// ORC conversion follows getOffset(), so only trust this flag after probing.
178+
if (!tz.useDaylightTime()) {
179+
return null;
180+
}
176181
rule = extractDstRuleFromZoneRules(timezoneId, tz, rules);
177182
if (rule != null) {
178183
return rule;
@@ -351,7 +356,10 @@ private static DstTransitions findDstTransitionsByProbing(TimeZone tz, int refYe
351356
private static DstRule buildDstRuleFromProbedTransitions(TimeZone tz,
352357
DstTransitions transitions) {
353358
DstRule rule = new DstRule();
354-
rule.dstSavings = tz.getDSTSavings();
359+
rule.dstSavings = tz.useDaylightTime()
360+
? tz.getDSTSavings()
361+
: tz.getOffset(transitions.dstOnTransition)
362+
- tz.getOffset(transitions.dstOnTransition - 1);
355363

356364
int[] startFields = decodeTransition(transitions.dstOnTransition, tz.getRawOffset());
357365
rule.startMonth = startFields[0];

src/test/java/com/nvidia/spark/rapids/jni/GpuTimeZoneDBTest.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,14 @@ private static ColumnVector convertOrcFromUtcOnCPU(
146146
}
147147

148148
private static Long[] getOrcFromUtcBoundaryMicros(String readerTzId) {
149+
long minSupportedUs = LocalDateTime.of(1, 1, 1, 0, 0)
150+
.toEpochSecond(ZoneOffset.UTC) * MICROS_PER_SECOND;
151+
long maxSupportedUs = LocalDateTime.of(9999, 12, 31, 23, 59, 59)
152+
.toEpochSecond(ZoneOffset.UTC) * MICROS_PER_SECOND + 999_999L;
149153
List<Long> values = new ArrayList<>(Arrays.asList(
150154
null,
151-
Long.MIN_VALUE,
152-
Long.MIN_VALUE + 1,
155+
minSupportedUs,
156+
minSupportedUs + 1,
153157
-3_649_379_812_521_628L,
154158
-2_957_649_381_472_612L,
155159
-1_501L,
@@ -161,8 +165,8 @@ private static Long[] getOrcFromUtcBoundaryMicros(String readerTzId) {
161165
999L,
162166
1_001L,
163167
514_952_012L,
164-
Long.MAX_VALUE - 1,
165-
Long.MAX_VALUE));
168+
maxSupportedUs - 1,
169+
maxSupportedUs));
166170

167171
OrcTimezoneInfo readerInfo = OrcTimezoneInfo.get(readerTzId);
168172
if (readerInfo.transitions != null) {
@@ -282,6 +286,7 @@ void testConvertOrcFromUtcAllTimezones() {
282286
List<String> timezones = Arrays.asList(
283287
"UTC",
284288
"America/New_York",
289+
"America/Vancouver",
285290
"America/Los_Angeles",
286291
"Europe/Paris",
287292
"Asia/Shanghai",
@@ -354,6 +359,7 @@ void testConvertOrcTimezones() {
354359

355360
List<String> timezones = Arrays.asList(
356361
"America/Los_Angeles",
362+
"America/Vancouver",
357363
"America/Cancun",
358364
"Asia/Shanghai",
359365
"Antarctica/DumontDUrville",

src/test/java/com/nvidia/spark/rapids/jni/OrcTimezoneInfoTest.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
import java.time.zone.ZoneRules;
3232
import java.util.ArrayList;
3333
import java.util.Arrays;
34+
import java.util.Calendar;
3435
import java.util.Collections;
3536
import java.util.Date;
3637
import java.util.List;
38+
import java.util.SimpleTimeZone;
3739
import java.util.TimeZone;
3840

3941
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@ -400,10 +402,50 @@ void testExtractDstRuleSouthernHemisphere() {
400402
@Test
401403
void testExtractDstRuleNoDstReturnsNull() {
402404
// Asia/Shanghai had DST historically (1940s, 1986-1991) but no current rule.
403-
// tz.useDaylightTime() must be false → extractDstRule returns null.
405+
// Probing finds no recurring transitions and useDaylightTime() is false.
404406
assertNull(extractDstRuleFor("Asia/Shanghai"));
405407
}
406408

409+
@Test
410+
void testExtractDstRuleProbesBeforeUsingDaylightFlag() {
411+
TimeZone tz = new SimpleTimeZone(
412+
-8 * 3_600_000,
413+
"Synthetic/FalseDaylightFlag",
414+
Calendar.MARCH,
415+
2,
416+
Calendar.SUNDAY,
417+
2 * 3_600_000,
418+
Calendar.NOVEMBER,
419+
1,
420+
Calendar.SUNDAY,
421+
2 * 3_600_000,
422+
3_600_000) {
423+
@Override
424+
public boolean useDaylightTime() {
425+
return false;
426+
}
427+
};
428+
ZoneOffset baseOffset = ZoneOffset.ofHours(-8);
429+
ZoneOffsetTransition historical = ZoneOffsetTransition.of(
430+
LocalDateTime.of(1900, 1, 1, 0, 0),
431+
ZoneOffset.ofHours(-9),
432+
baseOffset);
433+
ZoneRules rules = ZoneRules.of(
434+
baseOffset,
435+
baseOffset,
436+
Collections.emptyList(),
437+
Collections.singletonList(historical),
438+
Collections.emptyList());
439+
440+
assertFalse(tz.useDaylightTime());
441+
OrcDstRuleExtractor.DstRule rule = OrcDstRuleExtractor.extractDstRule(
442+
tz.getID(), tz, rules);
443+
assertNotNull(rule);
444+
assertEquals(3_600_000, rule.dstSavings);
445+
assertEquals(Calendar.MARCH, rule.startMonth);
446+
assertEquals(Calendar.NOVEMBER, rule.endMonth);
447+
}
448+
407449
@Test
408450
void testExtractDstRuleFixedOffsetReturnsNull() {
409451
// Fixed-offset zones never observe DST.

0 commit comments

Comments
 (0)