]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
common: Add ISO-8601 Date Support
authorAdam C. Emerson <aemerson@redhat.com>
Wed, 5 Oct 2016 22:02:48 +0000 (18:02 -0400)
committerAdam C. Emerson <aemerson@redhat.com>
Wed, 17 May 2017 18:22:15 +0000 (14:22 -0400)
For parsing and unparsing from ceph::real_time.

Signed-off-by: Adam C. Emerson <aemerson@redhat.com>
src/CMakeLists.txt
src/common/backport14.h
src/common/iso_8601.cc [new file with mode: 0644]
src/common/iso_8601.h [new file with mode: 0644]
src/test/common/CMakeLists.txt
src/test/common/test_iso_8601.cc [new file with mode: 0644]

index c68c5fe142710ec7de2ecf7adb9f2b8f439b0678..01f3de06eeec03b20e996186ff95a50720bd3f3d 100644 (file)
@@ -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
index a7afd49e4d6fdd7ccea8ab4a1d902d95c15d9132..a574cd06f939cec07f9e9155fbbdf9980d4ff130 100644 (file)
 namespace ceph {
 template<typename T>
 using remove_extent_t = typename std::remove_extent<T>::type;
+template<typename T>
+using remove_reference_t = typename std::remove_reference<T>::type;
+template<typename T>
+using result_of_t = typename std::result_of<T>::type;
 
 namespace _backport14 {
 template<typename T>
diff --git a/src/common/iso_8601.cc b/src/common/iso_8601.cc
new file mode 100644 (file)
index 0000000..88828da
--- /dev/null
@@ -0,0 +1,209 @@
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+
+#include <cctype>
+#include <chrono>
+#include <ctime>
+#include <cstdint>
+#include <iostream>
+#include <stdexcept>
+#include <string>
+#include <sstream>
+#include <utility>
+
+#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<uint16_t>(c - '0');
+}
+
+optional<real_time> calculate(const tm& t, uint32_t n = 0) {
+  ceph_assert(n < 1000000000);
+  time_t tt = internal_timegm(&t);
+  if (tt == static_cast<time_t>(-1)) {
+    return none;
+  }
+
+  return real_clock::from_time_t(tt) + nanoseconds(n);
+}
+}
+
+optional<real_time> 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<nanoseconds>(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 (file)
index 0000000..5aa6398
--- /dev/null
@@ -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 <boost/optional.hpp>
+#include <boost/utility/string_ref.hpp>
+
+#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<ceph::real_time> 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
index 3a34fd05c1bde4c4839c44fd0e5452d9184990ee..45f8a43a9a3435d2b3d75bde829409e2b6116b1c 100644 (file)
@@ -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 (file)
index 0000000..dbb3aa2
--- /dev/null
@@ -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 <contact@redhat.com>
+ *
+ * LGPL2.1 (see COPYING-LGPL2.1) or later
+ */
+
+#include <chrono>
+
+#include <gtest/gtest.h>
+
+#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<minutes>(now)),
+           *from_iso_8601(to_iso_8601(now, iso_8601_format::YMDhm)));
+  ASSERT_EQ(real_time(time_point_cast<seconds>(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)));
+}