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).
### Added
- We have added new utilities in `caf::chrono` for making it easier to handle
ISO 8601 timestamps. The new function `std::chrono::to_string` converts system
time to an ISO timestamp. For reading an ISO timestamp, CAF now provides the
class `caf::chrono::datetime`. It can parse ISO-formatted strings via `parse`
(or `datetime::from_string`) and convert them to a local representation via
- Add new utilities in `caf::chrono` for making it easier to handle ISO 8601
timestamps. The new function `std::chrono::to_string` converts system time to
an ISO timestamp. For reading an ISO timestamp, CAF now provides the class
`caf::chrono::datetime`. It can parse ISO-formatted strings via `parse` (or
`datetime::from_string`) and convert them to a local representation via
`to_local_time`. Please refer to the class documentation for more details.
## [0.19.1] - 2023-05-01
......
......@@ -109,24 +109,16 @@ public:
/// Returns whether this object is equal to `other`.
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.
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`.
void value(const datetime& x) noexcept {
*this = x;
}
/// Converts this object to a time_point object with given Duration type.
/// @pre `valid()` returns `true`.
template <class Duration = std::chrono::system_clock::duration>
auto to_local_time() const noexcept {
// TODO: consider using year_month_day once we have C++20.
......@@ -143,6 +135,8 @@ private:
time_t to_time_t() const noexcept;
};
// -- free functions -----------------------------------------------------------
/// @relates datetime
inline bool operator==(const datetime& x, const datetime& y) noexcept {
return x.equals(y);
......@@ -153,7 +147,20 @@ inline bool operator!=(const datetime& x, const datetime& y) noexcept {
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
std::string to_string(const datetime& x);
CAF_CORE_EXPORT std::string to_string(const datetime& x);
} // namespace caf::chrono
......@@ -188,7 +188,7 @@ template <class Duration>
void parse(string_parser_state& ps,
std::chrono::time_point<std::chrono::system_clock, Duration>& x) {
chrono::datetime dt;
dt.parse(ps);
parse(ps, dt);
if (ps.code != pec::success)
return;
x = dt.to_local_time<Duration>();
......
......@@ -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,
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.
CAF_ASSERT(buf_size >= 36);
// 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 {
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) {
datetime tmp;
if (auto err = tmp.parse(str))
if (auto err = parse(str, tmp))
return err;
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 result;
result.reserve(32);
......
......@@ -17,12 +17,12 @@ TEST_CASE("a default constructed date and time is invalid") {
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") {
WHEN("parsing the string") {
THEN("the date and time is valid and has an UTC offset is nullopt") {
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_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
......@@ -39,7 +39,7 @@ SCENARIO("a date and time can be parsed from a string") {
WHEN("parsing the string") {
THEN("the date and time is valid and has an UTC offset of 0") {
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_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
......@@ -56,7 +56,7 @@ SCENARIO("a date and time can be parsed from a string") {
WHEN("parsing the string") {
THEN("the date and time is valid and has the specified UTC offset") {
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_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
......@@ -73,7 +73,7 @@ SCENARIO("a date and time can be parsed from a string") {
WHEN("parsing the string") {
THEN("the date and time is valid and has the specified UTC offset") {
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_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
......@@ -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 milliseconds can be parsed from a string") {
SCENARIO("a date and time with fractional seconds can be parsed from strings") {
GIVEN("a valid date and time string with no UTC time zone information") {
WHEN("parsing the string") {
THEN("the date and time is valid and has an UTC offset is nullopt") {
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_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
......@@ -111,7 +110,7 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") {
WHEN("parsing the string") {
THEN("the date and time is valid and has an UTC offset of 0") {
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_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
......@@ -128,7 +127,7 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") {
WHEN("parsing the string") {
THEN("the date and time is valid and has the specified UTC offset") {
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_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
......@@ -145,7 +144,7 @@ SCENARIO("a date and time with milliseconds can be parsed from a string") {
WHEN("parsing the string") {
THEN("the date and time is valid and has the specified UTC offset") {
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_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
......@@ -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") {
datetime x;
auto invalid = make_error(pec::invalid_argument);
CHECK_EQ(x.parse("2021-02-29T01:00:00"), invalid); // Not a leap year.
CHECK_EQ(x.parse("2021-00-10T01:00:00"), invalid); // Month < 1.
CHECK_EQ(x.parse("2021-13-10T01:00:00"), invalid); // Month > 12.
CHECK_EQ(x.parse("2021-01-00T01:00:00"), invalid); // Day < 1.
CHECK_EQ(x.parse("2021-01-32T01:00:00"), invalid); // Day > 31.
CHECK_EQ(x.parse("2021-01-01T24:00:00"), invalid); // Hour > 23.
CHECK_EQ(x.parse("2021-01-01T00:60:00"), invalid); // Minute > 56.
CHECK_EQ(x.parse("2021-01-01T00:00:60"), invalid); // Second > 56.
CHECK_EQ(parse("2021-02-29T01:00:00", x), invalid); // Not a leap year.
CHECK_EQ(parse("2021-00-10T01:00:00", x), invalid); // Month < 1.
CHECK_EQ(parse("2021-13-10T01:00:00", x), invalid); // Month > 12.
CHECK_EQ(parse("2021-01-00T01:00:00", x), invalid); // Day < 1.
CHECK_EQ(parse("2021-01-32T01:00:00", x), invalid); // Day > 31.
CHECK_EQ(parse("2021-01-01T24:00:00", x), invalid); // Hour > 23.
CHECK_EQ(parse("2021-01-01T00:60:00", x), invalid); // Minute > 59.
CHECK_EQ(parse("2021-01-01T00:00:60", x), invalid); // Second > 59.
}
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);
CHECK_EQ(x_str, "1999-09-09T09:09:09.009");
auto y = datetime{};
CHECK_EQ(y.parse(x_str), none);
CHECK_EQ(parse(x_str, y), none);
CHECK_EQ(x, y);
CHECK_EQ(x_str, to_string(y));
}
......@@ -209,7 +208,7 @@ SCENARIO("to_string produces valid input for parse") {
auto x_str = to_string(x);
CHECK_EQ(x_str, "2010-10-10T10:10:10.099Z");
auto y = datetime{};
CHECK_EQ(y.parse(x_str), none);
CHECK_EQ(parse(x_str, y), none);
CHECK_EQ(x, y);
CHECK_EQ(x_str, to_string(y));
}
......@@ -230,7 +229,7 @@ SCENARIO("to_string produces valid input for parse") {
auto x_str = to_string(x);
CHECK_EQ(x_str, "2211-11-11T11:11:11.999+01:30");
auto y = datetime{};
CHECK_EQ(y.parse(x_str), none);
CHECK_EQ(parse(x_str, y), none);
CHECK_EQ(x, y);
CHECK_EQ(x_str, to_string(y));
}
......@@ -251,7 +250,7 @@ SCENARIO("to_string produces valid input for parse") {
auto x_str = to_string(x);
CHECK_EQ(x_str, "1122-12-12T12:12:12.999-01:30");
auto y = datetime{};
CHECK_EQ(y.parse(x_str), none);
CHECK_EQ(parse(x_str, y), none);
CHECK_EQ(x, y);
CHECK_EQ(x_str, to_string(y));
}
......@@ -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") {
// 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.
/* std::chrono time point */ {
{ // std::chrono time point
auto str = chrono::to_string(std::chrono::system_clock::now());
CHECK(datetime::from_string(str));
}
/* caf::timestamp */ {
{ // caf::timestamp
auto str = chrono::to_string(make_timestamp());
CHECK(datetime::from_string(str));
}
......@@ -369,3 +368,17 @@ TEST_CASE("chrono::to_string and chrono::print generate the same string") {
chrono::print(str2, ts);
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