Implement ToString function for temporal datatypes (#429)

* Modify `toString` to be able to handle `Date`, `LocalTime`, `LocalDateTime` and `Duration`

* Add unit tests

* Make `operator<<` use the `ToString()` implementations

* Add tests to verify the correctness of negative durations

* Add more tests to look for cases when the individual duration entities overflow.
This commit is contained in:
gvolfing 2022-07-11 13:44:27 +02:00 committed by GitHub
parent 063e297e1e
commit 7fc0fb6520
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 154 additions and 37 deletions

View File

@ -882,21 +882,36 @@ TypedValue Id(const TypedValue *args, int64_t nargs, const FunctionContext &ctx)
}
TypedValue ToString(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
FType<Or<Null, String, Number, Bool>>("toString", args, nargs);
FType<Or<Null, String, Number, Date, LocalTime, LocalDateTime, Duration, Bool>>("toString", args, nargs);
const auto &arg = args[0];
if (arg.IsNull()) {
return TypedValue(ctx.memory);
} else if (arg.IsString()) {
}
if (arg.IsString()) {
return TypedValue(arg, ctx.memory);
} else if (arg.IsInt()) {
}
if (arg.IsInt()) {
// TODO: This is making a pointless copy of std::string, we may want to
// use a different conversion to string
return TypedValue(std::to_string(arg.ValueInt()), ctx.memory);
} else if (arg.IsDouble()) {
return TypedValue(std::to_string(arg.ValueDouble()), ctx.memory);
} else {
return TypedValue(arg.ValueBool() ? "true" : "false", ctx.memory);
}
if (arg.IsDouble()) {
return TypedValue(std::to_string(arg.ValueDouble()), ctx.memory);
}
if (arg.IsDate()) {
return TypedValue(arg.ValueDate().ToString(), ctx.memory);
}
if (arg.IsLocalTime()) {
return TypedValue(arg.ValueLocalTime().ToString(), ctx.memory);
}
if (arg.IsLocalDateTime()) {
return TypedValue(arg.ValueLocalDateTime().ToString(), ctx.memory);
}
if (arg.IsDuration()) {
return TypedValue(arg.ValueDuration().ToString(), ctx.memory);
}
return TypedValue(arg.ValueBool() ? "true" : "false", ctx.memory);
}
TypedValue Timestamp(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {

View File

@ -15,6 +15,7 @@
#include <chrono>
#include <ctime>
#include <limits>
#include <string>
#include <string_view>
#include "utils/exceptions.hpp"
@ -175,6 +176,10 @@ int64_t Date::MicrosecondsSinceEpoch() const {
int64_t Date::DaysSinceEpoch() const { return utils::DaysSinceEpoch(year, month, day).count(); }
std::string Date::ToString() const {
return fmt::format("{:0>4}-{:0>2}-{:0>2}", year, static_cast<int>(month), static_cast<int>(day));
}
size_t DateHash::operator()(const Date &date) const {
utils::HashCombine<uint64_t, uint64_t> hasher;
size_t result = hasher(0, date.year);
@ -377,6 +382,15 @@ int64_t LocalTime::NanosecondsSinceEpoch() const {
return chrono::duration_cast<chrono::nanoseconds>(SumLocalTimeParts()).count();
}
std::string LocalTime::ToString() const {
using milli = std::chrono::milliseconds;
using micro = std::chrono::microseconds;
const auto subseconds = milli(millisecond) + micro(microsecond);
return fmt::format("{:0>2}:{:0>2}:{:0>2}.{:0>6}", static_cast<int>(hour), static_cast<int>(minute),
static_cast<int>(second), subseconds.count());
}
size_t LocalTimeHash::operator()(const LocalTime &local_time) const {
utils::HashCombine<uint64_t, uint64_t> hasher;
size_t result = hasher(0, local_time.hour);
@ -486,6 +500,8 @@ int64_t LocalDateTime::SubSecondsAsNanoseconds() const {
return (milli_as_nanos + micros_as_nanos).count();
}
std::string LocalDateTime::ToString() const { return date.ToString() + 'T' + local_time.ToString(); }
LocalDateTime::LocalDateTime(const DateParameters &date_parameters, const LocalTimeParameters &local_time_parameters)
: date(date_parameters), local_time(local_time_parameters) {}
@ -699,6 +715,23 @@ int64_t Duration::SubSecondsAsNanoseconds() const {
return chrono::duration_cast<chrono::nanoseconds>(micros - secs).count();
}
std::string Duration::ToString() const {
// Format [nD]T[nH]:[nM]:[nS].
namespace chrono = std::chrono;
auto micros = chrono::microseconds(microseconds);
const auto dd = GetAndSubtractDuration<chrono::days>(micros);
const auto h = GetAndSubtractDuration<chrono::hours>(micros);
const auto m = GetAndSubtractDuration<chrono::minutes>(micros);
const auto s = GetAndSubtractDuration<chrono::seconds>(micros);
auto first_half = fmt::format("P{}DT{}H{}M", dd, h, m);
auto second_half = fmt::format("{}.{:0>6}S", s, std::abs(micros.count()));
if (s == 0 && micros.count() < 0) {
return first_half + '-' + second_half;
}
return first_half + second_half;
}
Duration Duration::operator-() const {
if (microseconds == std::numeric_limits<decltype(microseconds)>::min()) [[unlikely]] {
throw temporal::InvalidArgumentException("Duration arithmetic overflows");

View File

@ -87,20 +87,9 @@ struct Duration {
int64_t SubDaysAsNanoseconds() const;
int64_t SubSecondsAsNanoseconds() const;
friend std::ostream &operator<<(std::ostream &os, const Duration &dur) {
// Format [nD]T[nH]:[nM]:[nS].
namespace chrono = std::chrono;
auto micros = chrono::microseconds(dur.microseconds);
const auto dd = GetAndSubtractDuration<chrono::days>(micros);
const auto h = GetAndSubtractDuration<chrono::hours>(micros);
const auto m = GetAndSubtractDuration<chrono::minutes>(micros);
const auto s = GetAndSubtractDuration<chrono::seconds>(micros);
os << fmt::format("P{}DT{}H{}M", dd, h, m);
if (s == 0 && micros.count() < 0) {
os << '-';
}
return os << fmt::format("{}.{:0>6}S", s, std::abs(micros.count()));
}
std::string ToString() const;
friend std::ostream &operator<<(std::ostream &os, const Duration &dur) { return os << dur.ToString(); }
Duration operator-() const;
@ -155,13 +144,11 @@ struct Date {
explicit Date(int64_t microseconds);
explicit Date(const DateParameters &date_parameters);
friend std::ostream &operator<<(std::ostream &os, const Date &date) {
return os << fmt::format("{:0>2}-{:0>2}-{:0>2}", date.year, static_cast<int>(date.month),
static_cast<int>(date.day));
}
friend std::ostream &operator<<(std::ostream &os, const Date &date) { return os << date.ToString(); }
int64_t MicrosecondsSinceEpoch() const;
int64_t DaysSinceEpoch() const;
std::string ToString() const;
friend Date operator+(const Date &date, const Duration &dur) {
namespace chrono = std::chrono;
@ -217,17 +204,11 @@ struct LocalTime {
// Epoch means the start of the day, i,e, midnight
int64_t MicrosecondsSinceEpoch() const;
int64_t NanosecondsSinceEpoch() const;
std::string ToString() const;
auto operator<=>(const LocalTime &) const = default;
friend std::ostream &operator<<(std::ostream &os, const LocalTime &lt) {
namespace chrono = std::chrono;
using milli = chrono::milliseconds;
using micro = chrono::microseconds;
const auto subseconds = milli(lt.millisecond) + micro(lt.microsecond);
return os << fmt::format("{:0>2}:{:0>2}:{:0>2}.{:0>6}", static_cast<int>(lt.hour), static_cast<int>(lt.minute),
static_cast<int>(lt.second), subseconds.count());
}
friend std::ostream &operator<<(std::ostream &os, const LocalTime &lt) { return os << lt.ToString(); }
friend LocalTime operator+(const LocalTime &local_time, const Duration &dur) {
namespace chrono = std::chrono;
@ -279,13 +260,11 @@ struct LocalDateTime {
int64_t MicrosecondsSinceEpoch() const;
int64_t SecondsSinceEpoch() const; // seconds since epoch
int64_t SubSecondsAsNanoseconds() const;
std::string ToString() const;
auto operator<=>(const LocalDateTime &) const = default;
friend std::ostream &operator<<(std::ostream &os, const LocalDateTime &ldt) {
os << ldt.date << 'T' << ldt.local_time;
return os;
}
friend std::ostream &operator<<(std::ostream &os, const LocalDateTime &ldt) { return os << ldt.ToString(); }
friend LocalDateTime operator+(const LocalDateTime &dt, const Duration &dur) {
const auto local_date_time_as_duration = Duration(dt.MicrosecondsSinceEpoch());

View File

@ -1747,6 +1747,26 @@ TEST_F(FunctionTest, ToStringBool) {
EXPECT_EQ(EvaluateFunction("TOSTRING", false).ValueString(), "false");
}
TEST_F(FunctionTest, ToStringDate) {
const auto date = memgraph::utils::Date({1970, 1, 2});
EXPECT_EQ(EvaluateFunction("TOSTRING", date).ValueString(), "1970-01-02");
}
TEST_F(FunctionTest, ToStringLocalTime) {
const auto lt = memgraph::utils::LocalTime({13, 2, 40, 100, 50});
EXPECT_EQ(EvaluateFunction("TOSTRING", lt).ValueString(), "13:02:40.100050");
}
TEST_F(FunctionTest, ToStringLocalDateTime) {
const auto ldt = memgraph::utils::LocalDateTime({1970, 1, 2}, {23, 02, 59});
EXPECT_EQ(EvaluateFunction("TOSTRING", ldt).ValueString(), "1970-01-02T23:02:59.000000");
}
TEST_F(FunctionTest, ToStringDuration) {
memgraph::utils::Duration duration{{.minute = 2, .second = 2, .microsecond = 33}};
EXPECT_EQ(EvaluateFunction("TOSTRING", duration).ValueString(), "P0DT0H2M2.000033S");
}
TEST_F(FunctionTest, ToStringExceptions) { EXPECT_THROW(EvaluateFunction("TOSTRING", 1, 2, 3), QueryRuntimeException); }
TEST_F(FunctionTest, TimestampVoid) {

View File

@ -609,3 +609,73 @@ TEST(TemporalTest, LocalDateTimeDelta) {
ASSERT_EQ(two_years_after_unix_epoch - unix_epoch,
memgraph::utils::Duration({.day = 761, .millisecond = 20, .microsecond = 34}));
}
TEST(TemporalTest, DateConvertsToString) {
const auto date1 = memgraph::utils::Date({1970, 1, 2});
const std::string date1_expected_str = "1970-01-02";
const auto date2 = memgraph::utils::Date({0000, 1, 1});
const std::string date2_expected_str = "0000-01-01";
const auto date3 = memgraph::utils::Date({2022, 7, 4});
const std::string date3_expected_str = "2022-07-04";
ASSERT_EQ(date1_expected_str, date1.ToString());
ASSERT_EQ(date2_expected_str, date2.ToString());
ASSERT_EQ(date3_expected_str, date3.ToString());
}
TEST(TemporalTest, LocalTimeConvertsToString) {
const auto lt1 = memgraph::utils::LocalTime({13, 2, 40, 100, 50});
const std::string lt1_expected_str = "13:02:40.100050";
const auto lt2 = memgraph::utils::LocalTime({13, 2, 40});
const std::string lt2_expected_str = "13:02:40.000000";
const auto lt3 = memgraph::utils::LocalTime({0, 0, 0});
const std::string lt3_expected_str = "00:00:00.000000";
const auto lt4 = memgraph::utils::LocalTime({3, 2, 4, 6, 7});
const std::string lt4_expected_str = "03:02:04.006007";
ASSERT_EQ(lt1_expected_str, lt1.ToString());
ASSERT_EQ(lt2_expected_str, lt2.ToString());
ASSERT_EQ(lt3_expected_str, lt3.ToString());
ASSERT_EQ(lt4_expected_str, lt4.ToString());
}
TEST(TemporalTest, LocalDateTimeConvertsToString) {
const auto ldt1 = memgraph::utils::LocalDateTime({1970, 1, 2}, {23, 02, 59});
const std::string ldt1_expected_str = "1970-01-02T23:02:59.000000";
const auto ldt2 = memgraph::utils::LocalDateTime({1970, 1, 2}, {23, 02, 59, 456, 123});
const std::string ldt2_expected_str = "1970-01-02T23:02:59.456123";
const auto ldt3 = memgraph::utils::LocalDateTime({1997, 8, 24}, {16, 32, 9});
const std::string ldt3_expected_str = "1997-08-24T16:32:09.000000";
ASSERT_EQ(ldt1_expected_str, ldt1.ToString());
ASSERT_EQ(ldt2_expected_str, ldt2.ToString());
ASSERT_EQ(ldt3_expected_str, ldt3.ToString());
}
TEST(TemporalTest, DurationConvertsToString) {
memgraph::utils::Duration duration1{{.minute = 2, .second = 2, .microsecond = 33}};
const std::string duration1_expected_str = "P0DT0H2M2.000033S";
memgraph::utils::Duration duration2{{.hour = 2.5, .minute = 2, .second = 2, .microsecond = 33}};
const std::string duration2_expected_str = "P0DT2H32M2.000033S";
memgraph::utils::Duration duration3{{.hour = 1.25, .minute = 2, .second = 2}};
const std::string duration3_expected_str = "P0DT1H17M2.000000S";
memgraph::utils::Duration duration4{{.day = 20, .hour = 1.25, .minute = 2, .second = 2}};
const std::string duration4_expected_str = "P20DT1H17M2.000000S";
memgraph::utils::Duration duration5{{.hour = -3, .minute = 2, .second = 2, .microsecond = -33}};
const std::string duration5_expected_str = "P0DT-2H-57M-58.000033S";
memgraph::utils::Duration duration6{{.day = -2, .hour = 3, .minute = 2, .second = 2, .microsecond = 33}};
const std::string duration6_expected_str = "P-1DT-20H-57M-57.999967S";
memgraph::utils::Duration duration7{{.day = 20, .hour = 72, .minute = 154, .second = 312}};
const std::string duration7_expected_str = "P23DT2H39M12.000000S";
memgraph::utils::Duration duration8{{.day = 1, .hour = 23, .minute = 59, .second = 60}};
const std::string duration8_expected_str = "P2DT0H0M0.000000S";
ASSERT_EQ(duration1_expected_str, duration1.ToString());
ASSERT_EQ(duration2_expected_str, duration2.ToString());
ASSERT_EQ(duration3_expected_str, duration3.ToString());
ASSERT_EQ(duration4_expected_str, duration4.ToString());
ASSERT_EQ(duration5_expected_str, duration5.ToString());
ASSERT_EQ(duration6_expected_str, duration6.ToString());
ASSERT_EQ(duration7_expected_str, duration7.ToString());
ASSERT_EQ(duration8_expected_str, duration8.ToString());
}