Commit 769b74ad authored by Dominik Charousset's avatar Dominik Charousset

Integrate review feedback

parent e576f54e
...@@ -24,11 +24,11 @@ is based on [Keep a Changelog](https://keepachangelog.com). ...@@ -24,11 +24,11 @@ is based on [Keep a Changelog](https://keepachangelog.com).
### Added ### Added
- We have added new utilities in `caf::chrono` for making it easier to handle - Add new utilities in `caf::chrono` for making it easier to handle ISO 8601
ISO 8601 timestamps. The new function `std::chrono::to_string` converts system timestamps. The new function `std::chrono::to_string` converts system time to
time to an ISO timestamp. For reading an ISO timestamp, CAF now provides the an ISO timestamp. For reading an ISO timestamp, CAF now provides the class
class `caf::chrono::datetime`. It can parse ISO-formatted strings via `parse` `caf::chrono::datetime`. It can parse ISO-formatted strings via `parse` (or
(or `datetime::from_string`) and convert them to a local representation via `datetime::from_string`) and convert them to a local representation via
`to_local_time`. Please refer to the class documentation for more details. `to_local_time`. Please refer to the class documentation for more details.
## [0.19.1] - 2023-05-01 ## [0.19.1] - 2023-05-01
......
...@@ -109,24 +109,16 @@ public: ...@@ -109,24 +109,16 @@ public:
/// Returns whether this object is equal to `other`. /// Returns whether this object is equal to `other`.
bool equals(const datetime& other) const noexcept; bool equals(const datetime& other) const noexcept;
/// Parses a date and time string in ISO 8601 format.
/// @param str The string to parse.
/// @returns The parsed date and time.
error parse(std::string_view str);
/// Convenience function for converting a string to a `datetime` object. /// Convenience function for converting a string to a `datetime` object.
static expected<datetime> from_string(std::string_view str); static expected<datetime> from_string(std::string_view str);
/// Parses a date and time string in ISO 8601 format.
/// @param ps The parser state to use.
void parse(string_parser_state& ps);
/// Overrides the current date and time with the values from `x`. /// Overrides the current date and time with the values from `x`.
void value(const datetime& x) noexcept { void value(const datetime& x) noexcept {
*this = x; *this = x;
} }
/// Converts this object to a time_point object with given Duration type. /// Converts this object to a time_point object with given Duration type.
/// @pre `valid()` returns `true`.
template <class Duration = std::chrono::system_clock::duration> template <class Duration = std::chrono::system_clock::duration>
auto to_local_time() const noexcept { auto to_local_time() const noexcept {
// TODO: consider using year_month_day once we have C++20. // TODO: consider using year_month_day once we have C++20.
...@@ -143,6 +135,8 @@ private: ...@@ -143,6 +135,8 @@ private:
time_t to_time_t() const noexcept; time_t to_time_t() const noexcept;
}; };
// -- free functions -----------------------------------------------------------
/// @relates datetime /// @relates datetime
inline bool operator==(const datetime& x, const datetime& y) noexcept { inline bool operator==(const datetime& x, const datetime& y) noexcept {
return x.equals(y); return x.equals(y);
...@@ -153,7 +147,20 @@ inline bool operator!=(const datetime& x, const datetime& y) noexcept { ...@@ -153,7 +147,20 @@ inline bool operator!=(const datetime& x, const datetime& y) noexcept {
return !(x == y); return !(x == y);
} }
/// Parses a date and time string in ISO 8601 format.
/// @param str The string to parse.
/// @param dest The output parameter for the parsed date and time.
/// @returns An error code if parsing failed.
/// @relates datetime
CAF_CORE_EXPORT error parse(std::string_view str, datetime& dest);
/// Parses a date and time string in ISO 8601 format.
/// @param ps The parser state to use.
/// @param dest The output parameter for the parsed date and time.
/// @relates datetime
CAF_CORE_EXPORT void parse(string_parser_state& ps, datetime& dest);
/// @relates datetime /// @relates datetime
std::string to_string(const datetime& x); CAF_CORE_EXPORT std::string to_string(const datetime& x);
} // namespace caf::chrono } // namespace caf::chrono
...@@ -188,7 +188,7 @@ template <class Duration> ...@@ -188,7 +188,7 @@ template <class Duration>
void parse(string_parser_state& ps, void parse(string_parser_state& ps,
std::chrono::time_point<std::chrono::system_clock, Duration>& x) { std::chrono::time_point<std::chrono::system_clock, Duration>& x) {
chrono::datetime dt; chrono::datetime dt;
dt.parse(ps); parse(ps, dt);
if (ps.code != pec::success) if (ps.code != pec::success)
return; return;
x = dt.to_local_time<Duration>(); x = dt.to_local_time<Duration>();
......
...@@ -85,7 +85,7 @@ char* print_localtime_fraction(char* pos, int val) noexcept { ...@@ -85,7 +85,7 @@ char* print_localtime_fraction(char* pos, int val) noexcept {
char* print_localtime_impl(char* buf, size_t buf_size, time_t ts, char* print_localtime_impl(char* buf, size_t buf_size, time_t ts,
int ns) noexcept { int ns) noexcept {
// Max size of a timestamp is 29 bytes: "2019-12-31T23:59:59.111222333+01:00". // Max size of a timestamp is 35 bytes: "2019-12-31T23:59:59.111222333+01:00".
// We need at least 36 bytes to store the timestamp and the null terminator. // We need at least 36 bytes to store the timestamp and the null terminator.
CAF_ASSERT(buf_size >= 36); CAF_ASSERT(buf_size >= 36);
// Read the time into a tm struct and print year plus time using strftime. // Read the time into a tm struct and print year plus time using strftime.
...@@ -210,23 +210,25 @@ time_t datetime::to_time_t() const noexcept { ...@@ -210,23 +210,25 @@ time_t datetime::to_time_t() const noexcept {
return tm_to_time_t(time_buf) - utc_offset.value_or(0); return tm_to_time_t(time_buf) - utc_offset.value_or(0);
} }
error datetime::parse(std::string_view str) {
string_parser_state ps{str.begin(), str.end()};
detail::parser::read_timestamp(ps, *this);
return ps.code;
}
void datetime::parse(string_parser_state& ps) {
detail::parser::read_timestamp(ps, *this);
}
expected<datetime> datetime::from_string(std::string_view str) { expected<datetime> datetime::from_string(std::string_view str) {
datetime tmp; datetime tmp;
if (auto err = tmp.parse(str)) if (auto err = parse(str, tmp))
return err; return err;
return tmp; return tmp;
} }
// -- free functions -----------------------------------------------------------
error parse(std::string_view str, datetime& dest) {
string_parser_state ps{str.begin(), str.end()};
detail::parser::read_timestamp(ps, dest);
return ps.code;
}
void parse(string_parser_state& ps, datetime& dest) {
detail::parser::read_timestamp(ps, dest);
}
std::string to_string(const datetime& x) { std::string to_string(const datetime& x) {
std::string result; std::string result;
result.reserve(32); result.reserve(32);
......
...@@ -17,12 +17,12 @@ TEST_CASE("a default constructed date and time is invalid") { ...@@ -17,12 +17,12 @@ TEST_CASE("a default constructed date and time is invalid") {
CHECK(!x.valid()); CHECK(!x.valid());
} }
SCENARIO("a date and time can be parsed from a string") { SCENARIO("a date and time can be parsed from strings") {
GIVEN("a valid date and time string with no UTC time zone information") { GIVEN("a valid date and time string with no UTC time zone information") {
WHEN("parsing the string") { WHEN("parsing the string") {
THEN("the date and time is valid and has an UTC offset is nullopt") { THEN("the date and time is valid and has an UTC offset is nullopt") {
datetime x; datetime x;
CHECK_EQ(x.parse("2021-02-03T14:25:36"), none); CHECK_EQ(parse("2021-02-03T14:25:36", x), none);
CHECK(x.valid()); CHECK(x.valid());
CHECK_EQ(x.year, 2021); CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2); CHECK_EQ(x.month, 2);
...@@ -39,7 +39,7 @@ SCENARIO("a date and time can be parsed from a string") { ...@@ -39,7 +39,7 @@ SCENARIO("a date and time can be parsed from a string") {
WHEN("parsing the string") { WHEN("parsing the string") {
THEN("the date and time is valid and has an UTC offset of 0") { THEN("the date and time is valid and has an UTC offset of 0") {
datetime x; datetime x;
CHECK_EQ(x.parse("2021-02-03T14:25:36Z"), none); CHECK_EQ(parse("2021-02-03T14:25:36Z", x), none);
CHECK(x.valid()); CHECK(x.valid());
CHECK_EQ(x.year, 2021); CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2); CHECK_EQ(x.month, 2);
...@@ -56,7 +56,7 @@ SCENARIO("a date and time can be parsed from a string") { ...@@ -56,7 +56,7 @@ SCENARIO("a date and time can be parsed from a string") {
WHEN("parsing the string") { WHEN("parsing the string") {
THEN("the date and time is valid and has the specified UTC offset") { THEN("the date and time is valid and has the specified UTC offset") {
datetime x; datetime x;
CHECK_EQ(x.parse("2021-02-03T14:25:36+02:00"), none); CHECK_EQ(parse("2021-02-03T14:25:36+02:00", x), none);
CHECK(x.valid()); CHECK(x.valid());
CHECK_EQ(x.year, 2021); CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2); CHECK_EQ(x.month, 2);
...@@ -73,7 +73,7 @@ SCENARIO("a date and time can be parsed from a string") { ...@@ -73,7 +73,7 @@ SCENARIO("a date and time can be parsed from a string") {
WHEN("parsing the string") { WHEN("parsing the string") {
THEN("the date and time is valid and has the specified UTC offset") { THEN("the date and time is valid and has the specified UTC offset") {
datetime x; datetime x;
CHECK_EQ(x.parse("2021-02-03T14:25:36-01:30"), none); CHECK_EQ(parse("2021-02-03T14:25:36-01:30", x), none);
CHECK(x.valid()); CHECK(x.valid());
CHECK_EQ(x.year, 2021); CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2); CHECK_EQ(x.month, 2);
...@@ -88,13 +88,12 @@ SCENARIO("a date and time can be parsed from a string") { ...@@ -88,13 +88,12 @@ SCENARIO("a date and time can be parsed from a string") {
} }
} }
// Same as above, but with milliseconds. SCENARIO("a date and time with fractional seconds can be parsed from strings") {
SCENARIO("a date and time with milliseconds can be parsed from a string") {
GIVEN("a valid date and time string with no UTC time zone information") { GIVEN("a valid date and time string with no UTC time zone information") {
WHEN("parsing the string") { WHEN("parsing the string") {
THEN("the date and time is valid and has an UTC offset is nullopt") { THEN("the date and time is valid and has an UTC offset is nullopt") {
datetime x; datetime x;
CHECK_EQ(x.parse("2021-02-03T14:25:36.000"), none); CHECK_EQ(parse("2021-02-03T14:25:36.000", x), none);
CHECK(x.valid()); CHECK(x.valid());
CHECK_EQ(x.year, 2021); CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2); CHECK_EQ(x.month, 2);
...@@ -111,7 +110,7 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") { ...@@ -111,7 +110,7 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") {
WHEN("parsing the string") { WHEN("parsing the string") {
THEN("the date and time is valid and has an UTC offset of 0") { THEN("the date and time is valid and has an UTC offset of 0") {
datetime x; datetime x;
CHECK_EQ(x.parse("2021-02-03T14:25:36.012Z"), none); CHECK_EQ(parse("2021-02-03T14:25:36.012Z", x), none);
CHECK(x.valid()); CHECK(x.valid());
CHECK_EQ(x.year, 2021); CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2); CHECK_EQ(x.month, 2);
...@@ -128,7 +127,7 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") { ...@@ -128,7 +127,7 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") {
WHEN("parsing the string") { WHEN("parsing the string") {
THEN("the date and time is valid and has the specified UTC offset") { THEN("the date and time is valid and has the specified UTC offset") {
datetime x; datetime x;
CHECK_EQ(x.parse("2021-02-03T14:25:36.123+02:00"), none); CHECK_EQ(parse("2021-02-03T14:25:36.123+02:00", x), none);
CHECK(x.valid()); CHECK(x.valid());
CHECK_EQ(x.year, 2021); CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2); CHECK_EQ(x.month, 2);
...@@ -145,7 +144,7 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") { ...@@ -145,7 +144,7 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") {
WHEN("parsing the string") { WHEN("parsing the string") {
THEN("the date and time is valid and has the specified UTC offset") { THEN("the date and time is valid and has the specified UTC offset") {
datetime x; datetime x;
CHECK_EQ(x.parse("2021-02-03T14:25:36.999-01:30"), none); CHECK_EQ(parse("2021-02-03T14:25:36.999-01:30", x), none);
CHECK(x.valid()); CHECK(x.valid());
CHECK_EQ(x.year, 2021); CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2); CHECK_EQ(x.month, 2);
...@@ -163,14 +162,14 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") { ...@@ -163,14 +162,14 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") {
TEST_CASE("the parser refuses invalid date time values") { TEST_CASE("the parser refuses invalid date time values") {
datetime x; datetime x;
auto invalid = make_error(pec::invalid_argument); auto invalid = make_error(pec::invalid_argument);
CHECK_EQ(x.parse("2021-02-29T01:00:00"), invalid); // Not a leap year. CHECK_EQ(parse("2021-02-29T01:00:00", x), invalid); // Not a leap year.
CHECK_EQ(x.parse("2021-00-10T01:00:00"), invalid); // Month < 1. CHECK_EQ(parse("2021-00-10T01:00:00", x), invalid); // Month < 1.
CHECK_EQ(x.parse("2021-13-10T01:00:00"), invalid); // Month > 12. CHECK_EQ(parse("2021-13-10T01:00:00", x), invalid); // Month > 12.
CHECK_EQ(x.parse("2021-01-00T01:00:00"), invalid); // Day < 1. CHECK_EQ(parse("2021-01-00T01:00:00", x), invalid); // Day < 1.
CHECK_EQ(x.parse("2021-01-32T01:00:00"), invalid); // Day > 31. CHECK_EQ(parse("2021-01-32T01:00:00", x), invalid); // Day > 31.
CHECK_EQ(x.parse("2021-01-01T24:00:00"), invalid); // Hour > 23. CHECK_EQ(parse("2021-01-01T24:00:00", x), invalid); // Hour > 23.
CHECK_EQ(x.parse("2021-01-01T00:60:00"), invalid); // Minute > 56. CHECK_EQ(parse("2021-01-01T00:60:00", x), invalid); // Minute > 59.
CHECK_EQ(x.parse("2021-01-01T00:00:60"), invalid); // Second > 56. CHECK_EQ(parse("2021-01-01T00:00:60", x), invalid); // Second > 59.
} }
SCENARIO("to_string produces valid input for parse") { SCENARIO("to_string produces valid input for parse") {
...@@ -188,7 +187,7 @@ SCENARIO("to_string produces valid input for parse") { ...@@ -188,7 +187,7 @@ SCENARIO("to_string produces valid input for parse") {
auto x_str = to_string(x); auto x_str = to_string(x);
CHECK_EQ(x_str, "1999-09-09T09:09:09.009"); CHECK_EQ(x_str, "1999-09-09T09:09:09.009");
auto y = datetime{}; auto y = datetime{};
CHECK_EQ(y.parse(x_str), none); CHECK_EQ(parse(x_str, y), none);
CHECK_EQ(x, y); CHECK_EQ(x, y);
CHECK_EQ(x_str, to_string(y)); CHECK_EQ(x_str, to_string(y));
} }
...@@ -209,7 +208,7 @@ SCENARIO("to_string produces valid input for parse") { ...@@ -209,7 +208,7 @@ SCENARIO("to_string produces valid input for parse") {
auto x_str = to_string(x); auto x_str = to_string(x);
CHECK_EQ(x_str, "2010-10-10T10:10:10.099Z"); CHECK_EQ(x_str, "2010-10-10T10:10:10.099Z");
auto y = datetime{}; auto y = datetime{};
CHECK_EQ(y.parse(x_str), none); CHECK_EQ(parse(x_str, y), none);
CHECK_EQ(x, y); CHECK_EQ(x, y);
CHECK_EQ(x_str, to_string(y)); CHECK_EQ(x_str, to_string(y));
} }
...@@ -230,7 +229,7 @@ SCENARIO("to_string produces valid input for parse") { ...@@ -230,7 +229,7 @@ SCENARIO("to_string produces valid input for parse") {
auto x_str = to_string(x); auto x_str = to_string(x);
CHECK_EQ(x_str, "2211-11-11T11:11:11.999+01:30"); CHECK_EQ(x_str, "2211-11-11T11:11:11.999+01:30");
auto y = datetime{}; auto y = datetime{};
CHECK_EQ(y.parse(x_str), none); CHECK_EQ(parse(x_str, y), none);
CHECK_EQ(x, y); CHECK_EQ(x, y);
CHECK_EQ(x_str, to_string(y)); CHECK_EQ(x_str, to_string(y));
} }
...@@ -251,7 +250,7 @@ SCENARIO("to_string produces valid input for parse") { ...@@ -251,7 +250,7 @@ SCENARIO("to_string produces valid input for parse") {
auto x_str = to_string(x); auto x_str = to_string(x);
CHECK_EQ(x_str, "1122-12-12T12:12:12.999-01:30"); CHECK_EQ(x_str, "1122-12-12T12:12:12.999-01:30");
auto y = datetime{}; auto y = datetime{};
CHECK_EQ(y.parse(x_str), none); CHECK_EQ(parse(x_str, y), none);
CHECK_EQ(x, y); CHECK_EQ(x, y);
CHECK_EQ(x_str, to_string(y)); CHECK_EQ(x_str, to_string(y));
} }
...@@ -352,11 +351,11 @@ TEST_CASE("the fractional component may have 1-9 digits") { ...@@ -352,11 +351,11 @@ TEST_CASE("the fractional component may have 1-9 digits") {
TEST_CASE("chrono::to_string generates valid input for datetime::parse") { TEST_CASE("chrono::to_string generates valid input for datetime::parse") {
// We know neither the local timezone nor what the system clock returns, so we // We know neither the local timezone nor what the system clock returns, so we
// can only check that the string is valid by parsing it again. // can only check that the string is valid by parsing it again.
/* std::chrono time point */ { { // std::chrono time point
auto str = chrono::to_string(std::chrono::system_clock::now()); auto str = chrono::to_string(std::chrono::system_clock::now());
CHECK(datetime::from_string(str)); CHECK(datetime::from_string(str));
} }
/* caf::timestamp */ { { // caf::timestamp
auto str = chrono::to_string(make_timestamp()); auto str = chrono::to_string(make_timestamp());
CHECK(datetime::from_string(str)); CHECK(datetime::from_string(str));
} }
...@@ -369,3 +368,17 @@ TEST_CASE("chrono::to_string and chrono::print generate the same string") { ...@@ -369,3 +368,17 @@ TEST_CASE("chrono::to_string and chrono::print generate the same string") {
chrono::print(str2, ts); chrono::print(str2, ts);
CHECK_EQ(str1, str2); CHECK_EQ(str1, str2);
} }
TEST_CASE("two timestamps with the same time point are equal") {
auto from_string = [](std::string_view str) {
return datetime::from_string(str);
};
CHECK_EQ(from_string("2021-02-03T14:25:36Z"),
from_string("2021-02-03T14:25:36+00:00"));
CHECK_EQ(from_string("2021-02-03T14:25:36Z"),
from_string("2021-02-03T15:25:36+01:00"));
CHECK_EQ(from_string("2021-02-03T14:25:36Z"),
from_string("2021-02-03T13:25:36-01:00"));
CHECK_EQ(from_string("2021-02-03T15:25:36+01:00"),
from_string("2021-02-03T13:25:36-01:00"));
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment