Repository: alexhsamuel/ora (language: python)

Issue:
don't allow creation of out-of-range times
```
>>> ora.Unix32Time(2049,1,1,0,0,0,"UTC").offset
-1801895296
```
This should raise instead.


Discussion / hints:

Fix patch:
diff --git a/cxx/include/ora/lib/math.hh b/cxx/include/ora/lib/math.hh
index 6e82b50..2ae389b 100644
--- a/cxx/include/ora/lib/math.hh
+++ b/cxx/include/ora/lib/math.hh
@@ -108,30 +108,35 @@ in_interval(
 }
 
 
-// FIXME: Not used. (?)  And probably doesn't work for __int128.
 /*
  * True if `val` overflows when conveted from integer types `FROM` to `TO`.
  */
 template<class TO, class FROM>
 inline bool constexpr
 overflows(
-  FROM val) 
+  FROM val)
 {
   static_assert(
-    std::numeric_limits<FROM>::is_integer, 
+    std::numeric_limits<FROM>::is_integer,
     "overflows() for integer types only");
   static_assert(
-    std::numeric_limits<TO>::is_integer, 
+    // With some compilers, std::numeric_limits<__int128>::is_integer is false.
+    std::numeric_limits<TO>::is_integer || sizeof(TO) == 16,
     "overflows() for integer types only");
 
   return
-    std::numeric_limits<TO>::is_signed
-    ? 
-       (   !std::numeric_limits<FROM>::is_signed 
-        && (uintmax_t) val > (uintmax_t) INTMAX_MAX) 
-    || (intmax_t) val < (intmax_t) std::numeric_limits<TO>::min() 
+    // Won't overflow if converting to int128.
+      sizeof(TO) == 16
+    ? false
+    // Signed case.
+    : std::numeric_limits<TO>::is_signed
+    ?
+       (   !std::numeric_limits<FROM>::is_signed
+        && (uintmax_t) val > (uintmax_t) INTMAX_MAX)
+    || (intmax_t) val < (intmax_t) std::numeric_limits<TO>::min()
     || (intmax_t) val > (intmax_t) std::numeric_limits<TO>::max()
-    : 
+    // Unsigned case.
+    :
        val < 0
     || (uintmax_t) val > (uintmax_t) std::numeric_limits<TO>::max();
 }
