From 3e948c55fbdd82515ef0215663f8d7f817cb7d8f Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Mon, 3 Nov 2025 22:44:06 +0000 Subject: [PATCH 01/16] Implement remaining format specifiers except for timezone and CAT sequences. --- .../timestamp_parser/TimestampParser.cpp | 417 +++++++++++++++++- 1 file changed, 397 insertions(+), 20 deletions(-) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index fcf32ed343..7fddc5a30b 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,72 @@ auto find_first_matching_prefix(std::string_view str, std::span ystdlib::error_handling::Result> { + constexpr int 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}; + } + + int 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}; + } + + 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 (is_negative) { + converted_value *= -1; + } + return std::make_pair(converted_value, num_decimal_digits); +} } // namespace // NOLINTBEGIN(readability-function-cognitive-complexity) @@ -135,10 +247,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 +403,260 @@ 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': { + 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': { + 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': { + 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': { + 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': { + 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': { + 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': { + 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': { + constexpr size_t cFieldLength{3}; + 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 '6': { + constexpr size_t cFieldLength{6}; + 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 '9': { + constexpr size_t cFieldLength{9}; + 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': { + 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 + = number * cPowersOfTen.at(cMaxFieldLength - num_digits); + break; + } + case 'E': { + 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 + * cPowersOfTen.at( + cNumNanosecondPrecisionSubsecondDigits + - cNumSecondPrecisionSubsecondDigits + ); + number_type_representation = true; + break; + } + case 'L': { + 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 + * cPowersOfTen.at( + cNumNanosecondPrecisionSubsecondDigits + - cNumMillisecondPrecisionSubsecondDigits + ); + number_type_representation = true; + break; + } + case 'C': { + 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 + * cPowersOfTen.at( + cNumNanosecondPrecisionSubsecondDigits + - cNumMicrosecondPrecisionSubsecondDigits + ); + number_type_representation = true; + break; + } + case 'N': { + 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 '?': @@ -325,13 +684,30 @@ auto parse_timestamp( return ErrorCode{ErrorCodeEnum::InvalidTimestampPattern}; } + 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}; + } + // Do not allow trailing unmatched content. if (pattern_idx != pattern.size() || timestamp_idx != timestamp.size()) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; } 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 +715,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)) From 09ff6fbb9ba91bfbc7a047275731396f87361ea0 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Wed, 5 Nov 2025 20:55:00 +0000 Subject: [PATCH 02/16] Cover some missed edge cases in TimestampParser --- .../core/src/clp_s/timestamp_parser/TimestampParser.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index 7fddc5a30b..db51543474 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -214,6 +214,11 @@ auto convert_variable_length_string_prefix_to_number(std::string_view str) return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; } + bool 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)}; @@ -228,6 +233,10 @@ auto convert_variable_length_string_prefix_to_number(std::string_view str) converted_value *= cTen; } + if (first_digit_zero && num_decimal_digits > 1) { + return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; + } + if (is_negative) { converted_value *= -1; } From 7a8d5047e983cec62df0bdc326f6d6ab97b9e0b4 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Wed, 5 Nov 2025 20:56:06 +0000 Subject: [PATCH 03/16] Add tests that ensure that newly implemented format specifiers accept expected content. --- .../test/test_TimestampParser.cpp | 107 +++++++++++++++++- 1 file changed, 105 insertions(+), 2 deletions(-) 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..a057560882 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 @@ -32,6 +32,24 @@ 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. + */ +auto generate_number_triangles(size_t max_num_digits) -> std::vector; + +/** + * @param num_digits + * @return All of the padded numbers with `num_digits` digits having a single unique digit. + */ +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. @@ -43,6 +61,7 @@ assert_specifier_accepts_valid_content(char specifier, std::vector CAPTURE(timestamp); auto const result{parse_timestamp(timestamp, pattern, generated_pattern)}; REQUIRE(false == result.has_error()); + REQUIRE(result.value().second == pattern); } } @@ -56,6 +75,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 +141,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,11 +156,77 @@ 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 pattern{"\\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)}; + auto const result{parse_timestamp(timestamp, pattern, generated_pattern)}; REQUIRE(false == result.has_error()); + REQUIRE(result.value().second == pattern); + } + + 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); + + constexpr std::array cPartsOfDay{"AM", "PM"}; + 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, ' ') + }; + 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("\\{} \\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; + 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); } } } // namespace clp_s::timestamp_parser::test From 0d28cf63a5780c916c10a8d4a3c0c0bffba5ca58 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Wed, 5 Nov 2025 21:56:14 +0000 Subject: [PATCH 04/16] Add tests for basic timestamp patterns; fix bug in millisecond and microsecond parsing. --- .../timestamp_parser/TimestampParser.cpp | 8 ++++ .../test/test_TimestampParser.cpp | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index db51543474..d8b3c53276 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -558,6 +558,10 @@ auto parse_timestamp( if (parsed_subsecond_nanoseconds < cMinParsedSubsecondNanoseconds) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; } + parsed_subsecond_nanoseconds *= cPowersOfTen.at( + cNumNanosecondPrecisionSubsecondDigits + - cNumMillisecondPrecisionSubsecondDigits + ); timestamp_idx += cFieldLength; break; } @@ -574,6 +578,10 @@ auto parse_timestamp( if (parsed_subsecond_nanoseconds < cMinParsedSubsecondNanoseconds) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; } + parsed_subsecond_nanoseconds *= cPowersOfTen.at( + cNumNanosecondPrecisionSubsecondDigits + - cNumMicrosecondPrecisionSubsecondDigits + ); timestamp_idx += cFieldLength; break; } 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 a057560882..b83f70b0ac 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,6 @@ #include #include +#include #include #include @@ -8,10 +9,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. @@ -228,5 +245,28 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { assert_specifier_accepts_valid_content('N', epoch_timestamps); assert_specifier_accepts_valid_content('N', negative_epoch_timestamps); } + + std::vector const expected_parsing_results{ + {"2015-02-01T01:02:03.004", "\\Y-\\m-\\dT\\H:\\M:\\S.\\3", 1'422'752'523'004'000'000}, + {"2015-02-01T01:02:03.004005", + "\\Y-\\m-\\dT\\H:\\M:\\S.\\6", + 1'422'752'523'004'005'000}, + {"2015-02-01T01:02:03.004005006", + "\\Y-\\m-\\dT\\H:\\M:\\S.\\9", + 1'422'752'523'004'005'006} + }; + SECTION("Timestamps are parsed accurately") { + 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); + } + } } } // namespace clp_s::timestamp_parser::test From f8a288f752fefb9c4f150919090143f0d732f70e Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Thu, 6 Nov 2025 16:49:38 +0000 Subject: [PATCH 05/16] Add tests for all timestamps from test-TimestampPattern.cpp plus common numeric patterns. --- .../test/test_TimestampParser.cpp | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) 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 b83f70b0ac..44abe89d8e 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 @@ -253,7 +253,45 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { 1'422'752'523'004'005'000}, {"2015-02-01T01:02:03.004005006", "\\Y-\\m-\\dT\\H:\\M:\\S.\\9", - 1'422'752'523'004'005'006} + 1'422'752'523'004'005'006}, + {"2015-02-01T01:02:03,004", "\\Y-\\m-\\dT\\H:\\M:\\S,\\3", 1'422'752'523'004'000'000}, + {"[2015-02-01T01:02:03", "[\\Y-\\m-\\dT\\H:\\M:\\S", 1'422'752'523'000'000'000}, + {"[20150201-01:02:03]", "[\\Y\\m\\d-\\H:\\M:\\S]", 1'422'752'523'000'000'000}, + {"2015-02-01 01:02:03,004", "\\Y-\\m-\\d \\H:\\M:\\S,\\3", 1'422'752'523'004'000'000}, + {"2015-02-01 01:02:03.004", "\\Y-\\m-\\d \\H:\\M:\\S.\\3", 1'422'752'523'004'000'000}, + {"[2015-02-01 01:02:03,004]", + "[\\Y-\\m-\\d \\H:\\M:\\S,\\3]", + 1'422'752'523'004'000'000}, + {"2015-02-01 01:02:03", "\\Y-\\m-\\d \\H:\\M:\\S", 1'422'752'523'000'000'000}, + {"2015/02/01 01:02:03", "\\Y/\\m/\\d \\H:\\M:\\S", 1'422'752'523'000'000'000}, + {"15/02/01 01:02:03", "\\y/\\m/\\d \\H:\\M:\\S", 1'422'752'523'000'000'000}, + {"150201 1:02:03", "\\y\\m\\d \\k:\\M:\\S", 1'422'752'523'000'000'000}, + {"01 Feb 2015 01:02:03,004", "\\d \\b \\Y \\H:\\M:\\S,\\3", 1'422'752'523'004'000'000}, + {"Feb 01, 2015 1:02:03 AM", "\\b \\d, \\Y \\l:\\M:\\S \\p", 1'422'752'523'000'000'000}, + {"February 01, 2015 01:02", "\\B \\d, \\Y \\H:\\M", 1'422'752'520'000'000'000}, + {"[01/Feb/2015:01:02:03", "[\\d/\\b/\\Y:\\H:\\M:\\S", 1'422'752'523'000'000'000}, + {"Sun Feb 1 01:02:03 2015", "\\a \\b \\e \\H:\\M:\\S \\Y", 1'422'752'523'000'000'000}, + {"<<<2015-02-01 01:02:03:004", + "<<<\\Y-\\m-\\d \\H:\\M:\\S:\\3", + 1'422'752'523'004'000'000}, + {"Jan 21 11:56:42", "\\b \\d \\H:\\M:\\S", 1'771'002'000'000'000}, + {"01-21 11:56:42.392", "\\m-\\d \\H:\\M:\\S.\\3", 1'771'002'392'000'000}, + {"2015/01/31 15:50:45.123", "\\Y/\\m/\\d \\H:\\M:\\S.\\3", 1'422'719'445'123'000'000}, + {"2015/01/31 15:50:45,123", "\\Y/\\m/\\d \\H:\\M:\\S,\\3", 1'422'719'445'123'000'000}, + {"2015/01/31T15:50:45", "\\Y/\\m/\\dT\\H:\\M:\\S", 1'422'719'445'000'000'000}, + {"2015/01/31T15:50:45.123", "\\Y/\\m/\\dT\\H:\\M:\\S.\\3", 1'422'719'445'123'000'000}, + {"2015/01/31T15:50:45,123", "\\Y/\\m/\\dT\\H:\\M:\\S,\\3", 1'422'719'445'123'000'000}, + {"2015-01-31T15:50:45", "\\Y-\\m-\\dT\\H:\\M:\\S", 1'422'719'445'000'000'000}, + {"1762445893", "\\E", 1'762'445'893'000'000'000}, + {"1762445893001", "\\L", 1'762'445'893'001'000'000}, + {"1762445893001002", "\\C", 1'762'445'893'001'002'000}, + {"1762445893001002003", "\\N", 1'762'445'893'001'002'003}, + {"1762445893.001", "\\E.\\3", 1'762'445'893'001'000'000}, + {"1762445893.001002", "\\E.\\6", 1'762'445'893'001'002'000}, + {"1762445893.001002003", "\\E.\\9", 1'762'445'893'001'002'003}, + {"1762445893.001002000", "\\E.\\9", 1'762'445'893'001'002'000}, + {"1762445893.00100201", "\\E.\\T", 1'762'445'893'001'002'010}, + {"1762445893.1", "\\E.\\T", 1'762'445'893'100'000'000} }; SECTION("Timestamps are parsed accurately") { std::string generated_pattern; From 4fd8fe6dfdb06e11a803be47c105918639e48a08 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Thu, 6 Nov 2025 16:52:55 +0000 Subject: [PATCH 06/16] Add missing const. --- components/core/src/clp_s/timestamp_parser/TimestampParser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index d8b3c53276..9cdb0c30b8 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -214,7 +214,7 @@ auto convert_variable_length_string_prefix_to_number(std::string_view str) return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; } - bool first_digit_zero{'0' == str.at(num_decimal_digits)}; + bool const first_digit_zero{'0' == str.at(num_decimal_digits)}; if (first_digit_zero && is_negative) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; } From 9b58db290ed9bd7cf0c7167969ce055d9b2bbda4 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Thu, 6 Nov 2025 17:14:42 +0000 Subject: [PATCH 07/16] Fix clang-tidy errors in test_TimestampParser.cpp --- .../test/test_TimestampParser.cpp | 94 +++++++++---------- 1 file changed, 44 insertions(+), 50 deletions(-) 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 44abe89d8e..ed65aa97d5 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,3 +1,4 @@ +#include #include #include #include @@ -70,7 +71,7 @@ auto generate_padded_number_subset(size_t num_digits) -> 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) { @@ -173,12 +174,12 @@ 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 pattern{"\\d \\aa"}; + 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, pattern, generated_pattern)}; + auto const result{parse_timestamp(timestamp, cPattern, generated_pattern)}; REQUIRE(false == result.has_error()); - REQUIRE(result.value().second == pattern); + REQUIRE(result.value().second == cPattern); } auto const two_digit_hours{generate_padded_numbers_in_range(0, 23, 2, '0')}; @@ -198,7 +199,7 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { 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("\\{} \\pa", hour_type)}; + 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)}; @@ -233,6 +234,7 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { 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)); } @@ -247,51 +249,43 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { } std::vector const expected_parsing_results{ - {"2015-02-01T01:02:03.004", "\\Y-\\m-\\dT\\H:\\M:\\S.\\3", 1'422'752'523'004'000'000}, - {"2015-02-01T01:02:03.004005", - "\\Y-\\m-\\dT\\H:\\M:\\S.\\6", - 1'422'752'523'004'005'000}, - {"2015-02-01T01:02:03.004005006", - "\\Y-\\m-\\dT\\H:\\M:\\S.\\9", - 1'422'752'523'004'005'006}, - {"2015-02-01T01:02:03,004", "\\Y-\\m-\\dT\\H:\\M:\\S,\\3", 1'422'752'523'004'000'000}, - {"[2015-02-01T01:02:03", "[\\Y-\\m-\\dT\\H:\\M:\\S", 1'422'752'523'000'000'000}, - {"[20150201-01:02:03]", "[\\Y\\m\\d-\\H:\\M:\\S]", 1'422'752'523'000'000'000}, - {"2015-02-01 01:02:03,004", "\\Y-\\m-\\d \\H:\\M:\\S,\\3", 1'422'752'523'004'000'000}, - {"2015-02-01 01:02:03.004", "\\Y-\\m-\\d \\H:\\M:\\S.\\3", 1'422'752'523'004'000'000}, - {"[2015-02-01 01:02:03,004]", - "[\\Y-\\m-\\d \\H:\\M:\\S,\\3]", - 1'422'752'523'004'000'000}, - {"2015-02-01 01:02:03", "\\Y-\\m-\\d \\H:\\M:\\S", 1'422'752'523'000'000'000}, - {"2015/02/01 01:02:03", "\\Y/\\m/\\d \\H:\\M:\\S", 1'422'752'523'000'000'000}, - {"15/02/01 01:02:03", "\\y/\\m/\\d \\H:\\M:\\S", 1'422'752'523'000'000'000}, - {"150201 1:02:03", "\\y\\m\\d \\k:\\M:\\S", 1'422'752'523'000'000'000}, - {"01 Feb 2015 01:02:03,004", "\\d \\b \\Y \\H:\\M:\\S,\\3", 1'422'752'523'004'000'000}, - {"Feb 01, 2015 1:02:03 AM", "\\b \\d, \\Y \\l:\\M:\\S \\p", 1'422'752'523'000'000'000}, - {"February 01, 2015 01:02", "\\B \\d, \\Y \\H:\\M", 1'422'752'520'000'000'000}, - {"[01/Feb/2015:01:02:03", "[\\d/\\b/\\Y:\\H:\\M:\\S", 1'422'752'523'000'000'000}, - {"Sun Feb 1 01:02:03 2015", "\\a \\b \\e \\H:\\M:\\S \\Y", 1'422'752'523'000'000'000}, - {"<<<2015-02-01 01:02:03:004", - "<<<\\Y-\\m-\\d \\H:\\M:\\S:\\3", - 1'422'752'523'004'000'000}, - {"Jan 21 11:56:42", "\\b \\d \\H:\\M:\\S", 1'771'002'000'000'000}, - {"01-21 11:56:42.392", "\\m-\\d \\H:\\M:\\S.\\3", 1'771'002'392'000'000}, - {"2015/01/31 15:50:45.123", "\\Y/\\m/\\d \\H:\\M:\\S.\\3", 1'422'719'445'123'000'000}, - {"2015/01/31 15:50:45,123", "\\Y/\\m/\\d \\H:\\M:\\S,\\3", 1'422'719'445'123'000'000}, - {"2015/01/31T15:50:45", "\\Y/\\m/\\dT\\H:\\M:\\S", 1'422'719'445'000'000'000}, - {"2015/01/31T15:50:45.123", "\\Y/\\m/\\dT\\H:\\M:\\S.\\3", 1'422'719'445'123'000'000}, - {"2015/01/31T15:50:45,123", "\\Y/\\m/\\dT\\H:\\M:\\S,\\3", 1'422'719'445'123'000'000}, - {"2015-01-31T15:50:45", "\\Y-\\m-\\dT\\H:\\M:\\S", 1'422'719'445'000'000'000}, - {"1762445893", "\\E", 1'762'445'893'000'000'000}, - {"1762445893001", "\\L", 1'762'445'893'001'000'000}, - {"1762445893001002", "\\C", 1'762'445'893'001'002'000}, - {"1762445893001002003", "\\N", 1'762'445'893'001'002'003}, - {"1762445893.001", "\\E.\\3", 1'762'445'893'001'000'000}, - {"1762445893.001002", "\\E.\\6", 1'762'445'893'001'002'000}, - {"1762445893.001002003", "\\E.\\9", 1'762'445'893'001'002'003}, - {"1762445893.001002000", "\\E.\\9", 1'762'445'893'001'002'000}, - {"1762445893.00100201", "\\E.\\T", 1'762'445'893'001'002'010}, - {"1762445893.1", "\\E.\\T", 1'762'445'893'100'000'000} + {"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} }; SECTION("Timestamps are parsed accurately") { std::string generated_pattern; From 64ed11be9b81ea2ca089c6b905cecc2ecc504694 Mon Sep 17 00:00:00 2001 From: Devin Gibson Date: Mon, 10 Nov 2025 10:56:46 -0500 Subject: [PATCH 08/16] Apply suggestions from code review Co-authored-by: Lin Zhihao <59785146+LinZhihao-723@users.noreply.github.com> --- .../clp_s/timestamp_parser/TimestampParser.cpp | 8 ++++---- .../test/test_TimestampParser.cpp | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index 9cdb0c30b8..27d5fda877 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -732,10 +732,10 @@ auto parse_timestamp( return ErrorCode{ErrorCodeEnum::InvalidDate}; } - 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); + 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 ed65aa97d5..2514121fb1 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 @@ -60,13 +60,13 @@ auto generate_padded_numbers_in_range(size_t begin, size_t end, size_t field_len * @param max_num_digits * @return The elements of all of the triangles of numbers up to the maximum number of digits. */ -auto generate_number_triangles(size_t max_num_digits) -> std::vector; +[[nodiscard]] auto generate_number_triangles(size_t max_num_digits) -> std::vector; /** * @param num_digits * @return All of the padded numbers with `num_digits` digits having a single unique digit. */ -auto generate_padded_number_subset(size_t num_digits) -> std::vector; +[[nodiscard]] auto generate_padded_number_subset(size_t num_digits) -> std::vector; void assert_specifier_accepts_valid_content(char specifier, std::vector const& content) { @@ -188,14 +188,14 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { auto const space_padded_hours{generate_padded_numbers_in_range(0, 23, 2, ' ')}; assert_specifier_accepts_valid_content('k', space_padded_hours); - constexpr std::array cPartsOfDay{"AM", "PM"}; 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, ' ') }; - for (auto const& part_of_day : cPartsOfDay) { + 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 { @@ -203,7 +203,7 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { 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_FALSE(result.has_error()); REQUIRE(result.value().second == pattern); } }; @@ -229,10 +229,10 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { auto const nanoseconds{generate_padded_number_subset(9)}; assert_specifier_accepts_valid_content('9', nanoseconds); - auto const variable_length_nanoseconds(generate_number_triangles(9)); + 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)); + 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) { @@ -295,7 +295,7 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { expected_result.pattern, generated_pattern )}; - REQUIRE(false == result.has_error()); + REQUIRE_FALSE(result.has_error()); REQUIRE(expected_result.epoch_timestamp == result.value().first); REQUIRE(expected_result.pattern == result.value().second); } From d533f35cd83821d3d101a7519a1bc11d6b16a788 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Mon, 10 Nov 2025 16:10:53 +0000 Subject: [PATCH 09/16] Address most review comments. --- .../timestamp_parser/TimestampParser.cpp | 37 +++++++++---------- .../test/test_TimestampParser.cpp | 3 +- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index 27d5fda877..7577e8d356 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -625,11 +625,10 @@ auto parse_timestamp( ) ); timestamp_idx += num_digits; - parsed_epoch_nanoseconds = number - * cPowersOfTen.at( - cNumNanosecondPrecisionSubsecondDigits - - cNumSecondPrecisionSubsecondDigits - ); + constexpr auto cFactor{cPowersOfTen + [cNumNanosecondPrecisionSubsecondDigits + - cNumSecondPrecisionSubsecondDigits]}; + parsed_epoch_nanoseconds = number * cFactor; number_type_representation = true; break; } @@ -640,11 +639,10 @@ auto parse_timestamp( ) ); timestamp_idx += num_digits; - parsed_epoch_nanoseconds = number - * cPowersOfTen.at( - cNumNanosecondPrecisionSubsecondDigits - - cNumMillisecondPrecisionSubsecondDigits - ); + constexpr auto cFactor{cPowersOfTen + [cNumNanosecondPrecisionSubsecondDigits + - cNumMillisecondPrecisionSubsecondDigits]}; + parsed_epoch_nanoseconds = number * cFactor; number_type_representation = true; break; } @@ -655,11 +653,10 @@ auto parse_timestamp( ) ); timestamp_idx += num_digits; - parsed_epoch_nanoseconds = number - * cPowersOfTen.at( - cNumNanosecondPrecisionSubsecondDigits - - cNumMicrosecondPrecisionSubsecondDigits - ); + constexpr auto cFactor{cPowersOfTen + [cNumNanosecondPrecisionSubsecondDigits + - cNumMicrosecondPrecisionSubsecondDigits]}; + parsed_epoch_nanoseconds = number * cFactor; number_type_representation = true; break; } @@ -701,17 +698,17 @@ auto parse_timestamp( return ErrorCode{ErrorCodeEnum::InvalidTimestampPattern}; } + // Do not allow trailing unmatched content. + if (pattern_idx != pattern.size() || timestamp_idx != timestamp.size()) { + 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}; } - // Do not allow trailing unmatched content. - if (pattern_idx != pattern.size() || timestamp_idx != timestamp.size()) { - return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; - } - if (number_type_representation) { epochtime_t epoch_nanoseconds{parsed_epoch_nanoseconds}; if (epoch_nanoseconds < 0) { 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 2514121fb1..5c93835b7e 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 @@ -64,7 +64,8 @@ auto generate_padded_numbers_in_range(size_t begin, size_t end, size_t field_len /** * @param num_digits - * @return All of the padded numbers with `num_digits` digits having a single unique digit. + * @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; From db68ff68e9f0a263d54e104c24f1ddaa4525d405 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Mon, 10 Nov 2025 16:23:10 +0000 Subject: [PATCH 10/16] Address more review comments. --- .../src/clp_s/timestamp_parser/TimestampParser.cpp | 12 ++++++------ .../timestamp_parser/test/test_TimestampParser.cpp | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index 7577e8d356..39bc3b125b 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -88,7 +88,7 @@ constexpr std::array cAbbreviatedMonthNames constexpr std::array cPartsOfDay = {std::string_view{"AM"}, std::string_view{"PM"}}; -constexpr std::array cPowersOfTen +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}; /** @@ -127,7 +127,7 @@ find_first_matching_prefix(std::string_view str, std::span ystdlib::error_handling::Result>; +) -> ystdlib::error_handling::Result>; /** * Converts the prefix of a string to a number. @@ -171,8 +171,8 @@ auto find_first_matching_prefix(std::string_view str, std::span ystdlib::error_handling::Result> { - constexpr int cTen{10}; +) -> ystdlib::error_handling::Result> { + constexpr int64_t cTen{10}; if (0ULL == max_num_digits) { return ErrorCode{ErrorCodeEnum::InvalidTimestampPattern}; } @@ -182,11 +182,11 @@ auto convert_positive_bounded_variable_length_string_prefix_to_number( return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; } - int converted_value{}; + 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'); + converted_value += static_cast(cur_digit - '0'); ++num_decimal_digits; if (num_decimal_digits >= str.length() || num_decimal_digits >= max_num_digits 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 5c93835b7e..a527e0fce7 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 @@ -179,7 +179,7 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { std::string generated_pattern; auto const timestamp{fmt::format("{}a", day_in_week_timestamp)}; auto const result{parse_timestamp(timestamp, cPattern, generated_pattern)}; - REQUIRE(false == result.has_error()); + REQUIRE_FALSE(result.has_error()); REQUIRE(result.value().second == cPattern); } From d8a68b09de6b91aaf09cdac471dd14b4c7ad2476 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Mon, 10 Nov 2025 16:31:32 +0000 Subject: [PATCH 11/16] Add inline comments about each format specifier. --- .../timestamp_parser/TimestampParser.cpp | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index 39bc3b125b..df8dfd14e0 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -412,7 +412,7 @@ auto parse_timestamp( date_type_representation = true; break; } - case 'p': { + 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) )}; @@ -421,7 +421,7 @@ auto parse_timestamp( date_type_representation = true; break; } - case 'H': { + case 'H': { // 24-hour clock, zero-padded hour. constexpr size_t cFieldLength{2}; if (timestamp_idx + cFieldLength > timestamp.size()) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; @@ -442,7 +442,7 @@ auto parse_timestamp( date_type_representation = true; break; } - case 'k': { + case 'k': { // 24-hour clock, space-padded hour. constexpr size_t cFieldLength{2}; if (timestamp_idx + cFieldLength > timestamp.size()) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; @@ -463,7 +463,7 @@ auto parse_timestamp( date_type_representation = true; break; } - case 'I': { + case 'I': { // 12-hour clock, zero-padded hour. constexpr size_t cFieldLength{2}; if (timestamp_idx + cFieldLength > timestamp.size()) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; @@ -485,7 +485,7 @@ auto parse_timestamp( date_type_representation = true; break; } - case 'l': { + case 'l': { // 12-hour clock, space-padded hour. constexpr size_t cFieldLength{2}; if (timestamp_idx + cFieldLength > timestamp.size()) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; @@ -507,7 +507,7 @@ auto parse_timestamp( date_type_representation = true; break; } - case 'M': { + case 'M': { // Zero-padded minute. constexpr size_t cFieldLength{2}; if (timestamp_idx + cFieldLength > timestamp.size()) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; @@ -526,7 +526,7 @@ auto parse_timestamp( date_type_representation = true; break; } - case 'S': { + case 'S': { // Zero-padded second. constexpr size_t cFieldLength{2}; if (timestamp_idx + cFieldLength > timestamp.size()) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; @@ -545,7 +545,7 @@ auto parse_timestamp( date_type_representation = true; break; } - case '3': { + case '3': { // Zero-padded 3-digit milliseconds. constexpr size_t cFieldLength{3}; if (false == clp::string_utils::convert_string_to_int( @@ -565,7 +565,7 @@ auto parse_timestamp( timestamp_idx += cFieldLength; break; } - case '6': { + case '6': { // Zero-padded 6-digit microseconds. constexpr size_t cFieldLength{6}; if (false == clp::string_utils::convert_string_to_int( @@ -585,7 +585,7 @@ auto parse_timestamp( timestamp_idx += cFieldLength; break; } - case '9': { + case '9': { // Zero-padded 9-digit nanoseconds. constexpr size_t cFieldLength{9}; if (false == clp::string_utils::convert_string_to_int( @@ -601,7 +601,7 @@ auto parse_timestamp( timestamp_idx += cFieldLength; break; } - case 'T': { + 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( @@ -618,7 +618,7 @@ auto parse_timestamp( = number * cPowersOfTen.at(cMaxFieldLength - num_digits); break; } - case 'E': { + case 'E': { // Epoch seconds. auto const [number, num_digits] = YSTDLIB_ERROR_HANDLING_TRYX( convert_variable_length_string_prefix_to_number( timestamp.substr(timestamp_idx) @@ -632,7 +632,7 @@ auto parse_timestamp( number_type_representation = true; break; } - case 'L': { + case 'L': { // Epoch milliseconds. auto const [number, num_digits] = YSTDLIB_ERROR_HANDLING_TRYX( convert_variable_length_string_prefix_to_number( timestamp.substr(timestamp_idx) @@ -646,7 +646,7 @@ auto parse_timestamp( number_type_representation = true; break; } - case 'C': { + case 'C': { // Epoch microseconds. auto const [number, num_digits] = YSTDLIB_ERROR_HANDLING_TRYX( convert_variable_length_string_prefix_to_number( timestamp.substr(timestamp_idx) @@ -660,7 +660,7 @@ auto parse_timestamp( number_type_representation = true; break; } - case 'N': { + case 'N': { // Epoch nanoseconds. auto const [number, num_digits] = YSTDLIB_ERROR_HANDLING_TRYX( convert_variable_length_string_prefix_to_number( timestamp.substr(timestamp_idx) From 48a1b4c3e7b221fbd42e4fedc5d5f624d44b7d56 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Mon, 10 Nov 2025 16:36:51 +0000 Subject: [PATCH 12/16] Move expected parsing cases into only test section that currently uses them. --- .../test/test_TimestampParser.cpp | 87 ++++++++++--------- 1 file changed, 48 insertions(+), 39 deletions(-) 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 a527e0fce7..febccf9685 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 @@ -249,46 +249,55 @@ TEST_CASE("timestamp_parser_parse_timestamp", "[clp-s][timestamp-parser]") { assert_specifier_accepts_valid_content('N', negative_epoch_timestamps); } - 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} - }; 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( From 7f41e7dcaff5436b2cd421e1c0ebdafc370b2f38 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Mon, 10 Nov 2025 16:59:48 +0000 Subject: [PATCH 13/16] Address rabbit comment. --- .../src/clp_s/timestamp_parser/test/test_TimestampParser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 febccf9685..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 @@ -79,7 +79,7 @@ assert_specifier_accepts_valid_content(char specifier, std::vector 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); } } From 1875c75bdbacdbc1e8367b3347502b5b95d2c1f6 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Mon, 10 Nov 2025 18:31:25 +0000 Subject: [PATCH 14/16] Use constexpr power of 10 in a few more instances. --- .../clp_s/timestamp_parser/TimestampParser.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index df8dfd14e0..0391cb9678 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -558,10 +558,10 @@ auto parse_timestamp( if (parsed_subsecond_nanoseconds < cMinParsedSubsecondNanoseconds) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; } - parsed_subsecond_nanoseconds *= cPowersOfTen.at( - cNumNanosecondPrecisionSubsecondDigits - - cNumMillisecondPrecisionSubsecondDigits - ); + constexpr auto cFactor{cPowersOfTen + [cNumNanosecondPrecisionSubsecondDigits + - cNumMillisecondPrecisionSubsecondDigits]}; + parsed_subsecond_nanoseconds *= cFactor; timestamp_idx += cFieldLength; break; } @@ -578,10 +578,10 @@ auto parse_timestamp( if (parsed_subsecond_nanoseconds < cMinParsedSubsecondNanoseconds) { return ErrorCode{ErrorCodeEnum::IncompatibleTimestampPattern}; } - parsed_subsecond_nanoseconds *= cPowersOfTen.at( - cNumNanosecondPrecisionSubsecondDigits - - cNumMicrosecondPrecisionSubsecondDigits - ); + constexpr auto cFactor{cPowersOfTen + [cNumNanosecondPrecisionSubsecondDigits + - cNumMicrosecondPrecisionSubsecondDigits]}; + parsed_subsecond_nanoseconds *= cFactor; timestamp_idx += cFieldLength; break; } From eaf68253507c641d5f838078995d9fe2c0839137 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Mon, 10 Nov 2025 18:40:34 +0000 Subject: [PATCH 15/16] Fix clang-tidy issue in TimestampParser --- components/core/src/clp_s/timestamp_parser/TimestampParser.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index 0391cb9678..ede094748b 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -615,7 +615,7 @@ auto parse_timestamp( } timestamp_idx += num_digits; parsed_subsecond_nanoseconds - = number * cPowersOfTen.at(cMaxFieldLength - num_digits); + = static_cast(number * cPowersOfTen.at(cMaxFieldLength - num_digits)); break; } case 'E': { // Epoch seconds. From 408c0fd2d54117eb7587838362b70e95a46684d5 Mon Sep 17 00:00:00 2001 From: gibber9809 Date: Mon, 10 Nov 2025 19:27:16 +0000 Subject: [PATCH 16/16] Address edge case caught by rabbit. --- .../core/src/clp_s/timestamp_parser/TimestampParser.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp index ede094748b..6211d40dfc 100644 --- a/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp +++ b/components/core/src/clp_s/timestamp_parser/TimestampParser.cpp @@ -547,6 +547,9 @@ auto parse_timestamp( } 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), @@ -567,6 +570,9 @@ auto parse_timestamp( } 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), @@ -587,6 +593,9 @@ auto parse_timestamp( } 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),