Repository: crsmithdev/arrow
Pull request #763 (2020-03-05 21:51:23)

Change GH watch button to star button
## Pull Request Checklist

Thank you for taking the time to improve Arrow! Before submitting your pull request, please check all *appropriate* boxes:

<!-- Check boxes by placing an x in the brackets: [x] -->
- [x] 🧪 Added **tests** for changed code.
- [x] 🛠️ All tests **pass** when run locally (run `tox` or `make test` to find out!).
- [x] 📚 Updated **documentation** for changed code.
- [x] ⏩ Code is **up-to-date** with the `master` branch.

If you have *any* questions about your code changes or any of the points above, please submit your questions along with the pull request and we will try our best to help!

## Description of Changes

<!--
Replace with description of code changes.

If your PR has an associated issue, insert the issue number (e.g. #703) or directly link to the GitHub issue (e.g. https://github.com/crsmithdev/arrow/issues/703).

For example, doing the following will automatically close issue #703 when this PR is merged:

Closes: #703
-->



Patch:
diff --git a/arrow/formatter.py b/arrow/formatter.py
index 80dd2c9..d29d5e8 100644
--- a/arrow/formatter.py
+++ b/arrow/formatter.py
@@ -11,12 +11,12 @@ from arrow import locales, util
 
 class DateTimeFormatter(object):
 
-    # This pattern matches characters enclosed in square brackes are matched as
+    # This pattern matches characters enclosed in square brackets are matched as
     # an atomic group. For more info on atomic groups and how to they are
     # emulated in Python's re library, see https://stackoverflow.com/a/13577411/2701578
-    # TODO: test against full timezone DB
+
     _FORMAT_RE = re.compile(
-        r"(\[(?:(?=(?P<literal>[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X)"
+        r"(\[(?:(?=(?P<literal>[^]]))(?P=literal))*\]|YYY?Y?|MM?M?M?|Do|DD?D?D?|d?dd?d?|HH?|hh?|mm?|ss?|SS?S?S?S?S?|ZZ?Z?|a|A|X|W)"
     )
 
     def __init__(self, locale="en_us"):
@@ -116,3 +116,7 @@ class DateTimeFormatter(object):
 
         if token in ("a", "A"):
             return self.locale.meridian(dt.hour, token)
+
+        if token == "W":
+            year, week, day = dt.isocalendar()
+            return "{}-W{:02d}-{}".format(year, week, day)
diff --git a/arrow/parser.py b/arrow/parser.py
index 379c687..0f932b5 100644
--- a/arrow/parser.py
+++ b/arrow/parser.py
@@ -8,6 +8,7 @@ from dateutil import tz
 
 from arrow import locales
 from arrow.constants import MAX_TIMESTAMP, MAX_TIMESTAMP_MS, MAX_TIMESTAMP_US
+from arrow.util import iso_to_gregorian
 
 try:
     from functools import lru_cache
@@ -31,7 +32,7 @@ class ParserMatchError(ParserError):
 class DateTimeParser(object):
 
     _FORMAT_RE = re.compile(
-        r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|x|X)"
+        r"(YYY?Y?|MM?M?M?|Do|DD?D?D?|d?d?d?d|HH?|hh?|mm?|ss?|S+|ZZ?Z?|a|A|x|X|W)"
     )
     _ESCAPE_RE = re.compile(r"\[[^\[\]]*\]")
 
@@ -49,6 +50,7 @@ class DateTimeParser(object):
     _TIMESTAMP_RE = re.compile(r"^\-?\d+\.?\d+$")
     _TIMESTAMP_EXPANDED_RE = re.compile(r"^\-?\d+$")
     _TIME_RE = re.compile(r"^(\d{2})(?:\:?(\d{2}))?(?:\:?(\d{2}))?(?:([\.\,])(\d+))?$")
+    _WEEK_DATE_RE = re.compile(r"(?P<year>\d{4})[\-]?W(?P<week>\d{2})[\-]?(?P<day>\d)?")
 
     _BASE_INPUT_RE_MAP = {
         "YYYY": _FOUR_DIGIT_RE,
@@ -73,6 +75,7 @@ class DateTimeParser(object):
         "ZZ": _TZ_ZZ_RE,
         "Z": _TZ_Z_RE,
         "S": _ONE_OR_MORE_DIGIT_RE,
+        "W": _WEEK_DATE_RE,
     }
 
     SEPARATORS = ["-", "/", "."]
