Support parsing of timestamp strings to C++11 time_points.

This commit is contained in:
U.G. Wilson 2015-04-01 00:27:02 -05:00
parent 908d38ebef
commit 96f9a6747d
3 changed files with 251 additions and 0 deletions

View File

@ -7,6 +7,7 @@
#pragma once #pragma once
#endif #endif
#include <chrono>
#include <limits> #include <limits>
#include <list> #include <list>
#include <map> #include <map>
@ -281,6 +282,25 @@ struct convert<Binary> {
return true; return true;
} }
}; };
// std::chrono::time_point<std::chrono::system_clock>
// Notes:
// - Formats supported in spec at http://http://yaml.org/type/timestamp.html
// canonical: 2001-12-15T02:59:43.1Z
// iso8601: 2001-12-14t21:59:43.10-05:00
// spaced: 2001-12-14 21:59:43.10 -5
// date: 2002-12-14
// - Some platforms cannot handle time_points before January 1st, 1970 00:00Z
template <>
struct convert<std::chrono::time_point<std::chrono::system_clock>> {
YAML_CPP_API static Node encode(
const std::chrono::time_point<std::chrono::system_clock>& rhs);
YAML_CPP_API static bool decode(const Node& node,
std::chrono::time_point<std::chrono::system_clock>& rhs);
};
} }
#endif // NODE_CONVERT_H_62B23520_7C8E_11DE_8A39_0800200C9A66 #endif // NODE_CONVERT_H_62B23520_7C8E_11DE_8A39_0800200C9A66

View File

