Add ZonedDateTime parser and its missing methods

This commit is contained in:
Ante Pušić 2024-03-25 09:17:32 +01:00
parent 0d4b057491
commit 848212f73e

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 Memgraph Ltd.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
@ -215,7 +215,7 @@ First 3 digits represent milliseconds, while the second 3 digits represent micro
} // namespace
std::pair<LocalTimeParameters, bool> ParseLocalTimeParameters(std::string_view local_time_string) {
LocalTimeParsing ParseLocalTimeParameters(std::string_view local_time_string, bool in_zoned_dt) {
// https://en.wikipedia.org/wiki/ISO_8601#Times
// supported formats:
// hh:mm:ss.ssssss | hhmmss.ssssss
@ -261,7 +261,7 @@ std::pair<LocalTimeParameters, bool> ParseLocalTimeParameters(std::string_view l
local_time_string.remove_prefix(2);
if (local_time_string.empty()) {
return {local_time_parameters, false};
return {local_time_parameters, false, 0};
}
process_optional_colon();
@ -274,7 +274,7 @@ std::pair<LocalTimeParameters, bool> ParseLocalTimeParameters(std::string_view l
local_time_string.remove_prefix(2);
if (local_time_string.empty()) {
return {local_time_parameters, *using_colon};
return {local_time_parameters, *using_colon, 0};
}
process_optional_colon();
@ -287,7 +287,7 @@ std::pair<LocalTimeParameters, bool> ParseLocalTimeParameters(std::string_view l
local_time_string.remove_prefix(2);
if (local_time_string.empty()) {
return {local_time_parameters, *using_colon};
return {local_time_parameters, *using_colon, 0};
}
if (local_time_string.front() != '.') {
@ -304,7 +304,7 @@ std::pair<LocalTimeParameters, bool> ParseLocalTimeParameters(std::string_view l
local_time_string.remove_prefix(3);
if (local_time_string.empty()) {
return {local_time_parameters, *using_colon};
return {local_time_parameters, *using_colon, 0};
}
const auto maybe_microseconds = ParseNumber<int64_t>(local_time_string, 3);
@ -315,11 +315,11 @@ std::pair<LocalTimeParameters, bool> ParseLocalTimeParameters(std::string_view l
local_time_parameters.microsecond = *maybe_microseconds;
local_time_string.remove_prefix(3);
if (!local_time_string.empty()) {
if (!in_zoned_dt && !local_time_string.empty()) {
throw temporal::InvalidArgumentException("Extra characters present at the end of the string.");
}
return {local_time_parameters, *using_colon};
return LocalTimeParsing{local_time_parameters, *using_colon, local_time_string.length()};
}
LocalTime::LocalTime(const int64_t microseconds) {
@ -403,7 +403,7 @@ size_t LocalTimeHash::operator()(const LocalTime &local_time) const {
namespace {
inline constexpr auto *kSupportedLocalDateTimeFormatsHelpMessage = R"help(
String representing the LocalDateTime should be in one of the following formats:
String representing LocalDateTime should be in one of the following formats:
- YYYY-MM-DDThh:mm:ss
- YYYY-MM-DDThh:mm
@ -436,7 +436,8 @@ It's important to note that the date and time parts should use both the correspo
or both parts should be written in their basic forms without the separators.)help";
} // namespace
std::pair<DateParameters, LocalTimeParameters> ParseLocalDateTimeParameters(std::string_view string) {
LocalDateTimeParsing ParseLocalDateTimeParameters(std::string_view string, bool in_zoned_dt) {
auto t_position = string.find('T');
if (t_position == std::string_view::npos) {
throw temporal::InvalidArgumentException("Invalid LocalDateTime format. {}",
@ -455,7 +456,8 @@ std::pair<DateParameters, LocalTimeParameters> ParseLocalDateTimeParameters(std:
kSupportedLocalDateTimeFormatsHelpMessage);
}
auto [local_time_parameters, extended_time_format] = ParseLocalTimeParameters(local_time_substring);
auto [local_time_parameters, extended_time_format, remainder_length] =
ParseLocalTimeParameters(local_time_substring, in_zoned_dt);
if (extended_date_format ^ extended_time_format) {
throw temporal::InvalidArgumentException(
@ -463,7 +465,7 @@ std::pair<DateParameters, LocalTimeParameters> ParseLocalDateTimeParameters(std:
kSupportedLocalDateTimeFormatsHelpMessage);
}
return {date_parameters, local_time_parameters};
return {date_parameters, local_time_parameters, remainder_length};
} catch (const temporal::InvalidArgumentException &e) {
throw temporal::InvalidArgumentException("Invalid LocalDateTime format. {}",
kSupportedLocalDateTimeFormatsHelpMessage);
@ -514,6 +516,252 @@ size_t LocalDateTimeHash::operator()(const LocalDateTime &local_date_time) const
return result;
}
namespace {
inline constexpr auto *kSupportedZonedDateTimeFormatsHelpMessage = R"help(
A string representing ZonedDateTime should have the following structure:
- <DateTime><timezone>
The <DateTime> substring should use one of the following formats:
- YYYY-MM-DDThh:mm:ss
- YYYY-MM-DDThh:mm
or
- YYYYMMDDThhmmss
- YYYYMMDDThhmm
- YYYYMMDDThh
The <timezone> substring should use one of the following formats:
- Z
- ±hh:mm
- ±hh:mm[ZoneName]
- ±hhmm
- ±hhmm[ZoneName]
- ±hh
- ±hh[ZoneName]
- [ZoneName]
Symbol table:
|---|---------|
| Y | YEAR |
|---|---------|
| M | MONTH |
|---|---------|
| D | DAY |
|---|---------|
| h | HOURS |
|---|---------|
| m | MINUTES |
|---|---------|
| s | SECONDS |
|---|---------|
| Z | UTC |
|---|---------|
ZoneName is a standardized timezone name from the IANA time zone database, given without quote marks.
Additionally, seconds can be defined as decimal fractions with 3 or 6 digits after the decimal point.
First 3 digits represent milliseconds, while the second 3 digits represent microseconds.
It's important to note that the date and time parts should use both the corresponding separators
or both parts should be written in their basic forms without the separators.)help";
} // namespace
Timezone ParseTimezoneFromName(std::string_view timezone_string) {
auto extract_timezone_name = [](std::string_view &string) {
if (string.front() != '[' or string.back() != ']') {
throw temporal::InvalidArgumentException("Timezone name is not enclosed by '[' ']'.");
}
string.remove_prefix(1);
string.remove_suffix(1);
return string;
};
const auto *timezone = [&]() {
try {
return std::chrono::locate_zone(extract_timezone_name(timezone_string));
} catch (const std::exception &_) {
throw temporal::InvalidArgumentException("Timezone name is not in the IANA time zone database.");
}
return (const std::chrono::time_zone *)nullptr;
}();
return Timezone(timezone);
}
std::pair<Timezone, uint64_t> ParseTimezoneFromOffset(std::string_view timezone_offset_string) {
// Supported formats:
// ±hh:mm
// ±hhmm
// ±hh
const auto process_optional_colon = [&] {
const bool has_colon = timezone_offset_string.front() == ':';
if (has_colon) {
timezone_offset_string.remove_prefix(1);
}
if (timezone_offset_string.empty()) {
throw temporal::InvalidArgumentException("Invalid format for the timezone offset. {}",
kSupportedZonedDateTimeFormatsHelpMessage);
}
};
auto compute_offset = [](const char sign, const int64_t hours, const int64_t minutes) {
return std::chrono::minutes{(sign == '+' ? 1 : -1) * (60 * hours + minutes)};
};
const auto sign = timezone_offset_string.front();
if (!(sign == '+' || sign == '-')) {
throw temporal::InvalidArgumentException("The timezone offset starts with either a '+' sign or a '-' sign.");
}
timezone_offset_string.remove_prefix(1);
const auto maybe_hours = ParseNumber<int64_t>(timezone_offset_string, 2);
if (!maybe_hours) {
throw temporal::InvalidArgumentException("Invalid hours in the string. {}",
kSupportedZonedDateTimeFormatsHelpMessage);
}
timezone_offset_string.remove_prefix(2);
if (timezone_offset_string.empty()) {
return {Timezone(compute_offset(sign, maybe_hours.value(), 0)), 0};
}
if (timezone_offset_string.front() == '[') {
return {Timezone(compute_offset(sign, maybe_hours.value(), 0)), timezone_offset_string.length()};
}
process_optional_colon();
const auto maybe_minutes = ParseNumber<int64_t>(timezone_offset_string, 2);
if (!maybe_minutes) {
throw temporal::InvalidArgumentException("Invalid minutes in the string. {}",
kSupportedZonedDateTimeFormatsHelpMessage);
}
timezone_offset_string.remove_prefix(2);
return {Timezone(compute_offset(sign, maybe_hours.value(), maybe_minutes.value())), timezone_offset_string.length()};
}
ZonedDateTimeParameters ParseZonedDateTimeParameters(std::string_view string) {
auto [date_parameters, local_time_parameters, remainder_length] = ParseLocalDateTimeParameters(string, true);
string.remove_prefix(string.length() - remainder_length);
if (string.empty()) {
throw temporal::InvalidArgumentException("Timezone is not designated.");
}
if (string.front() == 'Z') {
if (string.length() != 1) {
throw temporal::InvalidArgumentException("Invalid timezone format. {}",
kSupportedZonedDateTimeFormatsHelpMessage);
}
return ZonedDateTimeParameters{
.date = date_parameters,
.local_time = local_time_parameters,
.timezone = Timezone(std::chrono::locate_zone("Etc/UTC")),
};
}
if (string.front() == '[') {
return ZonedDateTimeParameters{
.date = date_parameters,
.local_time = local_time_parameters,
.timezone = ParseTimezoneFromName(string),
};
}
auto [timezone_from_offset, maybe_timezone_name_length] = ParseTimezoneFromOffset(string);
string.remove_prefix(string.length() - maybe_timezone_name_length);
if (string.empty()) {
return ZonedDateTimeParameters{
.date = date_parameters,
.local_time = local_time_parameters,
.timezone = timezone_from_offset,
};
}
auto timezone_from_name = ParseTimezoneFromName(string);
auto unzoned_date_time = std::chrono::sys_time<std::chrono::microseconds>{
std::chrono::microseconds{LocalDateTime(date_parameters, local_time_parameters).MicrosecondsSinceEpoch()}};
if (timezone_from_name.OffsetInMinutes(unzoned_date_time) !=
timezone_from_offset.OffsetInMinutes(unzoned_date_time)) {
throw temporal::InvalidArgumentException("The number offset doesnt match the timezone offset.");
}
return ZonedDateTimeParameters{
.date = date_parameters,
.local_time = local_time_parameters,
.timezone = timezone_from_name,
};
}
ZonedDateTime::ZonedDateTime(const ZonedDateTimeParameters &zoned_date_time_parameters) {
auto timezone = zoned_date_time_parameters.timezone;
std::chrono::sys_time<std::chrono::microseconds> duration{std::chrono::microseconds(
LocalDateTime(zoned_date_time_parameters.date, zoned_date_time_parameters.local_time).MicrosecondsSinceEpoch())};
zoned_time = std::chrono::zoned_time(timezone, duration);
// TODO antepusic if time ambiguous, e.g. when 3->2 AM, round up/down?
}
ZonedDateTime::ZonedDateTime(const ZonedDateTime &zoned_date_time) : zoned_time(zoned_date_time.zoned_time) {}
ZonedDateTime::ZonedDateTime(const std::chrono::zoned_time<std::chrono::microseconds, Timezone> &zoned_time)
: zoned_time(zoned_time) {}
int64_t ZonedDateTime::MicrosecondsSinceEpoch() const { return zoned_time.get_sys_time().time_since_epoch().count(); }
int64_t ZonedDateTime::SecondsSinceEpoch() const {
return std::chrono::duration_cast<std::chrono::seconds>(zoned_time.get_sys_time().time_since_epoch()).count();
}
int64_t ZonedDateTime::SubSecondsAsNanoseconds() const {
return 0;
const auto time_since_epoch = zoned_time.get_sys_time().time_since_epoch();
const auto full_seconds = std::chrono::duration_cast<std::chrono::seconds>(time_since_epoch);
return (time_since_epoch - full_seconds).count();
}
std::string ZonedDateTime::ToString() const {
const auto without_timezone_name = std::format("{0:%Y}-{0:%m}-{0:%d}T{0:%H}:{0:%M}:{0:%S}{0:%Ez}", zoned_time);
const auto timezone_name = zoned_time.get_time_zone().TimezoneName();
if (timezone_name.empty()) {
return without_timezone_name;
}
return std::format("{0}[{1}]", without_timezone_name, timezone_name);
}
bool ZonedDateTime::operator==(const ZonedDateTime &other) const {
return MicrosecondsSinceEpoch() == other.MicrosecondsSinceEpoch() && OffsetInMinutes() == other.OffsetInMinutes() &&
TimezoneName() == other.TimezoneName();
}
std::strong_ordering ZonedDateTime::operator<=>(const ZonedDateTime &other) const {
const auto duration_ordering = MicrosecondsSinceEpoch() <=> other.MicrosecondsSinceEpoch();
if (duration_ordering != 0) {
return duration_ordering;
}
const auto offset_ordering = OffsetInMinutes() <=> other.OffsetInMinutes();
if (offset_ordering != 0) {
return offset_ordering;
}
const auto timezone_name_ordering = TimezoneName() <=> other.TimezoneName();
return timezone_name_ordering;
}
namespace {
std::optional<DurationParameters> TryParseDurationString(std::string_view string) {
DurationParameters duration_parameters;