@@ -147,6 +150,7 @@ class DateTimeParser(object):
             "YYYY/MM",
             "YYYY.MM",
             "YYYY",
+            "W",
         ]
 
         if has_time:
@@ -228,6 +232,8 @@ class DateTimeParser(object):
         for token in fmt_tokens:
             if token == "Do":
                 value = match.group("value")
+            elif token == "W":
+                value = (match.group("year"), match.group("week"), match.group("day"))
             else:
                 value = match.group(token)
             self._parse_token(token, value, parts)
@@ -377,9 +383,29 @@ class DateTimeParser(object):
             elif value in (self.locale.meridians["pm"], self.locale.meridians["PM"]):
                 parts["am_pm"] = "pm"
 
+        elif token == "W":
+            parts["weekdate"] = value
+
     @staticmethod
     def _build_datetime(parts):
 
+        weekdate = parts.get("weekdate")
+
+        if weekdate is not None:
+            # we can use strptime (%G, %V, %u) in python 3.6 but these tokens aren't available before that
+            year, week = int(weekdate[0]), int(weekdate[1])
+
+            if weekdate[2] is not None:
+                day = int(weekdate[2])
+            else:
+                # day not given, default to 1
+                day = 1
+
+            dt = iso_to_gregorian(year, week, day)
+            parts["year"] = dt.year
+            parts["month"] = dt.month
+            parts["day"] = dt.day
+
         timestamp = parts.get("timestamp")
 
         if timestamp is not None:
diff --git a/docs/conf.py b/docs/conf.py
index 9ae9b07..97cc30c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -15,7 +15,7 @@ with io.open("../arrow/_version.py", "r", encoding="utf-8") as f:
 # -- Project information -----------------------------------------------------
 
 project = u"Arrow 🏹"
-copyright = "2019, Chris Smith"
+copyright = "2020, Chris Smith"
 author = "Chris Smith"
 
 release = about["__version__"]
@@ -44,6 +44,7 @@ html_show_sourcelink = False
 html_show_sphinx = False
 html_show_copyright = True
 
