Compare commits

...

4 Commits

Author SHA1 Message Date
Ante Pušić
6f228ad1a1 Fix changed temporal type parser calls 2024-03-25 09:19:10 +01:00
Ante Pušić
848212f73e Add ZonedDateTime parser and its missing methods 2024-03-25 09:17:32 +01:00
Ante Pušić
0d4b057491 Add ZonedDateType definition & helper structures 2024-03-25 09:16:11 +01:00
Ante Pušić
1e04831295 Add ZonedDateTime unit tests 2024-03-25 09:10:54 +01:00
6 changed files with 565 additions and 30 deletions

View File

@ -1225,7 +1225,7 @@ TypedValue LocalTime(const TypedValue *args, int64_t nargs, const FunctionContex
}
if (args[0].IsString()) {
const auto &[local_time_parameters, is_extended] = utils::ParseLocalTimeParameters(args[0].ValueString());
const auto &[local_time_parameters, is_extended, _] = utils::ParseLocalTimeParameters(args[0].ValueString());
return TypedValue(utils::LocalTime(local_time_parameters), ctx.memory);
}
@ -1252,7 +1252,7 @@ TypedValue LocalDateTime(const TypedValue *args, int64_t nargs, const FunctionCo
}
if (args[0].IsString()) {
const auto &[date_parameters, local_time_parameters] = ParseLocalDateTimeParameters(args[0].ValueString());
const auto &[date_parameters, local_time_parameters, _] = ParseLocalDateTimeParameters(args[0].ValueString());
return TypedValue(utils::LocalDateTime(date_parameters, local_time_parameters), ctx.memory);
}

View File

