Skip to content

Commit

Permalink
cras_cpp_common: time_utils: Added converters between ros::Time and s…
Browse files Browse the repository at this point in the history
…truct tm.
  • Loading branch information
peci1 committed Nov 13, 2024
1 parent e157d32 commit 987c09e
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 6 deletions.
13 changes: 13 additions & 0 deletions cras_cpp_common/include/cras_cpp_common/string_utils/ros.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@ inline ::std::string to_string(const T& value)
return ss.str();
}

/**
* \brief Convert the given time to a human-readable date-time representation according to ISO 8601.
* \tparam T Time type
* \param value The time to convert.
* \return Human-readable date-time representation.
*/
template<typename T, typename ::std::enable_if_t<
::std::is_same<T, ::ros::Time>::value ||
::std::is_same<T, ::ros::WallTime>::value ||
::std::is_same<T, ::ros::SteadyTime>::value
>* = nullptr>
::std::string to_pretty_string(const T& value);

template<typename T, typename ::std::enable_if_t<
::std::is_same<T, ::ros::Rate>::value ||
::std::is_same<T, ::ros::WallRate>::value
Expand Down
63 changes: 63 additions & 0 deletions cras_cpp_common/include/cras_cpp_common/time_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

#pragma once

#include <ctime>
#include <string>

#include <cras_cpp_common/expected.hpp>
#include <ros/duration.h>
#include <ros/rate.h>
#include <ros/time.h>
Expand Down Expand Up @@ -149,6 +153,65 @@ ::ros::WallTime saturateAdd(const ::ros::WallTime& time, const ::ros::WallDurati
*/
::ros::SteadyTime saturateAdd(const ::ros::SteadyTime& time, const ::ros::WallDuration& duration);

/**
* \brief Convert the given ROS time to C tm struct representing UTC time.
* \param[in] time The ROS time to convert.
* \return The struct tm with corresponding time (please remember that year is offset from 1900 and month is 0-based).
* \note The fractional seconds will be lost during the conversion.
*/
::tm toStructTm(const ::ros::Time& time);

/**
* \brief Convert the given ROS time to C tm struct representing UTC time.
* \param[in] time The ROS time to convert.
* \return The struct tm with corresponding time (please remember that year is offset from 1900 and month is 0-based).
* \note The fractional seconds will be lost during the conversion.
*/
inline ::tm toStructTm(const ::ros::WallTime& time)
{
return ::cras::toStructTm(::cras::convertTime<::ros::Time>(time));
}

/**
* \brief Convert the given ROS time to C tm struct representing UTC time.
* \param[in] time The ROS time to convert.
* \return The struct tm with corresponding time (please remember that year is offset from 1900 and month is 0-based).
* \note The fractional seconds will be lost during the conversion.
*/
inline ::tm toStructTm(const ::ros::SteadyTime& time)
{
return ::cras::toStructTm(::cras::convertTime<::ros::Time>(time));
}

::cras::expected<::ros::Time, ::std::string> fromStructTm(const ::tm& time);

/**
* \brief Get the year represented by the given ROS time when interpreted as UTC time.
* \param[in] time The ROS time.
* \return The year.
*/
int getYear(const ::ros::Time& time);

/**
* \brief Get the year represented by the given ROS time when interpreted as UTC time.
* \param[in] time The ROS time.
* \return The year.
*/
inline int getYear(const ::ros::WallTime& time)
{
return ::cras::getYear(::cras::convertTime<::ros::Time>(time));
}

/**
* \brief Get the year represented by the given ROS time when interpreted as UTC time.
* \param[in] time The ROS time.
* \return The year.
*/
inline int getYear(const ::ros::SteadyTime& time)
{
return ::cras::getYear(::cras::convertTime<::ros::Time>(time));
}

}

namespace ros
Expand Down
30 changes: 24 additions & 6 deletions cras_cpp_common/src/string_utils/ros.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
* \author Martin Pecka
*/

#include <ctime>
#include <limits>
#include <regex>
#include <string>

#include <boost/date_time/posix_time/posix_time.hpp>
#include <boost/date_time/posix_time/ptime.hpp>

