implement chrono conversions for unusual duration types
authorAdam Simpkins <simpkins@fb.com>
Fri, 1 Dec 2017 19:18:17 +0000 (11:18 -0800)
committerFacebook Github Bot <facebook-github-bot@users.noreply.github.com>
Fri, 1 Dec 2017 19:26:53 +0000 (11:26 -0800)
Summary:
Implement conversions between std::chrono::duration and POSIX-style time
structures even when neither the numerator nor the denominator of the duration
ratio are 1.

Both of these are done by first converting to an intermediate type where the
numerator is 1, and then using the conversion routines for that case.

Reviewed By: yfeldblum

Differential Revision: D6366647

fbshipit-source-id: 8f9495fb4101cac6d8b4cf0353a679107007b298

folly/chrono/Conv.h
folly/chrono/test/ConvTest.cpp

index 688658e..1cd6a8b 100644 (file)
@@ -187,6 +187,36 @@ static Expected<std::pair<time_t, long>, ConversionCode> durationToPosixTime(
   return std::pair<time_t, long>{sec, subsec};
 }
 
+/*
+ * Helper classes for picking an intermediate duration type to use
+ * when doing conversions to/from durations where neither the numerator nor
+ * denominator are 1.
+ */
+template <typename T, bool IsFloatingPoint, bool IsSigned>
+struct IntermediateTimeRep {};
+template <typename T, bool IsSigned>
+struct IntermediateTimeRep<T, true, IsSigned> {
+  using type = T;
+};
+template <typename T>
+struct IntermediateTimeRep<T, false, true> {
+  using type = intmax_t;
+};
+template <typename T>
+struct IntermediateTimeRep<T, false, false> {
+  using type = uintmax_t;
+};
+// For IntermediateDuration we always use 1 as the numerator, and the original
+// Period denominator.  This ensures that we do not lose precision when
+// performing the conversion.
+template <typename Rep, typename Period>
+using IntermediateDuration = std::chrono::duration<
+    typename IntermediateTimeRep<
+        Rep,
+        std::is_floating_point<Rep>::value,
+        std::is_signed<Rep>::value>::type,
+    std::ratio<1, Period::den>>;
+
 /**
  * Convert a std::chrono::duration to a pair of (seconds, subseconds)
  *
@@ -201,18 +231,28 @@ Expected<std::pair<time_t, long>, ConversionCode> durationToPosixTime(
   static_assert(
       SubsecondRatio::num == 1, "subsecond numerator should always be 1");
 
-  // TODO: We need to implement an overflow-checking tryTo() function for
-  // duration-to-duration casts for the code above to work.
-  //
-  // For now this is unimplemented, and we just have a static_assert that
-  // will always fail if someone tries to instantiate this.  Unusual duration
-  // types should be extremely rare, and I'm not aware of any code at the
-  // moment that actually needs this.
-  static_assert(
-      Period::num == 1,
-      "conversion from unusual duration types is not implemented yet");
-  (void)duration;
-  return makeUnexpected(ConversionCode::SUCCESS);
+  // Perform this conversion by first converting to a duration where the
+  // numerator is 1, then convert to the output type.
+  using IntermediateType = IntermediateDuration<Rep, Period>;
+  using IntermediateRep = typename IntermediateType::rep;
+
+  // Check to see if we would have overflow converting to the intermediate
+  // type.
+  constexpr auto maxInput =
+      std::numeric_limits<IntermediateRep>::max() / Period::num;
+  if (duration.count() > maxInput) {
+    return makeUnexpected(ConversionCode::POSITIVE_OVERFLOW);
+  }
+  constexpr auto minInput =
+      std::numeric_limits<IntermediateRep>::min() / Period::num;
+  if (duration.count() < minInput) {
+    return makeUnexpected(ConversionCode::NEGATIVE_OVERFLOW);
+  }
+  auto intermediate =
+      IntermediateType{static_cast<IntermediateRep>(duration.count()) *
+                       static_cast<IntermediateRep>(Period::num)};
+
+  return durationToPosixTime<SubsecondRatio>(intermediate);
 }
 
 /**
@@ -489,19 +529,25 @@ auto posixTimeToDuration(
   static_assert(
       SubsecondRatio::num == 1, "subsecond numerator should always be 1");
 
-  // TODO: We need to implement an overflow-checking tryTo() function for
-  // duration-to-duration casts for the code above to work.
+  // Cast through an intermediate type with subsecond granularity.
+  // Note that this could fail due to overflow during the initial conversion
+  // even if the result is representable in the output POSIX-style types.
   //
-  // For now this is unimplemented, and we just have a static_assert that
-  // will always fail if someone tries to instantiate this.  Unusual duration
-  // types should be extremely rare, and I'm not aware of any code at the
-  // moment that actually needs this.
-  static_assert(
-      Tgt::period::num == 1,
-      "conversion to unusual duration types is not implemented yet");
-  (void)seconds;
-  (void)subseconds;
-  return makeUnexpected(ConversionCode::SUCCESS);
+  // Note that for integer type conversions going through this intermediate
+  // type can result in slight imprecision due to truncating the intermediate
+  // calculation to an integer.
+  using IntermediateType =
+      IntermediateDuration<typename Tgt::rep, typename Tgt::period>;
+  auto intermediate = posixTimeToDuration<SubsecondRatio>(
+      seconds, subseconds, IntermediateType{});
+  if (intermediate.hasError()) {
+    return makeUnexpected(intermediate.error());
+  }
+  // Now convert back to the target duration.  Use tryTo() to confirm that the
+  // result fits in the target representation type.
+  return tryTo<typename Tgt::rep>(
+             intermediate.value().count() / Tgt::period::num)
+      .then([](typename Tgt::rep tgt) { return Tgt{tgt}; });
 }
 
 template <
index 172bdd6..7331292 100644 (file)
@@ -124,6 +124,24 @@ TEST(Conv, timespecToStdChrono) {
   ts.tv_nsec = 0;
   auto doubleMinutes = to<duration<double, std::ratio<60>>>(ts);
   EXPECT_EQ(1.5, doubleMinutes.count());
+
+  // Test with unusual durations where neither the numerator nor denominator
+  // are 1.
+  using five_sevenths = std::chrono::duration<int64_t, std::ratio<5, 7>>;
+  ts.tv_sec = 1;
+  ts.tv_nsec = 0;
+  EXPECT_EQ(1, to<five_sevenths>(ts).count());
+  ts.tv_sec = 1;
+  ts.tv_nsec = 428571500;
+  EXPECT_EQ(2, to<five_sevenths>(ts).count());
+
+  using thirteen_thirds = std::chrono::duration<double, std::ratio<13, 3>>;
+  ts.tv_sec = 39;
+  ts.tv_nsec = 0;
+  EXPECT_NEAR(9.0, to<thirteen_thirds>(ts).count(), 0.000000001);
+  ts.tv_sec = 1;
+  ts.tv_nsec = 0;
+  EXPECT_NEAR(0.230769230, to<thirteen_thirds>(ts).count(), 0.000000001);
 }
 
 TEST(Conv, timespecToStdChronoOverflow) {
@@ -241,6 +259,22 @@ TEST(Conv, timespecToStdChronoOverflow) {
   EXPECT_EQ(
       std::numeric_limits<decltype(ts.tv_sec)>::max() / 3600,
       to<hours_u64>(ts).count());
+
+  // Test overflow with an unusual duration where neither the numerator nor
+  // denominator are 1.
+  using unusual_time = std::chrono::duration<int16_t, std::ratio<13, 3>>;
+  ts.tv_sec = 141994;
+  ts.tv_nsec = 666666666;
+  EXPECT_EQ(32767, to<unusual_time>(ts).count());
+  ts.tv_nsec = 666666667;
+  EXPECT_THROW(to<unusual_time>(ts), std::range_error);
+
+  ts.tv_sec = -141998;
+  ts.tv_nsec = 999999999;
+  EXPECT_EQ(-32768, to<unusual_time>(ts).count());
+  ts.tv_sec = -141999;
+  ts.tv_nsec = 0;
+  EXPECT_THROW(to<unusual_time>(ts), std::range_error);
 }
 
 TEST(Conv, timevalToStdChrono) {
@@ -334,6 +368,20 @@ TEST(Conv, stdChronoToTimespec) {
   ts = to<struct timespec>(createTimePoint<system_clock>(123ns));
   EXPECT_EQ(0, ts.tv_sec);
   EXPECT_EQ(123, ts.tv_nsec);
+
+  // Test with some unusual durations where neither the numerator nor
+  // denominator are 1.
+  using five_sevenths = std::chrono::duration<int64_t, std::ratio<5, 7>>;
+  ts = to<struct timespec>(five_sevenths(7));
+  EXPECT_EQ(5, ts.tv_sec);
+  EXPECT_EQ(0, ts.tv_nsec);
+  ts = to<struct timespec>(five_sevenths(19));
+  EXPECT_EQ(13, ts.tv_sec);
+  EXPECT_EQ(571428571, ts.tv_nsec);
+  using seven_fifths = std::chrono::duration<int64_t, std::ratio<7, 5>>;
+  ts = to<struct timespec>(seven_fifths(5));
+  EXPECT_EQ(7, ts.tv_sec);
+  EXPECT_EQ(0, ts.tv_nsec);
 }
 
 TEST(Conv, stdChronoToTimespecOverflow) {
@@ -359,6 +407,13 @@ TEST(Conv, stdChronoToTimespecOverflow) {
     EXPECT_EQ(ts.tv_nsec, 0);
     EXPECT_THROW(
         to<struct timespec>(hours_i64(2562047788015216LL)), std::range_error);
+
+    // Test overflows from an unusual duration where neither the numerator nor
+    // denominator are 1.
+    using three_halves = std::chrono::duration<uint64_t, std::ratio<3, 2>>;
+    EXPECT_THROW(
+        to<struct timespec>(three_halves(6148914691236517206ULL)),
+        std::range_error);
   }
 
   // Test for overflow.