From 6913feacc741ef36e7e15d29804de15b1e77296b Mon Sep 17 00:00:00 2001 From: Kostas Kyrimis Date: Wed, 11 Aug 2021 19:04:01 +0300 Subject: [PATCH] Add support for temporal data types in the communication Bolt layer (#198) --- src/communication/bolt/v1/codes.hpp | 6 + src/communication/bolt/v1/decoder/decoder.hpp | 151 ++++++++-- .../bolt/v1/encoder/base_encoder.hpp | 44 ++- src/communication/bolt/v1/value.cpp | 80 ++++++ src/communication/bolt/v1/value.hpp | 41 ++- src/glue/communication.cpp | 40 ++- src/utils/temporal.cpp | 117 +++++--- src/utils/temporal.hpp | 73 ++++- tests/unit/bolt_decoder.cpp | 258 ++++++++++++++++- tests/unit/bolt_encoder.cpp | 268 +++++++++++++++++- tests/unit/utils_temporal.cpp | 54 ++++ 11 files changed, 1058 insertions(+), 74 deletions(-) diff --git a/src/communication/bolt/v1/codes.hpp b/src/communication/bolt/v1/codes.hpp index 6d7b1f627..74a31fe4d 100644 --- a/src/communication/bolt/v1/codes.hpp +++ b/src/communication/bolt/v1/codes.hpp @@ -30,6 +30,12 @@ enum class Signature : uint8_t { Relationship = 0x52, Path = 0x50, UnboundRelationship = 0x72, + + /// Temporal data types + Date = 0x44, + Duration = 0x45, + LocalDateTime = 0x64, + LocalTime = 0x74, }; enum class Marker : uint8_t { diff --git a/src/communication/bolt/v1/decoder/decoder.hpp b/src/communication/bolt/v1/decoder/decoder.hpp index 83fa72130..1878f6a57 100644 --- a/src/communication/bolt/v1/decoder/decoder.hpp +++ b/src/communication/bolt/v1/decoder/decoder.hpp @@ -1,5 +1,7 @@ #pragma once +#include +#include #include #include "communication/bolt/v1/codes.hpp" @@ -7,6 +9,7 @@ #include "utils/cast.hpp" #include "utils/endian.hpp" #include "utils/logging.hpp" +#include "utils/temporal.hpp" namespace communication::bolt { @@ -69,12 +72,37 @@ class Decoder { case Marker::Map16: case Marker::Map32: return ReadMap(marker, data); - + case Marker::TinyStruct1: { + uint8_t signature = 0; + if (!buffer_.Read(&signature, 1)) { + return false; + } + switch (static_cast(signature)) { + case Signature::Date: + return ReadDate(data); + case Signature::LocalTime: + return ReadLocalTime(data); + default: + return false; + } + } + case Marker::TinyStruct2: { + uint8_t signature = 0; + if (!buffer_.Read(&signature, 1)) { + return false; + } + switch (static_cast(signature)) { + case Signature::LocalDateTime: + return ReadLocalDateTime(data); + default: + return false; + } + } case Marker::TinyStruct3: { // For tiny struct 3 we will also read the Signature to switch between // vertex, unbounded_edge and path. Note that in those functions we // won't perform an additional signature read. - uint8_t signature; + uint8_t signature = 0; if (!buffer_.Read(&signature, 1)) { return false; } @@ -89,10 +117,30 @@ class Decoder { return false; } } - - case Marker::TinyStruct5: - return ReadEdge(marker, data); - + case Marker::TinyStruct4: { + uint8_t signature = 0; + if (!buffer_.Read(&signature, 1)) { + return false; + } + switch (static_cast(signature)) { + case Signature::Duration: + return ReadDuration(data); + default: + return false; + } + } + case Marker::TinyStruct5: { + uint8_t signature = 0; + if (!buffer_.Read(&signature, 1)) { + return false; + } + switch (static_cast(signature)) { + case Signature::Relationship: + return ReadEdge(data); + default: + return false; + } + } default: if ((value & 0xF0) == utils::UnderlyingCast(Marker::TinyString)) { return ReadString(marker, data); @@ -351,24 +399,11 @@ class Decoder { return true; } - bool ReadEdge(const Marker &marker, Value *data) { - uint8_t value; + bool ReadEdge(Value *data) { Value dv; *data = Value(Edge()); auto &edge = data->ValueEdge(); - if (!buffer_.Read(&value, 1)) { - return false; - } - - // check header - if (marker != Marker::TinyStruct5) { - return false; - } - if (value != utils::UnderlyingCast(Signature::Relationship)) { - return false; - } - // read ID if (!ReadValue(&dv, Value::Type::Int)) { return false; @@ -468,5 +503,81 @@ class Decoder { return true; } + + bool ReadDate(Value *data) { + Value dv; + if (!ReadValue(&dv, Value::Type::Int)) { + return false; + } + const auto chrono_days = std::chrono::days(dv.ValueInt()); + const auto sys_days = std::chrono::sys_days(chrono_days); + const auto date = std::chrono::year_month_day(sys_days); + *data = Value(utils::Date( + {static_cast(date.year()), static_cast(date.month()), static_cast(date.day())})); + return true; + } + + bool ReadLocalTime(Value *data) { + Value dv; + if (!ReadValue(&dv, Value::Type::Int)) { + return false; + } + namespace chrono = std::chrono; + const auto nanos = chrono::nanoseconds(dv.ValueInt()); + const auto microseconds = chrono::duration_cast(nanos); + *data = Value(utils::LocalTime(microseconds.count())); + return true; + } + + bool ReadLocalDateTime(Value *data) { + Value secs; + if (!ReadValue(&secs, Value::Type::Int)) { + return false; + } + + Value nanos; + if (!ReadValue(&nanos, Value::Type::Int)) { + return false; + } + namespace chrono = std::chrono; + const auto chrono_seconds = chrono::seconds(secs.ValueInt()); + const auto sys_seconds = chrono::sys_seconds(chrono_seconds); + const auto sys_days = chrono::time_point_cast(sys_seconds); + const auto date = chrono::year_month_day(sys_days); + + const auto ldt = utils::Date( + {static_cast(date.year()), static_cast(date.month()), static_cast(date.day())}); + + auto secs_leftover = chrono::seconds(sys_seconds - sys_days); + const auto h = utils::GetAndSubtractDuration(secs_leftover); + const auto m = utils::GetAndSubtractDuration(secs_leftover); + const auto s = secs_leftover.count(); + auto nanos_leftover = chrono::nanoseconds(nanos.ValueInt()); + const auto ml = utils::GetAndSubtractDuration(nanos_leftover); + const auto mi = chrono::duration_cast(nanos_leftover).count(); + const auto params = utils::LocalTimeParameters{h, m, s, ml, mi}; + const auto tm = utils::LocalTime(params); + *data = utils::LocalDateTime(ldt, tm); + return true; + } + + bool ReadDuration(Value *data) { + Value dv; + std::array values{0}; + for (auto &val : values) { + if (!ReadValue(&dv, Value::Type::Int)) { + return false; + } + val = dv.ValueInt(); + } + namespace chrono = std::chrono; + const auto months = chrono::months(values[0]); + const auto days = chrono::days(values[1]); + const auto secs = chrono::seconds(values[2]); + const auto nanos = chrono::nanoseconds(values[3]); + const auto micros = months + days + secs + chrono::duration_cast(nanos); + *data = Value(utils::Duration(micros.count())); + return true; + } }; } // namespace communication::bolt diff --git a/src/communication/bolt/v1/encoder/base_encoder.hpp b/src/communication/bolt/v1/encoder/base_encoder.hpp index 543a3d451..9aaef0e0a 100644 --- a/src/communication/bolt/v1/encoder/base_encoder.hpp +++ b/src/communication/bolt/v1/encoder/base_encoder.hpp @@ -15,7 +15,8 @@ namespace communication::bolt { /** * Bolt BaseEncoder. Has public interfaces for writing Bolt encoded data. - * Supported types are: Null, Bool, Int, Double, String, List, Map, Vertex, Edge + * Supported types are: Null, Bool, Int, Double, String, List, Map, Vertex, + * Edge, Date, LocalDate, LocalDateTime, Duration. * * The purpose of this class is to stream bolt data into the given Buffer. * @@ -89,6 +90,7 @@ class BaseEncoder { WriteTypeSize(value.size(), MarkerString); WriteRAW(value.c_str(), value.size()); } + void WriteList(const std::vector &value) { WriteTypeSize(value.size(), MarkerList); for (auto &x : value) WriteValue(x); @@ -168,6 +170,34 @@ class BaseEncoder { for (auto &i : path.indices) WriteInt(i); } + void WriteDate(const utils::Date &date) { + WriteRAW(utils::UnderlyingCast(Marker::TinyStruct1)); + WriteRAW(utils::UnderlyingCast(Signature::Date)); + WriteInt(date.DaysSinceEpoch()); + } + + void WriteLocalTime(const utils::LocalTime &local_time) { + WriteRAW(utils::UnderlyingCast(Marker::TinyStruct1)); + WriteRAW(utils::UnderlyingCast(Signature::LocalTime)); + WriteInt(local_time.NanosecondsSinceEpoch()); + } + + void WriteLocalDateTime(const utils::LocalDateTime &local_date_time) { + WriteRAW(utils::UnderlyingCast(Marker::TinyStruct2)); + WriteRAW(utils::UnderlyingCast(Signature::LocalDateTime)); + WriteInt(local_date_time.SecondsSinceEpoch()); + WriteInt(local_date_time.SubSecondsAsNanoseconds()); + } + + void WriteDuration(const utils::Duration &duration) { + WriteRAW(utils::UnderlyingCast(Marker::TinyStruct4)); + WriteRAW(utils::UnderlyingCast(Signature::Duration)); + WriteInt(duration.Months()); + WriteInt(duration.SubMonthsAsDays()); + WriteInt(duration.SubDaysAsSeconds()); + WriteInt(duration.SubSecondsAsNanoseconds()); + } + void WriteValue(const Value &value) { switch (value.type()) { case Value::Type::Null: @@ -203,6 +233,18 @@ class BaseEncoder { case Value::Type::Path: WritePath(value.ValuePath()); break; + case Value::Type::Date: + WriteDate(value.ValueDate()); + break; + case Value::Type::LocalTime: + WriteLocalTime(value.ValueLocalTime()); + break; + case Value::Type::LocalDateTime: + WriteLocalDateTime(value.ValueLocalDateTime()); + break; + case Value::Type::Duration: + WriteDuration(value.ValueDuration()); + break; } } diff --git a/src/communication/bolt/v1/value.cpp b/src/communication/bolt/v1/value.cpp index 8babac83a..e2ddb6846 100644 --- a/src/communication/bolt/v1/value.cpp +++ b/src/communication/bolt/v1/value.cpp @@ -39,6 +39,10 @@ DEF_GETTER_BY_REF(Vertex, Vertex, vertex_v) DEF_GETTER_BY_REF(Edge, Edge, edge_v) DEF_GETTER_BY_REF(UnboundedEdge, UnboundedEdge, unbounded_edge_v) DEF_GETTER_BY_REF(Path, Path, path_v) +DEF_GETTER_BY_REF(Date, utils::Date, date_v) +DEF_GETTER_BY_REF(LocalTime, utils::LocalTime, local_time_v) +DEF_GETTER_BY_REF(LocalDateTime, utils::LocalDateTime, local_date_time_v) +DEF_GETTER_BY_REF(Duration, utils::Duration, duration_v) #undef DEF_GETTER_BY_REF @@ -76,6 +80,18 @@ Value::Value(const Value &other) : type_(other.type_) { case Type::Path: new (&path_v) Path(other.path_v); return; + case Type::Date: + new (&date_v) utils::Date(other.date_v); + return; + case Type::LocalTime: + new (&local_time_v) utils::LocalTime(other.local_time_v); + return; + case Type::LocalDateTime: + new (&local_date_time_v) utils::LocalDateTime(other.local_date_time_v); + return; + case Type::Duration: + new (&duration_v) utils::Duration(other.duration_v); + return; } } @@ -118,6 +134,18 @@ Value &Value::operator=(const Value &other) { case Type::Path: new (&path_v) Path(other.path_v); return *this; + case Type::Date: + new (&date_v) utils::Date(other.date_v); + return *this; + case Type::LocalTime: + new (&local_time_v) utils::LocalTime(other.local_time_v); + return *this; + case Type::LocalDateTime: + new (&local_date_time_v) utils::LocalDateTime(other.local_date_time_v); + return *this; + case Type::Duration: + new (&duration_v) utils::Duration(other.duration_v); + return *this; } } return *this; @@ -157,6 +185,18 @@ Value::Value(Value &&other) noexcept : type_(other.type_) { case Type::Path: new (&path_v) Path(std::move(other.path_v)); break; + case Type::Date: + new (&date_v) utils::Date(other.date_v); + break; + case Type::LocalTime: + new (&local_time_v) utils::LocalTime(other.local_time_v); + break; + case Type::LocalDateTime: + new (&local_date_time_v) utils::LocalDateTime(other.local_date_time_v); + break; + case Type::Duration: + new (&duration_v) utils::Duration(other.duration_v); + break; } // reset the type of other @@ -203,6 +243,18 @@ Value &Value::operator=(Value &&other) noexcept { case Type::Path: new (&path_v) Path(std::move(other.path_v)); break; + case Type::Date: + new (&date_v) utils::Date(other.date_v); + break; + case Type::LocalTime: + new (&local_time_v) utils::LocalTime(other.local_time_v); + break; + case Type::LocalDateTime: + new (&local_date_time_v) utils::LocalDateTime(other.local_date_time_v); + break; + case Type::Duration: + new (&duration_v) utils::Duration(other.duration_v); + break; } // reset the type of other @@ -249,6 +301,18 @@ Value::~Value() { case Type::Path: path_v.~Path(); return; + case Type::Date: + date_v.~Date(); + return; + case Type::LocalTime: + local_time_v.~LocalTime(); + return; + case Type::LocalDateTime: + local_date_time_v.~LocalDateTime(); + return; + case Type::Duration: + duration_v.~Duration(); + return; } } @@ -341,6 +405,14 @@ std::ostream &operator<<(std::ostream &os, const Value &value) { return os << value.ValueUnboundedEdge(); case Value::Type::Path: return os << value.ValuePath(); + case Value::Type::Date: + return os << value.ValueDate(); + case Value::Type::LocalTime: + return os << value.ValueLocalTime(); + case Value::Type::LocalDateTime: + return os << value.ValueLocalDateTime(); + case Value::Type::Duration: + return os << value.ValueDuration(); } } @@ -368,6 +440,14 @@ std::ostream &operator<<(std::ostream &os, const Value::Type type) { return os << "unbounded_edge"; case Value::Type::Path: return os << "path"; + case Value::Type::Date: + return os << "date"; + case Value::Type::LocalTime: + return os << "local_time"; + case Value::Type::LocalDateTime: + return os << "local_date_time"; + case Value::Type::Duration: + return os << "duration"; } } } // namespace communication::bolt diff --git a/src/communication/bolt/v1/value.hpp b/src/communication/bolt/v1/value.hpp index 543370e47..db34157b3 100644 --- a/src/communication/bolt/v1/value.hpp +++ b/src/communication/bolt/v1/value.hpp @@ -7,6 +7,7 @@ #include "utils/cast.hpp" #include "utils/exceptions.hpp" +#include "utils/temporal.hpp" namespace communication::bolt { @@ -120,7 +121,23 @@ class Value { Value() : type_(Type::Null) {} /** Types that can be stored in a Value. */ - enum class Type : unsigned { Null, Bool, Int, Double, String, List, Map, Vertex, Edge, UnboundedEdge, Path }; + enum class Type : unsigned { + Null, + Bool, + Int, + Double, + String, + List, + Map, + Vertex, + Edge, + UnboundedEdge, + Path, + Date, + LocalTime, + LocalDateTime, + Duration + }; // constructors for primitive types Value(bool value) : type_(Type::Bool) { bool_v = value; } @@ -139,7 +156,12 @@ class Value { Value(const Edge &value) : type_(Type::Edge) { new (&edge_v) Edge(value); } Value(const UnboundedEdge &value) : type_(Type::UnboundedEdge) { new (&unbounded_edge_v) UnboundedEdge(value); } Value(const Path &value) : type_(Type::Path) { new (&path_v) Path(value); } - + Value(const utils::Date &date) : type_(Type::Date) { new (&date_v) utils::Date(date); } + Value(const utils::LocalTime &time) : type_(Type::LocalTime) { new (&local_time_v) utils::LocalTime(time); } + Value(const utils::LocalDateTime &date_time) : type_(Type::LocalDateTime) { + new (&local_date_time_v) utils::LocalDateTime(date_time); + } + Value(const utils::Duration &dur) : type_(Type::Duration) { new (&duration_v) utils::Duration(dur); } // move constructors for non-primitive values Value(std::string &&value) noexcept : type_(Type::String) { new (&string_v) std::string(std::move(value)); } Value(std::vector &&value) noexcept : type_(Type::List) { new (&list_v) std::vector(std::move(value)); } @@ -183,7 +205,10 @@ class Value { DECL_GETTER_BY_REFERENCE(Edge, Edge) DECL_GETTER_BY_REFERENCE(UnboundedEdge, UnboundedEdge) DECL_GETTER_BY_REFERENCE(Path, Path) - + DECL_GETTER_BY_REFERENCE(Date, utils::Date) + DECL_GETTER_BY_REFERENCE(LocalTime, utils::LocalTime) + DECL_GETTER_BY_REFERENCE(LocalDateTime, utils::LocalDateTime) + DECL_GETTER_BY_REFERENCE(Duration, utils::Duration) #undef DECL_GETTER_BY_REFERNCE #define TYPE_CHECKER(type) \ @@ -199,7 +224,10 @@ class Value { TYPE_CHECKER(Edge) TYPE_CHECKER(UnboundedEdge) TYPE_CHECKER(Path) - + TYPE_CHECKER(Date) + TYPE_CHECKER(LocalTime) + TYPE_CHECKER(LocalDateTime) + TYPE_CHECKER(Duration) #undef TYPE_CHECKER friend std::ostream &operator<<(std::ostream &os, const Value &value); @@ -219,9 +247,12 @@ class Value { Edge edge_v; UnboundedEdge unbounded_edge_v; Path path_v; + utils::Date date_v; + utils::LocalTime local_time_v; + utils::LocalDateTime local_date_time_v; + utils::Duration duration_v; }; }; - /** * An exception raised by the Value system. */ diff --git a/src/glue/communication.cpp b/src/glue/communication.cpp index aa26bd953..44dbbb099 100644 --- a/src/glue/communication.cpp +++ b/src/glue/communication.cpp @@ -7,6 +7,7 @@ #include "storage/v2/edge_accessor.hpp" #include "storage/v2/storage.hpp" #include "storage/v2/vertex_accessor.hpp" +#include "utils/temporal.hpp" using communication::bolt::Value; @@ -40,6 +41,14 @@ query::TypedValue ToTypedValue(const Value &value) { case Value::Type::UnboundedEdge: case Value::Type::Path: throw communication::bolt::ValueException("Unsupported conversion from Value to TypedValue"); + case Value::Type::Date: + return query::TypedValue(value.ValueDate()); + case Value::Type::LocalTime: + return query::TypedValue(value.ValueLocalTime()); + case Value::Type::LocalDateTime: + return query::TypedValue(value.ValueLocalDateTime()); + case Value::Type::Duration: + return query::TypedValue(value.ValueDuration()); } } @@ -100,12 +109,13 @@ storage::Result ToBoltValue(const query::TypedValue &value, const storage return Value(std::move(*maybe_path)); } case query::TypedValue::Type::Date: + return Value(value.ValueDate()); case query::TypedValue::Type::LocalTime: + return Value(value.ValueLocalTime()); case query::TypedValue::Type::LocalDateTime: + return Value(value.ValueLocalDateTime()); case query::TypedValue::Type::Duration: - // TODO(antonio2368): Change this when Bolt value for temporal types - // are implemented - LOG_FATAL("Temporal types not yet supported"); + return Value(value.ValueDuration()); } } @@ -190,6 +200,18 @@ storage::PropertyValue ToPropertyValue(const Value &value) { case Value::Type::UnboundedEdge: case Value::Type::Path: throw communication::bolt::ValueException("Unsupported conversion from Value to PropertyValue"); + case Value::Type::Date: + return storage::PropertyValue( + storage::TemporalData(storage::TemporalType::Date, value.ValueDate().MicrosecondsSinceEpoch())); + case Value::Type::LocalTime: + return storage::PropertyValue( + storage::TemporalData(storage::TemporalType::LocalTime, value.ValueLocalTime().MicrosecondsSinceEpoch())); + case Value::Type::LocalDateTime: + return storage::PropertyValue(storage::TemporalData(storage::TemporalType::LocalDateTime, + value.ValueLocalDateTime().MicrosecondsSinceEpoch())); + case Value::Type::Duration: + return storage::PropertyValue( + storage::TemporalData(storage::TemporalType::Duration, value.ValueDuration().microseconds)); } } @@ -224,7 +246,17 @@ Value ToBoltValue(const storage::PropertyValue &value) { return Value(std::move(dv_map)); } case storage::PropertyValue::Type::TemporalData: - LOG_FATAL("Unsupported type"); + const auto &type = value.ValueTemporalData(); + switch (type.type) { + case storage::TemporalType::Date: + return Value(utils::Date(type.microseconds)); + case storage::TemporalType::LocalTime: + return Value(utils::LocalTime(type.microseconds)); + case storage::TemporalType::LocalDateTime: + return Value(utils::LocalDateTime(type.microseconds)); + case storage::TemporalType::Duration: + return Value(utils::Duration(type.microseconds)); + } } } diff --git a/src/utils/temporal.cpp b/src/utils/temporal.cpp index 9c5d1a8ec..3efa49c28 100644 --- a/src/utils/temporal.cpp +++ b/src/utils/temporal.cpp @@ -9,20 +9,6 @@ namespace utils { namespace { -template -concept Chrono = requires(T) { - typename T::rep; - typename T::period; -}; - -template -constexpr auto GetAndSubtractDuration(TSecond &base_duration) { - const auto duration = std::chrono::duration_cast(base_duration); - base_duration -= duration; - return duration.count(); -} - -constexpr std::chrono::microseconds epoch{std::chrono::years{1970} + std::chrono::months{1} + std::chrono::days{1}}; constexpr bool IsInBounds(const auto low, const auto high, const auto value) { return low <= value && value <= high; } @@ -48,12 +34,13 @@ std::optional ParseNumber(const std::string_view string, const size_t size) { } // namespace Date::Date(const int64_t microseconds) { - auto chrono_microseconds = std::chrono::microseconds(microseconds); - chrono_microseconds += epoch; - MG_ASSERT(chrono_microseconds.count() >= 0, "Invalid Date specified in microseconds"); - years = GetAndSubtractDuration(chrono_microseconds); - months = GetAndSubtractDuration(chrono_microseconds); - days = GetAndSubtractDuration(chrono_microseconds); + namespace chrono = std::chrono; + const auto chrono_micros = chrono::microseconds(microseconds); + const auto s_days = chrono::sys_days(chrono::duration_cast(chrono_micros)); + const auto date = chrono::year_month_day(s_days); + years = static_cast(date.year()); + months = static_cast(date.month()); + days = static_cast(date.day()); } Date::Date(const DateParameters &date_parameters) { @@ -159,13 +146,12 @@ std::pair ParseDateParameters(std::string_view date_string } int64_t Date::MicrosecondsSinceEpoch() const { - auto result = std::chrono::duration_cast( - std::chrono::years{years} + std::chrono::months{months} + std::chrono::days{days}); - - result -= epoch; - return result.count(); + namespace chrono = std::chrono; + return chrono::duration_cast(utils::DaysSinceEpoch(years, months, days)).count(); } +int64_t Date::DaysSinceEpoch() const { return utils::DaysSinceEpoch(years, months, days).count(); } + size_t DateHash::operator()(const Date &date) const { utils::HashCombine hasher; size_t result = hasher(0, date.years); @@ -307,10 +293,14 @@ std::pair ParseLocalTimeParameters(std::string_view l LocalTime::LocalTime(const int64_t microseconds) { auto chrono_microseconds = std::chrono::microseconds(microseconds); - MG_ASSERT(chrono_microseconds.count() >= 0, "Negative LocalTime specified in microseconds"); + if (chrono_microseconds.count() < 0) { + throw utils::BasicException("Negative LocalTime specified in microseconds"); + } const auto parsed_hours = GetAndSubtractDuration(chrono_microseconds); - MG_ASSERT(parsed_hours <= 23, "Invalid LocalTime specified in microseconds"); + if (parsed_hours > 23) { + throw utils::BasicException("Invalid LocalTime specified in microseconds"); + } hours = parsed_hours; minutes = GetAndSubtractDuration(chrono_microseconds); @@ -334,11 +324,11 @@ LocalTime::LocalTime(const LocalTimeParameters &local_time_parameters) { } if (!IsInBounds(0, 999, local_time_parameters.milliseconds)) { - throw utils::BasicException("Creating a LocalTime with invalid seconds parameter."); + throw utils::BasicException("Creating a LocalTime with invalid milliseconds parameter."); } if (!IsInBounds(0, 999, local_time_parameters.microseconds)) { - throw utils::BasicException("Creating a LocalTime with invalid seconds parameter."); + throw utils::BasicException("Creating a LocalTime with invalid microseconds parameter."); } hours = local_time_parameters.hours; @@ -348,11 +338,17 @@ LocalTime::LocalTime(const LocalTimeParameters &local_time_parameters) { microseconds = local_time_parameters.microseconds; } -int64_t LocalTime::MicrosecondsSinceEpoch() const { - return std::chrono::duration_cast( - std::chrono::hours{hours} + std::chrono::minutes{minutes} + std::chrono::seconds{seconds} + - std::chrono::milliseconds{milliseconds} + std::chrono::microseconds{microseconds}) - .count(); +std::chrono::microseconds LocalTime::SumLocalTimeParts() const { + namespace chrono = std::chrono; + return chrono::hours{hours} + chrono::minutes{minutes} + chrono::seconds{seconds} + + chrono::milliseconds{milliseconds} + chrono::microseconds{microseconds}; +} + +int64_t LocalTime::MicrosecondsSinceEpoch() const { return SumLocalTimeParts().count(); } + +int64_t LocalTime::NanosecondsSinceEpoch() const { + namespace chrono = std::chrono; + return chrono::duration_cast(SumLocalTimeParts()).count(); } size_t LocalTimeHash::operator()(const LocalTime &local_time) const { @@ -443,6 +439,23 @@ int64_t LocalDateTime::MicrosecondsSinceEpoch() const { return date.MicrosecondsSinceEpoch() + local_time.MicrosecondsSinceEpoch(); } +int64_t LocalDateTime::SecondsSinceEpoch() const { + namespace chrono = std::chrono; + const auto to_sec = chrono::duration_cast(DaysSinceEpoch(date.years, date.months, date.days)); + const auto local_time_seconds = + chrono::hours(local_time.hours) + chrono::minutes(local_time.minutes) + chrono::seconds(local_time.seconds); + return (to_sec + local_time_seconds).count(); +} + +int64_t LocalDateTime::SubSecondsAsNanoseconds() const { + namespace chrono = std::chrono; + const auto milli_as_nanos = chrono::duration_cast(chrono::milliseconds(local_time.milliseconds)); + const auto micros_as_nanos = + chrono::duration_cast(chrono::microseconds(local_time.microseconds)); + + return (milli_as_nanos + micros_as_nanos).count(); +} + LocalDateTime::LocalDateTime(const DateParameters date_parameters, const LocalTimeParameters &local_time_parameters) : date(date_parameters), local_time(local_time_parameters) {} @@ -643,15 +656,14 @@ DurationParameters ParseDurationParameters(std::string_view string) { } } -Duration::Duration(int64_t microseconds) { this->microseconds = microseconds; } - namespace { template constexpr To CastChronoDouble(const double value) { return std::chrono::duration_cast(std::chrono::duration(value)); -}; +}; +} // namespace -} // namespace +Duration::Duration(int64_t microseconds) { this->microseconds = microseconds; } Duration::Duration(const DurationParameters ¶meters) { microseconds = (CastChronoDouble(parameters.years) + @@ -665,6 +677,35 @@ Duration::Duration(const DurationParameters ¶meters) { .count(); } +int64_t Duration::Months() const { + std::chrono::microseconds ms(microseconds); + return std::chrono::duration_cast(ms).count(); +} + +int64_t Duration::SubMonthsAsDays() const { + namespace chrono = std::chrono; + const auto months = chrono::months(Months()); + const auto micros = chrono::microseconds(microseconds); + return chrono::duration_cast(micros - months).count(); +} + +int64_t Duration::SubDaysAsSeconds() const { + namespace chrono = std::chrono; + const auto months = chrono::months(Months()); + const auto days = chrono::days(SubMonthsAsDays()); + const auto micros = chrono::microseconds(microseconds); + return chrono::duration_cast(micros - months - days).count(); +} + +int64_t Duration::SubSecondsAsNanoseconds() const { + namespace chrono = std::chrono; + const auto months = chrono::months(Months()); + const auto days = chrono::days(SubMonthsAsDays()); + const auto secs = chrono::seconds(SubDaysAsSeconds()); + const auto micros = chrono::microseconds(microseconds); + return chrono::duration_cast(micros - months - days - secs).count(); +} + Duration Duration::operator-() const { Duration result{-microseconds}; return result; diff --git a/src/utils/temporal.hpp b/src/utils/temporal.hpp index eba752267..8c30448e3 100644 --- a/src/utils/temporal.hpp +++ b/src/utils/temporal.hpp @@ -1,12 +1,29 @@ #pragma once -#include + #include +#include +#include + +#include "fmt/format.h" #include "utils/exceptions.hpp" #include "utils/logging.hpp" namespace utils { +template +concept Chrono = requires(T) { + typename T::rep; + typename T::period; +}; + +template +constexpr auto GetAndSubtractDuration(TSecond &base_duration) { + const auto duration = std::chrono::duration_cast(base_duration); + base_duration -= duration; + return duration.count(); +} + struct DateParameters { int64_t years{0}; int64_t months{1}; @@ -24,7 +41,13 @@ 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.years, static_cast(date.months), + static_cast(date.days)); + } + int64_t MicrosecondsSinceEpoch() const; + int64_t DaysSinceEpoch() const; auto operator<=>(const Date &) const = default; @@ -55,10 +78,23 @@ struct LocalTime { explicit LocalTime(int64_t microseconds); explicit LocalTime(const LocalTimeParameters &local_time_parameters); + std::chrono::microseconds SumLocalTimeParts() const; + + // Epoch means the start of the day, i,e, midnight int64_t MicrosecondsSinceEpoch() const; + int64_t NanosecondsSinceEpoch() const; auto operator<=>(const LocalTime &) const = default; + friend std::ostream &operator<<(std::ostream &os, const LocalTime <) { + namespace chrono = std::chrono; + using milli = chrono::milliseconds; + using micro = chrono::microseconds; + const auto subseconds = milli(lt.milliseconds) + micro(lt.microseconds); + return os << fmt::format("{:0>2}:{:0>2}:{:0>2}.{:0>6}", static_cast(lt.hours), static_cast(lt.minutes), + static_cast(lt.seconds), subseconds.count()); + } + uint8_t hours; uint8_t minutes; uint8_t seconds; @@ -74,12 +110,21 @@ std::pair ParseLocalDateTimeParameters(std: struct LocalDateTime { explicit LocalDateTime(int64_t microseconds); - explicit LocalDateTime(DateParameters date, const LocalTimeParameters &local_time); + explicit LocalDateTime(DateParameters date_parameters, const LocalTimeParameters &local_time_parameters); + + LocalDateTime(const Date &dt, const LocalTime <) : date(dt), local_time(lt) {} int64_t MicrosecondsSinceEpoch() const; + int64_t SecondsSinceEpoch() const; // seconds since epoch + int64_t SubSecondsAsNanoseconds() 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; + } + Date date; LocalTime local_time; }; @@ -107,6 +152,24 @@ struct Duration { auto operator<=>(const Duration &) const = default; + int64_t Months() const; + int64_t SubMonthsAsDays() const; + int64_t SubDaysAsSeconds() const; + int64_t SubSecondsAsNanoseconds() const; + + friend std::ostream &operator<<(std::ostream &os, const Duration &dur) { + // ISO 8601 extended format: P[YYYY]-[MM]-[DD]T[hh]:[mm]:[ss]. + namespace chrono = std::chrono; + auto micros = chrono::microseconds(dur.microseconds); + const auto y = GetAndSubtractDuration(micros); + const auto mo = GetAndSubtractDuration(micros); + const auto dd = GetAndSubtractDuration(micros); + const auto h = GetAndSubtractDuration(micros); + const auto m = GetAndSubtractDuration(micros); + const auto s = GetAndSubtractDuration(micros); + return os << fmt::format("P{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}.{:0>6}", y, mo, dd, h, m, s, micros.count()); + } + Duration operator-() const; int64_t microseconds; @@ -116,4 +179,10 @@ struct DurationHash { size_t operator()(const Duration &duration) const; }; +constexpr std::chrono::days DaysSinceEpoch(uint16_t years, uint8_t months, uint8_t days) { + namespace chrono = std::chrono; + const auto ymd = chrono::year_month_day(chrono::year(years), chrono::month(months), chrono::day(days)); + return chrono::sys_days{ymd}.time_since_epoch(); +} + } // namespace utils diff --git a/tests/unit/bolt_decoder.cpp b/tests/unit/bolt_decoder.cpp index 133c867bc..01d0df08d 100644 --- a/tests/unit/bolt_decoder.cpp +++ b/tests/unit/bolt_decoder.cpp @@ -1,6 +1,7 @@ +#include + #include "bolt_common.hpp" #include "bolt_testdata.hpp" - #include "communication/bolt/v1/decoder/decoder.hpp" using communication::bolt::Value; @@ -434,3 +435,258 @@ TEST_F(BoltDecoder, Edge) { ASSERT_EQ(edge.type, std::string("a")); ASSERT_EQ(edge.properties[std::string("a")].ValueInt(), 1); } + +// Temporal types testing starts here + +template +constexpr uint8_t Cast(T marker) { + return static_cast(marker); +} + +void AssertThatDatesAreEqual(const utils::Date &d1, const utils::Date &d2) { + ASSERT_EQ(d1.days, d2.days); + ASSERT_EQ(d1.months, d2.months); + ASSERT_EQ(d1.years, d2.years); +} + +TEST_F(BoltDecoder, DateOld) { + TestDecoderBuffer buffer; + DecoderT decoder(buffer); + + Value dv; + + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + const auto date = utils::Date({1970, 1, 1}); + const auto days = date.DaysSinceEpoch(); + ASSERT_EQ(days, 0); + // clang-format off + std::array data = { + Cast(Marker::TinyStruct1), + Cast(Sig::Date), + 0x0 }; + // clang-format on + buffer.Clear(); + buffer.Write(data.data(), data.size()); + ASSERT_EQ(decoder.ReadValue(&dv, Value::Type::Date), true); + AssertThatDatesAreEqual(dv.ValueDate(), date); +} + +TEST_F(BoltDecoder, DateRecent) { + TestDecoderBuffer buffer; + DecoderT decoder(buffer); + Value dv; + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + const auto date = utils::Date({2021, 7, 20}); + const auto days = date.DaysSinceEpoch(); + ASSERT_EQ(days, 18828); + const auto *d_bytes = std::bit_cast(&days); + // clang-format off + std::array data = { + Cast(Marker::TinyStruct1), + Cast(Sig::Date), + Cast(Marker::Int32), + d_bytes[3], + d_bytes[2], + d_bytes[1], + d_bytes[0] }; + // clang-format on + buffer.Clear(); + buffer.Write(data.data(), data.size()); + ASSERT_EQ(decoder.ReadValue(&dv, Value::Type::Date), true); + AssertThatDatesAreEqual(dv.ValueDate(), date); +} + +TEST_F(BoltDecoder, DurationOneSec) { + TestDecoderBuffer buffer; + DecoderT decoder(buffer); + Value dv; + const auto value = Value(utils::Duration(1)); + const auto &dur = value.ValueDuration(); + const auto nanos = dur.SubSecondsAsNanoseconds(); + ASSERT_EQ(dur.Months(), 0); + ASSERT_EQ(dur.SubMonthsAsDays(), 0); + ASSERT_EQ(dur.SubDaysAsSeconds(), 0); + ASSERT_EQ(nanos, 1000); + const auto *n_bytes = std::bit_cast(&nanos); + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + std::array data = { + Cast(Marker::TinyStruct4), + Cast(Sig::Duration), + 0x0, + 0x0, + 0x0, + Cast(Marker::Int16), + n_bytes[1], n_bytes[0] }; + // clang-format on + buffer.Clear(); + buffer.Write(data.data(), data.size()); + ASSERT_EQ(decoder.ReadValue(&dv, Value::Type::Duration), true); + ASSERT_EQ(dv.ValueDuration().microseconds, dur.microseconds); +} + +TEST_F(BoltDecoder, DurationMinusOneSec) { + TestDecoderBuffer buffer; + DecoderT decoder(buffer); + Value dv; + const auto value = Value(utils::Duration(-1)); + const auto &dur = value.ValueDuration(); + const auto nanos = dur.SubSecondsAsNanoseconds(); + ASSERT_EQ(dur.Months(), 0); + ASSERT_EQ(dur.SubMonthsAsDays(), 0); + ASSERT_EQ(dur.SubDaysAsSeconds(), 0); + ASSERT_EQ(nanos, -1000); + const auto *n_bytes = std::bit_cast(&nanos); + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + std::array data = { + Cast(Marker::TinyStruct4), + Cast(Sig::Duration), + 0x0, + 0x0, + 0x0, + Cast(Marker::Int16), + n_bytes[1], n_bytes[0] }; + // clang-format on + buffer.Clear(); + buffer.Write(data.data(), data.size()); + ASSERT_EQ(decoder.ReadValue(&dv, Value::Type::Duration), true); + ASSERT_EQ(dv.ValueDuration().microseconds, dur.microseconds); +} + +TEST_F(BoltDecoder, ArbitraryDuration) { + TestDecoderBuffer buffer; + DecoderT decoder(buffer); + Value dv; + const auto value = Value(utils::Duration({1, 1, 1, 1, 1, 1, 1, 0})); + const auto &dur = value.ValueDuration(); + ASSERT_EQ(dur.Months(), 13); + ASSERT_EQ(dur.SubMonthsAsDays(), 1); + const auto secs = dur.SubDaysAsSeconds(); + ASSERT_EQ(secs, 3661); + const auto *sec_bytes = std::bit_cast(&secs); + const auto nanos = dur.SubSecondsAsNanoseconds(); + ASSERT_EQ(nanos, 1000000); + const auto *nano_bytes = std::bit_cast(&nanos); + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + std::array data = { + Cast(Marker::TinyStruct4), + Cast(Sig::Duration), + 0xD, + 0x1, + Cast(Marker::Int16), + sec_bytes[1], + sec_bytes[0], + Cast(Marker::Int32), + nano_bytes[3], + nano_bytes[2], + nano_bytes[1], + nano_bytes[0] }; + + // clang-format on + buffer.Clear(); + buffer.Write(data.data(), data.size()); + ASSERT_EQ(decoder.ReadValue(&dv, Value::Type::Duration), true); + ASSERT_EQ(dv.ValueDuration().microseconds, dur.microseconds); +} + +void AssertThatLocalTimeIsEqual(utils::LocalTime t1, utils::LocalTime t2) { + ASSERT_EQ(t1.hours, t2.hours); + ASSERT_EQ(t1.minutes, t2.minutes); + ASSERT_EQ(t1.seconds, t2.seconds); + ASSERT_EQ(t1.microseconds, t2.microseconds); + ASSERT_EQ(t1.milliseconds, t2.milliseconds); +} + +TEST_F(BoltDecoder, LocalTimeOneMicro) { + TestDecoderBuffer buffer; + DecoderT decoder(buffer); + Value dv; + const auto value = Value(utils::LocalTime(1)); + const auto &local_time = value.ValueLocalTime(); + const auto nanos = local_time.NanosecondsSinceEpoch(); + ASSERT_EQ(nanos, 1000); + const auto *n_bytes = std::bit_cast(&nanos); + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + std::array data = { + Cast(Marker::TinyStruct1), + Cast(Sig::LocalTime), + Cast(Marker::Int16), + n_bytes[1], + n_bytes[0] }; + // clang-format on + buffer.Clear(); + buffer.Write(data.data(), data.size()); + ASSERT_EQ(decoder.ReadValue(&dv, Value::Type::LocalTime), true); + AssertThatLocalTimeIsEqual(dv.ValueLocalTime(), local_time); +} + +TEST_F(BoltDecoder, LocalTimeOneThousandMicro) { + TestDecoderBuffer buffer; + DecoderT decoder(buffer); + Value dv; + const auto value = Value(utils::LocalTime(1000)); + const auto &local_time = value.ValueLocalTime(); + const auto nanos = local_time.NanosecondsSinceEpoch(); + ASSERT_EQ(nanos, 1000000); + const auto *n_bytes = std::bit_cast(&nanos); + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + std::array data = { + Cast(Marker::TinyStruct1), + Cast(Sig::LocalTime), + Cast(Marker::Int32), + n_bytes[3], n_bytes[2], + n_bytes[1], n_bytes[0] }; + // clang-format on + buffer.Clear(); + buffer.Write(data.data(), data.size()); + ASSERT_EQ(decoder.ReadValue(&dv, Value::Type::LocalTime), true); + AssertThatLocalTimeIsEqual(dv.ValueLocalTime(), local_time); +} + +TEST_F(BoltDecoder, LocalDateTime) { + TestDecoderBuffer buffer; + DecoderT decoder(buffer); + + Value dv; + + const auto local_time = utils::LocalTime(utils::LocalTimeParameters({0, 0, 30, 1, 0})); + const auto date = utils::Date(1); + const auto value = Value(utils::LocalDateTime(date, local_time)); + const auto local_date_time = value.ValueLocalDateTime(); + const auto secs = local_date_time.SecondsSinceEpoch(); + ASSERT_EQ(secs, 30); + const auto *sec_bytes = std::bit_cast(&secs); + const auto nanos = local_date_time.SubSecondsAsNanoseconds(); + ASSERT_EQ(nanos, 1000000); + const auto *nano_bytes = std::bit_cast(&nanos); + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + std::array data = { + Cast(Marker::TinyStruct2), + Cast(Sig::LocalDateTime), + // Seconds + sec_bytes[0], + // Nanoseconds + Cast(Marker::Int32), + nano_bytes[3], nano_bytes[2], + nano_bytes[1], nano_bytes[0] }; + + // clang-format on + buffer.Clear(); + buffer.Write(data.data(), data.size()); + ASSERT_EQ(decoder.ReadValue(&dv, Value::Type::LocalDateTime), true); + AssertThatDatesAreEqual(dv.ValueLocalDateTime().date, local_date_time.date); + AssertThatLocalTimeIsEqual(dv.ValueLocalDateTime().local_time, local_date_time.local_time); +} diff --git a/tests/unit/bolt_encoder.cpp b/tests/unit/bolt_encoder.cpp index fd4e425dd..c7e13b591 100644 --- a/tests/unit/bolt_encoder.cpp +++ b/tests/unit/bolt_encoder.cpp @@ -1,10 +1,13 @@ +#include +#include + #include "bolt_common.hpp" #include "bolt_testdata.hpp" - +#include "communication/bolt/v1/codes.hpp" #include "communication/bolt/v1/encoder/encoder.hpp" #include "glue/communication.hpp" #include "storage/v2/storage.hpp" - +#include "utils/temporal.hpp" using communication::bolt::Value; /** @@ -240,10 +243,269 @@ TEST_F(BoltEncoder, BoltV1ExampleMessages) { fvals.insert(std::make_pair(fk2, ftv2)); bolt_encoder.MessageFailure(fvals); CheckOutput(output, - (const uint8_t *) "\xB1\x7F\xA2\x84\x63\x6F\x64\x65\xD0\x25\x4E\x65\x6F\x2E\x43\x6C\x69\x65\x6E\x74\x45\x72\x72\x6F\x72\x2E\x53\x74\x61\x74\x65\x6D\x65\x6E\x74\x2E\x53\x79\x6E\x74\x61\x78\x45\x72\x72\x6F\x72\x87\x6D\x65\x73\x73\x61\x67\x65\x8F\x49\x6E\x76\x61\x6C\x69\x64\x20\x73\x79\x6E\x74\x61\x78\x2E", + (const uint8_t *) +"\xB1\x7F\xA2\x84\x63\x6F\x64\x65\xD0\x25\x4E\x65\x6F\x2E\x43\x6C\x69\x65\x6E\x74\x45\x72\x72\x6F\x72\x2E\x53\x74\x61\x74\x65\x6D\x65\x6E\x74\x2E\x53\x79\x6E\x74\x61\x78\x45\x72\x72\x6F\x72\x87\x6D\x65\x73\x73\x61\x67\x65\x8F\x49\x6E\x76\x61\x6C\x69\x64\x20\x73\x79\x6E\x74\x61\x78\x2E", 71); // ignored message bolt_encoder.MessageIgnored(); CheckOutput(output, (const uint8_t *)"\xB0\x7E", 2); } + +// Temporal types testing starts here +template +constexpr uint8_t Cast(T marker) { + return static_cast(marker); +} + +TEST_F(BoltEncoder, DateOld) { + output.clear(); + std::vector vals; + const auto value = Value(utils::Date({1970, 1, 1})); + vals.push_back(value); + ASSERT_EQ(bolt_encoder.MessageRecord(vals), true); + const auto &date = value.ValueDate(); + const auto days = date.DaysSinceEpoch(); + ASSERT_EQ(days, 0); + const auto *d_bytes = std::bit_cast(&days); + // 0x91 denotes the size of vals (it's 0x91 because it's anded -- see + // WriteTypeSize() in base_encoder.hpp). + // We reverse the order of d_bytes because after the encoding + // it has BigEndian orderring. + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + const auto expected = std::array { + Cast(Marker::TinyStruct1), + Cast(Sig::Record), + 0x91, + Cast(Marker::TinyStruct1), + Cast(Sig::Date), + d_bytes[0] }; + // clang-format on + CheckOutput(output, expected.data(), expected.size()); +} + +TEST_F(BoltEncoder, DateRecent) { + output.clear(); + std::vector vals; + const auto value = Value(utils::Date({2021, 7, 20})); + vals.push_back(value); + ASSERT_EQ(bolt_encoder.MessageRecord(vals), true); + const auto &date = value.ValueDate(); + const auto days = date.DaysSinceEpoch(); + ASSERT_EQ(days, 18828); + const auto *d_bytes = std::bit_cast(&days); + // 0x91 denotes the size of vals (it's 0x91 because it's anded -- see + // WriteTypeSize() in base_encoder.hpp). + // We reverse the order of d_bytes because after the encoding + // it has BigEndian orderring. + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + const auto expected = std::array { + Cast(Marker::TinyStruct1), + Cast(Sig::Record), + 0x91, + Cast(Marker::TinyStruct1), + Cast(Sig::Date), + Cast(Marker::Int16), + d_bytes[1], + d_bytes[0] }; + // clang-format on + CheckOutput(output, expected.data(), expected.size()); +} + +TEST_F(BoltEncoder, DurationOneSec) { + output.clear(); + std::vector vals; + const auto value = Value(utils::Duration(1)); + vals.push_back(value); + ASSERT_EQ(bolt_encoder.MessageRecord(vals), true); + const auto &dur = value.ValueDuration(); + ASSERT_EQ(dur.Months(), 0); + ASSERT_EQ(dur.SubMonthsAsDays(), 0); + ASSERT_EQ(dur.SubDaysAsSeconds(), 0); + const auto nanos = dur.SubSecondsAsNanoseconds(); + ASSERT_EQ(nanos, 1000); + const auto *d_bytes = std::bit_cast(&nanos); + // 0x91 denotes the size of vals (it's 0x91 because it's anded -- see + // WriteTypeSize in base_encoder.hpp). + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + const auto expected = std::array { + Cast(Marker::TinyStruct1), + Cast(Sig::Record), + 0x91, + Cast(Marker::TinyStruct4), + Cast(Sig::Duration), + 0x0, + 0x0, + 0x0, + Cast(Marker::Int16), + d_bytes[1], + d_bytes[0] }; + // clang-format on + CheckOutput(output, expected.data(), expected.size()); +} + +TEST_F(BoltEncoder, DurationMinusOneSec) { + output.clear(); + std::vector vals; + const auto value = Value(utils::Duration(-1)); + vals.push_back(value); + ASSERT_EQ(bolt_encoder.MessageRecord(vals), true); + const auto &dur = value.ValueDuration(); + ASSERT_EQ(dur.Months(), 0); + ASSERT_EQ(dur.SubMonthsAsDays(), 0); + ASSERT_EQ(dur.SubDaysAsSeconds(), 0); + const auto nanos = dur.SubSecondsAsNanoseconds(); + const auto *d_bytes = std::bit_cast(&nanos); + ASSERT_EQ(nanos, -1000); + // 0x91 denotes the size of vals (it's 0x91 because it's anded -- see + // WriteTypeSize in base_encoder.hpp). + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + const auto expected = std::array { + Cast(Marker::TinyStruct1), + Cast(Sig::Record), + 0x91, + Cast(Marker::TinyStruct4), + Cast(Sig::Duration), + 0x0, + 0x0, + 0x0, + Cast(Marker::Int16), + d_bytes[1], + d_bytes[0] }; + // clang-format on + CheckOutput(output, expected.data(), expected.size()); +} + +TEST_F(BoltEncoder, ArbitraryDuration) { + output.clear(); + std::vector vals; + const auto value = Value(utils::Duration({1, 1, 1, 1, 1, 1, 1, 0})); + vals.push_back(value); + ASSERT_EQ(bolt_encoder.MessageRecord(vals), true); + const auto &dur = value.ValueDuration(); + ASSERT_EQ(dur.Months(), 13); + ASSERT_EQ(dur.SubMonthsAsDays(), 1); + const auto secs = dur.SubDaysAsSeconds(); + ASSERT_EQ(secs, 3661); + const auto *sec_bytes = std::bit_cast(&secs); + const auto nanos = dur.SubSecondsAsNanoseconds(); + ASSERT_EQ(nanos, 1000000); + const auto *nano_bytes = std::bit_cast(&nanos); + // 0x91 denotes the size of vals (it's 0x91 because it's anded -- see + // WriteTypeSize in base_encoder.hpp). + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + const auto expected = std::array { + Cast(Marker::TinyStruct1), + Cast(Sig::Record), + 0x91, + Cast(Marker::TinyStruct4), + Cast(Sig::Duration), + 0xD, + 0x1, + Cast(Marker::Int16), + sec_bytes[1], + sec_bytes[0], + Cast(Marker::Int32), + nano_bytes[3], + nano_bytes[2], + nano_bytes[1], + nano_bytes[0] }; + // clang-format on + CheckOutput(output, expected.data(), expected.size()); +} + +TEST_F(BoltEncoder, LocalTimeOneMicro) { + output.clear(); + std::vector vals; + const auto value = Value(utils::LocalTime(1)); + vals.push_back(value); + ASSERT_EQ(bolt_encoder.MessageRecord(vals), true); + const auto &local_time = value.ValueLocalTime(); + const auto nanos = local_time.NanosecondsSinceEpoch(); + ASSERT_EQ(nanos, 1000); + const auto *n_bytes = std::bit_cast(&nanos); + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + const auto expected = std::array { + Cast(Marker::TinyStruct1), + Cast(Sig::Record), + 0x91, + Cast(Marker::TinyStruct1), + Cast(Sig::LocalTime), + Cast(Marker::Int16), + n_bytes[1], n_bytes[0] }; + // clang-format on + CheckOutput(output, expected.data(), expected.size()); +} + +TEST_F(BoltEncoder, LocalTimeOneThousandMicro) { + output.clear(); + std::vector vals; + const auto value = Value(utils::LocalTime(1000)); + vals.push_back(value); + ASSERT_EQ(bolt_encoder.MessageRecord(vals), true); + const auto &local_time = value.ValueLocalTime(); + const auto nanos = local_time.NanosecondsSinceEpoch(); + ASSERT_EQ(nanos, 1000000); + const auto *n_bytes = std::bit_cast(&nanos); + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + const auto expected = std::array { + Cast(Marker::TinyStruct1), + Cast(Sig::Record), + 0x91, + Cast(Marker::TinyStruct1), + Cast(Sig::LocalTime), + Cast(Marker::Int32), + n_bytes[3], n_bytes[2], + n_bytes[1], n_bytes[0] }; + // clang-format on + CheckOutput(output, expected.data(), expected.size()); +} + +TEST_F(BoltEncoder, LocalDateTime) { + output.clear(); + std::vector vals; + const auto value = Value(utils::LocalDateTime(utils::Date(1), utils::LocalTime({0, 0, 30, 1, 0}))); + const auto &local_date_time = value.ValueLocalDateTime(); + vals.push_back(value); + ASSERT_EQ(bolt_encoder.MessageRecord(vals), true); + const auto secs = local_date_time.SecondsSinceEpoch(); + ASSERT_EQ(secs, 30); + const auto *sec_bytes = std::bit_cast(&secs); + const auto nanos = local_date_time.SubSecondsAsNanoseconds(); + ASSERT_EQ(nanos, 1000000); + const auto *nano_bytes = std::bit_cast(&nanos); + // 0x91 denotes the size of vals (it's 0x91 because it's anded -- see + // WriteTypeSize in base_encoder.hpp). + // The rest of the expected results follow logically from LocalTime and Date test cases + using Marker = communication::bolt::Marker; + using Sig = communication::bolt::Signature; + // clang-format off + const auto expected = std::array { + Cast(Marker::TinyStruct1), + Cast(Sig::Record), + 0x91, + Cast(Marker::TinyStruct2), + Cast(Sig::LocalDateTime), + // SuperSeconds + sec_bytes[0], + // SubSeconds + Cast(Marker::Int32), + nano_bytes[3], nano_bytes[2], + nano_bytes[1], nano_bytes[0] }; + // clang-format on + CheckOutput(output, expected.data(), expected.size()); +} diff --git a/tests/unit/utils_temporal.cpp b/tests/unit/utils_temporal.cpp index 687e0c42b..d1cbf174b 100644 --- a/tests/unit/utils_temporal.cpp +++ b/tests/unit/utils_temporal.cpp @@ -1,5 +1,7 @@ #include +#include #include +#include #include @@ -65,6 +67,7 @@ TEST(TemporalTest, DateMicrosecondsSinceEpochConversion) { check_microseconds(utils::DateParameters{2020, 11, 22}); check_microseconds(utils::DateParameters{1900, 2, 22}); check_microseconds(utils::DateParameters{0, 1, 1}); + check_microseconds(utils::DateParameters{1994, 12, 7}); ASSERT_THROW(check_microseconds(utils::DateParameters{-10, 1, 1}), utils::BasicException); @@ -289,3 +292,54 @@ TEST(TemporalTest, DurationParsing) { CheckDurationParameters(utils::ParseDurationParameters("P2020-11-22T19:20:32"), utils::DurationParameters{2020, 11, 22, 19, 20, 32}); } + +TEST(TemporalTest, PrintDate) { + const auto unix_epoch = utils::Date(utils::DateParameters{1970, 1, 1}); + std::ostringstream stream; + stream << unix_epoch; + ASSERT_TRUE(stream); + ASSERT_EQ(stream.view(), "1970-01-01"); +} + +TEST(TemporalTest, PrintLocalTime) { + const auto lt = utils::LocalTime({13, 2, 40, 100, 50}); + std::ostringstream stream; + stream << lt; + ASSERT_TRUE(stream); + ASSERT_EQ(stream.view(), "13:02:40.100050"); +} + +TEST(TemporalTest, PrintDuration) { + const auto dur = utils::Duration({1, 0, 0, 0, 0, 0, 0, 0}); + std::ostringstream stream; + stream << dur; + ASSERT_TRUE(stream); + ASSERT_EQ(stream.view(), "P0001-00-00T00:00:00.000000"); + stream.str(""); + stream.clear(); + const auto complex_dur = utils::Duration({1, 5, 10, 3, 30, 33, 100, 50}); + stream << complex_dur; + ASSERT_TRUE(stream); + ASSERT_EQ(stream.view(), "P0001-05-10T03:30:33.100050"); + /// stream.str(""); + /// stream.clear(); + /// TODO (kostasrim) + /// We do not support pasring negative Durations yet. We are the only ones we have access + /// to the constructor below. + /* + const auto negative_dur = utils::Duration({-1, 5, -10, -3, -30, -33}); + stream << negative_dur; + ASSERT_TRUE(stream); + ASSERT_EQ(stream.view(), "P0001-05-10T03:30:33.000000"); + */ +} + +TEST(TemporalTest, PrintLocalDateTime) { + const auto unix_epoch = utils::Date(utils::DateParameters{1970, 1, 1}); + const auto lt = utils::LocalTime({13, 2, 40, 100, 50}); + utils::LocalDateTime ldt(unix_epoch, lt); + std::ostringstream stream; + stream << ldt; + ASSERT_TRUE(stream); + ASSERT_EQ(stream.view(), "1970-01-01T13:02:40.100050"); +}