Skip to content

Commit

Permalink
Add support for z, zz, zzz in format_datetime (facebookincubator#11323)
Browse files Browse the repository at this point in the history
Summary:

This diff adds support for JODA's z, zz, zzz patterns (all equivalent) in Presto's
forma_datetime function.

This is used to format time zone abbreviations.

Reviewed By: pedroerp

Differential Revision: D64774281
  • Loading branch information
Kevin Wilfong authored and facebook-github-bot committed Oct 28, 2024
1 parent a218607 commit 0b636fb
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 57 deletions.
31 changes: 24 additions & 7 deletions velox/functions/lib/DateTimeFormatter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1203,10 +1203,17 @@ uint32_t DateTimeFormatter::maxResultSize(const tz::TimeZone* timezone) const {
size += std::max((int)token.pattern.minRepresentDigits, 9);
break;
case DateTimeFormatSpecifier::TIMEZONE:
VELOX_NYI(
"Date format specifier is not yet implemented: {} ({})",
getSpecifierName(token.pattern.specifier),
token.pattern.minRepresentDigits);
if (token.pattern.minRepresentDigits <= 3) {
// The longest abbreviation according to here is 5, e.g. some time
// zones use the offset as the abbreviation, like +0530.
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
size += 5;
} else {
VELOX_NYI(
"Date format specifier is not yet implemented: {} ({})",
getSpecifierName(token.pattern.specifier),
token.pattern.minRepresentDigits);
}

break;
case DateTimeFormatSpecifier::TIMEZONE_OFFSET_ID:
Expand Down Expand Up @@ -1451,9 +1458,19 @@ int32_t DateTimeFormatter::format(
} break;

case DateTimeFormatSpecifier::TIMEZONE: {
// TODO: implement short name time zone, need a map from full name to
// short name
VELOX_UNSUPPORTED("time zone name is not yet supported");
VELOX_USER_CHECK_NOT_NULL(
timezone,
"The time zone cannot be formatted if it is not present.");
if (token.pattern.minRepresentDigits <= 3) {
const std::string& abbrev = timezone->getShortName(
std::chrono::milliseconds(timestamp.toMillis()),
tz::TimeZone::TChoose::kEarliest);
std::memcpy(result, abbrev.data(), abbrev.length());
result += abbrev.length();
} else {
// TODO: implement full name time zone
VELOX_NYI("full time zone name is not yet supported");
}
} break;

case DateTimeFormatSpecifier::TIMEZONE_OFFSET_ID: {
Expand Down
33 changes: 27 additions & 6 deletions velox/functions/prestosql/tests/DateTimeFunctionsTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3269,6 +3269,28 @@ TEST_F(DateTimeFunctionsTest, formatDateTime) {
EXPECT_EQ("+05:30", formatDatetime(parseTimestamp("1970-01-01"), "ZZ"));
EXPECT_EQ("+0530", formatDatetime(parseTimestamp("1970-01-01"), "Z"));

EXPECT_EQ("IST", formatDatetime(parseTimestamp("1970-01-01"), "zzz"));
EXPECT_EQ("IST", formatDatetime(parseTimestamp("1970-01-01"), "zz"));
EXPECT_EQ("IST", formatDatetime(parseTimestamp("1970-01-01"), "z"));

// Test daylight savings.
setQueryTimeZone("America/Los_Angeles");
EXPECT_EQ("PST", formatDatetime(parseTimestamp("1970-01-01"), "z"));
EXPECT_EQ("PDT", formatDatetime(parseTimestamp("1970-10-01"), "z"));
EXPECT_EQ("PST", formatDatetime(parseTimestamp("2024-03-10 01:00"), "z"));
EXPECT_EQ("PDT", formatDatetime(parseTimestamp("2024-03-10 03:00"), "z"));
EXPECT_EQ("PDT", formatDatetime(parseTimestamp("2024-11-03 01:00"), "z"));
EXPECT_EQ("PST", formatDatetime(parseTimestamp("2024-11-03 02:00"), "z"));

// Test a long abbreviation.
setQueryTimeZone("Asia/Colombo");
EXPECT_EQ("+0530", formatDatetime(parseTimestamp("1970-10-01"), "z"));

setQueryTimeZone("Asia/Kolkata");
// We don't support more than 3 'z's yet.
EXPECT_THROW(
formatDatetime(parseTimestamp("1970-01-01"), "zzzz"), VeloxRuntimeError);

// Literal test cases.
EXPECT_EQ("hello", formatDatetime(parseTimestamp("1970-01-01"), "'hello'"));
EXPECT_EQ("'", formatDatetime(parseTimestamp("1970-01-01"), "''"));
Expand Down Expand Up @@ -3313,15 +3335,14 @@ TEST_F(DateTimeFunctionsTest, formatDateTime) {
EXPECT_THROW(
formatDatetime(parseTimestamp("1970-01-01"), "'abcd"), VeloxUserError);

// System errors for patterns we haven't implemented yet.
// Time zone name patterns aren't supported when there isn't a time zone
// available.
EXPECT_THROW(
formatDatetime(parseTimestamp("1970-01-01"), "z"), VeloxRuntimeError);
formatDatetime(parseTimestamp("1970-01-01"), "z"), VeloxUserError);
EXPECT_THROW(
formatDatetime(parseTimestamp("1970-01-01"), "zz"), VeloxRuntimeError);
formatDatetime(parseTimestamp("1970-01-01"), "zz"), VeloxUserError);
EXPECT_THROW(
formatDatetime(parseTimestamp("1970-01-01"), "zzz"), VeloxRuntimeError);
EXPECT_THROW(
formatDatetime(parseTimestamp("1970-01-01"), "zzzz"), VeloxRuntimeError);
formatDatetime(parseTimestamp("1970-01-01"), "zzz"), VeloxUserError);
}

TEST_F(DateTimeFunctionsTest, formatDateTimeTimezone) {
Expand Down
43 changes: 1 addition & 42 deletions velox/type/tz/TimeZoneMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,7 @@
#include <fmt/core.h>
#include <folly/container/F14Map.h>
#include <folly/container/F14Set.h>
#include "velox/common/base/Exceptions.h"
#include "velox/common/testutil/TestValue.h"
#include "velox/external/date/tz.h"

using facebook::velox::common::testutil::TestValue;

Expand Down Expand Up @@ -222,36 +220,8 @@ std::string normalizeTimeZone(const std::string& originalZoneId) {
}
return originalZoneId;
}

template <typename TDuration>
void validateRangeImpl(time_point<TDuration> timePoint) {
using namespace velox::date;
static constexpr auto kMinYear = date::year::min();
static constexpr auto kMaxYear = date::year::max();

auto year = year_month_day(floor<days>(timePoint)).year();

if (year < kMinYear || year > kMaxYear) {
// This is a special case where we intentionally throw
// VeloxRuntimeError to avoid it being suppressed by TRY().
VELOX_FAIL_UNSUPPORTED_INPUT_UNCATCHABLE(
"Timepoint is outside of supported year range: [{}, {}], got {}",
(int)kMinYear,
(int)kMaxYear,
(int)year);
}
}

} // namespace

