Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 43 additions & 21 deletions src/elements/time.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "adm/elements/time.hpp"
#include <boost/integer/common_factor.hpp>
#include <cmath>
#include <iomanip>
#include <regex>
#include <sstream>
Expand Down Expand Up @@ -49,13 +50,14 @@ 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;
Expand All @@ -64,18 +66,21 @@ 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;
Expand All @@ -84,7 +89,9 @@ 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;
Expand All @@ -95,18 +102,27 @@ namespace adm {
struct FormatTimeVisitor : public boost::static_visitor<std::string> {
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<std::chrono::hours>(time).count();
<< std::chrono::duration_cast<std::chrono::hours>(formatTime).count();
ss << ":";
ss << std::setw(2) << std::setfill('0')
<< std::chrono::duration_cast<std::chrono::minutes>(time).count() % 60;
<< std::chrono::duration_cast<std::chrono::minutes>(formatTime)
.count() %
60;
ss << ":";
ss << std::setw(2) << std::setfill('0')
<< std::chrono::duration_cast<std::chrono::seconds>(time).count() % 60;
<< std::chrono::duration_cast<std::chrono::seconds>(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) {
Expand All @@ -120,18 +136,24 @@ namespace adm {
}

std::string operator()(const FractionalTime& time) const {
int64_t whole_seconds = time.numerator() / time.denominator();
int64_t frac_numerator =
time.numerator() - whole_seconds * time.denominator();
auto absNum = abs(time.numerator());
auto absDenom = abs(time.denominator());
int64_t whole_seconds = absNum / absDenom;
int64_t frac_numerator = absNum - whole_seconds * absDenom;

std::stringstream ss;
double floating =
static_cast<double>(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();
}
};
Expand Down
52 changes: 52 additions & 0 deletions tests/adm_time_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,55 @@ 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}));
}
Loading