diff --git a/cxx/include/ora/time_math.hh b/cxx/include/ora/time_math.hh
index c54e335..f1de669 100644
--- a/cxx/include/ora/time_math.hh
+++ b/cxx/include/ora/time_math.hh
@@ -43,10 +43,10 @@ datenum_daytick_to_offset(
   TimeZoneOffset const tz_offset)
 {
   using Offset = typename TRAITS::Offset;
-  static auto constexpr denominator = TRAITS::denominator;
+  static Offset constexpr denominator = TRAITS::denominator;
   static auto constexpr base = TRAITS::base;
   // The datenum of the day containing the minimum time.
-  static auto constexpr min_datenum 
+  static auto constexpr min_datenum
     = (Datenum) (TRAITS::base + TRAITS::min / (Offset) SECS_PER_DAY);
 
   // To obtain the final offset, we need to add these three terms:
@@ -68,7 +68,7 @@ datenum_daytick_to_offset(
   int64_t const date_secs = ((int64_t) datenum - base) * SECS_PER_DAY;
   int64_t date_tz_secs = date_secs - tz_offset;
 
-  auto daytime_offset = rescale_int(daytick, DAYTICK_PER_SEC, denominator);
+  Offset daytime_offset = rescale_int(daytick, DAYTICK_PER_SEC, denominator);
 
   // Special case: if the time occurs on the first representable date, but
   // midnight of that date is not representable, we'd overflow if we computed
@@ -81,9 +81,10 @@ datenum_daytick_to_offset(
     date_tz_secs += SECS_PER_DAY;
     daytime_offset -= SECS_PER_DAY * denominator;
   }
-    
+
   Offset offset;
-  if (   mul_overflow(denominator, (Offset) date_tz_secs, offset)
+  if (   overflows<Offset, int64_t>(date_tz_secs)
+      || mul_overflow(denominator, (Offset) date_tz_secs, offset)
       || add_overflow(offset, daytime_offset, offset))
     throw TimeRangeError();
   return offset;
diff --git a/python/ora/ext/py_time.hh b/python/ora/ext/py_time.hh
index e5943de..8b78f4c 100644
--- a/python/ora/ext/py_time.hh
+++ b/python/ora/ext/py_time.hh
@@ -341,17 +341,17 @@ PyTime<TIME>::tp_init(
   if (kw_args != nullptr)
     throw TypeError("function takes no keyword arguments");
   auto const num_args = args->Length();
-  Time time;
+  TIME time;
   if (num_args == 0)
     ;
   else if (num_args == 1)
-    time = convert_to_time<Time>(args->GetItem(0));
+    time = convert_to_time<TIME>(args->GetItem(0));
   else if (num_args == 2)
-    time = localtime_to_time<Time>(args);
+    time = localtime_to_time<TIME>(args);
   else if (num_args == 3)
-    time = date_daytime_to_time<Time>(args);
+    time = date_daytime_to_time<TIME>(args);
   else if (num_args == 7 || num_args == 8)
-    time = parts_to_time<Time>(args);
+    time = parts_to_time<TIME>(args);
   else
     throw TypeError("function takes 0, 1, 2, 3, 7, or 8 arguments");
 


Test patch:
diff --git a/python/ora/test/test_time_basic.py b/python/ora/test/test_time_basic.py
index 7d9ffde..2fb17cc 100644
--- a/python/ora/test/test_time_basic.py
+++ b/python/ora/test/test_time_basic.py
@@ -338,7 +338,7 @@ def test_add_overfow(Time):
     with pytest.raises(OverflowError):  # FIXME: Wrong exception?
         t0 + float("nan")
 
-    
+
 @pytest.mark.parametrize("Time0, Time1", TIME_TYPE_PAIRS)
 def test_time_convert(Time0, Time1):
     assert Time1(Time0.INVALID).invalid
@@ -367,3 +367,33 @@ def test_string_convert():
     assert Time("2020-01-28 18:13:25R") == t
 
 
+def test_out_of_range():
+    with pytest.raises(ora.TimeRangeError):
+        Unix32Time(2038,  1, 19,  3, 14,  6, UTC)
+    with pytest.raises(ora.TimeRangeError):
+        Unix32Time(2039,  1,  1,  0,  0,  0, UTC)
+    with pytest.raises(ora.TimeRangeError):
+        Unix32Time(9999, 12, 31, 23, 59, 59, UTC)
+
+    with pytest.raises(ora.TimeRangeError):
+        Unix32Time(1901, 12, 13, 20, 45, 51, UTC)
+    with pytest.raises(ora.TimeRangeError):
+        Unix32Time(1901,  1,  1,  0,  0,  0, UTC)
+    with pytest.raises(ora.TimeRangeError):
+        Unix32Time(   1,  1,  1,  0,  0,  0, UTC)
+
+    with pytest.raises(ora.TimeRangeError):
+        HiTime(1969, 12, 31, 23, 59, 59, UTC)
+    with pytest.raises(ora.TimeRangeError):
+        HiTime(2106,  2,  7,  6, 28, 17, UTC)
+
+    with pytest.raises(ora.TimeRangeError):
+        NsTime(1677,  9, 21,  0, 12, 43.145224191, UTC)
+    with pytest.raises(ora.TimeRangeError):
+        NsTime(2262,  4, 11, 23, 47, 16.854775806, UTC)
+
+    with pytest.raises(ora.TimeRangeError):
+        SmallTime(1969, 12, 31, 23, 59, 59, UTC)
+    with pytest.raises(ora.TimeRangeError):
+        SmallTime(2106,  2,  7,  6, 28, 14, UTC)
+