From 96f9a6747dd18f82136e528817d7eda03b979ced Mon Sep 17 00:00:00 2001 From: "U.G. Wilson" Date: Wed, 1 Apr 2015 00:27:02 -0500 Subject: [PATCH] Support parsing of timestamp strings to C++11 time_points. --- include/yaml-cpp/node/convert.h | 20 ++++ src/convert.cpp | 159 ++++++++++++++++++++++++++++ test/integration/node_spec_test.cpp | 72 +++++++++++++ 3 files changed, 251 insertions(+) diff --git a/include/yaml-cpp/node/convert.h b/include/yaml-cpp/node/convert.h index cbd3189..a850e38 100644 --- a/include/yaml-cpp/node/convert.h +++ b/include/yaml-cpp/node/convert.h @@ -7,6 +7,7 @@ #pragma once #endif +#include #include #include #include @@ -281,6 +282,25 @@ struct convert { return true; } }; + +// std::chrono::time_point +// 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> { + + YAML_CPP_API static Node encode( + const std::chrono::time_point& rhs); + + YAML_CPP_API static bool decode(const Node& node, + std::chrono::time_point& rhs); +}; + } #endif // NODE_CONVERT_H_62B23520_7C8E_11DE_8A39_0800200C9A66 diff --git a/src/convert.cpp b/src/convert.cpp index ec05b77..4bf3d8d 100644 --- a/src/convert.cpp +++ b/src/convert.cpp @@ -1,5 +1,10 @@ #include +#include +#include +#include +#include +#include "yaml-cpp/exceptions.h" #include "yaml-cpp/node/convert.h" namespace { @@ -73,3 +78,157 @@ bool convert::decode(const Node& node, bool& rhs) { return false; } } + +namespace YAML { +Node convert>::encode( + const std::chrono::time_point& 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(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>::decode( + const Node& node, + std::chrono::time_point& 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 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>(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 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 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>(node.Mark()); + } + l_time_t -= static_cast(l_time_t % S_PER_DAY); + + // Add time of day in seconds having dodged mktime's annoyances. + l_time_t += static_cast( + // Hours Minutes Seconds + (hour * S_PER_HR) + (minute * S_PER_MIN) + second + ); + + // Adjust for the time zone; + l_time_t -= static_cast(zone * S_PER_HR); + + // Create time_point. + using namespace std::chrono; + time_point l_time_point = system_clock::from_time_t(l_time_t); + + // Add milliseconds. + uint32_t ms = static_cast(second * MS_PER_S) % MS_PER_S; + std::chrono::milliseconds dur_ms{ms}; + l_time_point += duration_cast(dur_ms); + + // Set rhs. + rhs = l_time_point; + + return true; +} + +} diff --git a/test/integration/node_spec_test.cpp b/test/integration/node_spec_test.cpp index aedf38b..dc28eba 100644 --- a/test/integration/node_spec_test.cpp +++ b/test/integration/node_spec_test.cpp @@ -239,6 +239,78 @@ TEST(NodeSpecTest, Ex2_18_MultiLineFlowScalars) { // 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(), + YAML::TypedBadConversion>); + ASSERT_THROW(us.as(), + YAML::TypedBadConversion>); + + // Valid timestamps + std::vector> 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(); + + EXPECT_TRUE(s_node.IsScalar()); + EXPECT_TRUE(tp_node.IsScalar()); + + EXPECT_EQ(std::get<1>(t), tp_node.Scalar()); + } +} + TEST(NodeSpecTest, Ex2_23_VariousExplicitTags) { Node doc = Load(ex2_23); EXPECT_EQ(3, doc.size());