#include <ros/duration.h>
Expand Down Expand Up @@ -103,11 +105,9 @@ template<> ros::Time parseTime(
t.tm_min = minute;
t.tm_sec = second;

errno = 0;
int32_t timeSecs = timegm(&t);
if (timeSecs < 0 || errno == EOVERFLOW)
throw std::invalid_argument("Invalid time format (timegm overflow).");
timeSecs -= zoneOffset.sec;
const auto maybeTime = cras::fromStructTm(t);
if (!maybeTime.has_value())
throw std::invalid_argument(cras::format("Invalid time format (%s).", maybeTime.error().c_str()));

uint32_t fracNsec = 0;
if (matches[7].matched)
Expand All @@ -120,7 +120,7 @@ template<> ros::Time parseTime(
fracNsec = cras::parseUInt32(paddedNsec, 10);
}

return {static_cast<uint32_t>(timeSecs), fracNsec};
return {maybeTime->sec - zoneOffset.sec, fracNsec};
}

template<> ros::WallTime parseTime(
Expand Down Expand Up @@ -209,4 +209,22 @@ template<> ros::WallDuration parseDuration(const std::string& s)
return cras::convertDuration<ros::WallDuration>(parseDuration<ros::Duration>(s));
}

template<>
std::string to_pretty_string(const ros::Time& value)
{
return boost::posix_time::to_iso_extended_string(value.toBoost()) + "Z";
}

template<>
std::string to_pretty_string(const ros::WallTime& value)
{
return to_pretty_string(cras::convertTime<ros::Time>(value));
}

template<>
std::string to_pretty_string(const ros::SteadyTime& value)
{
return to_pretty_string(cras::convertTime<ros::Time>(value));
}

}
57 changes: 57 additions & 0 deletions cras_cpp_common/src/time_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@
*/

#include <limits>
#include <string>

#include <ros/duration.h>
#include <ros/rate.h>
#include <ros/time.h>

#include <cras_cpp_common/expected.hpp>
#include <cras_cpp_common/string_utils/ros.hpp>
#include <cras_cpp_common/string_utils/xmlrpc.hpp>
#include <cras_cpp_common/time_utils.hpp>

// Fallback for 128bit ints on armhf or non-gcc compilers
Expand Down Expand Up @@ -127,6 +131,59 @@ ros::SteadyTime saturateAdd(const ros::SteadyTime& time, const ros::WallDuration
return time + duration;
}

tm toStructTm(const ros::Time& time)
{
const auto timet = static_cast<time_t>(time.sec);

tm structTm{};
const auto result = gmtime_r(&timet, &structTm);

// This shouldn't ever happen. gmtime can return nullptr only if year overflows, and max year of ros::Time is far
// from being able to overflow an int (even if 16-bit).
if (result == nullptr)
return structTm;

return structTm;
}

cras::expected<ros::Time, std::string> fromStructTm(const tm& time)
{
tm t = time;
#if _DEFAULT_SOURCE
errno = 0;
const auto timeSecs = timegm(&t);
#else
const auto tz = getenv("TZ");
setenv("TZ", "", 1);
tzset();
const auto timeSecs = mktime(&t);
if (tz)
setenv("TZ", tz, 1);
else
unsetenv("TZ");
tzset();
#endif
if (timeSecs == static_cast<time_t>(-1) || errno == EOVERFLOW)
return cras::make_unexpected(cras::format(
"Cannot convert the given tm struct to ROS time (timegm failed, errno=%d).", errno));
if (timeSecs < 0)
return cras::make_unexpected("Cannot convert the given tm struct to ROS time (negative seconds since 1970).");

try
{
return ros::Time(timeSecs, 0);
}
catch (const std::runtime_error& e)
{
return cras::make_unexpected(cras::format("Cannot convert the given tm struct to ROS time (%s).", e.what()));
}
}

int getYear(const ros::Time& time)
{
return toStructTm(time).tm_year + 1900;
}

}

namespace ros
Expand Down
8 changes: 8 additions & 0 deletions cras_cpp_common/test/test_string_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,14 @@ TEST(StringUtils, ToStringRos) // NOLINT
EXPECT_EQ("1.500000000", to_string(ros::Duration(1, 500000000)));
EXPECT_EQ("1.500000000", to_string(ros::WallDuration(1, 500000000)));