@ -1,5 +1,10 @@
#include <algorithm> #include <algorithm>
#include <ctime>
#include <iomanip>
#include <regex>
#include <sstream>
#include "yaml-cpp/exceptions.h"
#include "yaml-cpp/node/convert.h" #include "yaml-cpp/node/convert.h"
namespace { namespace {
@ -73,3 +78,157 @@ bool convert<bool>::decode(const Node& node, bool& rhs) {
return false; return false;
} }
} }
namespace YAML {
Node convert<std::chrono::time_point<std::chrono::system_clock>>::encode(
const std::chrono::time_point<std::chrono::system_clock>& rhs
) {
// Constants
const uint16_t MS_PER_S = 1000;
const uint16_t TM_BASE_YEAR = 1900;
std::time_t tt = std::chrono::system_clock::to_time_t(rhs);
std::tm utc_tm = *std::gmtime(&tt);
using namespace std::chrono;
uint16_t ms = duration_cast<milliseconds>(rhs.time_since_epoch())
.count() % MS_PER_S;
std::stringstream canonical;
{
using namespace std;
canonical << setw(4) << setfill('0') << TM_BASE_YEAR + utc_tm.tm_year << "-"
<< setw(2) << setfill('0') << utc_tm.tm_mon + 1 << "-"
<< setw(2) << setfill('0') << utc_tm.tm_mday << "T"
<< setw(2) << setfill('0') << utc_tm.tm_hour << ":"
<< setw(2) << setfill('0') << utc_tm.tm_min << ":"
<< std::setw(2) << std::setfill('0') << utc_tm.tm_sec << "."
<< ms / 100 << "Z";
}
return Node(canonical.str());
}
bool convert<std::chrono::time_point<std::chrono::system_clock>>::decode(
const Node& node,
std::chrono::time_point<std::chrono::system_clock>& rhs
) {
if (!node.IsScalar()) { return false; }
// Constants
const uint16_t TM_BASE_YEAR = 1900;
const uint16_t MIN_PER_HR = 60;
const uint16_t MS_PER_S = 1000;
const uint16_t S_PER_MIN = 60;
const uint16_t S_PER_HR = 3600;
const uint32_t S_PER_DAY = 86400;
const std::string s = node.Scalar();
// Working variables.
std::tm l_tm;
std::time_t l_time_t;
uint16_t year, month, day, hour = 0, minute = 0;
double second = 0.0f, zone = 0.0f;
// Regex patterns for date, time, and timezone.
std::regex r_yr("[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?");
std::regex r_tm("[0-9][0-9]?:[0-9][0-9]:[0-9][0-9].[0-9][0-9]?");
std::regex r_zn("[Z]|[+|-][0-9][0-9]:[0-9][0-9]| [+|-][0-9]");
std::smatch m;
// Parse the date string.
if(std::regex_search(s.begin(), s.end(), m, r_yr)) {
std::stringstream ss(m[0]);
std::string item;
std::vector<std::string> elems;
while (std::getline(ss, item, '-')) {
elems.push_back(item);
}
year = std::stoi(elems[0]); // Year
month = std::stoi(elems[1]); // Month
day = std::stoi(elems[2]); // Day
}
else {
// No date found in the string.
throw YAML::TypedBadConversion<
std::chrono::time_point<std::chrono::system_clock>>(node.Mark());
}
// Parse the time string.
if(std::regex_search(s.begin(), s.end(), m, r_tm)) {
std::stringstream ss(m[0]);
std::string item;
std::vector<std::string> elems;
while (std::getline(ss, item, ':')) {
elems.push_back(item);
}
hour = std::stoi(elems[0]); // Hours
minute = std::stoi(elems[1]); // Minutes
second = std::stod(elems[2]); // Seconds
}
// Parse the timezone string.
if(std::regex_search(s.begin(), s.end(), m, r_zn)) {
std::string z = m[0];
// Declared Zulu.
if(z.find('Z') != std::string::npos) {
zone = 0.0f;
} // Potentially non-whole hour.
else if (z.find(':') != std::string::npos) {
std::stringstream ss(m[0]);
std::string item;
std::vector<std::string> elems;
while (std::getline(ss, item, ':')) {
elems.push_back(item);
}
zone = stod(elems[0]); // Apply whole hours.
zone > 0 // Apply partial hours.
? zone += stod(elems[1]) / MIN_PER_HR
: zone -= stod(elems[1]) / MIN_PER_HR;
} // Assumed Zulu.
else {
zone = stod(z);
}
}
// Get the date stored avoiding mktime's problematic local-time return.
l_tm.tm_mday = day;
l_tm.tm_mon = month - 1;
l_tm.tm_year = year - TM_BASE_YEAR;
l_tm.tm_hour = 12; // Noon
l_tm.tm_min = 0;
l_tm.tm_sec = 0;
l_time_t = std::mktime(&l_tm);
if(l_time_t == -1) {
// Invalid std::time_t value parsed from the std::tm;
throw YAML::TypedBadConversion<
std::chrono::time_point<std::chrono::system_clock>>(node.Mark());
}
l_time_t -= static_cast<time_t>(l_time_t % S_PER_DAY);
// Add time of day in seconds having dodged mktime's annoyances.
l_time_t += static_cast<time_t>(
// Hours Minutes Seconds
(hour * S_PER_HR) + (minute * S_PER_MIN) + second
);
// Adjust for the time zone;
l_time_t -= static_cast<time_t>(zone * S_PER_HR);
// Create time_point.
using namespace std::chrono;
time_point<system_clock> l_time_point = system_clock::from_time_t(l_time_t);
// Add milliseconds.
uint32_t ms = static_cast<uint32_t>(second * MS_PER_S) % MS_PER_S;
std::chrono::milliseconds dur_ms{ms};
l_time_point += duration_cast<system_clock::duration>(dur_ms);
// Set rhs.
rhs = l_time_point;
return true;
}
}

View File

