From 7cd1ccf42228dcdeb361d569e5b11f88a505a745 Mon Sep 17 00:00:00 2001 From: "Adam C. Emerson" Date: Wed, 5 Oct 2016 18:02:48 -0400 Subject: [PATCH] common: Add ISO-8601 Date Support For parsing and unparsing from ceph::real_time. Signed-off-by: Adam C. Emerson --- src/CMakeLists.txt | 1 + src/common/backport14.h | 4 + src/common/iso_8601.cc | 209 +++++++++++++++++++++++++++++++ src/common/iso_8601.h | 44 +++++++ src/test/common/CMakeLists.txt | 6 + src/test/common/test_iso_8601.cc | 60 +++++++++ 6 files changed, 324 insertions(+) create mode 100644 src/common/iso_8601.cc create mode 100644 src/common/iso_8601.h create mode 100644 src/test/common/test_iso_8601.cc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c68c5fe142710..01f3de06eeec0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -439,6 +439,7 @@ set(libcommon_files common/TrackedOp.cc common/SloppyCRCMap.cc common/types.cc + common/iso_8601.cc log/Log.cc log/SubsystemMap.cc mon/MonCap.cc diff --git a/src/common/backport14.h b/src/common/backport14.h index a7afd49e4d6fd..a574cd06f939c 100644 --- a/src/common/backport14.h +++ b/src/common/backport14.h @@ -23,6 +23,10 @@ namespace ceph { template using remove_extent_t = typename std::remove_extent::type; +template +using remove_reference_t = typename std::remove_reference::type; +template +using result_of_t = typename std::result_of::type; namespace _backport14 { template diff --git a/src/common/iso_8601.cc b/src/common/iso_8601.cc new file mode 100644 index 0000000000000..88828dac0cad2 --- /dev/null +++ b/src/common/iso_8601.cc @@ -0,0 +1,209 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "include/timegm.h" +#include "iso_8601.h" + + +namespace ceph { +using std::chrono::duration_cast; +using std::chrono::nanoseconds; +using std::chrono::seconds; +using std::setfill; +using std::setw; +using std::size_t; +using std::stringstream; +using std::string; +using std::uint16_t; + +using boost::none; +using boost::optional; +using boost::string_ref; + +using ceph::real_clock; +using ceph::real_time; + +using sriter = string_ref::const_iterator; + +namespace { +// This assumes a contiguous block of numbers in the correct order. +uint16_t digit(char c) { + if (!(c >= '0' && c <= '9')) { + throw std::invalid_argument("Not a digit."); + } + return static_cast(c - '0'); +} + +optional calculate(const tm& t, uint32_t n = 0) { + ceph_assert(n < 1000000000); + time_t tt = internal_timegm(&t); + if (tt == static_cast(-1)) { + return none; + } + + return real_clock::from_time_t(tt) + nanoseconds(n); +} +} + +optional from_iso_8601(const string_ref s, + const bool ws_terminates) noexcept { + auto end = s.cend(); + auto read_digit = [end](sriter& c) mutable { + if (c == end) { + throw std::invalid_argument("End of input."); + } + auto f = digit(*c); + ++c; + return f; + }; + + auto read_digits = [end, &read_digit](sriter& c, std::size_t n) { + auto v = 0ULL; + for (auto i = 0U; i < n; ++i) { + auto d = read_digit(c); + v = (10ULL * v) + d; + } + return v; + }; + auto partial_date = [end, ws_terminates](sriter& c) { + return (c == end || (ws_terminates && std::isspace(*c))); + }; + auto time_end = [end, ws_terminates](sriter& c) { + return (c != end && *c == 'Z' && + ((c + 1) == end || + (ws_terminates && std::isspace(*(c + 1))))); + }; + auto consume_delimiter = [end](sriter& c, char q) { + if (c == end || *c != q) { + throw std::invalid_argument("Expected delimiter not found."); + } else { + ++c; + } + }; + + tm t = { 0, // tm_sec + 0, // tm_min + 0, // tm_hour + 1, // tm_mday + 0, // tm_mon + 70, // tm_year + 0, // tm_wday + 0, // tm_yday + 0, // tm_isdst + }; + try { + auto c = s.cbegin(); + { + auto y = read_digits(c, 4); + if (y < 1970) { + return none; + } + t.tm_year = y - 1900; + } + if (partial_date(c)) { + return calculate(t, 0); + } + + consume_delimiter(c, '-'); + t.tm_mon = (read_digits(c, 2) - 1); + if (partial_date(c)) { + return calculate(t); + } + consume_delimiter(c, '-'); + t.tm_mday = read_digits(c, 2); + if (partial_date(c)) { + return calculate(t); + } + consume_delimiter(c, 'T'); + t.tm_hour = read_digits(c, 2); + if (time_end(c)) { + return calculate(t); + } + consume_delimiter(c, ':'); + t.tm_min = read_digits(c, 2); + if (time_end(c)) { + return calculate(t); + } + consume_delimiter(c, ':'); + t.tm_sec = read_digits(c, 2); + if (time_end(c)) { + return calculate(t); + } + consume_delimiter(c, '.'); + + auto n = 0UL; + auto multiplier = 100000000UL; + for (auto i = 0U; i < 9U; ++i) { + auto d = read_digit(c); + n += d * multiplier; + multiplier /= 10; + if (time_end(c)) { + return calculate(t, n); + } + } + } catch (std::invalid_argument& e) { + // fallthrough + } + return none; +} + +string to_iso_8601(const real_time t, + const iso_8601_format f) noexcept { + ceph_assert(f >= iso_8601_format::Y && + f <= iso_8601_format::YMDhmsn); + stringstream out(std::ios_base::out); + + auto sec = real_clock::to_time_t(t); + auto nsec = duration_cast(t.time_since_epoch() % + seconds(1)).count(); + + struct tm bt; + gmtime_r(&sec, &bt); + out.fill('0'); + + out << 1900 + bt.tm_year; + if (f == iso_8601_format::Y) { + return out.str(); + } + + out << '-' << setw(2) << bt.tm_mon + 1; + if (f == iso_8601_format::YM) { + return out.str(); + } + + out << '-' << setw(2) << bt.tm_mday; + if (f == iso_8601_format::YMD) { + return out.str(); + } + + out << 'T' << setw(2) << bt.tm_hour; + if (f == iso_8601_format::YMDh) { + out << 'Z'; + return out.str(); + } + + out << ':' << setw(2) << bt.tm_min; + if (f == iso_8601_format::YMDhm) { + out << 'Z'; + return out.str(); + } + + out << ':' << setw(2) << bt.tm_sec; + if (f == iso_8601_format::YMDhms) { + out << 'Z'; + return out.str(); + } + out << '.' << setw(9) << nsec << 'Z'; + return out.str(); +} +} diff --git a/src/common/iso_8601.h b/src/common/iso_8601.h new file mode 100644 index 0000000000000..5aa6398338694 --- /dev/null +++ b/src/common/iso_8601.h @@ -0,0 +1,44 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#ifndef CEPH_COMMON_ISO_8601_H +#define CEPH_COMMON_ISO_8601_H + +#include +#include + +#include "common/ceph_time.h" + +namespace ceph { + +// Here, we support the W3C profile of ISO 8601 with the following +// restrictions: +// - Subsecond resolution is supported to nanosecond +// granularity. Any number of digits between 1 and 9 may be +// specified after the decimal point. +// - All times must be UTC. +// - All times must be representable as a sixty-four bit count of +// nanoseconds since the epoch. +// - Partial times are handled thus: +// * If there are no subseconds, they are assumed to be zero. +// * If there are no seconds, they are assumed to be zero. +// * If there are no minutes, they are assumed to be zero. +// * If there is no time, it is assumed to midnight. +// * If there is no day, it is assumed to be the first. +// * If there is no month, it is assumed to be January. +// +// If a date is invalid, boost::none is returned. + +boost::optional from_iso_8601( + boost::string_ref s, const bool ws_terminates = true) noexcept; + +enum class iso_8601_format { + Y, YM, YMD, YMDh, YMDhm, YMDhms, YMDhmsn +}; + +std::string to_iso_8601(const ceph::real_time t, + const iso_8601_format f = iso_8601_format::YMDhmsn) + noexcept; +} + +#endif diff --git a/src/test/common/CMakeLists.txt b/src/test/common/CMakeLists.txt index 3a34fd05c1bde..45f8a43a9a343 100644 --- a/src/test/common/CMakeLists.txt +++ b/src/test/common/CMakeLists.txt @@ -259,3 +259,9 @@ add_executable(unittest_hostname target_link_libraries(unittest_hostname ceph-common) add_ceph_unittest(unittest_hostname ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/unittest_hostname) + +add_executable(unittest_iso_8601 + test_iso_8601.cc) +target_link_libraries(unittest_iso_8601 ceph-common) +add_ceph_unittest(unittest_iso_8601 + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/unittest_hostname) diff --git a/src/test/common/test_iso_8601.cc b/src/test/common/test_iso_8601.cc new file mode 100644 index 0000000000000..dbb3aa2a366e5 --- /dev/null +++ b/src/test/common/test_iso_8601.cc @@ -0,0 +1,60 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab +/* + * Ceph - scalable distributed file system + * + * Copyright (C) 2017 Red Hat + * + * LGPL2.1 (see COPYING-LGPL2.1) or later + */ + +#include + +#include + +#include "common/ceph_time.h" +#include "common/iso_8601.h" + +using std::chrono::minutes; +using std::chrono::seconds; +using std::chrono::time_point_cast; + +using ceph::from_iso_8601; +using ceph::iso_8601_format; +using ceph::real_clock; +using ceph::real_time; +using ceph::to_iso_8601; + +TEST(iso_8601, epoch) { + const auto epoch = real_clock::from_time_t(0); + + ASSERT_EQ("1970", to_iso_8601(epoch, iso_8601_format::Y)); + ASSERT_EQ("1970-01", to_iso_8601(epoch, iso_8601_format::YM)); + ASSERT_EQ("1970-01-01", to_iso_8601(epoch, iso_8601_format::YMD)); + ASSERT_EQ("1970-01-01T00Z", to_iso_8601(epoch, iso_8601_format::YMDh)); + ASSERT_EQ("1970-01-01T00:00Z", to_iso_8601(epoch, iso_8601_format::YMDhm)); + ASSERT_EQ("1970-01-01T00:00:00Z", + to_iso_8601(epoch, iso_8601_format::YMDhms)); + ASSERT_EQ("1970-01-01T00:00:00.000000000Z", + to_iso_8601(epoch, iso_8601_format::YMDhmsn)); + + ASSERT_EQ(epoch, *from_iso_8601("1970")); + ASSERT_EQ(epoch, *from_iso_8601("1970-01")); + ASSERT_EQ(epoch, *from_iso_8601("1970-01-01")); + ASSERT_EQ(epoch, *from_iso_8601("1970-01-01T00:00Z")); + ASSERT_EQ(epoch, *from_iso_8601("1970-01-01T00:00:00Z")); + ASSERT_EQ(epoch, *from_iso_8601("1970-01-01T00:00:00.000000000Z")); +} + +TEST(iso_8601, now) { + const auto now = real_clock::now(); + + ASSERT_EQ(real_time(time_point_cast(now)), + *from_iso_8601(to_iso_8601(now, iso_8601_format::YMDhm))); + ASSERT_EQ(real_time(time_point_cast(now)), + *from_iso_8601( + to_iso_8601(now, iso_8601_format::YMDhms))); + ASSERT_EQ(now, + *from_iso_8601( + to_iso_8601(now, iso_8601_format::YMDhmsn))); +} -- 2.39.5