diff --git a/c++/src/Timezone.cc b/c++/src/Timezone.cc index b9499bd6d0..4789fcb907 100644 --- a/c++/src/Timezone.cc +++ b/c++/src/Timezone.cc @@ -713,6 +713,65 @@ namespace orc { return buffer; } + // Java TimeZone IDs use GMT followed by an ISO-style offset, not the + // reversed sign used by POSIX TZ strings. ZoneOffset can also include seconds. + static bool parseFixedOffset(const std::string& zone, int64_t& offset) { + if ((zone.size() != 9 && zone.size() != 12) || zone.compare(0, 3, "GMT") != 0 || + (zone[3] != '+' && zone[3] != '-')) { + return false; + } + int64_t parts[3] = {0, 0, 0}; + for (size_t i = 0; i < (zone.size() == 9 ? 2 : 3); ++i) { + size_t start = 4 + i * 3; + if ((i != 0 && zone[start - 1] != ':') || zone[start] < '0' || zone[start] > '9' || + zone[start + 1] < '0' || zone[start + 1] > '9') { + return false; + } + parts[i] = (zone[start] - '0') * 10 + zone[start + 1] - '0'; + } + if (parts[0] > 23 || parts[1] > 59 || parts[2] > 59) { + return false; + } + offset = (parts[0] * 60 + parts[1]) * 60 + parts[2]; + if (zone[3] == '-') { + offset = -offset; + } + return true; + } + + class FixedOffsetTimezone : public Timezone { + public: + FixedOffsetTimezone(const std::string& name, int64_t offset) : variant_{offset, false, name} {} + + const TimezoneVariant& getVariant(int64_t) const override { + return variant_; + } + + int64_t getEpoch() const override { + // 2015-01-01 00:00:00 in this timezone, expressed as Unix seconds. + return 1420070400 - variant_.gmtOffset; + } + + void print(std::ostream& out) const override { + out << variant_.toString() << "\n"; + } + + uint64_t getVersion() const override { + return 0; // No TZif file. + } + + int64_t convertToUTC(int64_t clk) const override { + return clk + variant_.gmtOffset; + } + + int64_t convertFromUTC(int64_t clk) const override { + return clk - variant_.gmtOffset; + } + + private: + TimezoneVariant variant_; + }; + class LazyTimezone : public Timezone { private: std::string filename_; @@ -766,6 +825,12 @@ namespace orc { if (itr != timezoneCache.end()) { return *(itr->second).get(); } + int64_t offset; + if (parseFixedOffset(zone, offset)) { + auto timezone = std::make_shared(zone, offset); + timezoneCache[filename] = timezone; + return *timezone; + } auto it = TZ_ALIASES.find(zone); if (it == TZ_ALIASES.end()) { timezoneCache[filename] = std::make_shared(filename); diff --git a/c++/test/TestTimezone.cc b/c++/test/TestTimezone.cc index ceb4b7b6ac..c9715b795d 100644 --- a/c++/test/TestTimezone.cc +++ b/c++/test/TestTimezone.cc @@ -17,13 +17,16 @@ */ #include "Adaptor.hh" +#include "MemoryInputStream.hh" #include "Timezone.hh" +#include "orc/OrcFile.hh" #include "wrap/gmock.h" #include "wrap/gtest-wrapper.h" #include #include #include +#include #include namespace orc { @@ -363,6 +366,91 @@ namespace orc { EXPECT_EQ("EST", getVariantFromZone(*ny1, "1974-10-27 06:00:00")); } + TEST(TestTimezone, testFixedOffsets) { + const std::pair cases[] = { + {"GMT-00:00", 0}, {"GMT+00:00", 0}, {"GMT+05:30", 19800}, + {"GMT-03:30", -12600}, {"GMT+05:45", 20700}, {"GMT+00:10", 600}, + {"GMT+23:59", 86340}, {"GMT-23:59", -86340}, {"GMT+05:30:15", 19815}, + {"GMT-03:30:15", -12615}, {"GMT+00:00:01", 1}, {"GMT-00:00:59", -59}}; + for (const auto& entry : cases) { + SCOPED_TRACE(entry.first); + const auto& zone = getTimezoneByName(entry.first); + EXPECT_EQ(&zone, &getTimezoneByName(entry.first)); + EXPECT_EQ(1420070400 - entry.second, zone.getEpoch()); + for (int64_t time : {-2208988800LL, 0LL, 1420070400LL, 4102444800LL}) { + const auto& variant = zone.getVariant(time); + EXPECT_EQ(entry.second, variant.gmtOffset); + EXPECT_FALSE(variant.isDst); + EXPECT_EQ(entry.first, variant.name); + EXPECT_EQ(time + entry.second, zone.convertToUTC(time)); + EXPECT_EQ(time - entry.second, zone.convertFromUTC(time)); + } + } + } + + TEST(TestTimezone, testReadJavaFixedOffsets) { + // Written by the Java ORC 2.1.2 writer with OpenJDK 22.0.2. Each file contains + // struct, compression NONE, rowIndexStride 0, and these values: + // 2001-11-12 18:31:01.123456789, 1960-01-02 03:04:05, and + // 2040-07-01 12:00:00.999999999. TimeZone.setDefault is called before creating + // the writer; the seconds offset uses TimeZone.getTimeZone(ZoneOffset.of(...)). + const std::pair files[] = { + {"GMT-00:00", + "T1JDdgIxaPE1zuYRtV/tFIB4AgA63mioAAAAAAAB3NZP+AoGCAEQARgOCgYIBRABGBESAggAEgIIAhoJR01U" + "LTAwOjAwChAKBAgAUAAKCAgASgBQAFgfCAMQRRoKCAMQABgfICMoAyIJCAwSAQEaAnRzIgIICTADOgQIAFAA" + "OggIAEoAUABYH0AASABYAWIFMi4xLjIIPhAAIgIADCgSMAmC9AMDT1JDEw=="}, + {"GMT+05:30", + "T1JDdgIxaPE1zuYRtV/tFIB4AgA63mioAAAAAAAB3NZP+AoGCAEQARgOCgYIBRABGBESAggAEgIIAhoJR01U" + "KzA1OjMwChAKBAgAUAAKCAgASgBQAFgfCAMQRRoKCAMQABgfICMoAyIJCAwSAQEaAnRzIgIICTADOgQIAFAA" + "OggIAEoAUABYH0AASABYAWIFMi4xLjIIPhAAIgIADCgSMAmC9AMDT1JDEw=="}, + {"GMT-03:30", + "T1JDdgIxaPE1zuYRtV/tFIB4AgA63mioAAAAAAAB3NZP+AoGCAEQARgOCgYIBRABGBESAggAEgIIAhoJR01U" + "LTAzOjMwChAKBAgAUAAKCAgASgBQAFgfCAMQRRoKCAMQABgfICMoAyIJCAwSAQEaAnRzIgIICTADOgQIAFAA" + "OggIAEoAUABYH0AASABYAWIFMi4xLjIIPhAAIgIADCgSMAmC9AMDT1JDEw=="}, + {"GMT+05:30:15", + "T1JDdgIxaPE1zuYRtV/tFIB4AgA63mioAAAAAAAB3NZP+AoGCAEQARgOCgYIBRABGBESAggAEgIIAhoMR01U" + "KzA1OjMwOjE1ChAKBAgAUAAKCAgASgBQAFgfCAMQSBoKCAMQABgfICYoAyIJCAwSAQEaAnRzIgIICTADOgQI" + "AFAAOggIAEoAUABYH0AASABYAWIFMi4xLjIIPhAAIgIADCgSMAmC9AMDT1JDEw=="}, + }; + const int64_t seconds[] = {1005589861, -315521755, 2224756800}; + const int64_t nanos[] = {123456789, 0, 999999999}; + for (const auto& file : files) { + SCOPED_TRACE(file.first); + auto bytes = decodeBase64(file.second); + auto reader = createReader(std::make_unique( + reinterpret_cast(bytes.data()), bytes.size()), + ReaderOptions()); + // Check conversion to both UTC and another fixed offset. In both cases + // the reader must preserve the original wall-clock timestamp. + for (const auto& readerZone : {"GMT+00:00", "GMT-03:30"}) { + SCOPED_TRACE(readerZone); + RowReaderOptions options; + options.setTimezoneName(readerZone); + auto rows = reader->createRowReader(options); + auto batch = rows->createRowBatch(10); + ASSERT_TRUE(rows->next(*batch)); + ASSERT_EQ(3, batch->numElements); + const auto& timestamps = dynamic_cast( + *dynamic_cast(*batch).fields[0]); + const int64_t readerOffset = std::string(readerZone) == "GMT-03:30" ? -12600 : 0; + for (size_t i = 0; i < 3; ++i) { + EXPECT_EQ(seconds[i] - readerOffset, timestamps.data[i]); + EXPECT_EQ(nanos[i], timestamps.nanoseconds[i]); + } + EXPECT_FALSE(rows->next(*batch)); + } + } + } + + TEST(TestTimezone, testInvalidFixedOffsets) { + for (const auto& name : + {"GMT+24:00", "GMT-00:60", "GMT+00:00:60", "GMT+05:30x", "GMT+05:30:15x", "GMT+0a:00", + "GMT+00/00", "GMT*05:30", "GMT+5:30", "GMT+05:3", "gmt+05:30", "GMT+"}) { + SCOPED_TRACE(name); + EXPECT_THROW(getTimezoneByName(name).getEpoch(), TimezoneError); + } + } + TEST(TestTimezone, testGMTv1) { const char GMT[] = ("VFppZgAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAQAAAA" @@ -437,6 +525,7 @@ namespace orc { testing::ThrowsMessage(testing::HasSubstr( "Time zone file /path/to/wrong/tzdb/America/Los_Angeles does not exist." " Please install IANA time zone database and set TZDIR env."))); + EXPECT_EQ(1420050600, getTimezoneByName("GMT+05:30").getEpoch()); if (!tzDirBackup.empty()) { ASSERT_TRUE(setEnv("TZDIR", tzDirBackup.c_str())); } else {