@ -239,6 +239,78 @@ TEST(NodeSpecTest, Ex2_18_MultiLineFlowScalars) {
// TODO: 2.19 - 2.22 schema tags // TODO: 2.19 - 2.22 schema tags
TEST(NodeSpecTest, Ex2_22_Timestamps_untagged) {
// Invalid time_t
Node tt("1234-99-99");
// Un-parsable Scalar
Node us("asdf");
ASSERT_THROW(tt.as<std::chrono::system_clock::time_point>(),
YAML::TypedBadConversion<std::chrono::time_point
<std::chrono::system_clock>>);
ASSERT_THROW(us.as<std::chrono::system_clock::time_point>(),
YAML::TypedBadConversion<std::chrono::time_point
<std::chrono::system_clock>>);
// Valid timestamps
std::vector<std::tuple<std::string, std::string>> timestamps;
// Spec Example Tests
timestamps.push_back(std::make_tuple("2001-12-15T02:59:43.1Z",
"2001-12-15T02:59:43.1Z"));
timestamps.push_back(std::make_tuple("2001-12-14t21:59:43.10-06:00",
"2001-12-15T03:59:43.1Z"));
timestamps.push_back(std::make_tuple("2001-12-14 21:59:43.10 -5",
"2001-12-15T02:59:43.1Z"));
timestamps.push_back(std::make_tuple("2001-12-15 2:59:43.10",
"2001-12-15T02:59:43.1Z"));
timestamps.push_back(std::make_tuple("2002-12-14",
"2002-12-14T00:00:00.0Z"));
// Variable Digit Count Tests
timestamps.push_back(std::make_tuple("2000-12-3",
"2000-12-03T00:00:00.0Z"));
timestamps.push_back(std::make_tuple("2000-1-23",
"2000-01-23T00:00:00.0Z"));
timestamps.push_back(std::make_tuple("2000-1-2",
"2000-01-02T00:00:00.0Z"));
// Canonical Tests
timestamps.push_back(std::make_tuple("2001-12-15T02:59:43.1Z",
"2001-12-15T02:59:43.1Z"));
timestamps.push_back(std::make_tuple("2001-12-15T2:09:43.1Z",
"2001-12-15T02:09:43.1Z"));
// ISO8601 Subset Tests
timestamps.push_back(std::make_tuple("2001-12-14t21:59:43.10-05:00",
"2001-12-15T02:59:43.1Z"));
timestamps.push_back(std::make_tuple("2001-12-14t1:59:43.10-05:30",
"2001-12-14T07:29:43.1Z"));
timestamps.push_back(std::make_tuple("2001-12-14t1:59:43.10+05:30",
"2001-12-13T20:29:43.1Z"));
timestamps.push_back(std::make_tuple("2001-12-14t1:59:43.10+05:00",
"2001-12-13T20:59:43.1Z"));
// Spaced Date/Time/Zone Tests
timestamps.push_back(std::make_tuple("2001-12-14 1:59:43.10 -5",
"2001-12-14T06:59:43.1Z"));
timestamps.push_back(std::make_tuple("2001-12-14 21:59:43.10 +5",
"2001-12-14T16:59:43.1Z"));
// No Timezone Test
timestamps.push_back(std::make_tuple("2001-12-14 1:59:43.1",
"2001-12-14T01:59:43.1Z"));
timestamps.push_back(std::make_tuple("2001-12-14 21:59:43.12",
"2001-12-14T21:59:43.1Z"));
Node s_node, tp_node;
for(auto t : timestamps) {
s_node = std::get<0>(t);
tp_node = s_node.as<std::chrono::system_clock::time_point>();
EXPECT_TRUE(s_node.IsScalar());
EXPECT_TRUE(tp_node.IsScalar());
EXPECT_EQ(std::get<1>(t), tp_node.Scalar());
}
}
TEST(NodeSpecTest, Ex2_23_VariousExplicitTags) { TEST(NodeSpecTest, Ex2_23_VariousExplicitTags) {
Node doc = Load(ex2_23); Node doc = Load(ex2_23);
EXPECT_EQ(3, doc.size()); EXPECT_EQ(3, doc.size());