From 1162f9af3dfc45dbe23521662842c2b519230f5d Mon Sep 17 00:00:00 2001 From: Matt Roeschke Date: Mon, 6 Nov 2017 22:05:24 -0800 Subject: [PATCH] Add month names Fix localiztion of months Add series test and whatsnew notes Modify test and lint Add Timestamp rst Address comments Lint, move whatsnew, remove hardcoded Monday, check that get_locale is None create functions instead of attributes Fix white space Modify tests Lint --- doc/source/api.rst | 6 +++ doc/source/whatsnew/v0.22.0.txt | 3 ++ pandas/_libs/tslib.pyx | 51 ++++++++++++++++++++ pandas/_libs/tslibs/fields.pyx | 50 +++++++++++++++++--- pandas/_libs/tslibs/nattype.pyx | 32 ++++++++++++- pandas/core/indexes/datetimes.py | 50 ++++++++++++++++++++ pandas/tests/indexes/datetimes/test_misc.py | 52 ++++++++++++++------- pandas/tests/scalar/test_timestamp.py | 20 +++++--- 8 files changed, 233 insertions(+), 31 deletions(-) diff --git a/doc/source/api.rst b/doc/source/api.rst index f3405fcdee608c..683064623de99a 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -582,6 +582,8 @@ These can be accessed like ``Series.dt.``. Series.dt.round Series.dt.floor Series.dt.ceil + Series.dt.month_name + Series.dt.day_name **Timedelta Properties** @@ -1751,6 +1753,8 @@ Time-specific operations DatetimeIndex.round DatetimeIndex.floor DatetimeIndex.ceil + DatetimeIndex.month_name + DatetimeIndex.day_name Conversion ~~~~~~~~~~ @@ -1947,6 +1951,7 @@ Methods Timestamp.combine Timestamp.ctime Timestamp.date + Timestamp.day_name Timestamp.dst Timestamp.floor Timestamp.freq @@ -1956,6 +1961,7 @@ Methods Timestamp.isocalendar Timestamp.isoformat Timestamp.isoweekday + Timestamp.month_name Timestamp.normalize Timestamp.now Timestamp.replace diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt index 54cc59917baccf..f7a0a00a0fede0 100644 --- a/doc/source/whatsnew/v0.22.0.txt +++ b/doc/source/whatsnew/v0.22.0.txt @@ -28,6 +28,9 @@ Other Enhancements - :class:`pandas.io.formats.style.Styler` now has method ``hide_index()`` to determine whether the index will be rendered in ouptut (:issue:`14194`) - :class:`pandas.io.formats.style.Styler` now has method ``hide_columns()`` to determine whether columns will be hidden in output (:issue:`14194`) - Improved wording of ``ValueError`` raised in :func:`to_datetime` when ``unit=`` is passed with a non-convertible value (:issue:`14350`) +- :meth:`Timestamp.month_name`, :meth:`DatetimeIndex.month_name`, and :meth:`Series.dt.month_name` are now available (:issue:`12805`) +- :meth:`Timestamp.day_name` and :meth:`DatetimeIndex.day_name` are now available to return day names with a specified locale (:issue:`12806`) +- .. _whatsnew_0220.api_breaking: diff --git a/pandas/_libs/tslib.pyx b/pandas/_libs/tslib.pyx index 705336dfadf90a..63c96fd8c928fb 100644 --- a/pandas/_libs/tslib.pyx +++ b/pandas/_libs/tslib.pyx @@ -55,11 +55,15 @@ from tslibs.np_datetime cimport (check_dts_bounds, from tslibs.np_datetime import OutOfBoundsDatetime from .tslibs.parsing import parse_datetime_string +from .tslibs.strptime import LocaleTime cimport cython import warnings +import locale +from pandas.util.testing import set_locale + import pytz UTC = pytz.utc @@ -534,6 +538,53 @@ class Timestamp(_Timestamp): return Period(self, freq=freq) + def day_name(self, time_locale=None): + """ + Return the day name of the Timestamp with specified locale. + + Parameters + ---------- + time_locale : string, default None (English locale) + locale determining the language in which to return the day name + + Returns + ------- + day_name : string + """ + if time_locale is None: + days = {0: 'monday', 1: 'tuesday', 2: 'wednesday', + 3: 'thursday', 4: 'friday', 5: 'saturday', + 6: 'sunday'} + else: + with set_locale(time_locale, locale.LC_TIME): + locale_time = LocaleTime() + days = dict(enumerate(locale_time.f_weekday)) + return days[self.weekday()].capitalize() + + def month_name(self, time_locale=None): + """ + Return the month name of the Timestamp with specified locale. + + Parameters + ---------- + time_locale : string, default None (English locale) + locale determining the language in which to return the month name + + Returns + ------- + month_name : string + """ + if time_locale is None: + months = {1: 'january', 2: 'february', 3: 'march', + 4: 'april', 5: 'may', 6: 'june', 7: 'july', + 8: 'august', 9: 'september', 10: 'october', + 11: 'november', 12: 'december'} + else: + with set_locale(time_locale, locale.LC_TIME): + locale_time = LocaleTime() + months = dict(enumerate(locale_time.f_month)) + return months[self.month].capitalize() + @property def dayofweek(self): return self.weekday() diff --git a/pandas/_libs/tslibs/fields.pyx b/pandas/_libs/tslibs/fields.pyx index e813fad1d3fa74..6b908a31d978ad 100644 --- a/pandas/_libs/tslibs/fields.pyx +++ b/pandas/_libs/tslibs/fields.pyx @@ -22,6 +22,10 @@ from np_datetime cimport (pandas_datetimestruct, pandas_timedeltastruct, days_per_month_table, is_leapyear, dayofweek) from nattype cimport NPY_NAT +from pandas._libs.tslibs.strptime import LocaleTime + +import locale +from pandas.util.testing import set_locale def build_field_sarray(ndarray[int64_t] dtindex): """ @@ -67,7 +71,8 @@ def build_field_sarray(ndarray[int64_t] dtindex): @cython.wraparound(False) @cython.boundscheck(False) -def get_date_name_field(ndarray[int64_t] dtindex, object field): +def get_date_name_field(ndarray[int64_t] dtindex, object field, + object time_locale=None): """ Given a int64-based datetime index, return array of strings of date name based on requested field (e.g. weekday_name) @@ -77,16 +82,15 @@ def get_date_name_field(ndarray[int64_t] dtindex, object field): ndarray[object] out pandas_datetimestruct dts int dow - - _dayname = np.array( - ['Monday', 'Tuesday', 'Wednesday', 'Thursday', - 'Friday', 'Saturday', 'Sunday'], - dtype=np.object_) + object locale_time = LocaleTime() count = len(dtindex) out = np.empty(count, dtype=object) if field == 'weekday_name': + _dayname = np.array(['Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday', 'Sunday'], + dtype=np.object_) for i in range(count): if dtindex[i] == NPY_NAT: out[i] = np.nan @@ -95,6 +99,40 @@ def get_date_name_field(ndarray[int64_t] dtindex, object field): dt64_to_dtstruct(dtindex[i], &dts) dow = dayofweek(dts.year, dts.month, dts.day) out[i] = _dayname[dow] + if field == 'day_name': + if time_locale is None: + _dayname = np.array(['monday', 'tuesday', 'wednesday', 'thursday', + 'friday', 'saturday', 'sunday'], + dtype=np.object_) + else: + with set_locale(time_locale, locale.LC_TIME): + locale_time = LocaleTime() + _dayname = np.array(locale_time.f_weekday, dtype=np.object_) + for i in range(count): + if dtindex[i] == NPY_NAT: + out[i] = np.nan + continue + + dt64_to_dtstruct(dtindex[i], &dts) + dow = dayofweek(dts.year, dts.month, dts.day) + out[i] = _dayname[dow].capitalize() + return out + elif field == 'month_name': + if time_locale is None: + _monthname = np.array(['', 'monday', 'tuesday', 'wednesday', + 'thursday', 'friday', 'saturday', 'sunday'], + dtype=np.object_) + else: + with set_locale(time_locale, locale.LC_TIME): + locale_time = LocaleTime() + _monthname = np.array(locale_time.f_month, dtype=np.object_) + for i in range(count): + if dtindex[i] == NPY_NAT: + out[i] = np.nan + continue + + dt64_to_dtstruct(dtindex[i], &dts) + out[i] = _monthname[dts.month].capitalize() return out raise ValueError("Field %s not supported" % field) diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index d2f6006b41f653..e9fec8a9a77f12 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -42,8 +42,13 @@ _nat_scalar_rules[Py_GE] = False def _make_nan_func(func_name, cls): def f(*args, **kwargs): return np.nan + f.__name__ = func_name - f.__doc__ = getattr(cls, func_name).__doc__ + if isinstance(cls, str): + # passed the literal docstring directly + f.__doc__ = cls + else: + f.__doc__ = getattr(cls, func_name).__doc__ return f @@ -320,7 +325,32 @@ class NaTType(_NaT): # nan methods weekday = _make_nan_func('weekday', datetime) isoweekday = _make_nan_func('isoweekday', datetime) + month_name = _make_nan_func('month_name', # noqa:E128 + """ + Return the month name of the Timestamp with specified locale. + Parameters + ---------- + time_locale : string, default None (English locale) + locale determining the language in which to return the month name + + Returns + ------- + month_name : string + """) + day_name = _make_nan_func('day_name', # noqa:E128 + """ + Return the day name of the Timestamp with specified locale. + + Parameters + ---------- + time_locale : string, default None (English locale) + locale determining the language in which to return the day name + + Returns + ------- + day_name : string + """) # _nat_methods date = _make_nat_func('date', datetime) diff --git a/pandas/core/indexes/datetimes.py b/pandas/core/indexes/datetimes.py index 3a11c80ecba64c..13dedf0dddf3d1 100644 --- a/pandas/core/indexes/datetimes.py +++ b/pandas/core/indexes/datetimes.py @@ -250,6 +250,8 @@ class DatetimeIndex(DatelikeOps, TimelikeOps, DatetimeIndexOpsMixin, to_pydatetime to_series to_frame + month_name + day_name Notes ----- @@ -2013,6 +2015,54 @@ def to_julian_date(self): self.nanosecond / 3600.0 / 1e+9 ) / 24.0) + def month_name(self, time_locale=None): + """ + Return the month names of the DateTimeIndex with specified locale. + + Parameters + ---------- + time_locale : string, default None (English locale) + locale determining the language in which to return the month name + + Returns + ------- + month_names : Index + Index of month names + """ + values = self.asi8 + if self.tz is not None: + if self.tz is not utc: + values = self._local_timestamps() + + result = fields.get_date_name_field(values, 'month_name', + time_locale=time_locale) + result = self._maybe_mask_results(result) + return Index(result, name=self.name) + + def day_name(self, time_locale=None): + """ + Return the day names of the DateTimeIndex with specified locale. + + Parameters + ---------- + time_locale : string, default None (English locale) + locale determining the language in which to return the day name + + Returns + ------- + month_names : Index + Index of day names + """ + values = self.asi8 + if self.tz is not None: + if self.tz is not utc: + values = self._local_timestamps() + + result = fields.get_date_name_field(values, 'day_name', + time_locale=time_locale) + result = self._maybe_mask_results(result) + return Index(result, name=self.name) + DatetimeIndex._add_numeric_methods_disabled() DatetimeIndex._add_logical_methods_disabled() diff --git a/pandas/tests/indexes/datetimes/test_misc.py b/pandas/tests/indexes/datetimes/test_misc.py index 951aa2c520d0fa..9fd028af5ba39b 100644 --- a/pandas/tests/indexes/datetimes/test_misc.py +++ b/pandas/tests/indexes/datetimes/test_misc.py @@ -1,3 +1,6 @@ +import locale +import calendar + import pytest import numpy as np @@ -173,7 +176,6 @@ def test_normalize(self): class TestDatetime64(object): def test_datetimeindex_accessors(self): - dti_naive = DatetimeIndex(freq='D', start=datetime(1998, 1, 1), periods=365) # GH 13303 @@ -220,23 +222,6 @@ def test_datetimeindex_accessors(self): assert not dti.is_year_end[0] assert dti.is_year_end[364] - # GH 11128 - assert dti.weekday_name[4] == u'Monday' - assert dti.weekday_name[5] == u'Tuesday' - assert dti.weekday_name[6] == u'Wednesday' - assert dti.weekday_name[7] == u'Thursday' - assert dti.weekday_name[8] == u'Friday' - assert dti.weekday_name[9] == u'Saturday' - assert dti.weekday_name[10] == u'Sunday' - - assert Timestamp('2016-04-04').weekday_name == u'Monday' - assert Timestamp('2016-04-05').weekday_name == u'Tuesday' - assert Timestamp('2016-04-06').weekday_name == u'Wednesday' - assert Timestamp('2016-04-07').weekday_name == u'Thursday' - assert Timestamp('2016-04-08').weekday_name == u'Friday' - assert Timestamp('2016-04-09').weekday_name == u'Saturday' - assert Timestamp('2016-04-10').weekday_name == u'Sunday' - assert len(dti.year) == 365 assert len(dti.month) == 365 assert len(dti.day) == 365 @@ -342,6 +327,37 @@ def test_datetimeindex_accessors(self): assert dates.weekofyear.tolist() == expected assert [d.weekofyear for d in dates] == expected + # GH 12806 + @pytest.mark.skipif(not tm.get_locales(), reason='No available locales') + @pytest.mark.parametrize('time_locale', tm.get_locales()) + def test_datetime_name_accessors(self, time_locale): + with tm.set_locale(time_locale, locale.LC_TIME): + # GH 11128 + dti = DatetimeIndex(freq='D', start=datetime(1998, 1, 1), + periods=365) + english_days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday', 'Sunday'] + for day, name, eng_name in zip(range(4, 11), + calendar.day_name, + english_days): + # Test Monday -> Sunday + assert dti.weekday_name[day] == eng_name + assert dti.day_name(time_locale=time_locale) == name + ts = Timestamp(datetime(2016, 4, day)) + assert ts.weekday_name == eng_name + assert ts.day_name(time_locale=time_locale) == name + + # GH 12805 + dti = DatetimeIndex(freq='M', start='2012', end='2013') + # Test January -> December + result = dti.month_name + expected = Index([month.capitalize() + for month in calendar.month_name[1:]]) + tm.assert_index_equal(result, expected) + for date, expected in zip(dti, calendar.month_name[1:]): + result = date.month_name(time_locale=time_locale) + assert result == expected.capitalize() + def test_nanosecond_field(self): dti = DatetimeIndex(np.arange(10)) diff --git a/pandas/tests/scalar/test_timestamp.py b/pandas/tests/scalar/test_timestamp.py index a79fb554f94548..cffcd51fee98cc 100644 --- a/pandas/tests/scalar/test_timestamp.py +++ b/pandas/tests/scalar/test_timestamp.py @@ -1,6 +1,7 @@ """ test the scalar Timestamp """ import sys +import locale import pytz import pytest import dateutil @@ -598,13 +599,20 @@ 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 12806 + @pytest.mark.skipif(not tm.get_locales(), reason='No available locales') + @pytest.mark.parametrize('data', + [Timestamp('2017-08-28 23:00:00'), + Timestamp('2017-08-28 23:00:00', tz='EST')]) + @pytest.mark.parametrize('time_locale', tm.get_locales()) + def test_day_name(self, data, time_locale): # GH 17354 - assert data.weekday_name == expected + # Test .weekday_name and .day_name() + assert data.weekday_name == 'Monday' + with tm.set_locale(time_locale, locale.LC_TIME): + expected = calendar.day_name[0].capitalize() + result = data.day_name(time_locale) + assert result == expected def test_pprint(self): # GH12622