@ -195,7 +195,7 @@ struct mgp_local_time {
static_assert(std::is_nothrow_copy_constructible_v<memgraph::utils::LocalTime>);
mgp_local_time(const std::string_view string, memgraph::utils::MemoryResource *memory)
: memory(memory), local_time(memgraph::utils::ParseLocalTimeParameters(string).first) {}
: memory(memory), local_time(memgraph::utils::ParseLocalTimeParameters(string).parameters) {}
mgp_local_time(const mgp_local_time_parameters *parameters, memgraph::utils::MemoryResource *memory)
: memory(memory), local_time(MapLocalTimeParameters(parameters)) {}
@ -229,7 +229,7 @@ struct mgp_local_time {
};
inline memgraph::utils::LocalDateTime CreateLocalDateTimeFromString(const std::string_view string) {
const auto &[date_parameters, local_time_parameters] = memgraph::utils::ParseLocalDateTimeParameters(string);
const auto &[date_parameters, local_time_parameters, _] = memgraph::utils::ParseLocalDateTimeParameters(string);
return memgraph::utils::LocalDateTime{date_parameters, local_time_parameters};
}

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;

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
@ -22,6 +22,8 @@
namespace memgraph::utils {
class Timezone;
template <typename T>
concept Chrono = requires(T) {
typename T::rep;
@ -192,8 +194,13 @@ struct LocalTimeParameters {
bool operator==(const LocalTimeParameters &) const = default;
};
// boolean indicates whether the parsed string was in extended format
std::pair<LocalTimeParameters, bool> ParseLocalTimeParameters(std::string_view string);
struct LocalTimeParsing {
LocalTimeParameters parameters;
bool extended_format;
uint64_t remainder_length;
};
LocalTimeParsing ParseLocalTimeParameters(std::string_view string, bool in_zoned_dt = false);
struct LocalTime {
explicit LocalTime() : LocalTime{LocalTimeParameters{}} {}
@ -251,7 +258,13 @@ struct LocalTimeHash {
size_t operator()(const LocalTime &local_time) const;
};
std::pair<DateParameters, LocalTimeParameters> ParseLocalDateTimeParameters(std::string_view string);
struct LocalDateTimeParsing {
DateParameters date_parameters;
LocalTimeParameters local_time_parameters;
uint64_t remainder_length;
};
LocalDateTimeParsing ParseLocalDateTimeParameters(std::string_view string, bool in_zoned_dt = false);
struct LocalDateTime {
explicit LocalDateTime(int64_t microseconds);
@ -294,6 +307,132 @@ struct LocalDateTimeHash {
size_t operator()(const LocalDateTime &local_date_time) const;
};
class Timezone {
private:
std::variant<std::chrono::minutes, const std::chrono::time_zone *> offset_;
public:
explicit Timezone(const std::chrono::minutes offset) : offset_{offset} {}
explicit Timezone(const std::chrono::time_zone *timezone) : offset_{timezone} {}
explicit Timezone(const std::string &timezone_name) : offset_{std::chrono::locate_zone(timezone_name)} {}
const Timezone *operator->() const { return this; }
bool operator==(const Timezone &) const = default;
template <class DurationT>
std::chrono::minutes OffsetInMinutes(std::chrono::sys_time<DurationT> time_point) const {
if (std::holds_alternative<std::chrono::minutes>(offset_)) {
return std::get<std::chrono::minutes>(offset_);
}
return std::chrono::duration_cast<std::chrono::minutes>(
std::get<const std::chrono::time_zone *>(offset_)->get_info(time_point).offset);
}
template <class DurationT>
std::chrono::sys_info get_info(std::chrono::sys_time<DurationT> time_point) const {
if (std::holds_alternative<std::chrono::minutes>(offset_)) {
const auto offset = std::get<std::chrono::minutes>(offset_);
return std::chrono::sys_info{
.begin = std::chrono::sys_seconds::min(),
.end = std::chrono::sys_seconds::max(),
.offset = std::chrono::duration_cast<std::chrono::seconds>(offset),
.save = std::chrono::minutes{0},
.abbrev = "",
// offset >= std::chrono::minutes{0} ? "+" + std::format("{0:%H}:{0:%M}", offset)
// : "-" + std::format("{0:%H}:{0:%M}", -offset)
};
}
return std::get<const std::chrono::time_zone *>(offset_)->get_info(time_point);
}
template <class DurationT>
auto to_local(std::chrono::sys_time<DurationT> time_point) const {
if (std::holds_alternative<std::chrono::minutes>(offset_)) {
using local_time = std::chrono::local_time<std::common_type_t<DurationT, std::chrono::minutes>>;
return local_time{(time_point + OffsetInMinutes(time_point)).time_since_epoch()};
}
return std::get<const std::chrono::time_zone *>(offset_)->to_local(time_point);
}
template <class DurationT>
auto to_sys(std::chrono::local_time<DurationT> time_point) const {
if (std::holds_alternative<std::chrono::minutes>(offset_)) {
using sys_time = std::chrono::sys_time<std::common_type_t<DurationT, std::chrono::minutes>>;
return sys_time{(time_point - std::get<std::chrono::minutes>(offset_)).time_since_epoch()};
}
return std::get<const std::chrono::time_zone *>(offset_)->to_sys(time_point);
}
std::string_view TimezoneName() const {
if (std::holds_alternative<std::chrono::minutes>(offset_)) {
return "";
}
return std::get<const std::chrono::time_zone *>(offset_)->name();
}
};
} // namespace memgraph::utils
namespace std::chrono {
template <>
struct zoned_traits<memgraph::utils::Timezone> {
static memgraph::utils::Timezone default_zone() { return memgraph::utils::Timezone{minutes{0}}; }
};
} // namespace std::chrono
namespace memgraph::utils {
struct ZonedDateTimeParameters {
DateParameters date;
LocalTimeParameters local_time;
Timezone timezone;
bool operator==(const ZonedDateTimeParameters &) const = default;
};
ZonedDateTimeParameters ParseZonedDateTimeParameters(std::string_view string);
struct ZonedDateTime {
explicit ZonedDateTime(const ZonedDateTimeParameters &zoned_date_time_parameters);
explicit ZonedDateTime(const ZonedDateTime &zoned_date_time);
explicit ZonedDateTime(const std::chrono::zoned_time<std::chrono::microseconds, Timezone> &zoned_time);
int64_t MicrosecondsSinceEpoch() const;
int64_t SecondsSinceEpoch() const;
int64_t SubSecondsAsNanoseconds() const;
std::string ToString() const;
bool operator==(const ZonedDateTime &other) const;
std::strong_ordering operator<=>(const ZonedDateTime &other) const;
std::chrono::minutes OffsetInMinutes() const {
return zoned_time.get_time_zone().OffsetInMinutes(zoned_time.get_sys_time());
}
std::string_view TimezoneName() const { return zoned_time.get_time_zone().TimezoneName(); }
friend std::ostream &operator<<(std::ostream &os, const ZonedDateTime &zdt) { return os << zdt.ToString(); }
friend ZonedDateTime operator+(const ZonedDateTime &zdt, const Duration &dur) {
return ZonedDateTime(std::chrono::zoned_time(
zdt.zoned_time.get_time_zone(), zdt.zoned_time.get_sys_time() + std::chrono::microseconds(dur.microseconds)));
}
friend ZonedDateTime operator+(const Duration &dur, const ZonedDateTime &zdt) { return zdt + dur; }
friend ZonedDateTime operator-(const ZonedDateTime &zdt, const Duration &dur) { return zdt + (-dur); }
friend Duration operator-(const ZonedDateTime &lhs, const ZonedDateTime &rhs) {
return Duration(lhs.MicrosecondsSinceEpoch()) - Duration(rhs.MicrosecondsSinceEpoch());
}
std::chrono::zoned_time<std::chrono::microseconds, Timezone> zoned_time;
};
Date CurrentDate();
LocalTime CurrentLocalTime();
LocalDateTime CurrentLocalDateTime();

View File

@ -1,4 +1,4 @@
// Copyright 2022 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
@ -127,7 +127,7 @@ void TestLocalTime(mg::Client &client) {
};
const auto parse = [](const std::string_view query) {
return memgraph::utils::LocalTime(memgraph::utils::ParseLocalTimeParameters(fmt::format("{}", query)).first);
return memgraph::utils::LocalTime(memgraph::utils::ParseLocalTimeParameters(fmt::format("{}", query)).parameters);
};
const auto str1 = lt(1, 3, 3, 33);
@ -154,7 +154,7 @@ void TestLocalDateTime(mg::Client &client) {
return fmt::format("{:0>2}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}", y, mo, d, h, m, s);
};
auto parse = [](const std::string_view str) {
const auto [dt, lt] = memgraph::utils::ParseLocalDateTimeParameters(str);
const auto [dt, lt, _] = memgraph::utils::ParseLocalDateTimeParameters(str);
return memgraph::utils::LocalDateTime(dt, lt);
};
auto ldt_query = [](const std::string_view str) { return fmt::format("\"{}\"", str); };

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
@ -10,6 +10,7 @@
// licenses/APL.txt.
#include <chrono>
#include <format>
#include <iostream>
#include <limits>
#include <optional>
@ -61,6 +62,11 @@ inline constexpr std::array test_local_times{TestLocalTimeParameters{{.hour = 24
TestLocalTimeParameters{{.microsecond = 1000}, true},
TestLocalTimeParameters{{23, 59, 59, 999, 999}, false},
TestLocalTimeParameters{{0, 0, 0, 0, 0}, false}};
// const std::chrono::sys_time LocalDateTimeToSysTime(const memgraph::utils::DateParameters &date_parameters,
// const memgraph::utils::LocalTimeParameters &local_time_parameters)
// {}
} // namespace
TEST(TemporalTest, DateConstruction) {
@ -180,6 +186,30 @@ TEST(TemporalTest, LocalDateTimeMicrosecondsSinceEpochConversion) {
}
}
TEST(TemporalTest, ZonedDateTimeMicrosecondsSinceEpochConversion) {
using namespace memgraph::utils;
const auto date_parameters = DateParameters{2024, 3, 22};
const auto local_time_parameters = LocalTimeParameters{12, 06, 03, 500, 500};
const auto local_date_time = LocalDateTime{date_parameters, local_time_parameters};
std::array timezone_offsets{
std::chrono::minutes{0}, std::chrono::minutes{60}, std::chrono::minutes{75}, std::chrono::minutes{90},
std::chrono::minutes{-60}, std::chrono::minutes{-75}, std::chrono::minutes{-90},
};
const auto check_faulty_timezones = [&date_parameters, &local_time_parameters, &local_date_time](const auto &cases) {
for (const auto &timezone_offset : cases) {
const auto zdt = ZonedDateTime({date_parameters, local_time_parameters, Timezone(timezone_offset)});
EXPECT_EQ(zdt.MicrosecondsSinceEpoch(),
local_date_time.MicrosecondsSinceEpoch() +
std::chrono::duration_cast<std::chrono::microseconds>(timezone_offset).count());
}
};
}
TEST(TemporalTest, DurationConversion) {
{
memgraph::utils::Duration duration{{.minute = 123.25}};
@ -260,17 +290,17 @@ TEST(TemporalTest, DateParsing) {
TEST(TemporalTest, LocalTimeParsing) {
for (const auto &[string, local_time_parameters] : parsing_test_local_time_extended) {
ASSERT_EQ(memgraph::utils::ParseLocalTimeParameters(string).first, local_time_parameters)
ASSERT_EQ(memgraph::utils::ParseLocalTimeParameters(string).parameters, local_time_parameters)
<< ToString(local_time_parameters);
const auto time_string = fmt::format("T{}", string);
ASSERT_EQ(memgraph::utils::ParseLocalTimeParameters(time_string).first, local_time_parameters)
ASSERT_EQ(memgraph::utils::ParseLocalTimeParameters(time_string).parameters, local_time_parameters)
<< ToString(local_time_parameters);
}
for (const auto &[string, local_time_parameters] : parsing_test_local_time_basic) {
ASSERT_EQ(memgraph::utils::ParseLocalTimeParameters(string).first, local_time_parameters)
ASSERT_EQ(memgraph::utils::ParseLocalTimeParameters(string).parameters, local_time_parameters)
<< ToString(local_time_parameters);
ASSERT_EQ(memgraph::utils::ParseLocalTimeParameters(fmt::format("T{}", string)).first, local_time_parameters)
ASSERT_EQ(memgraph::utils::ParseLocalTimeParameters(fmt::format("T{}", string)).parameters, local_time_parameters)
<< ToString(local_time_parameters);
}
@ -284,8 +314,9 @@ TEST(TemporalTest, LocalDateTimeParsing) {
for (const auto &[local_time_string, local_time_parameters] : local_times) {
const auto local_date_time_string = fmt::format("{}T{}", date_string, local_time_string);
if (is_valid) {
EXPECT_EQ(memgraph::utils::ParseLocalDateTimeParameters(local_date_time_string),
(std::pair{date_parameters, local_time_parameters}));
const auto parsed = memgraph::utils::ParseLocalDateTimeParameters(local_date_time_string);
EXPECT_EQ(parsed.date_parameters, date_parameters);
EXPECT_EQ(parsed.local_time_parameters, local_time_parameters);
}
}
}
@ -297,6 +328,71 @@ TEST(TemporalTest, LocalDateTimeParsing) {
check_local_date_time_combinations(parsing_test_dates_extended, parsing_test_local_time_basic, false);
}
TEST(TemporalTest, ZonedDateTimeParsing) {
// The ZonedDateTime format is the LocalDateTime format & the timezone designation. As the first part is parsed with
// the existing LocalDateTime parser, the LocalDateTime data is shared and the test cases focus on timezone parsing.
using namespace memgraph::utils;
const auto shared_date_time = "2020-11-22T19:23:21.123456"sv;
const auto shared_expected_date_params = DateParameters{2020, 11, 22};
const auto shared_expected_local_time_params = LocalTimeParameters{19, 23, 21, 123, 456};
std::array timezone_parsing_cases{
std::make_pair("Z"sv, Timezone("Etc/UTC")),
std::make_pair("+01:00"sv, Timezone(std::chrono::minutes{60})),
std::make_pair("+01:00[Europe/Zagreb]"sv, Timezone("Europe/Zagreb")),
std::make_pair("-08:00"sv, Timezone(std::chrono::minutes{-480})),
std::make_pair("-08:00[America/Los_Angeles]"sv, Timezone("America/Los_Angeles")),
std::make_pair("+0100"sv, Timezone(std::chrono::minutes{60})),
std::make_pair("+0100[Europe/Zagreb]"sv, Timezone("Europe/Zagreb")),
std::make_pair("-0800"sv, Timezone(std::chrono::minutes{-480})),
std::make_pair("-0800[America/Los_Angeles]"sv, Timezone("America/Los_Angeles")),
std::make_pair("+01"sv, Timezone(std::chrono::minutes{60})),
std::make_pair("+01[Europe/Zagreb]"sv, Timezone("Europe/Zagreb")),
std::make_pair("-08"sv, Timezone(std::chrono::minutes{-480})),
std::make_pair("-08[America/Los_Angeles]"sv, Timezone("America/Los_Angeles")),
std::make_pair("[Europe/Zagreb]"sv, Timezone("Europe/Zagreb")),
std::make_pair("[US/Pacific]"sv, Timezone("America/Los_Angeles")), // US/Pacific links to America/Los_Angeles
std::make_pair("[GMT]"sv, Timezone("GMT")),
};
std::array faulty_timezone_cases{
"Z_extra_text"sv,
"+01:00_extra_text"sv,
"+01:00[Europe/Zagreb]_extra_text"sv,
"+01:00[America/Los_Angeles]"sv,
"+01:00[America/Los_Angeles"sv,
"+01:00[nonexistent/timezone]"sv,
"+01.44"sv,
"01:00"sv,
};
const auto join_strings = [](const auto &date_time, const auto &timezone) {
return std::format("{0}{1}", date_time, timezone);
};
const auto check_timezone_parsing_cases = [&shared_date_time, &shared_expected_date_params,
&shared_expected_local_time_params, &join_strings](const auto &cases) {
for (const auto &[timezone_string, timezone_parameter] : cases) {
auto zdt_string = join_strings(shared_date_time, timezone_string);
auto zdt_parameters =
ZonedDateTimeParameters{shared_expected_date_params, shared_expected_local_time_params, timezone_parameter};
EXPECT_EQ(ParseZonedDateTimeParameters(zdt_string), zdt_parameters);
}
};
const auto check_faulty_timezones = [&shared_date_time, &join_strings](const auto &cases) {
for (const auto &timezone_string : cases) {
auto zdt_string = join_strings(shared_date_time, timezone_string);
EXPECT_ANY_THROW(ParseZonedDateTimeParameters(zdt_string));
}
};
check_timezone_parsing_cases(timezone_parsing_cases);
check_faulty_timezones(faulty_timezone_cases);
}
void CheckDurationParameters(const auto &values, const auto &expected) {
ASSERT_EQ(values.day, expected.day);
ASSERT_EQ(values.hour, expected.hour);
@ -412,6 +508,16 @@ TEST(TemporalTest, PrintLocalDateTime) {
ASSERT_EQ(stream.view(), "1970-01-01T13:02:40.100050");
}
TEST(TemporalTest, PrintZonedDateTime) {
using namespace memgraph::utils;
const auto zdt = ZonedDateTime({{1970, 1, 1}, {13, 2, 40, 100, 50}, Timezone("Europe/Zagreb")});
std::ostringstream stream;
stream << zdt;
ASSERT_TRUE(stream);
ASSERT_EQ(stream.view(), "1970-01-01T14:02:40.100050+01:00[Europe/Zagreb]");
}
TEST(TemporalTest, DurationAddition) {
// a >= 0 && b >= 0
const auto zero = memgraph::utils::Duration(0);
@ -620,6 +726,18 @@ TEST(TemporalTest, LocalDateTimeAdditionSubtraction) {
memgraph::utils::BasicException);
}
TEST(TemporalTest, ZonedDateTimeAdditionSubtraction) {
using namespace memgraph::utils;
const auto zdt = ZonedDateTime({{2024, 3, 22}, {12, 00, 00}, Timezone("Europe/Zagreb")});
const auto one_day = Duration({.day = 1});
EXPECT_EQ(zdt + one_day, ZonedDateTime({{2024, 3, 23}, {12, 00, 00}, Timezone("Europe/Zagreb")}));
EXPECT_EQ(one_day + zdt, ZonedDateTime({{2024, 3, 23}, {12, 00, 00}, Timezone("Europe/Zagreb")}));
EXPECT_EQ(zdt - one_day, ZonedDateTime({{2024, 3, 21}, {12, 00, 00}, Timezone("Europe/Zagreb")}));
}
TEST(TemporalTest, LocalDateTimeDelta) {
const auto unix_epoch = memgraph::utils::LocalDateTime({1970, 1, 1}, {1, 1, 1});
const auto one_year_after_unix_epoch = memgraph::utils::LocalDateTime({1971, 2, 1}, {12, 1, 1});
@ -630,6 +748,16 @@ TEST(TemporalTest, LocalDateTimeDelta) {
memgraph::utils::Duration({.day = 761, .millisecond = 20, .microsecond = 34}));
}
TEST(TemporalTest, ZonedDateTimeDelta) {
using namespace memgraph::utils;
const auto zdt = ZonedDateTime({{2024, 3, 22}, {12, 00, 00}, Timezone("Europe/Zagreb")});
const auto zdt_plus_time = ZonedDateTime({{2024, 3, 25}, {14, 18, 13, 206, 22}, Timezone("Europe/Zagreb")});
EXPECT_EQ(zdt_plus_time - zdt,
Duration({.day = 3, .hour = 2, .minute = 18, .second = 13, .millisecond = 206, .microsecond = 22}));
}
TEST(TemporalTest, DateConvertsToString) {
const auto date1 = memgraph::utils::Date({1970, 1, 2});
const std::string date1_expected_str = "1970-01-02";
@ -672,6 +800,26 @@ TEST(TemporalTest, LocalDateTimeConvertsToString) {
ASSERT_EQ(ldt3_expected_str, ldt3.ToString());
}
TEST(TemporalTest, ZonedDateTimeConvertsToString) {
using namespace memgraph::utils;
const auto zdt1 = ZonedDateTime({{1970, 1, 2}, {23, 02, 59}, Timezone("Europe/Zagreb")});
const std::string zdt1_expected_str = "1970-01-03T00:02:59.000000+01:00[Europe/Zagreb]";
const auto zdt2 = ZonedDateTime({{1970, 1, 2}, {23, 02, 59, 456, 123}, Timezone("Europe/Zagreb")});
const std::string zdt2_expected_str = "1970-01-03T00:02:59.456123+01:00[Europe/Zagreb]";
const auto zdt3 = ZonedDateTime({{1997, 8, 24}, {16, 32, 9}, Timezone("US/Pacific")});
const std::string zdt3_expected_str = "1997-08-24T09:32:09.000000-07:00[America/Los_Angeles]";
// expected (US/Pacific links to America/Los_Angeles, see
// https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List)
const auto zdt4 = ZonedDateTime({{1997, 8, 24}, {16, 32, 9}, Timezone(std::chrono::minutes(90))});
const std::string zdt4_expected_str = "1997-08-24T18:02:09.000000+01:30";
ASSERT_EQ(zdt1_expected_str, zdt1.ToString());
ASSERT_EQ(zdt2_expected_str, zdt2.ToString());
ASSERT_EQ(zdt3_expected_str, zdt3.ToString());
ASSERT_EQ(zdt4_expected_str, zdt4.ToString());
}
TEST(TemporalTest, DurationConvertsToString) {
memgraph::utils::Duration duration1{{.minute = 2, .second = 2, .microsecond = 33}};
const std::string duration1_expected_str = "P0DT0H2M2.000033S";