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