void validateRange(time_point<std::chrono::seconds> timePoint) {
validateRangeImpl(timePoint);
}

void validateRange(time_point<std::chrono::milliseconds> timePoint) {
validateRangeImpl(timePoint);
}

std::string getTimeZoneName(int64_t timeZoneID) {
return locateZone(timeZoneID, true)->name();
}
Expand Down Expand Up @@ -337,17 +307,7 @@ TimeZone::seconds TimeZone::to_sys(
return (timePoint - offset_).time_since_epoch();
}

if (choose == TimeZone::TChoose::kFail) {
// By default, throws.
return date::zoned_time{tz_, timePoint}.get_sys_time().time_since_epoch();
}

auto dateChoose = (choose == TimeZone::TChoose::kEarliest)
? date::choose::earliest
: date::choose::latest;
return date::zoned_time{tz_, timePoint, dateChoose}
.get_sys_time()
.time_since_epoch();
return getZonedTime(timePoint, choose).get_sys_time().time_since_epoch();
}

TimeZone::seconds TimeZone::to_local(TimeZone::seconds timestamp) const {
Expand All @@ -360,5 +320,4 @@ TimeZone::seconds TimeZone::to_local(TimeZone::seconds timestamp) const {
}
return date::zoned_time{tz_, timePoint}.get_local_time().time_since_epoch();
}

} // namespace facebook::velox::tz
49 changes: 47 additions & 2 deletions velox/type/tz/TimeZoneMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

#include <chrono>
#include <string>
#include "velox/common/base/Exceptions.h"
#include "velox/external/date/tz.h"

namespace facebook::velox::date {
class time_zone;
Expand Down Expand Up @@ -68,8 +70,24 @@ int16_t getTimeZoneID(int32_t offsetMinutes);
template <typename T>
using time_point = std::chrono::time_point<std::chrono::system_clock, T>;

void validateRange(time_point<std::chrono::seconds> timePoint);
void validateRange(time_point<std::chrono::milliseconds> timePoint);
template <typename TDuration>
void validateRange(time_point<TDuration> timePoint) {
using namespace velox::date;
static constexpr auto kMinYear = date::year::min();
static constexpr auto kMaxYear = date::year::max();

auto year = year_month_day(floor<days>(timePoint)).year();

if (year < kMinYear || year > kMaxYear) {
// This is a special case where we intentionally throw
// VeloxRuntimeError to avoid it being suppressed by TRY().
VELOX_FAIL_UNSUPPORTED_INPUT_UNCATCHABLE(
"Timepoint is outside of supported year range: [{}, {}], got {}",
(int)kMinYear,
(int)kMaxYear,
(int)year);
}
}

/// TimeZone is the proxy object for time zone management. It provides access to
/// time zone names, their IDs (as defined in TimeZoneDatabase.cpp and
Expand Down Expand Up @@ -151,7 +169,34 @@ class TimeZone {
return tz_;
}

/// Returns the short name (abbreviation) of the time zone for the given
/// timestamp. Note that the timestamp is needed for time zones that support
/// daylight savings time as the short name will change depending on the date
/// (e.g. PST/PDT).
template <typename TDuration>
std::string getShortName(TDuration timestamp, TChoose choose = TChoose::kFail)
const {
date::local_time<TDuration> timePoint{timestamp};
validateRange(date::sys_time<TDuration>(timestamp));

return getZonedTime(timePoint, choose).get_info().abbrev;
}

private:
template <typename TDuration>
date::zoned_time<TDuration> getZonedTime(
date::local_time<TDuration> timestamp,
TChoose choose) const {
if (choose == TChoose::kFail) {
// By default, throws.
return date::zoned_time{tz_, timestamp};
}

auto dateChoose = (choose == TChoose::kEarliest) ? date::choose::earliest
: date::choose::latest;
return date::zoned_time{tz_, timestamp, dateChoose};
}

const date::time_zone* tz_{nullptr};
const std::chrono::minutes offset_{0};
const std::string timeZoneName_;
Expand Down

0 comments on commit 0b636fb

Please sign in to comment.