+# https://alabaster.readthedocs.io/en/latest/customization.html
 html_theme_options = {
     "description": "Arrow is a sensible and human-friendly approach to dates, times and timestamps.",
     "github_user": "crsmithdev",
@@ -51,6 +52,9 @@ html_theme_options = {
     "github_banner": True,
     "show_related": False,
     "show_powered_by": False,
+    "github_button": True,
+    "github_type": "star",
+    "github_count": "true",  # must be a string
 }
 
 html_sidebars = {
diff --git a/docs/index.rst b/docs/index.rst
index c310f05..95f659b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,7 +1,7 @@
 Arrow: Better dates & times for Python
 ======================================
 
-Release v\ |release|. (`Installation`_) (`Changelog <releases.html>`_)
+Release v\ |release| (`Installation`_) (`Changelog <releases.html>`_)
 
 .. include:: ../README.rst
   :start-after: start-inclusion-marker-do-not-remove
@@ -332,7 +332,7 @@ Then get and use a factory for it:
 Supported Tokens
 ~~~~~~~~~~~~~~~~
 
-Use the following tokens in parsing and formatting.  Note that they're not the same as the tokens for `strptime(3) <https://www.gnu.org/software/libc/manual/html_node/Low_002dLevel-Time-String-Parsing.html#index-strptime>`_:
+Use the following tokens for parsing and formatting. Note that they are **not** the same as the tokens for `strptime <https://linux.die.net/man/3/strptime>`_:
 
 +--------------------------------+--------------+-------------------------------------------+
 |                                |Token         |Output                                     |
@@ -365,6 +365,8 @@ Use the following tokens in parsing and formatting.  Note that they're not the s
 +--------------------------------+--------------+-------------------------------------------+
 |                                |d             |1, 2, 3 ... 6, 7                           |
 +--------------------------------+--------------+-------------------------------------------+
+|**ISO week date**               |W             |2011-W05-4, 2019-W17                       |
++--------------------------------+--------------+-------------------------------------------+
 |**Hour**                        |HH            |00, 01, 02 ... 23, 24                      |
 +--------------------------------+--------------+-------------------------------------------+
 |                                |H             |0, 1, 2 ... 23, 24                         |


Test patch:
diff --git a/tests/test_formatter.py b/tests/test_formatter.py
index dbf2873..5ae484d 100644
--- a/tests/test_formatter.py
+++ b/tests/test_formatter.py
@@ -141,6 +141,14 @@ class TestDateTimeFormatterFormatToken:
         assert self.formatter._format_token(dt, "a") == "pm"
         assert self.formatter._format_token(dt, "A") == "PM"
 
+    def test_week(self):
+        dt = datetime(2017, 5, 19)
+        assert self.formatter._format_token(dt, "W") == "2017-W20-5"
+
+        # make sure week is zero padded when needed
+        dt_early = datetime(2011, 1, 20)
+        assert self.formatter._format_token(dt_early, "W") == "2011-W03-4"
+
     def test_nonsense(self):
         dt = datetime(2012, 1, 1, 11)
         assert self.formatter._format_token(dt, None) is None
@@ -193,5 +201,5 @@ class TestDateTimeFormatterFormatToken:
             == "Dec 31, 2017 |^${}().*+?<>-& 2:00 AM"
         )
 
-        # Escaping is atomic: brackets inside brackets are treated litterally
+        # Escaping is atomic: brackets inside brackets are treated literally
         assert self.formatter.format(datetime(1, 1, 1), "[[[ ]]") == "[[ ]"
diff --git a/tests/test_parser.py b/tests/test_parser.py
index 039eb4c..3c7ee28 100644
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -621,6 +621,43 @@ class TestDateTimeParserParse:
         with pytest.raises(ParserError):
             self.parser.parse("2019-12-31T24:00:00.999999", "YYYY-MM-DDTHH:mm:ss.S")
 
+    def test_parse_W(self):
+
+        assert self.parser.parse("2011-W05-4", "W") == datetime(2011, 2, 3)
+        assert self.parser.parse("2011W054", "W") == datetime(2011, 2, 3)
+        assert self.parser.parse("2011-W05", "W") == datetime(2011, 1, 31)
+        assert self.parser.parse("2011W05", "W") == datetime(2011, 1, 31)
+        assert self.parser.parse("2011-W05-4T14:17:01", "WTHH:mm:ss") == datetime(
+            2011, 2, 3, 14, 17, 1
+        )
+        assert self.parser.parse("2011W054T14:17:01", "WTHH:mm:ss") == datetime(
+            2011, 2, 3, 14, 17, 1
+        )
+        assert self.parser.parse("2011-W05T14:17:01", "WTHH:mm:ss") == datetime(
+            2011, 1, 31, 14, 17, 1
+        )
+        assert self.parser.parse("2011W05T141701", "WTHHmmss") == datetime(
+            2011, 1, 31, 14, 17, 1
+        )
+        assert self.parser.parse("2011W054T141701", "WTHHmmss") == datetime(
+            2011, 2, 3, 14, 17, 1
+        )
+
+        bad_formats = [
+            "201W22",
+            "1995-W1-4",
+            "2001-W34-90",
+            "2001--W34",
+            "2011-W03--3",
+            "thstrdjtrsrd676776r65",
+            "2002-W66-1T14:17:01",
+            "2002-W23-03T14:17:01",
+        ]
+
+        for fmt in bad_formats:
+            with pytest.raises(ParserError):
+                self.parser.parse(fmt, "W")
+
 
 class TestDateTimeParserRegex:
     @classmethod
@@ -964,6 +1001,20 @@ class TestDateTimeParserISO:
             2013, 2, 3, 4, 5, 6, 789120, tzinfo=tz.tzutc()
         )
 
+    def test_W(self):
+
+        assert self.parser.parse_iso("2011-W05-4") == datetime(2011, 2, 3)
+
+        assert self.parser.parse_iso("2011-W05-4T14:17:01") == datetime(
+            2011, 2, 3, 14, 17, 1
+        )
+
+        assert self.parser.parse_iso("2011W054") == datetime(2011, 2, 3)
+
+        assert self.parser.parse_iso("2011W054T141701") == datetime(
+            2011, 2, 3, 14, 17, 1
+        )
+
     def test_invalid_Z(self):
 
         with pytest.raises(ParserError):