Commit d3cfa1fe authored by Dominik Charousset's avatar Dominik Charousset Committed by Dominik Charousset

Implement better utilities for timestamp parsing

(cherry picked from commit e576f54e)
parent 571c4d19
......@@ -93,6 +93,7 @@ caf_add_component(
src/binary_deserializer.cpp
src/binary_serializer.cpp
src/blocking_actor.cpp
src/chrono.cpp
src/config_option.cpp
src/config_option_adder.cpp
src/config_option_set.cpp
......@@ -234,8 +235,7 @@ caf_add_component(
binary_deserializer
binary_serializer
blocking_actor
byte
composition
chrono
config_option
config_option_set
config_value
......
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/detail/core_export.hpp"
#include "caf/fwd.hpp"
#include "caf/parser_state.hpp"
#include "caf/string_view.hpp"
#include <chrono>
#include <ctime>
#include <optional>
#include <string>
namespace caf::detail {
/// The size of the buffer used for formatting.
constexpr size_t format_buffer_size = 40;
/// Splits the time point into seconds and nanoseconds since epoch.
template <class TimePoint>
std::pair<time_t, int> split_time_point(TimePoint ts) {
namespace sc = std::chrono;
using clock_type = sc::system_clock;
using clock_timestamp = typename clock_type::time_point;
using clock_duration = typename clock_type::duration;
auto tse = ts.time_since_epoch();
clock_timestamp as_sys_time{sc::duration_cast<clock_duration>(tse)};
auto secs = clock_type::to_time_t(as_sys_time);
auto ns = sc::duration_cast<sc::nanoseconds>(tse).count() % 1'000'000'000;
return {secs, static_cast<int>(ns)};
}
/// Prints the date and time to a buffer.
/// @param buf The buffer to write to.
/// @param buf_size The size of `buf` in bytes.
/// @param secs The number of seconds since epoch.
/// @param nsecs The number of nanoseconds since the last full second.
/// @returns A pointer to the null terminator of `buf`.
/// @pre `buf_size >= 36`.
CAF_CORE_EXPORT
char* print_localtime(char* buf, size_t buf_size, time_t secs,
int nsecs) noexcept;
} // namespace caf::detail
namespace caf::chrono {
// TODO: becomes obsolete when switching to C++20.
template <class Duration>
using sys_time = std::chrono::time_point<std::chrono::system_clock, Duration>;
/// Formats `ts` in ISO 8601 format.
/// @param ts The time point to format.
/// @returns The formatted string.
template <class Duration>
std::string to_string(sys_time<Duration> ts) {
char buf[detail::format_buffer_size];
auto [secs, nsecs] = detail::split_time_point(ts);
detail::print_localtime(buf, sizeof(buf), secs, nsecs);
return buf;
}
/// Prints `ts` to `out` in ISO 8601 format.
/// @param out The buffer to write to.
/// @param ts The time point to print.
template <class Buffer, class Duration>
void print(Buffer& out, sys_time<Duration> ts) {
char buf[detail::format_buffer_size];
auto [secs, nsecs] = detail::split_time_point(ts);
auto end = detail::print_localtime(buf, sizeof(buf), secs, nsecs);
out.insert(out.end(), buf, end);
}
/// Represents a point in time, expressed as a date and time of day. Also
/// provides formatting and parsing functionality for ISO 8601.
class CAF_CORE_EXPORT datetime {
public:
/// The year.
int year = 0;
/// The month of the year, starting with 1 for January.
int month = 0;
/// The day of the month, starting with 1.
int day = 0;
/// The hour of the day, starting with 0.
int hour = 0;
/// The minute of the hour, starting with 0.
int minute = 0;
/// The second of the minute, starting with 0.
int second = 0;
/// The nanosecond of the second, starting with 0.
int nanosecond = 0;
/// The offset from UTC in seconds.
std::optional<int> utc_offset;
/// Returns whether this object contains a valid date and time. A default
/// constructed object is invalid.
bool valid() const noexcept;
/// 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(string_view str);
/// Convenience function for converting a string to a `datetime` object.
static expected<datetime> from_string(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.
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.
namespace sc = std::chrono;
auto converted = sc::system_clock::from_time_t(to_time_t());
auto result = sc::time_point_cast<Duration>(converted);
auto ns = sc::nanoseconds{nanosecond};
result += sc::duration_cast<Duration>(ns);
return sc::time_point_cast<Duration>(result);
}
private:
/// Converts this object to a time_t value.
time_t to_time_t() const noexcept;
};
/// @relates datetime
inline bool operator==(const datetime& x, const datetime& y) noexcept {
return x.equals(y);
}
/// @relates datetime
inline bool operator!=(const datetime& x, const datetime& y) noexcept {
return !(x == y);
}
/// @relates datetime
std::string to_string(const datetime& x);
} // namespace caf::chrono
......@@ -9,6 +9,7 @@
#include <optional>
#include <type_traits>
#include <utility>
#include <variant>
namespace caf::detail {
......
......@@ -11,6 +11,7 @@
#include <utility>
#include <vector>
#include "caf/chrono.hpp"
#include "caf/detail/core_export.hpp"
#include "caf/detail/squashed_int.hpp"
#include "caf/detail/type_traits.hpp"
......@@ -186,45 +187,11 @@ void parse(string_parser_state& ps, std::chrono::duration<Rep, Period>& x) {
template <class Duration>
void parse(string_parser_state& ps,
std::chrono::time_point<std::chrono::system_clock, Duration>& x) {
namespace sc = std::chrono;
using value_type = sc::time_point<sc::system_clock, Duration>;
// Parse ISO 8601 as generated by detail::print.
using int_wrapper = zero_padded_integer<int32_t>;
auto year = int_wrapper{};
auto month = int_wrapper{};
auto day = int_wrapper{};
auto hour = int_wrapper{};
auto minute = int_wrapper{};
auto second = int_wrapper{};
auto milliseconds = int_wrapper{};
if (!parse_sequence(ps,
// YYYY-MM-DD
year, literal{{"-"}}, month, literal{{"-"}}, day,
// "T"
literal{{"T"}},
// hh:mm:ss
hour, literal{{":"}}, minute, literal{{":"}}, second,
// "." xxx
literal{{"."}}, milliseconds))
return;
chrono::datetime dt;
dt.parse(ps);
if (ps.code != pec::success)
return;
// Fill tm record.
tm record;
record.tm_sec = second.val;
record.tm_min = minute.val;
record.tm_hour = hour.val;
record.tm_mday = day.val;
record.tm_mon = month.val - 1; // months since January
record.tm_year = year.val - 1900; // years since 1900
record.tm_wday = -1; // n/a
record.tm_yday = -1; // n/a
record.tm_isdst = -1; // n/a
// Convert to chrono time and add the milliseconds.
auto tstamp = sc::system_clock::from_time_t(mktime(&record));
auto since_epoch = sc::duration_cast<Duration>(tstamp.time_since_epoch());
since_epoch += sc::milliseconds{milliseconds.val};
x = value_type{since_epoch};
x = dt.to_local_time<Duration>();
}
// -- convenience functions ----------------------------------------------------
......
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#pragma once
#include "caf/chrono.hpp"
#include "caf/config.hpp"
#include "caf/detail/consumer.hpp"
#include "caf/detail/parser/add_ascii.hpp"
#include "caf/detail/parser/chars.hpp"
#include "caf/detail/parser/read_signed_integer.hpp"
#include "caf/detail/scope_guard.hpp"
CAF_PUSH_UNUSED_LABEL_WARNING
#include "caf/detail/parser/fsm.hpp"
namespace caf::detail::parser {
// Parses an integer in the form "00". We can't use `read_int` here because
// it will interpret the leading zero as an octal prefix.
template <class State, class Consumer>
void read_two_digit_int(State& ps, Consumer&& consumer) {
auto result = 0;
auto g = make_scope_guard([&] {
if (ps.code <= pec::trailing_character) {
consumer.value(result);
}
});
// clang-format off
start();
state(init) {
transition(at_digit2, decimal_chars, add_ascii<10>(result, ch))
}
state(at_digit2) {
transition(done, decimal_chars, add_ascii<10>(result, ch))
}
term_state(done) {
// nop
}
fin();
// clang-format on
}
/// Reads a UTC offset in ISO 8601 format, i.e., a string of the form
/// `[+-]HH:MM`, `[+-]HHMM`, or `[+-]HH`.
template <class State, class Consumer>
void read_utc_offset(State& ps, Consumer&& consumer) {
bool negative = false;
auto hh = 0;
auto mm = 0;
auto g = make_scope_guard([&] {
if (ps.code <= pec::trailing_character) {
auto result = hh * 3600 + mm * 60;
consumer.value(negative ? -result : result);
}
});
// clang-format off
start();
state(init) {
fsm_transition(read_two_digit_int(ps, make_consumer(hh)), after_hh, '+')
fsm_transition(read_two_digit_int(ps, make_consumer(hh)), after_hh, '-',
negative = true)
}
term_state(after_hh) {
fsm_transition(read_two_digit_int(ps, make_consumer(mm)), done, ':')
fsm_epsilon(read_two_digit_int(ps, make_consumer(mm)), done, decimal_chars)
}
term_state(done) {
// nop
}
fin();
// clang-format on
}
/// Reads a date and time in ISO 8601 format.
template <class State, class Consumer>
void read_timestamp(State& ps, Consumer&& consumer) {
auto fractional_seconds = 0;
auto fractional_seconds_decimals = 0;
auto add_fractional = [&](char ch) {
++fractional_seconds_decimals;
return add_ascii<10>(fractional_seconds, ch);
};
chrono::datetime result;
auto g = make_scope_guard([&] {
if (ps.code <= pec::trailing_character) {
if (fractional_seconds_decimals > 0) {
auto divisor = 1;
for (auto i = 0; i < fractional_seconds_decimals; ++i)
divisor *= 10;
result.nanosecond = fractional_seconds * (1'000'000'000 / divisor);
}
if (result.valid())
consumer.value(result);
else
ps.code = pec::invalid_argument;
}
});
// clang-format off
start();
state(init) {
fsm_epsilon(read_signed_integer(ps, make_consumer(result.year)),
has_year, decimal_chars);
}
state(has_year) {
fsm_transition(read_two_digit_int(ps, make_consumer(result.month)),
has_month, '-')
}
state(has_month) {
fsm_transition(read_two_digit_int(ps, make_consumer(result.day)),
has_day, '-')
}
state(has_day) {
transition(at_hour, 'T')
}
state(at_hour) {
fsm_epsilon(read_two_digit_int(ps, make_consumer(result.hour)),
has_hour, decimal_chars);
}
state(has_hour) {
transition(at_minute, ':')
}
state(at_minute) {
fsm_epsilon(read_two_digit_int(ps, make_consumer(result.minute)),
has_minute, decimal_chars);
}
state(has_minute) {
transition(at_second, ':')
}
state(at_second) {
fsm_epsilon(read_two_digit_int(ps, make_consumer(result.second)),
has_second, decimal_chars);
}
term_state(has_second) {
transition(has_fractional, '.')
transition(done, 'Z', result.utc_offset = 0)
fsm_epsilon(read_utc_offset(ps, make_consumer(result.utc_offset.emplace(0))),
done, "+-")
}
term_state(has_fractional) {
transition_if(fractional_seconds_decimals < 9, has_fractional, decimal_chars,
add_fractional(ch))
epsilon_if(fractional_seconds_decimals > 0, has_utc_offset)
}
term_state(has_utc_offset) {
transition(done, 'Z', result.utc_offset = 0)
fsm_epsilon(read_utc_offset(ps, make_consumer(result.utc_offset.emplace(0))),
done, "+-")
}
term_state(done) {
// nop
}
fin();
// clang-format on
}
} // namespace caf::detail::parser
#include "caf/detail/parser/fsm_undef.hpp"
CAF_POP_WARNINGS
......@@ -4,6 +4,8 @@
#pragma once
#include "caf/chrono.hpp"
#include "caf/detail/core_export.hpp"
#include "caf/none.hpp"
#include "caf/string_view.hpp"
......@@ -15,6 +17,7 @@
namespace caf::detail {
// TODO: this function is obsolete. Deprecate/remove with future versions.
CAF_CORE_EXPORT
size_t print_timestamp(char* buf, size_t buf_size, time_t ts, size_t ms);
......@@ -220,20 +223,7 @@ void print(Buffer& buf, std::chrono::duration<Rep, Period> x) {
template <class Buffer, class Duration>
void print(Buffer& buf,
std::chrono::time_point<std::chrono::system_clock, Duration> x) {
namespace sc = std::chrono;
using clock_type = sc::system_clock;
using clock_timestamp = typename clock_type::time_point;
using clock_duration = typename clock_type::duration;
auto tse = x.time_since_epoch();
clock_timestamp as_sys_time{sc::duration_cast<clock_duration>(tse)};
auto secs = clock_type::to_time_t(as_sys_time);
auto msecs = sc::duration_cast<sc::milliseconds>(tse).count() % 1000;
// We print in ISO 8601 format, e.g., "2020-09-01T15:58:42.372". 32-Bytes are
// more than enough space.
char stack_buffer[32];
auto pos = print_timestamp(stack_buffer, 32, secs,
static_cast<size_t>(msecs));
buf.insert(buf.end(), stack_buffer, stack_buffer + pos);
chrono::print(buf, x);
}
} // namespace caf::detail
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#include "caf/config.hpp"
#ifdef CAF_WINDOWS
# include <time.h>
namespace {
int utc_offset(const tm& time_buf) noexcept {
# if !defined(WINAPI_FAMILY) || (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP)
// Call _tzset once to initialize the timezone information.
static bool unused = [] {
_tzset();
return true;
}();
CAF_IGNORE_UNUSED(unused);
# endif
// Get the timezone offset in seconds.
long offset = 0;
_get_timezone(&offset);
// Add the DST bias if DST is active.
if (time_buf.tm_isdst) {
long dstbias = 0;
_get_dstbias(&dstbias);
offset += dstbias;
}
return static_cast<int>(-offset);
}
void to_localtime(time_t ts, tm& time_buf) noexcept {
localtime_s(&time_buf, &ts);
}
time_t tm_to_time_t(tm& time_buf) noexcept {
return _mkgmtime(&time_buf);
}
} // namespace
#else
# ifndef _GNU_SOURCE
# define _GNU_SOURCE
# endif
# include <ctime>
# include <ratio>
namespace {
int utc_offset(const tm& time_buf) noexcept {
return time_buf.tm_gmtoff;
}
void to_localtime(time_t ts, tm& time_buf) noexcept {
localtime_r(&ts, &time_buf);
}
time_t tm_to_time_t(tm& time_buf) noexcept {
return timegm(&time_buf);
}
} // namespace
#endif
#include "caf/chrono.hpp"
#include "caf/detail/parser/read_timestamp.hpp"
#include "caf/detail/print.hpp"
#include "caf/error.hpp"
#include "caf/expected.hpp"
#include "caf/parser_state.hpp"
namespace {
// Prints a fractional part of a timestamp, i.e., a value between 0 and 999.
// Adds leading zeros.
char* print_localtime_fraction(char* pos, int val) noexcept {
CAF_ASSERT(val < 1000);
*pos++ = static_cast<char>((val / 100) + '0');
*pos++ = static_cast<char>(((val % 100) / 10) + '0');
*pos++ = static_cast<char>((val % 10) + '0');
return pos;
}
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".
// 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.
auto time_buf = tm{};
to_localtime(ts, time_buf);
auto index = strftime(buf, buf_size, "%FT%T", &time_buf);
auto pos = buf + index;
// Add the fractional component.
if (ns > 0) {
*pos++ = '.';
auto ms_val = ns / 1'000'000;
auto us_val = (ns / 1'000) % 1'000;
auto ns_val = ns % 1'000;
pos = print_localtime_fraction(pos, ms_val);
if (ns_val > 0) {
pos = print_localtime_fraction(pos, us_val);
pos = print_localtime_fraction(pos, ns_val);
} else if (us_val > 0) {
pos = print_localtime_fraction(pos, us_val);
}
}
// Add the timezone part.
auto offset = utc_offset(time_buf);
if (offset == 0) {
*pos++ = 'Z';
*pos = '\0';
return pos;
}
if (offset > 0) {
*pos++ = '+';
} else {
*pos++ = '-';
offset = -offset;
}
auto hours = offset / 3600;
auto minutes = (offset % 3600) / 60;
*pos++ = static_cast<char>(hours / 10 + '0');
*pos++ = static_cast<char>(hours % 10 + '0');
*pos++ = ':';
*pos++ = static_cast<char>(minutes / 10 + '0');
*pos++ = static_cast<char>(minutes % 10 + '0');
*pos = '\0';
return pos;
}
} // namespace
namespace caf::detail {
char* print_localtime(char* buf, size_t buf_size, time_t secs,
int nsecs) noexcept {
return print_localtime_impl(buf, buf_size, secs, nsecs);
}
} // namespace caf::detail
namespace caf::chrono {
namespace {
constexpr bool in_range(int x, int lower, int upper) noexcept {
return lower <= x && x <= upper;
}
constexpr bool is_leap_year(int year) noexcept {
// A leap year can be divided by 4 (2020, 2024 ...)
// -> but it's *not* if it can be divided by 100 (2100, 2200, ...)
// -> but it's *always* if it can be divided by 400 (2400, 2800, ...)
return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
}
constexpr bool is_valid_day(int day, int month, int year) noexcept {
if (day < 1)
return false;
switch (month) {
default:
return false;
case 1: // January
case 3: // March
case 5: // May
case 7: // July
case 8: // August
case 10: // October
case 12: // December
return day <= 31;
case 4: // April
case 6: // June
case 9: // September
case 11: // November
return day <= 30;
case 2: // February
return is_leap_year(year) ? day <= 29 : day <= 28;
}
}
} // namespace
bool datetime::valid() const noexcept {
return in_range(month, 1, 12) // Month 1 is January.
&& is_valid_day(day, month, year) // Valid days depend on month & year.
&& in_range(hour, 0, 23) // Hour: 00 to 23.
&& in_range(minute, 0, 59) // Minute: 00 to 59.
&& in_range(second, 0, 59) // Second: 00 to 59.
&& in_range(nanosecond, 0, 999'999'999);
}
bool datetime::equals(const datetime& other) const noexcept {
// Two timestamps can be equal even if they have different UTC offsets. Hence,
// we need to normalize them before comparing.
return to_time_t() == other.to_time_t() && nanosecond == other.nanosecond;
}
time_t datetime::to_time_t() const noexcept {
auto time_buf = tm{};
time_buf.tm_year = year - 1900;
time_buf.tm_mon = month - 1;
time_buf.tm_mday = day;
time_buf.tm_hour = hour;
time_buf.tm_min = minute;
time_buf.tm_sec = second;
time_buf.tm_isdst = -1;
return tm_to_time_t(time_buf) - utc_offset.value_or(0);
}
error datetime::parse(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(string_view str) {
datetime tmp;
if (auto err = tmp.parse(str))
return err;
return tmp;
}
std::string to_string(const datetime& x) {
std::string result;
result.reserve(32);
auto add_two_digits = [&](int x) {
if (x < 10)
result.push_back('0');
detail::print(result, x);
};
auto add_three_digits = [&](int x) {
if (x < 10)
result += "00";
else if (x < 100)
result.push_back('0');
detail::print(result, x);
};
detail::print(result, x.year);
result.push_back('-');
add_two_digits(x.month);
result.push_back('-');
add_two_digits(x.day);
result.push_back('T');
add_two_digits(x.hour);
result.push_back(':');
add_two_digits(x.minute);
result.push_back(':');
add_two_digits(x.second);
if (x.nanosecond > 0) {
result.push_back('.');
auto ms = x.nanosecond / 1'000'000;
add_three_digits(ms);
if ((x.nanosecond % 1'000'000) > 0) {
auto us = (x.nanosecond % 1'000'000) / 1'000;
add_three_digits(us);
if ((x.nanosecond % 1'000) > 0) {
auto ns = x.nanosecond % 1'000;
add_three_digits(ns);
}
}
}
if (!x.utc_offset)
return result;
auto utc_offset = *x.utc_offset;
if (utc_offset == 0) {
result.push_back('Z');
return result;
}
if (utc_offset > 0) {
result.push_back('+');
} else {
result.push_back('-');
utc_offset = -utc_offset;
}
add_two_digits(utc_offset / 3600); // hours
result.push_back(':');
add_two_digits((utc_offset % 3600) / 60); // minutes
return result;
}
} // namespace caf::chrono
// This file is part of CAF, the C++ Actor Framework. See the file LICENSE in
// the main distribution directory for license terms and copyright or visit
// https://github.com/actor-framework/actor-framework/blob/master/LICENSE.
#define CAF_SUITE chrono
#include "caf/chrono.hpp"
#include "caf/test/bdd_dsl.hpp"
using namespace caf;
using datetime = chrono::datetime;
TEST_CASE("a default constructed date and time is invalid") {
datetime x;
CHECK(!x.valid());
}
SCENARIO("a date and time can be parsed from a string") {
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(x.valid());
CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
CHECK_EQ(x.day, 3);
CHECK_EQ(x.hour, 14);
CHECK_EQ(x.minute, 25);
CHECK_EQ(x.second, 36);
CHECK_EQ(x.nanosecond, 0);
CHECK_EQ(x.utc_offset, std::nullopt);
}
}
}
GIVEN("a valid date and time string with Z suffix") {
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(x.valid());
CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
CHECK_EQ(x.day, 3);
CHECK_EQ(x.hour, 14);
CHECK_EQ(x.minute, 25);
CHECK_EQ(x.second, 36);
CHECK_EQ(x.nanosecond, 0);
CHECK_EQ(x.utc_offset, 0);
}
}
}
GIVEN("a valid date and time string with a positive UTC offset") {
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(x.valid());
CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
CHECK_EQ(x.day, 3);
CHECK_EQ(x.hour, 14);
CHECK_EQ(x.minute, 25);
CHECK_EQ(x.second, 36);
CHECK_EQ(x.nanosecond, 0);
CHECK_EQ(x.utc_offset, 7200);
}
}
}
GIVEN("a valid date and time string with a negative UTC offset") {
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(x.valid());
CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
CHECK_EQ(x.day, 3);
CHECK_EQ(x.hour, 14);
CHECK_EQ(x.minute, 25);
CHECK_EQ(x.second, 36);
CHECK_EQ(x.nanosecond, 0);
CHECK_EQ(x.utc_offset, -5400);
}
}
}
}
// Same as above, but with milliseconds.
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") {
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(x.valid());
CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
CHECK_EQ(x.day, 3);
CHECK_EQ(x.hour, 14);
CHECK_EQ(x.minute, 25);
CHECK_EQ(x.second, 36);
CHECK_EQ(x.nanosecond, 0);
CHECK_EQ(x.utc_offset, std::nullopt);
}
}
}
GIVEN("a valid date and time string with Z suffix") {
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(x.valid());
CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
CHECK_EQ(x.day, 3);
CHECK_EQ(x.hour, 14);
CHECK_EQ(x.minute, 25);
CHECK_EQ(x.second, 36);
CHECK_EQ(x.nanosecond, 12'000'000);
CHECK_EQ(x.utc_offset, 0);
}
}
}
GIVEN("a valid date and time string with a positive UTC offset") {
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(x.valid());
CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
CHECK_EQ(x.day, 3);
CHECK_EQ(x.hour, 14);
CHECK_EQ(x.minute, 25);
CHECK_EQ(x.second, 36);
CHECK_EQ(x.nanosecond, 123'000'000);
CHECK_EQ(x.utc_offset, 7200);
}
}
}
GIVEN("a valid date and time string with a negative UTC offset") {
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(x.valid());
CHECK_EQ(x.year, 2021);
CHECK_EQ(x.month, 2);
CHECK_EQ(x.day, 3);
CHECK_EQ(x.hour, 14);
CHECK_EQ(x.minute, 25);
CHECK_EQ(x.second, 36);
CHECK_EQ(x.nanosecond, 999'000'000);
CHECK_EQ(x.utc_offset, -5400);
}
}
}
}
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.
}
SCENARIO("to_string produces valid input for parse") {
GIVEN("a datetime without UTC offset") {
WHEN("calling to_string() on it") {
THEN("the result can be parsed again") {
auto x = datetime{};
x.year = 1999;
x.month = 9;
x.day = 9;
x.hour = 9;
x.minute = 9;
x.second = 9;
x.nanosecond = 9'000'000;
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(x, y);
CHECK_EQ(x_str, to_string(y));
}
}
}
GIVEN("a datetime with a UTC offset of zero") {
WHEN("calling to_string() on it") {
THEN("the result can be parsed again") {
auto x = datetime{};
x.year = 2010;
x.month = 10;
x.day = 10;
x.hour = 10;
x.minute = 10;
x.second = 10;
x.nanosecond = 99'000'000;
x.utc_offset = 0;
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(x, y);
CHECK_EQ(x_str, to_string(y));
}
}
}
GIVEN("a datetime with positive UTC offset") {
WHEN("calling to_string() on it") {
THEN("the result can be parsed again") {
auto x = datetime{};
x.year = 2211;
x.month = 11;
x.day = 11;
x.hour = 11;
x.minute = 11;
x.second = 11;
x.nanosecond = 999'000'000;
x.utc_offset = 5400;
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(x, y);
CHECK_EQ(x_str, to_string(y));
}
}
}
GIVEN("a datetime with negative UTC offset") {
WHEN("calling to_string() on it") {
THEN("the result can be parsed again") {
auto x = datetime{};
x.year = 1122;
x.month = 12;
x.day = 12;
x.hour = 12;
x.minute = 12;
x.second = 12;
x.nanosecond = 999'000'000;
x.utc_offset = -5400;
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(x, y);
CHECK_EQ(x_str, to_string(y));
}
}
}
}
TEST_CASE("the fractional component may have 1-9 digits") {
auto from_string = [](std::string_view str) {
return datetime::from_string(str);
};
// 1 digit.
CHECK_EQ(from_string("2021-02-03T14:25:36.1"),
from_string("2021-02-03T14:25:36.10"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1"),
from_string("2021-02-03T14:25:36.100"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1"),
from_string("2021-02-03T14:25:36.1000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1"),
from_string("2021-02-03T14:25:36.10000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1"),
from_string("2021-02-03T14:25:36.100000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1"),
from_string("2021-02-03T14:25:36.1000000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1"),
from_string("2021-02-03T14:25:36.10000000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1"),
from_string("2021-02-03T14:25:36.100000000"));
// 2 digits.
CHECK_EQ(from_string("2021-02-03T14:25:36.12"),
from_string("2021-02-03T14:25:36.120"));
CHECK_EQ(from_string("2021-02-03T14:25:36.12"),
from_string("2021-02-03T14:25:36.1200"));
CHECK_EQ(from_string("2021-02-03T14:25:36.12"),
from_string("2021-02-03T14:25:36.12000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.12"),
from_string("2021-02-03T14:25:36.120000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.12"),
from_string("2021-02-03T14:25:36.1200000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.12"),
from_string("2021-02-03T14:25:36.12000000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.12"),
from_string("2021-02-03T14:25:36.120000000"));
// 3 digits.
CHECK_EQ(from_string("2021-02-03T14:25:36.123"),
from_string("2021-02-03T14:25:36.1230"));
CHECK_EQ(from_string("2021-02-03T14:25:36.123"),
from_string("2021-02-03T14:25:36.12300"));
CHECK_EQ(from_string("2021-02-03T14:25:36.123"),
from_string("2021-02-03T14:25:36.123000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.123"),
from_string("2021-02-03T14:25:36.1230000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.123"),
from_string("2021-02-03T14:25:36.12300000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.123"),
from_string("2021-02-03T14:25:36.123000000"));
// 4 digits.
CHECK_EQ(from_string("2021-02-03T14:25:36.1234"),
from_string("2021-02-03T14:25:36.12340"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1234"),
from_string("2021-02-03T14:25:36.123400"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1234"),
from_string("2021-02-03T14:25:36.1234000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1234"),
from_string("2021-02-03T14:25:36.12340000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1234"),
from_string("2021-02-03T14:25:36.123400000"));
// 5 digits.
CHECK_EQ(from_string("2021-02-03T14:25:36.12345"),
from_string("2021-02-03T14:25:36.123450"));
CHECK_EQ(from_string("2021-02-03T14:25:36.12345"),
from_string("2021-02-03T14:25:36.1234500"));
CHECK_EQ(from_string("2021-02-03T14:25:36.12345"),
from_string("2021-02-03T14:25:36.12345000"));
CHECK_EQ(from_string("2021-02-03T14:25:36.12345"),
from_string("2021-02-03T14:25:36.123450000"));
// 6 digits.
CHECK_EQ(from_string("2021-02-03T14:25:36.123456"),
from_string("2021-02-03T14:25:36.1234560"));
CHECK_EQ(from_string("2021-02-03T14:25:36.123456"),
from_string("2021-02-03T14:25:36.12345600"));
CHECK_EQ(from_string("2021-02-03T14:25:36.123456"),
from_string("2021-02-03T14:25:36.123456000"));
// 7 digits.
CHECK_EQ(from_string("2021-02-03T14:25:36.1234567"),
from_string("2021-02-03T14:25:36.12345670"));
CHECK_EQ(from_string("2021-02-03T14:25:36.1234567"),
from_string("2021-02-03T14:25:36.123456700"));
// 8 digits.
CHECK_EQ(from_string("2021-02-03T14:25:36.12345678"),
from_string("2021-02-03T14:25:36.123456780"));
// 9 digits
if (auto x = from_string("2021-02-03T14:25:36.123456789"); CHECK(x)) {
CHECK_EQ(x->nanosecond, 123456789);
}
}
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 */ {
auto str = chrono::to_string(std::chrono::system_clock::now());
CHECK(datetime::from_string(str));
}
/* caf::timestamp */ {
auto str = chrono::to_string(make_timestamp());
CHECK(datetime::from_string(str));
}
}
TEST_CASE("chrono::to_string and chrono::print generate the same string") {
auto ts = std::chrono::system_clock::now();
auto str1 = chrono::to_string(ts);
auto str2 = std::string{};
chrono::print(str2, ts);
CHECK_EQ(str1, str2);
}
......@@ -157,9 +157,9 @@ CAF_TEST(rendering) {
timestamp t0;
time_t t0_t = 0;
char t0_buf[50];
CAF_REQUIRE(strftime(t0_buf, sizeof(t0_buf),
"%Y-%m-%dT%H:%M:%S.000", localtime(&t0_t)));
CAF_CHECK_EQUAL(render(logger::render_date, t0), t0_buf);
strftime(t0_buf, sizeof(t0_buf), "%Y-%m-%dT%H:%M:%S", localtime(&t0_t));
// Note: we use starts_with because we cannot predict the exact time zone.
CHECK(starts_with(render(logger::render_date, t0), t0_buf));
// Rendering of events.
logger::event e{
CAF_LOG_LEVEL_WARNING,
......
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