EXPECT_EQ("1970-01-01T00:00:01.500000Z", to_pretty_string(ros::Time(1, 500000000)));
EXPECT_EQ("1970-01-01T00:00:01.500000Z", to_pretty_string(ros::WallTime(1, 500000000)));
EXPECT_EQ("1970-01-01T00:00:01.500000Z", to_pretty_string(ros::SteadyTime(1, 500000000)));

EXPECT_EQ("2024-11-13T13:44:04Z", to_pretty_string(ros::Time(1731505444, 0)));
EXPECT_EQ("2024-11-13T13:44:04Z", to_pretty_string(ros::WallTime(1731505444, 0)));
EXPECT_EQ("2024-11-13T13:44:04Z", to_pretty_string(ros::SteadyTime(1731505444, 0)));

std_msgs::Bool b;
EXPECT_EQ("data: 0", to_string(b));
b.data = true;
Expand Down
70 changes: 70 additions & 0 deletions cras_cpp_common/test/test_time_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,76 @@ TEST(TimeUtils, ConvertDuration) // NOLINT
EXPECT_EQ(ros::WallDuration(1, 2), cras::convertDuration<ros::WallDuration>(ros::Duration(1, 2)));
}

bool operator==(const tm& t1, const tm& t2)
{
return
t1.tm_year == t2.tm_year &&
t1.tm_mon == t2.tm_mon &&
t1.tm_mday == t2.tm_mday &&
t1.tm_hour == t2.tm_hour &&
t1.tm_min == t2.tm_min &&
t1.tm_sec == t2.tm_sec &&
t1.tm_gmtoff == t2.tm_gmtoff;
}

TEST(TimeUtils, ToStructTm) // NOLINT
{
tm t{};
t.tm_year = 1970 - 1900;
t.tm_mon = 1 - 1;
t.tm_mday = 1;
t.tm_hour = 0;
t.tm_min = 0;
t.tm_sec = 0;
t.tm_isdst = 0;
t.tm_gmtoff = 0;

EXPECT_EQ(t, cras::toStructTm(ros::Time()));
EXPECT_EQ(t, cras::toStructTm(ros::WallTime()));
EXPECT_EQ(t, cras::toStructTm(ros::SteadyTime()));

t.tm_year = 2024 - 1900;
t.tm_mon = 11 - 1;
t.tm_mday = 13;
t.tm_hour = 13;
t.tm_min = 44;
t.tm_sec = 04;
t.tm_isdst = 0;
t.tm_gmtoff = 0;

EXPECT_EQ(t, cras::toStructTm(ros::Time(1731505444, 0)));
EXPECT_EQ(t, cras::toStructTm(ros::WallTime(1731505444, 0)));
EXPECT_EQ(t, cras::toStructTm(ros::SteadyTime(1731505444, 0)));
EXPECT_EQ(t, cras::toStructTm(ros::Time(1731505444, 500000000)));
}

TEST(TimeUtils, GetYear) // NOLINT
{
EXPECT_EQ(1970, cras::getYear(ros::Time()));
EXPECT_EQ(1970, cras::getYear(ros::WallTime()));
EXPECT_EQ(1970, cras::getYear(ros::SteadyTime()));

EXPECT_EQ(2024, cras::getYear(ros::Time(1731505444, 0)));
EXPECT_EQ(2024, cras::getYear(ros::WallTime(1731505444, 0)));
EXPECT_EQ(2024, cras::getYear(ros::SteadyTime(1731505444, 0)));
EXPECT_EQ(2024, cras::getYear(ros::Time(1731505444, 500000000)));
}

TEST(TimeUtils, FromStructTm) // NOLINT
{
tm t{};

EXPECT_FALSE(cras::fromStructTm(t).has_value());

t.tm_year = 1970 - 1900; t.tm_mon = 1 - 1; t.tm_mday = 1; t.tm_hour = 0; t.tm_min = 0; t.tm_sec = 0;
ASSERT_TRUE(cras::fromStructTm(t).has_value());
EXPECT_EQ(ros::Time(), cras::fromStructTm(t));

t.tm_year = 2024 - 1900; t.tm_mon = 11 - 1; t.tm_mday = 13; t.tm_hour = 13; t.tm_min = 44; t.tm_sec = 4;
ASSERT_TRUE(cras::fromStructTm(t).has_value());
EXPECT_EQ(ros::Time(1731505444, 0), cras::fromStructTm(t));
}

int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv);
Expand Down

0 comments on commit 987c09e

Please sign in to comment.