From 2d71bd6c6a00619ea5bb86f1db2b3c81da930008 Mon Sep 17 00:00:00 2001 From: Richard Bailey Date: Tue, 2 Dec 2025 12:34:13 +0000 Subject: [PATCH 1/3] allow parsing and formatting of negative times --- src/elements/time.cpp | 55 +++++++++++++++++++++++++--------------- tests/adm_time_tests.cpp | 53 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/src/elements/time.cpp b/src/elements/time.cpp index a5fe7316..0377cc97 100644 --- a/src/elements/time.cpp +++ b/src/elements/time.cpp @@ -49,13 +49,13 @@ namespace adm { } Time parseTimecode(const std::string& timecode) { - const static std::regex commonFormat("(\\d{2}):(\\d{2}):(\\d{2}).(\\d+)"); + const static std::regex commonFormat(R"((-?)(\d{2}):(\d{2}):(\d{2}).(\d+))"); const static std::regex fractionalFormat( - "(\\d{2}):(\\d{2}):(\\d{2}).(\\d+)S(\\d+)"); + R"((-?)(\d{2}):(\d{2}):(\d{2}).(\d+)S(\d+))"); std::smatch timecodeMatch; if (std::regex_match(timecode, timecodeMatch, commonFormat)) { - const std::string& ns_str = timecodeMatch[4]; + const std::string& ns_str = timecodeMatch[5]; // parse number of nanoseconds as if it always had 9 digits int64_t ns = 0; @@ -64,18 +64,22 @@ namespace adm { if (i < ns_str.size()) ns += place_value * (ns_str[i] - '0'); place_value *= 10; } + auto isPositive = timecodeMatch[1].str().empty(); - return std::chrono::hours(stoi(timecodeMatch[1])) + - std::chrono::minutes(stoi(timecodeMatch[2])) + - std::chrono::seconds(stoi(timecodeMatch[3])) + - std::chrono::nanoseconds(ns); + return (isPositive ? 1 : -1) * + (std::chrono::hours(stoi(timecodeMatch[2])) + + std::chrono::minutes(stoi(timecodeMatch[3])) + + std::chrono::seconds(stoi(timecodeMatch[4])) + + std::chrono::nanoseconds(ns)); } else if (std::regex_match(timecode, timecodeMatch, fractionalFormat)) { - int64_t seconds = 3600 * stoi(timecodeMatch[1]) + - 60 * stoi(timecodeMatch[2]) + - 1 * stoi(timecodeMatch[3]); + auto isPositive = timecodeMatch[1].str().empty(); + int64_t seconds = + 3600 * stoi(timecodeMatch[2]) + + 60 * stoi(timecodeMatch[3]) + + 1 * stoi(timecodeMatch[4]); - int64_t numerator = stoi(timecodeMatch[4]); - int64_t denominator = stoi(timecodeMatch[5]); + int64_t numerator = stoi(timecodeMatch[5]); + int64_t denominator = stoi(timecodeMatch[6]); if (denominator == 0) { std::stringstream errorString; @@ -84,7 +88,7 @@ namespace adm { throw std::runtime_error(errorString.str()); } - return FractionalTime{seconds * denominator + numerator, denominator}; + return FractionalTime{(isPositive ? 1 : -1) * (seconds * denominator + numerator), denominator}; } else { std::stringstream errorString; errorString << "invalid timecode: " << timecode; @@ -95,18 +99,23 @@ namespace adm { struct FormatTimeVisitor : public boost::static_visitor { std::string operator()(const std::chrono::nanoseconds& time) const { std::stringstream ss; + auto formatTime = time; + if (formatTime.count() < 0) { + formatTime = -formatTime; + ss << '-'; + } ss << std::setw(2) << std::setfill('0') - << std::chrono::duration_cast(time).count(); + << std::chrono::duration_cast(formatTime).count(); ss << ":"; ss << std::setw(2) << std::setfill('0') - << std::chrono::duration_cast(time).count() % 60; + << std::chrono::duration_cast(formatTime).count() % 60; ss << ":"; ss << std::setw(2) << std::setfill('0') - << std::chrono::duration_cast(time).count() % 60; + << std::chrono::duration_cast(formatTime).count() % 60; ss << "."; { - auto ns = time.count() % 1000000000; + auto ns = formatTime.count() % 1000000000; // drop trailing zero digits, while keeping at least 5 to satisfy BS.2076-2 int precision = 9; while (ns % 10 == 0 && precision > 5) { @@ -120,18 +129,24 @@ namespace adm { } std::string operator()(const FractionalTime& time) const { - int64_t whole_seconds = time.numerator() / time.denominator(); + auto absNum = abs(time.numerator()); + auto absDenom = abs(time.denominator()); + int64_t whole_seconds = absNum / absDenom; int64_t frac_numerator = - time.numerator() - whole_seconds * time.denominator(); + absNum - whole_seconds * absDenom; std::stringstream ss; + double floating = static_cast(time.numerator()) / time.denominator(); + if (std::signbit(floating)) { + ss << "-"; + } ss << std::setw(2) << std::setfill('0') << whole_seconds / 3600; ss << ":"; ss << std::setw(2) << std::setfill('0') << (whole_seconds / 60) % 60; ss << ":"; ss << std::setw(2) << std::setfill('0') << whole_seconds % 60; ss << "."; - ss << frac_numerator << "S" << time.denominator(); + ss << frac_numerator << "S" << absDenom; return ss.str(); } }; diff --git a/tests/adm_time_tests.cpp b/tests/adm_time_tests.cpp index fe932881..2ee273e0 100644 --- a/tests/adm_time_tests.cpp +++ b/tests/adm_time_tests.cpp @@ -114,3 +114,56 @@ TEST_CASE("rational conversion") { REQUIRE(asTime(RationalTime{1, 2}) == FractionalTime{1, 2}); } + +TEST_CASE("Parsed negative zero time == zero time") { + auto time = parseTimecode("-00:00:00.00000"); + REQUIRE(time == Time{}); +} + +TEST_CASE("Negative nanosecond times parsed correctly") { + { + auto time = parseTimecode("-00:00:01.00000"); + REQUIRE(time == Time{std::chrono::seconds{-1}}); + } + { + auto time = parseTimecode("-00:10:00.00000"); + REQUIRE(time == Time{std::chrono::minutes{-10}}); + } +} + +TEST_CASE("Format negative ns timecode") { + { + auto code = formatTimecode(Time{std::chrono::seconds{-1}}); + REQUIRE(code == "-00:00:01.00000"); + } + { + auto code = formatTimecode(Time{std::chrono::nanoseconds{-1}}); + REQUIRE(code == "-00:00:00.000000001"); + } + { + auto code = formatTimecode(Time{std::chrono::minutes{-1}}); + REQUIRE(code == "-00:01:00.00000"); + } + { + auto code = formatTimecode(Time{std::chrono::hours{-1}}); + REQUIRE(code == "-01:00:00.00000"); + } +} + +TEST_CASE("Parse negative fractional time") { + REQUIRE(parseTimecode("-01:00:00.0S1") == Time(FractionalTime{-3600, 1})); + REQUIRE(parseTimecode("-00:01:00.0S1") == Time(FractionalTime{-60, 1})); + REQUIRE(parseTimecode("-00:00:01.0S1") == Time(FractionalTime{-1, 1})); + REQUIRE(parseTimecode("-00:00:00.1S10") == Time(FractionalTime{-1, 10})); + + // test leading zeros + REQUIRE(parseTimecode("-00:00:00.01S010") == Time(FractionalTime{-1, 10})); +} + +TEST_CASE("Format negative fractional time") { + REQUIRE("-01:00:00.0S1" == formatTimecode(FractionalTime{-3600, 1})); + REQUIRE("-00:01:00.0S1" == formatTimecode(FractionalTime{-60, 1})); + REQUIRE("-00:00:01.0S1" == formatTimecode(FractionalTime{-1, 1})); + REQUIRE("-00:00:00.1S10" == formatTimecode(FractionalTime{-1, 10})); +} + From 763ca4557d93c85f41362d50d3cdf1b4b589910a Mon Sep 17 00:00:00 2001 From: Richard Bailey Date: Tue, 2 Dec 2025 14:53:32 +0000 Subject: [PATCH 2/3] fix formatting --- src/elements/time.cpp | 32 +++++++++++++++++++------------- tests/adm_time_tests.cpp | 13 ++++++------- 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/elements/time.cpp b/src/elements/time.cpp index 0377cc97..aff06665 100644 --- a/src/elements/time.cpp +++ b/src/elements/time.cpp @@ -49,7 +49,8 @@ namespace adm { } Time parseTimecode(const std::string& timecode) { - const static std::regex commonFormat(R"((-?)(\d{2}):(\d{2}):(\d{2}).(\d+))"); + const static std::regex commonFormat( + R"((-?)(\d{2}):(\d{2}):(\d{2}).(\d+))"); const static std::regex fractionalFormat( R"((-?)(\d{2}):(\d{2}):(\d{2}).(\d+)S(\d+))"); @@ -67,14 +68,13 @@ namespace adm { auto isPositive = timecodeMatch[1].str().empty(); return (isPositive ? 1 : -1) * - (std::chrono::hours(stoi(timecodeMatch[2])) + - std::chrono::minutes(stoi(timecodeMatch[3])) + - std::chrono::seconds(stoi(timecodeMatch[4])) + - std::chrono::nanoseconds(ns)); + (std::chrono::hours(stoi(timecodeMatch[2])) + + std::chrono::minutes(stoi(timecodeMatch[3])) + + std::chrono::seconds(stoi(timecodeMatch[4])) + + std::chrono::nanoseconds(ns)); } else if (std::regex_match(timecode, timecodeMatch, fractionalFormat)) { auto isPositive = timecodeMatch[1].str().empty(); - int64_t seconds = - 3600 * stoi(timecodeMatch[2]) + + int64_t seconds = 3600 * stoi(timecodeMatch[2]) + 60 * stoi(timecodeMatch[3]) + 1 * stoi(timecodeMatch[4]); @@ -88,7 +88,9 @@ namespace adm { throw std::runtime_error(errorString.str()); } - return FractionalTime{(isPositive ? 1 : -1) * (seconds * denominator + numerator), denominator}; + return FractionalTime{ + (isPositive ? 1 : -1) * (seconds * denominator + numerator), + denominator}; } else { std::stringstream errorString; errorString << "invalid timecode: " << timecode; @@ -108,10 +110,14 @@ namespace adm { << std::chrono::duration_cast(formatTime).count(); ss << ":"; ss << std::setw(2) << std::setfill('0') - << std::chrono::duration_cast(formatTime).count() % 60; + << std::chrono::duration_cast(formatTime) + .count() % + 60; ss << ":"; ss << std::setw(2) << std::setfill('0') - << std::chrono::duration_cast(formatTime).count() % 60; + << std::chrono::duration_cast(formatTime) + .count() % + 60; ss << "."; { @@ -132,11 +138,11 @@ namespace adm { auto absNum = abs(time.numerator()); auto absDenom = abs(time.denominator()); int64_t whole_seconds = absNum / absDenom; - int64_t frac_numerator = - absNum - whole_seconds * absDenom; + int64_t frac_numerator = absNum - whole_seconds * absDenom; std::stringstream ss; - double floating = static_cast(time.numerator()) / time.denominator(); + double floating = + static_cast(time.numerator()) / time.denominator(); if (std::signbit(floating)) { ss << "-"; } diff --git a/tests/adm_time_tests.cpp b/tests/adm_time_tests.cpp index 2ee273e0..8c77a12e 100644 --- a/tests/adm_time_tests.cpp +++ b/tests/adm_time_tests.cpp @@ -151,13 +151,13 @@ TEST_CASE("Format negative ns timecode") { } TEST_CASE("Parse negative fractional time") { - REQUIRE(parseTimecode("-01:00:00.0S1") == Time(FractionalTime{-3600, 1})); - REQUIRE(parseTimecode("-00:01:00.0S1") == Time(FractionalTime{-60, 1})); - REQUIRE(parseTimecode("-00:00:01.0S1") == Time(FractionalTime{-1, 1})); - REQUIRE(parseTimecode("-00:00:00.1S10") == Time(FractionalTime{-1, 10})); + REQUIRE(parseTimecode("-01:00:00.0S1") == Time(FractionalTime{-3600, 1})); + REQUIRE(parseTimecode("-00:01:00.0S1") == Time(FractionalTime{-60, 1})); + REQUIRE(parseTimecode("-00:00:01.0S1") == Time(FractionalTime{-1, 1})); + REQUIRE(parseTimecode("-00:00:00.1S10") == Time(FractionalTime{-1, 10})); - // test leading zeros - REQUIRE(parseTimecode("-00:00:00.01S010") == Time(FractionalTime{-1, 10})); + // test leading zeros + REQUIRE(parseTimecode("-00:00:00.01S010") == Time(FractionalTime{-1, 10})); } TEST_CASE("Format negative fractional time") { @@ -166,4 +166,3 @@ TEST_CASE("Format negative fractional time") { REQUIRE("-00:00:01.0S1" == formatTimecode(FractionalTime{-1, 1})); REQUIRE("-00:00:00.1S10" == formatTimecode(FractionalTime{-1, 10})); } - From 1388db39f433de5f6f5952e5e52f7d99e629e0ed Mon Sep 17 00:00:00 2001 From: Richard Bailey Date: Tue, 2 Dec 2025 14:57:22 +0000 Subject: [PATCH 3/3] Add missing header --- src/elements/time.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/elements/time.cpp b/src/elements/time.cpp index aff06665..647ef5d3 100644 --- a/src/elements/time.cpp +++ b/src/elements/time.cpp @@ -1,5 +1,6 @@ #include "adm/elements/time.hpp" #include +#include #include #include #include