diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index fcf32ed343..6211d40dfc 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,20 @@ constexpr int cTwoDigitYearOffsetBoundary{69}; constexpr int cMaxTwoDigitYear{99}; constexpr int cTwoDigitYearLowOffset{1900}; constexpr int cTwoDigitYearHighOffset{2000}; +constexpr int cMinParsedHour24HourClock{0}; +constexpr int cMaxParsedHour24HourClock{23}; +constexpr int cMinParsedHour12HourClock{1}; +constexpr int cMaxParsedHour12HourClock{12}; +constexpr int cMinParsedMinute{0}; +constexpr int cMaxParsedMinute{59}; +constexpr int cMinParsedSecond{0}; +constexpr int cMaxParsedSecond{60}; +constexpr int cMinParsedSubsecondNanoseconds{0}; + +constexpr size_t cNumNanosecondPrecisionSubsecondDigits{9ULL}; +constexpr size_t cNumMicrosecondPrecisionSubsecondDigits{6ULL}; +constexpr size_t cNumMillisecondPrecisionSubsecondDigits{3ULL}; +constexpr size_t cNumSecondPrecisionSubsecondDigits{0ULL}; constexpr int cDefaultYear{1970}; constexpr int cDefaultMonth{1}; @@ -71,6 +86,11 @@ constexpr std::array cAbbreviatedMonthNames std::string_view{"Nov"}, std::string_view{"Dec"}}; +constexpr std::array cPartsOfDay = {std::string_view{"AM"}, std::string_view{"PM"}}; + +constexpr std::array cPowersOfTen + = {1, 10, 100, 1000, 10'000, 100'000, 1'000'000, 10'000'000, 100'000'000, 1'000'000'000}; + /** * Converts a padded decimal integer string to an integer. * @param str Substring containing the padded decimal integer string. @@ -94,6 +114,32 @@ constexpr std::array cAbbreviatedMonthNames find_first_matching_prefix(std::string_view str, std::span candidates) -> ystdlib::error_handling::Result; +/** + * Converts the prefix of a string to a positive number up to a maximum number of digits. + * @param str Substring with a prefix potentially corresponding to a number. + * @param max_num_digits The maximum number of digits to convert to a number. + * @return A result containing a pair holding the integer value and number of digits consumed, or an + * error code indicating the failure: + * - ErrorCodeEnum::InvalidTimestampPattern if `max_num_digits` is zero. + * - ErrorCodeEnum::IncompatibleTimestampPattern if the prefix of the string is negative or doesn't + * correspond to a number. + */ +[[nodiscard]] auto convert_positive_bounded_variable_length_string_prefix_to_number( + std::string_view str, + size_t max_num_digits +) -> ystdlib::error_handling::Result>; + +/** + * Converts the prefix of a string to a number. + * @param str Substring with a prefix potentially corresponding to a number. + * @return A result containing a pair holding the integer value and number of digits consumed, or an + * error code indicating the failure: + * - ErrorCodeEnum::IncompatibleTimestampPattern if the prefix of the string doesn't correspond to a + * number. + */ +[[nodiscard]] auto convert_variable_length_string_prefix_to_number(std::string_view str) + -> ystdlib::error_handling::Result>; + auto convert_padded_string_to_number(std::string_view str, char padding_character) -> ystdlib::error_handling::Result { if (str.empty()) { @@ -121,6 +167,81 @@ auto find_first_matching_prefix(std::string_view str, std::span ystdlib::error_handling::Result> { + constexpr int64_t cTen{10}; + if (0ULL == max_num_digits) { + return ErrorCode{ErrorCodeEnum::InvalidTimestampPattern}; + } + if (str.empty() || '-' == str.at(0ULL) + || false == clp::string_utils::is_decimal_digit(str.at(0ULL))) + { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + int64_t converted_value{}; + size_t num_decimal_digits{}; + while (true) { + char const cur_digit{str.at(num_decimal_digits)}; + converted_value += static_cast(cur_digit - '0'); + ++num_decimal_digits; + + if (num_decimal_digits >= str.length() || num_decimal_digits >= max_num_digits + || false == clp::string_utils::is_decimal_digit(str.at(num_decimal_digits))) + { + break; + } + converted_value *= cTen; + } + return std::make_pair(converted_value, num_decimal_digits); +} + +auto convert_variable_length_string_prefix_to_number(std::string_view str) + -> ystdlib::error_handling::Result> { + constexpr int64_t cTen{10}; + if (str.empty()) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + bool const is_negative{'-' == str.at(0ULL)}; + size_t num_decimal_digits{is_negative ? 1ULL : 0ULL}; + if (num_decimal_digits >= str.length() + || false == clp::string_utils::is_decimal_digit(str.at(num_decimal_digits))) + { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + bool const first_digit_zero{'0' == str.at(num_decimal_digits)}; + if (first_digit_zero && is_negative) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + int64_t converted_value{}; + while (true) { + char const cur_digit{str.at(num_decimal_digits)}; + converted_value += static_cast(cur_digit - '0'); + ++num_decimal_digits; + + if (num_decimal_digits >= str.length() + || false == clp::string_utils::is_decimal_digit(str.at(num_decimal_digits))) + { + break; + } + converted_value *= cTen; + } + + if (first_digit_zero && num_decimal_digits > 1) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + if (is_negative) { + converted_value *= -1; + } + return std::make_pair(converted_value, num_decimal_digits); +} } // namespace // NOLINTBEGIN(readability-function-cognitive-complexity) @@ -135,10 +256,18 @@ auto parse_timestamp( int parsed_year{cDefaultYear}; int parsed_month{cDefaultMonth}; int parsed_day{cDefaultDay}; + int parsed_hour{}; + int parsed_minute{}; + int parsed_second{}; + int parsed_subsecond_nanoseconds{}; std::optional optional_day_of_week_idx; + std::optional optional_part_of_day_idx; + + int64_t parsed_epoch_nanoseconds{}; bool date_type_representation{false}; - bool const number_type_representation{false}; + bool number_type_representation{false}; + bool uses_12_hour_clock{false}; bool escaped{false}; for (; pattern_idx < pattern.size() && timestamp_idx < timestamp.size(); ++pattern_idx) { @@ -283,21 +412,274 @@ auto parse_timestamp( date_type_representation = true; break; } - case 'p': - case 'H': - case 'k': - case 'I': - case 'l': - case 'M': - case 'S': - case '3': - case '6': - case '9': - case 'T': - case 'E': - case 'L': - case 'C': - case 'N': + case 'p': { // Part of day (AM/PM). + auto const part_of_day_idx{YSTDLIB_ERROR_HANDLING_TRYX( + find_first_matching_prefix(timestamp.substr(timestamp_idx), cPartsOfDay) + )}; + timestamp_idx += cPartsOfDay.at(part_of_day_idx).length(); + optional_part_of_day_idx = static_cast(part_of_day_idx); + date_type_representation = true; + break; + } + case 'H': { // 24-hour clock, zero-padded hour. + constexpr size_t cFieldLength{2}; + if (timestamp_idx + cFieldLength > timestamp.size()) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + parsed_hour = YSTDLIB_ERROR_HANDLING_TRYX(convert_padded_string_to_number( + timestamp.substr(timestamp_idx, cFieldLength), + '0' + )); + + if (parsed_hour < cMinParsedHour24HourClock + || parsed_hour > cMaxParsedHour24HourClock) + { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + timestamp_idx += cFieldLength; + date_type_representation = true; + break; + } + case 'k': { // 24-hour clock, space-padded hour. + constexpr size_t cFieldLength{2}; + if (timestamp_idx + cFieldLength > timestamp.size()) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + parsed_hour = YSTDLIB_ERROR_HANDLING_TRYX(convert_padded_string_to_number( + timestamp.substr(timestamp_idx, cFieldLength), + ' ' + )); + + if (parsed_hour < cMinParsedHour24HourClock + || parsed_hour > cMaxParsedHour24HourClock) + { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + timestamp_idx += cFieldLength; + date_type_representation = true; + break; + } + case 'I': { // 12-hour clock, zero-padded hour. + constexpr size_t cFieldLength{2}; + if (timestamp_idx + cFieldLength > timestamp.size()) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + parsed_hour = YSTDLIB_ERROR_HANDLING_TRYX(convert_padded_string_to_number( + timestamp.substr(timestamp_idx, cFieldLength), + '0' + )); + + if (parsed_hour < cMinParsedHour12HourClock + || parsed_hour > cMaxParsedHour12HourClock) + { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + timestamp_idx += cFieldLength; + uses_12_hour_clock = true; + date_type_representation = true; + break; + } + case 'l': { // 12-hour clock, space-padded hour. + constexpr size_t cFieldLength{2}; + if (timestamp_idx + cFieldLength > timestamp.size()) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + parsed_hour = YSTDLIB_ERROR_HANDLING_TRYX(convert_padded_string_to_number( + timestamp.substr(timestamp_idx, cFieldLength), + ' ' + )); + + if (parsed_hour < cMinParsedHour12HourClock + || parsed_hour > cMaxParsedHour12HourClock) + { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + timestamp_idx += cFieldLength; + uses_12_hour_clock = true; + date_type_representation = true; + break; + } + case 'M': { // Zero-padded minute. + constexpr size_t cFieldLength{2}; + if (timestamp_idx + cFieldLength > timestamp.size()) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + parsed_minute = YSTDLIB_ERROR_HANDLING_TRYX(convert_padded_string_to_number( + timestamp.substr(timestamp_idx, cFieldLength), + '0' + )); + + if (parsed_minute < cMinParsedMinute || parsed_minute > cMaxParsedMinute) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + timestamp_idx += cFieldLength; + date_type_representation = true; + break; + } + case 'S': { // Zero-padded second. + constexpr size_t cFieldLength{2}; + if (timestamp_idx + cFieldLength > timestamp.size()) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + parsed_second = YSTDLIB_ERROR_HANDLING_TRYX(convert_padded_string_to_number( + timestamp.substr(timestamp_idx, cFieldLength), + '0' + )); + + if (parsed_second < cMinParsedSecond || parsed_second > cMaxParsedSecond) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + + timestamp_idx += cFieldLength; + date_type_representation = true; + break; + } + case '3': { // Zero-padded 3-digit milliseconds. + constexpr size_t cFieldLength{3}; + if (timestamp_idx + cFieldLength > timestamp.size()) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + if (false + == clp::string_utils::convert_string_to_int( + timestamp.substr(timestamp_idx, cFieldLength), + parsed_subsecond_nanoseconds + )) + { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + if (parsed_subsecond_nanoseconds < cMinParsedSubsecondNanoseconds) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + constexpr auto cFactor{cPowersOfTen + [cNumNanosecondPrecisionSubsecondDigits + - cNumMillisecondPrecisionSubsecondDigits]}; + parsed_subsecond_nanoseconds *= cFactor; + timestamp_idx += cFieldLength; + break; + } + case '6': { // Zero-padded 6-digit microseconds. + constexpr size_t cFieldLength{6}; + if (timestamp_idx + cFieldLength > timestamp.size()) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + if (false + == clp::string_utils::convert_string_to_int( + timestamp.substr(timestamp_idx, cFieldLength), + parsed_subsecond_nanoseconds + )) + { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + if (parsed_subsecond_nanoseconds < cMinParsedSubsecondNanoseconds) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + constexpr auto cFactor{cPowersOfTen + [cNumNanosecondPrecisionSubsecondDigits + - cNumMicrosecondPrecisionSubsecondDigits]}; + parsed_subsecond_nanoseconds *= cFactor; + timestamp_idx += cFieldLength; + break; + } + case '9': { // Zero-padded 9-digit nanoseconds. + constexpr size_t cFieldLength{9}; + if (timestamp_idx + cFieldLength > timestamp.size()) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + if (false + == clp::string_utils::convert_string_to_int( + timestamp.substr(timestamp_idx, cFieldLength), + parsed_subsecond_nanoseconds + )) + { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + if (parsed_subsecond_nanoseconds < cMinParsedSubsecondNanoseconds) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + timestamp_idx += cFieldLength; + break; + } + case 'T': { // Zero-padded fractional seconds without trailing zeroes, max 9-digits. + constexpr size_t cMaxFieldLength{9}; + auto const remaining_unparsed_content{timestamp.substr(timestamp_idx)}; + auto const [number, num_digits] = YSTDLIB_ERROR_HANDLING_TRYX( + convert_positive_bounded_variable_length_string_prefix_to_number( + remaining_unparsed_content, + cMaxFieldLength + ) + ); + if ('0' == remaining_unparsed_content.at(num_digits - 1ULL)) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + timestamp_idx += num_digits; + parsed_subsecond_nanoseconds + = static_cast(number * cPowersOfTen.at(cMaxFieldLength - num_digits)); + break; + } + case 'E': { // Epoch seconds. + auto const [number, num_digits] = YSTDLIB_ERROR_HANDLING_TRYX( + convert_variable_length_string_prefix_to_number( + timestamp.substr(timestamp_idx) + ) + ); + timestamp_idx += num_digits; + constexpr auto cFactor{cPowersOfTen + [cNumNanosecondPrecisionSubsecondDigits + - cNumSecondPrecisionSubsecondDigits]}; + parsed_epoch_nanoseconds = number * cFactor; + number_type_representation = true; + break; + } + case 'L': { // Epoch milliseconds. + auto const [number, num_digits] = YSTDLIB_ERROR_HANDLING_TRYX( + convert_variable_length_string_prefix_to_number( + timestamp.substr(timestamp_idx) + ) + ); + timestamp_idx += num_digits; + constexpr auto cFactor{cPowersOfTen + [cNumNanosecondPrecisionSubsecondDigits + - cNumMillisecondPrecisionSubsecondDigits]}; + parsed_epoch_nanoseconds = number * cFactor; + number_type_representation = true; + break; + } + case 'C': { // Epoch microseconds. + auto const [number, num_digits] = YSTDLIB_ERROR_HANDLING_TRYX( + convert_variable_length_string_prefix_to_number( + timestamp.substr(timestamp_idx) + ) + ); + timestamp_idx += num_digits; + constexpr auto cFactor{cPowersOfTen + [cNumNanosecondPrecisionSubsecondDigits + - cNumMicrosecondPrecisionSubsecondDigits]}; + parsed_epoch_nanoseconds = number * cFactor; + number_type_representation = true; + break; + } + case 'N': { // Epoch nanoseconds. + auto const [number, num_digits] = YSTDLIB_ERROR_HANDLING_TRYX( + convert_variable_length_string_prefix_to_number( + timestamp.substr(timestamp_idx) + ) + ); + timestamp_idx += num_digits; + parsed_epoch_nanoseconds = number; + number_type_representation = true; + break; + } case 'z': case 'Z': case '?': @@ -330,8 +712,25 @@ auto parse_timestamp( return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; } + if ((uses_12_hour_clock && false == optional_part_of_day_idx.has_value()) + || (false == uses_12_hour_clock && optional_part_of_day_idx.has_value())) + { + return ErrorCode{ErrorCodeEnum::InvalidTimestampPattern}; + } + if (number_type_representation) { - return ErrorCode{ErrorCodeEnum::FormatSpecifierNotImplemented}; + epochtime_t epoch_nanoseconds{parsed_epoch_nanoseconds}; + if (epoch_nanoseconds < 0) { + epoch_nanoseconds -= static_cast(parsed_subsecond_nanoseconds); + } else { + epoch_nanoseconds += static_cast(parsed_subsecond_nanoseconds); + } + return {epoch_nanoseconds, pattern}; + } + + if (uses_12_hour_clock) { + parsed_hour = (parsed_hour % cMaxParsedHour12HourClock) + + optional_part_of_day_idx.value() * cMaxParsedHour12HourClock; } auto const year_month_day{date::year(parsed_year) / parsed_month / parsed_day}; @@ -339,9 +738,10 @@ auto parse_timestamp( return ErrorCode{ErrorCodeEnum::InvalidDate}; } - auto const time_point = date::sys_days(year_month_day) + std::chrono::hours(0) - + std::chrono::minutes(0) + std::chrono::seconds(0) - + std::chrono::nanoseconds(0); + auto const time_point = date::sys_days{year_month_day} + std::chrono::hours{parsed_hour} + + std::chrono::minutes{parsed_minute} + + std::chrono::seconds{parsed_second} + + std::chrono::nanoseconds{parsed_subsecond_nanoseconds}; if (optional_day_of_week_idx.has_value()) { auto const actual_day_of_week_idx{(date::year_month_weekday(date::sys_days(year_month_day)) diff --git a/components/core/src/clp_s/timestamp_parser/test/test_TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/test/test_TimestampParser.cpp index 29c719f205..a3c2955aa8 100644 --- a/components/core/src/clp_s/timestamp_parser/test/test_TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/test/test_TimestampParser.cpp @@ -1,5 +1,7 @@ +#include #include #include +#include #include #include @@ -8,10 +10,26 @@ #include #include +#include "../../Defs.hpp" #include "../TimestampParser.hpp" namespace clp_s::timestamp_parser::test { namespace { +struct ExpectedParsingResult { + ExpectedParsingResult( + std::string_view timestamp, + std::string_view pattern, + epochtime_t epoch_timestamp + ) + : timestamp(timestamp), + pattern(pattern), + epoch_timestamp(epoch_timestamp) {} + + std::string timestamp; + std::string pattern; + epochtime_t epoch_timestamp; +}; + /** * Asserts that a format specifier is able to parse a variety of valid content. * @param specifier The format specifier. @@ -32,17 +50,37 @@ assert_specifier_accepts_valid_content(char specifier, std::vector auto generate_padded_numbers_in_range(size_t begin, size_t end, size_t field_length, char padding) -> std::vector; +/** + * Generates triangles of numbers up to a maximum number of digits each composed of a single digit + * 1-9. + * + * E.g., generate_number_triangles(3) -> + * "1", "11", "111", ..., "9", "99", "999". + * + * @param max_num_digits + * @return The elements of all of the triangles of numbers up to the maximum number of digits. + */ +[[nodiscard]] auto generate_number_triangles(size_t max_num_digits) -> std::vector; + +/** + * @param num_digits + * @return A vector containing all single-unique-digit (0-9) padded numbers with the unique digit + * repeated to a length of `num_digits`. + */ +[[nodiscard]] auto generate_padded_number_subset(size_t num_digits) -> std::vector; + void assert_specifier_accepts_valid_content(char specifier, std::vector const& content) { // We use a trailing literal to ensure that the specifier exactly consumes all of the content. - auto const pattern{fmt::format("\\{}a", specifier)}; + auto const pattern{fmt::format(R"(\{}a)", specifier)}; std::string generated_pattern; CAPTURE(pattern); for (auto const& test_case : content) { auto const timestamp{fmt::format("{}a", test_case)}; CAPTURE(timestamp); auto const result{parse_timestamp(timestamp, pattern, generated_pattern)}; - REQUIRE(false == result.has_error()); + REQUIRE_FALSE(result.has_error()); + REQUIRE(result.value().second == pattern); } } @@ -56,6 +94,24 @@ auto generate_padded_numbers_in_range(size_t begin, size_t end, size_t field_len } return generated_numbers; } + +auto generate_number_triangles(size_t max_num_digits) -> std::vector { + std::vector generated_numbers; + for (char digit{'1'}; digit <= '9'; ++digit) { + for (size_t i{1ULL}; i <= max_num_digits; ++i) { + generated_numbers.emplace_back(i, digit); + } + } + return generated_numbers; +} + +auto generate_padded_number_subset(size_t num_digits) -> std::vector { + std::vector generated_numbers; + for (char digit{'0'}; digit <= '9'; ++digit) { + generated_numbers.emplace_back(num_digits, digit); + } + return generated_numbers; +} } // namespace TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { @@ -104,7 +160,7 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { auto const two_digit_days{generate_padded_numbers_in_range(1, 31, 2, '0')}; assert_specifier_accepts_valid_content('d', two_digit_days); - auto const space_padded_days(generate_padded_numbers_in_range(1, 31, 2, ' ')); + auto const space_padded_days{generate_padded_numbers_in_range(1, 31, 2, ' ')}; assert_specifier_accepts_valid_content('e', space_padded_days); // The parser asserts that the day of the week in the timestamp is actually correct, so we @@ -119,10 +175,139 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { "03 Sat" }; for (auto const& day_in_week_timestamp : abbreviated_day_in_week_timestamps) { + constexpr std::string_view cPattern{R"(\d \aa)"}; std::string generated_pattern; auto const timestamp{fmt::format("{}a", day_in_week_timestamp)}; - auto const result{parse_timestamp(timestamp, "\\d \\aa", generated_pattern)}; - REQUIRE(false == result.has_error()); + auto const result{parse_timestamp(timestamp, cPattern, generated_pattern)}; + REQUIRE_FALSE(result.has_error()); + REQUIRE(result.value().second == cPattern); + } + + auto const two_digit_hours{generate_padded_numbers_in_range(0, 23, 2, '0')}; + assert_specifier_accepts_valid_content('H', two_digit_hours); + + auto const space_padded_hours{generate_padded_numbers_in_range(0, 23, 2, ' ')}; + assert_specifier_accepts_valid_content('k', space_padded_hours); + + auto const twelve_hour_clock_two_digit_hours{ + generate_padded_numbers_in_range(1, 12, 2, '0') + }; + auto const twelve_hour_clock_zero_padded_hours{ + generate_padded_numbers_in_range(1, 12, 2, ' ') + }; + constexpr std::array cPartsOfDay{std::string_view{"AM"}, std::string_view{"PM"}}; + for (auto const part_of_day : cPartsOfDay) { + std::string generated_pattern; + auto assert_twelve_hour_clock_accepts_valid_content + = [&](char hour_type, std::vector const& hours) -> void { + auto const pattern{fmt::format(R"(\{} \pa)", hour_type)}; + for (auto const& hour : hours) { + auto const timestamp{fmt::format("{} {}a", hour, part_of_day)}; + auto const result{parse_timestamp(timestamp, pattern, generated_pattern)}; + REQUIRE_FALSE(result.has_error()); + REQUIRE(result.value().second == pattern); + } + }; + assert_twelve_hour_clock_accepts_valid_content('I', twelve_hour_clock_two_digit_hours); + assert_twelve_hour_clock_accepts_valid_content( + 'l', + twelve_hour_clock_zero_padded_hours + ); + } + + auto const two_digit_minutes{generate_padded_numbers_in_range(0, 59, 2, '0')}; + assert_specifier_accepts_valid_content('M', two_digit_minutes); + + auto const two_digit_seconds{generate_padded_numbers_in_range(0, 60, 2, '0')}; + assert_specifier_accepts_valid_content('S', two_digit_seconds); + + auto const milliseconds{generate_padded_number_subset(3)}; + assert_specifier_accepts_valid_content('3', milliseconds); + + auto const microseconds{generate_padded_number_subset(6)}; + assert_specifier_accepts_valid_content('6', microseconds); + + auto const nanoseconds{generate_padded_number_subset(9)}; + assert_specifier_accepts_valid_content('9', nanoseconds); + + auto const variable_length_nanoseconds{generate_number_triangles(9)}; + assert_specifier_accepts_valid_content('T', variable_length_nanoseconds); + + auto const epoch_timestamps{generate_number_triangles(15)}; + std::vector negative_epoch_timestamps; + negative_epoch_timestamps.reserve(epoch_timestamps.size()); + for (auto const& timestamp : epoch_timestamps) { + negative_epoch_timestamps.emplace_back(fmt::format("-{}", timestamp)); + } + assert_specifier_accepts_valid_content('E', epoch_timestamps); + assert_specifier_accepts_valid_content('E', negative_epoch_timestamps); + assert_specifier_accepts_valid_content('L', epoch_timestamps); + assert_specifier_accepts_valid_content('L', negative_epoch_timestamps); + assert_specifier_accepts_valid_content('C', epoch_timestamps); + assert_specifier_accepts_valid_content('C', negative_epoch_timestamps); + assert_specifier_accepts_valid_content('N', epoch_timestamps); + assert_specifier_accepts_valid_content('N', negative_epoch_timestamps); + } + + SECTION("Timestamps are parsed accurately") { + std::vector const expected_parsing_results{ + {"2015-02-01T01:02:03.004", R"(\Y-\m-\dT\H:\M:\S.\3)", 1'422'752'523'004'000'000}, + {"2015-02-01T01:02:03.004005", + R"(\Y-\m-\dT\H:\M:\S.\6)", + 1'422'752'523'004'005'000}, + {"2015-02-01T01:02:03.004005006", + R"(\Y-\m-\dT\H:\M:\S.\9)", + 1'422'752'523'004'005'006}, + {"2015-02-01T01:02:03,004", R"(\Y-\m-\dT\H:\M:\S,\3)", 1'422'752'523'004'000'000}, + {"[2015-02-01T01:02:03", R"([\Y-\m-\dT\H:\M:\S)", 1'422'752'523'000'000'000}, + {"[20150201-01:02:03]", R"([\Y\m\d-\H:\M:\S])", 1'422'752'523'000'000'000}, + {"2015-02-01 01:02:03,004", R"(\Y-\m-\d \H:\M:\S,\3)", 1'422'752'523'004'000'000}, + {"2015-02-01 01:02:03.004", R"(\Y-\m-\d \H:\M:\S.\3)", 1'422'752'523'004'000'000}, + {"[2015-02-01 01:02:03,004]", + R"([\Y-\m-\d \H:\M:\S,\3])", + 1'422'752'523'004'000'000}, + {"2015-02-01 01:02:03", R"(\Y-\m-\d \H:\M:\S)", 1'422'752'523'000'000'000}, + {"2015/02/01 01:02:03", R"(\Y/\m/\d \H:\M:\S)", 1'422'752'523'000'000'000}, + {"15/02/01 01:02:03", R"(\y/\m/\d \H:\M:\S)", 1'422'752'523'000'000'000}, + {"150201 1:02:03", R"(\y\m\d \k:\M:\S)", 1'422'752'523'000'000'000}, + {"01 Feb 2015 01:02:03,004", R"(\d \b \Y \H:\M:\S,\3)", 1'422'752'523'004'000'000}, + {"Feb 01, 2015 1:02:03 AM", R"(\b \d, \Y \l:\M:\S \p)", 1'422'752'523'000'000'000}, + {"February 01, 2015 01:02", R"(\B \d, \Y \H:\M)", 1'422'752'520'000'000'000}, + {"[01/Feb/2015:01:02:03", R"([\d/\b/\Y:\H:\M:\S)", 1'422'752'523'000'000'000}, + {"Sun Feb 1 01:02:03 2015", R"(\a \b \e \H:\M:\S \Y)", 1'422'752'523'000'000'000}, + {"<<<2015-02-01 01:02:03:004", + R"(<<<\Y-\m-\d \H:\M:\S:\3)", + 1'422'752'523'004'000'000}, + {"Jan 21 11:56:42", R"(\b \d \H:\M:\S)", 1'771'002'000'000'000}, + {"01-21 11:56:42.392", R"(\m-\d \H:\M:\S.\3)", 1'771'002'392'000'000}, + {"2015/01/31 15:50:45.123", R"(\Y/\m/\d \H:\M:\S.\3)", 1'422'719'445'123'000'000}, + {"2015/01/31 15:50:45,123", R"(\Y/\m/\d \H:\M:\S,\3)", 1'422'719'445'123'000'000}, + {"2015/01/31T15:50:45", R"(\Y/\m/\dT\H:\M:\S)", 1'422'719'445'000'000'000}, + {"2015/01/31T15:50:45.123", R"(\Y/\m/\dT\H:\M:\S.\3)", 1'422'719'445'123'000'000}, + {"2015/01/31T15:50:45,123", R"(\Y/\m/\dT\H:\M:\S,\3)", 1'422'719'445'123'000'000}, + {"2015-01-31T15:50:45", R"(\Y-\m-\dT\H:\M:\S)", 1'422'719'445'000'000'000}, + {"1762445893", R"(\E)", 1'762'445'893'000'000'000}, + {"1762445893001", R"(\L)", 1'762'445'893'001'000'000}, + {"1762445893001002", R"(\C)", 1'762'445'893'001'002'000}, + {"1762445893001002003", R"(\N)", 1'762'445'893'001'002'003}, + {"1762445893.001", R"(\E.\3)", 1'762'445'893'001'000'000}, + {"1762445893.001002", R"(\E.\6)", 1'762'445'893'001'002'000}, + {"1762445893.001002003", R"(\E.\9)", 1'762'445'893'001'002'003}, + {"1762445893.001002000", R"(\E.\9)", 1'762'445'893'001'002'000}, + {"1762445893.00100201", R"(\E.\T)", 1'762'445'893'001'002'010}, + {"1762445893.1", R"(\E.\T)", 1'762'445'893'100'000'000} + }; + + std::string generated_pattern; + for (auto const& expected_result : expected_parsing_results) { + auto const result{parse_timestamp( + expected_result.timestamp, + expected_result.pattern, + generated_pattern + )}; + REQUIRE_FALSE(result.has_error()); + REQUIRE(expected_result.epoch_timestamp == result.value().first); + REQUIRE(expected_result.pattern == result.value().second); } } }