Repository: pandas-dev/pandas
Pull request: BUG: Return local Timestamp.weekday_name attribute (#17354)
- [x] closes #17354
- [x] tests added / passed
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

@jreback created a new method `_get_named_field` based on your [comment](https://github.com/pandas-dev/pandas/issues/17354#issuecomment-325319229) in the issue thread.

Problem statement:
BUG: Timestamp.weekday_name uses UTC instead of given timezone
#### Code Sample, a copy-pastable example if possible

```python
import pandas as pd

# Create a Timestamp for 11pm Monday 28th
ts = pd.Timestamp('2017-08-28 23:00:00')
# Both weekday() and weekday_name indicate this is a Monday
assert ts.weekday() == 0
assert ts.weekday_name == 'Monday'

# Now localize the timezone
ts = ts.tz_localize('EST')
# weekday() still says Monday, but weekday_name says Tuesday!
assert ts.weekday() == 0
assert ts.weekday_name == 'Tuesday'
```
#### Problem description

For `pd.Timestamp` objects with timezone information, the `weekday_name` attribute appears to give the name of the weekday in UTC (in the above example, this would be 2017-08-29 04:00:00), whereas `weekday()` uses local time. I could find no mention of this difference in the documentation for `weekday` and `weekday_name`. I would expect both to work in the given timezone.

#### Expected Output

```
Expected:
>>> pd.Timestamp('2017-08-28 23:00:00', tz='EST').weekday_name
'Monday'

Actual:
>>> pd.Timestamp('2017-08-28 23:00:00', tz='EST').weekday_name
'Tuesday'
```

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.1.final.0
python-bits: 64
OS: Windows
OS-release: 10
machine: AMD64
processor: Intel64 Family 6 Model 58 Stepping 9, GenuineIntel
byteorder: little
LC_ALL: C
LANG: None
LOCALE: None.None

pandas: 0.20.3
pytest: None
pip: 9.0.1
setuptools: 36.2.7
Cython: None
numpy: 1.13.1
scipy: None
xarray: None
IPython: None
sphinx: None
patsy: None
dateutil: 2.6.1
pytz: 2017.2
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: None
matplotlib: None
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
sqlalchemy: None
pymysql: None
psycopg2: 2.7.3.1 (dt dec pq3 ext lo64)
jinja2: 2.9.6
s3fs: None
pandas_gbq: None
pandas_datareader: None

</details>

Hints from issue discussion:
Oops.  ¯ \ _ (ツ) _ / ¯

PR to patch this is welcome!
this is correct for a DTI, but the impl for ``Timestamp`` is not converting to local timestamps (and ``Timestamp.weeday`` is in the super class. should be an easy fix.
needs (see ``_get_field``). note that we should make a ``_get_named_field`` (similar to ``_get_field``) to handle the dispatch, and have ``weekday_name`` call this (its the only one that calls ``get_date_name_field`` atm).

```
        val = self.value
        if self.tz is not None and not _is_utc(self.tz):
            val = tz_convert_single(self.value, 'UTC', self.tz)
```

Commit message: Ctype array and leftover val

Patch to review:
diff --git a/doc/source/whatsnew/v0.21.0.txt b/doc/source/whatsnew/v0.21.0.txt
index 81e52266f972e..14e07978c7adc 100644
--- a/doc/source/whatsnew/v0.21.0.txt
+++ b/doc/source/whatsnew/v0.21.0.txt
@@ -399,6 +399,7 @@ Conversion
 - Fixed the return type of ``IntervalIndex.is_non_overlapping_monotonic`` to be a Python ``bool`` for consistency with similar attributes/methods.  Previously returned a ``numpy.bool_``. (:issue:`17237`)
 - Bug in ``IntervalIndex.is_non_overlapping_monotonic`` when intervals are closed on both sides and overlap at a point (:issue:`16560`)
 - Bug in :func:`Series.fillna` returns frame when ``inplace=True`` and ``value`` is dict (:issue:`16156`)
+- Bug in :attr:`Timestamp.weekday_name` returning a UTC-based weekday name when localized to a timezone (:issue:`17354`)
 
 Indexing
 ^^^^^^^^
diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx
index 50e0b77c6d3a0..8628cd396524e 100644
--- a/pandas/_libs/tslib.pyx
+++ b/pandas/_libs/tslib.pyx
@@ -532,9 +532,7 @@ class Timestamp(_Timestamp):
 
     @property
     def weekday_name(self):
-        out = get_date_name_field(
-            np.array([self.value], dtype=np.int64), 'weekday_name')
-        return out[0]
+        return self._get_named_field('weekday_name')
 
     @property
     def dayofyear(self):
@@ -1268,13 +1266,33 @@ cdef class _Timestamp(datetime):
         # same timezone if specified)
         return datetime.__sub__(self, other)
 
-    cpdef _get_field(self, field):
+    cdef int64_t _maybe_convert_value_to_local(self):
+        """Convert UTC i8 value to local i8 value if tz exists"""
+        cdef:
+            int64_t val
         val = self.value
         if self.tz is not None and not _is_utc(self.tz):
             val = tz_convert_single(self.value, 'UTC', self.tz)
-        out = get_date_field(np.array([val], dtype=np.int64), field)
+        return val
+
+    cpdef _get_field(self, field):
+        cdef:
+            int64_t val
+            ndarray[int64_t] date_array = np.empty(1, dtype=np.int64)
+        val = self._maybe_convert_value_to_local()
+        date_array[0] = val
+        out = get_date_field(date_array, field)
         return int(out[0])
 
+    cpdef _get_named_field(self, field):
+        cdef:
+            int64_t val
+            ndarray[int64_t] date_array = np.empty(1, dtype=np.int64)
+        val = self._maybe_convert_value_to_local()
+        date_array[0] = val
+        out = get_date_name_field(date_array, field)
+        return out[0]
+
     cpdef _get_start_end_field(self, field):
         month_kw = self.freq.kwds.get(
             'startingMonth', self.freq.kwds.get(
diff --git a/pandas/tests/scalar/test_timestamp.py b/pandas/tests/scalar/test_timestamp.py
index 7cd1a7db0f9fe..8d47ce4802ac6 100644
--- a/pandas/tests/scalar/test_timestamp.py
+++ b/pandas/tests/scalar/test_timestamp.py
@@ -555,6 +555,14 @@ def check(value, equal):
         for end in ends:
             assert getattr(ts, end)
 
+    @pytest.mark.parametrize('data, expected',
+                             [(Timestamp('2017-08-28 23:00:00'), 'Monday'),
+                              (Timestamp('2017-08-28 23:00:00', tz='EST'),
+                               'Monday')])
+    def test_weekday_name(self, data, expected):
+        # GH 17354
+        assert data.weekday_name == expected
+
     def test_pprint(self):
         # GH12622
         import pprint


Code review comments:
diff_hunk: @@ -1268,13 +1266,33 @@ cdef class _Timestamp(datetime):
         # same timezone if specified)
         return datetime.__sub__(self, other)
 
-    cpdef _get_field(self, field):
+    cdef int64_t _maybe_convert_value_to_local(self):
+        """Convert UTC i8 value to local i8 value if tz exists"""
+        cdef:
+            int64_t val
         val = self.value
         if self.tz is not None and not _is_utc(self.tz):
             val = tz_convert_single(self.value, 'UTC', self.tz)
-        out = get_date_field(np.array([val], dtype=np.int64), field)
+        return val
+
+    cpdef _get_field(self, field):
+        cdef:
+            int64_t val
+            ndarray[int64_t] date_array = np.empty(1, dtype=np.int64)
+        val = self._maybe_convert_value_to_local()
+        date_array[0] = val
path: pandas/_libs/tslib.pyx
text: @user1:
this is overkill to type the input ndarray itself

it was ``out`` which was looking to type, though not a big deal, so just remove the date_array temp. ping on green.