Repository: crsmithdev/arrow
Pull request #670 (2019-09-11 19:54:15)

Allow factory.get() to create from a ISO calendar tuple
ref #189 

**TODO**

- [x] Error handling.
- [x] Test edge cases.
- [x] Docstring.
- [x] Docs


Patch:
diff --git a/arrow/factory.py b/arrow/factory.py
index f3f3b7f..f9f2d65 100644
--- a/arrow/factory.py
+++ b/arrow/factory.py
@@ -17,7 +17,7 @@ from dateutil import tz as dateutil_tz
 
 from arrow import parser
 from arrow.arrow import Arrow
-from arrow.util import is_timestamp, isstr
+from arrow.util import is_timestamp, iso_to_gregorian, isstr
 
 
 class ArrowFactory(object):
@@ -99,6 +99,16 @@ class ArrowFactory(object):
             >>> arrow.get(date(2013, 5, 5))
             <Arrow [2013-05-05T00:00:00+00:00]>
 
+        **One** time.struct time::
+
+            >>> arrow.get(gmtime(0))
+            <Arrow [1970-01-01T00:00:00+00:00]>
+
+        **One** iso calendar ``tuple``, to get that week date in UTC::
+
+            >>> arrow.get((2013, 18, 7))
+            <Arrow [2013-05-05T00:00:00+00:00]>
+
         **Two** arguments, a naive or aware ``datetime``, and a replacement
         :ref:`timezone expression <tz-expr>`::
 
@@ -126,11 +136,6 @@ class ArrowFactory(object):
             >>> arrow.get(2013, 5, 5, 12, 30, 45)
             <Arrow [2013-05-05T12:30:45+00:00]>
 
-        **One** time.struct time::
-
-            >>> arrow.get(gmtime(0))
-            <Arrow [1970-01-01T00:00:00+00:00]>
-
         """
 
         arg_count = len(args)
@@ -192,6 +197,11 @@ class ArrowFactory(object):
             elif isinstance(arg, struct_time):
                 return self.type.utcfromtimestamp(calendar.timegm(arg))
 
+            # (iso calendar) -> convert then from date
+            elif isinstance(arg, tuple) and len(arg) == 3:
+                dt = iso_to_gregorian(*arg)
+                return self.type.fromdate(dt)
+
             else:
                 raise TypeError(
                     "Can't parse single argument type of '{}'".format(type(arg))
diff --git a/arrow/util.py b/arrow/util.py
index 8a379a4..7faea5c 100644
--- a/arrow/util.py
+++ b/arrow/util.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
-from datetime import datetime
+import datetime
 
 
 def total_seconds(td):  # pragma: no cover
@@ -12,12 +12,31 @@ def is_timestamp(value):
     if isinstance(value, bool):
         return False
     try:
-        datetime.fromtimestamp(value)
+        datetime.datetime.fromtimestamp(value)
         return True
     except TypeError:
         return False
 
 
+# Credit to https://stackoverflow.com/a/1700069
+def iso_to_gregorian(iso_year, iso_week, iso_day):
+    """Converts an ISO week date tuple into a datetime object."""
+
+    if not 1 <= iso_week <= 53:
+        raise ValueError("ISO Calendar week value must be between 1-53.")
+
+    if not 1 <= iso_day <= 7:
+        raise ValueError("ISO Calendar day value must be between 1-7")
+
+    # The first week of the year always contains 4 Jan.
+    fourth_jan = datetime.date(iso_year, 1, 4)
+    delta = datetime.timedelta(fourth_jan.isoweekday() - 1)
+    year_start = fourth_jan - delta
+    gregorian = year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1)
+
+    return gregorian
+
+
 # Python 2.7 / 3.0+ definitions for isstr function.
 
 try:  # pragma: no cover
@@ -33,4 +52,4 @@ except NameError:  # pragma: no cover
         return isinstance(s, str)
 
 
-__all__ = ["total_seconds", "is_timestamp", "isstr"]
+__all__ = ["total_seconds", "is_timestamp", "isstr", "iso_to_gregorian"]


Test patch:
diff --git a/tests/factory_tests.py b/tests/factory_tests.py
index bccc163..3b4c7a9 100644
--- a/tests/factory_tests.py
+++ b/tests/factory_tests.py
@@ -140,6 +140,35 @@ class GetTests(Chai):
 
         assertDtEqual(self.factory.get(dt.isoformat()), dt.replace(tzinfo=tz.tzutc()))
 
+    def test_one_arg_iso_calendar(self):
+
+        pairs = [
+            (datetime(2004, 1, 4), (2004, 1, 7)),
+            (datetime(2008, 12, 30), (2009, 1, 2)),
+            (datetime(2010, 1, 2), (2009, 53, 6)),
+            (datetime(2000, 2, 29), (2000, 9, 2)),
+            (datetime(2005, 1, 1), (2004, 53, 6)),
+            (datetime(2010, 1, 4), (2010, 1, 1)),
+            (datetime(2010, 1, 3), (2009, 53, 7)),
+            (datetime(2003, 12, 29), (2004, 1, 1)),
+        ]
+
+        for pair in pairs:
+            dt, iso = pair
+            self.assertEqual(self.factory.get(iso), self.factory.get(dt))
+
+        with self.assertRaises(TypeError):
+            self.factory.get((2014, 7, 1, 4))
+
+        with self.assertRaises(TypeError):
+            self.factory.get((2014, 7))
+
+        with self.assertRaises(ValueError):
+            self.factory.get((2014, 70, 1))
+
+        with self.assertRaises(ValueError):
+            self.factory.get((2014, 7, 10))
+
     def test_one_arg_other(self):
 
         with self.assertRaises(TypeError):
diff --git a/tests/util_tests.py b/tests/util_tests.py
index 7216977..21e98c7 100644
--- a/tests/util_tests.py
+++ b/tests/util_tests.py
@@ -29,3 +29,10 @@ class UtilTests(Chai):
         overflow_timestamp_int = int(overflow_timestamp_float)
         with self.assertRaises((OverflowError, ValueError)):
             util.is_timestamp(overflow_timestamp_int)
+
+    def test_iso_gregorian(self):
+        with self.assertRaises(ValueError):
+            util.iso_to_gregorian(2013, 0, 5)
+
+        with self.assertRaises(ValueError):
+            util.iso_to_gregorian(2013, 8, 0)