From e873169d1ab2b6c8062659534059d90a1ae1d351 Mon Sep 17 00:00:00 2001 From: josham Date: Fri, 6 Mar 2015 12:16:44 -0500 Subject: [PATCH] ENH: Add days_in_month property to Timestamp/DatetimeIndex/... (GH9572) days_in_month (compatibility alias ``daysinmonth``) added to Timestamp, DatetimeIndex, Period, PeriodIndex, Series.dt --- doc/source/whatsnew/v0.16.0.txt | 1 + pandas/src/period.pyx | 9 +++++++++ pandas/src/period_helper.c | 10 ++++++++++ pandas/src/period_helper.h | 1 + pandas/tests/test_series.py | 2 +- pandas/tseries/index.py | 4 +++- pandas/tseries/period.py | 6 ++++-- pandas/tseries/tests/test_period.py | 22 +++++++++++++++++++--- pandas/tseries/tests/test_timeseries.py | 13 +++++++++---- pandas/tslib.pyx | 16 +++++++++++++++- 10 files changed, 72 insertions(+), 12 deletions(-) diff --git a/doc/source/whatsnew/v0.16.0.txt b/doc/source/whatsnew/v0.16.0.txt index d45125f2be7f3..9df0234da93be 100644 --- a/doc/source/whatsnew/v0.16.0.txt +++ b/doc/source/whatsnew/v0.16.0.txt @@ -61,6 +61,7 @@ New features - Added ``StringMethods.ljust()`` and ``rjust()`` which behave as the same as standard ``str`` (:issue:`9352`) - ``StringMethods.pad()`` and ``center()`` now accept ``fillchar`` option to specify filling character (:issue:`9352`) - Added ``StringMethods.zfill()`` which behave as the same as standard ``str`` (:issue:`9387`) +- Added ``days_in_month`` (compatibility alias ``daysinmonth``) property to ``Timestamp``, ``DatetimeIndex``, ``Period``, ``PeriodIndex``, and ``Series.dt`` (:issue:`9572`) DataFrame Assign ~~~~~~~~~~~~~~~~ diff --git a/pandas/src/period.pyx b/pandas/src/period.pyx index 05e8ed22fc316..cc6ad3defe4f3 100644 --- a/pandas/src/period.pyx +++ b/pandas/src/period.pyx @@ -95,6 +95,7 @@ cdef extern from "period_helper.h": int phour(int64_t ordinal, int freq) except INT32_MIN int pminute(int64_t ordinal, int freq) except INT32_MIN int psecond(int64_t ordinal, int freq) except INT32_MIN + int pdays_in_month(int64_t ordinal, int freq) except INT32_MIN char *c_strftime(date_info *dinfo, char *fmt) int get_yq(int64_t ordinal, int freq, int *quarter, int *year) @@ -427,6 +428,8 @@ cdef accessor _get_accessor_func(int code): return &pday_of_year elif code == 10: return &pweekday + elif code == 11: + return &pdays_in_month return NULL @@ -925,6 +928,12 @@ cdef class Period(object): property qyear: def __get__(self): return self._field(1) + property days_in_month: + def __get__(self): + return self._field(11) + property daysinmonth: + def __get__(self): + return self.days_in_month @classmethod def now(cls, freq=None): diff --git a/pandas/src/period_helper.c b/pandas/src/period_helper.c index 6641000544858..032bc44de6355 100644 --- a/pandas/src/period_helper.c +++ b/pandas/src/period_helper.c @@ -1439,3 +1439,13 @@ int psecond(npy_int64 ordinal, int freq) { return INT_ERR_CODE; return (int)dinfo.second; } + +int pdays_in_month(npy_int64 ordinal, int freq) { + int days; + struct date_info dinfo; + if(get_date_info(ordinal, freq, &dinfo) == INT_ERR_CODE) + return INT_ERR_CODE; + + days = days_in_month[dInfoCalc_Leapyear(dinfo.year, dinfo.calendar)][dinfo.month-1]; + return days; +} diff --git a/pandas/src/period_helper.h b/pandas/src/period_helper.h index 55c3722ebaae7..19b186afb9fc8 100644 --- a/pandas/src/period_helper.h +++ b/pandas/src/period_helper.h @@ -160,6 +160,7 @@ int pweek(npy_int64 ordinal, int freq); int phour(npy_int64 ordinal, int freq); int pminute(npy_int64 ordinal, int freq); int psecond(npy_int64 ordinal, int freq); +int pdays_in_month(npy_int64 ordinal, int freq); double getAbsTime(int freq, npy_int64 dailyDate, npy_int64 originalDate); char *c_strftime(struct date_info *dinfo, char *fmt); diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py index 5f1cad11f72fe..e5d983472256f 100644 --- a/pandas/tests/test_series.py +++ b/pandas/tests/test_series.py @@ -79,7 +79,7 @@ def test_dt_namespace_accessor(self): # GH 7207 # test .dt namespace accessor - ok_for_base = ['year','month','day','hour','minute','second','weekofyear','week','dayofweek','weekday','dayofyear','quarter','freq'] + ok_for_base = ['year','month','day','hour','minute','second','weekofyear','week','dayofweek','weekday','dayofyear','quarter','freq','days_in_month','daysinmonth'] ok_for_period = ok_for_base + ['qyear'] ok_for_dt = ok_for_base + ['date','time','microsecond','nanosecond', 'is_month_start', 'is_month_end', 'is_quarter_start', 'is_quarter_end', 'is_year_start', 'is_year_end', 'tz'] diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py index 24d12078fd7f0..ca5119acc8b99 100644 --- a/pandas/tseries/index.py +++ b/pandas/tseries/index.py @@ -187,7 +187,7 @@ def _join_i8_wrapper(joinf, **kwargs): _comparables = ['name','freqstr','tz'] _attributes = ['name','freq','tz'] _datetimelike_ops = ['year','month','day','hour','minute','second', - 'weekofyear','week','dayofweek','weekday','dayofyear','quarter', + 'weekofyear','week','dayofweek','weekday','dayofyear','quarter', 'days_in_month', 'daysinmonth', 'date','time','microsecond','nanosecond','is_month_start','is_month_end', 'is_quarter_start','is_quarter_end','is_year_start','is_year_end','tz','freq'] _is_numeric_dtype = False @@ -1401,6 +1401,8 @@ def _set_freq(self, value): weekday = dayofweek dayofyear = _field_accessor('dayofyear', 'doy', "The ordinal day of the year") quarter = _field_accessor('quarter', 'q', "The quarter of the date") + days_in_month = _field_accessor('days_in_month', 'dim', "The number of days in the month") + daysinmonth = days_in_month is_month_start = _field_accessor('is_month_start', 'is_month_start', "Logical indicating if first day of month (defined by frequency)") is_month_end = _field_accessor('is_month_end', 'is_month_end', "Logical indicating if last day of month (defined by frequency)") is_quarter_start = _field_accessor('is_quarter_start', 'is_quarter_start', "Logical indicating if first day of quarter (defined by frequency)") diff --git a/pandas/tseries/period.py b/pandas/tseries/period.py index 1a2381441ab8d..b1f0ba1f127fa 100644 --- a/pandas/tseries/period.py +++ b/pandas/tseries/period.py @@ -150,7 +150,7 @@ class PeriodIndex(DatetimeIndexOpsMixin, Int64Index): _typ = 'periodindex' _attributes = ['name','freq'] _datetimelike_ops = ['year','month','day','hour','minute','second', - 'weekofyear','week','dayofweek','weekday','dayofyear','quarter', 'qyear', 'freq'] + 'weekofyear','week','dayofweek','weekday','dayofyear','quarter', 'qyear', 'freq', 'days_in_month', 'daysinmonth'] _is_numeric_dtype = False freq = None @@ -385,7 +385,9 @@ def to_datetime(self, dayfirst=False): dayofyear = day_of_year = _field_accessor('dayofyear', 9, "The ordinal day of the year") quarter = _field_accessor('quarter', 2, "The quarter of the date") qyear = _field_accessor('qyear', 1) - + days_in_month = _field_accessor('days_in_month', 11, "The number of days in the month") + daysinmonth = days_in_month + def _get_object_array(self): freq = self.freq return np.array([ Period._from_ordinal(ordinal=x, freq=freq) for x in self.values], copy=False) diff --git a/pandas/tseries/tests/test_period.py b/pandas/tseries/tests/test_period.py index 5f48861097b6d..17edcd7504102 100644 --- a/pandas/tseries/tests/test_period.py +++ b/pandas/tseries/tests/test_period.py @@ -432,7 +432,9 @@ def test_properties_weekly(self): assert_equal(w_date.month, 1) assert_equal(w_date.week, 1) assert_equal((w_date - 1).week, 52) - + assert_equal(w_date.days_in_month, 31) + assert_equal(Period(freq='WK', year=2012, month=2, day=1).days_in_month, 29) + def test_properties_daily(self): # Test properties on Periods with daily frequency. b_date = Period(freq='B', year=2007, month=1, day=1) @@ -443,6 +445,8 @@ def test_properties_daily(self): assert_equal(b_date.day, 1) assert_equal(b_date.weekday, 0) assert_equal(b_date.dayofyear, 1) + assert_equal(b_date.days_in_month, 31) + assert_equal(Period(freq='B', year=2012, month=2, day=1).days_in_month, 29) # d_date = Period(freq='D', year=2007, month=1, day=1) # @@ -452,6 +456,9 @@ def test_properties_daily(self): assert_equal(d_date.day, 1) assert_equal(d_date.weekday, 0) assert_equal(d_date.dayofyear, 1) + assert_equal(d_date.days_in_month, 31) + assert_equal(Period(freq='D', year=2012, month=2, + day=1).days_in_month, 29) def test_properties_hourly(self): # Test properties on Periods with hourly frequency. @@ -464,6 +471,9 @@ def test_properties_hourly(self): assert_equal(h_date.weekday, 0) assert_equal(h_date.dayofyear, 1) assert_equal(h_date.hour, 0) + assert_equal(h_date.days_in_month, 31) + assert_equal(Period(freq='H', year=2012, month=2, day=1, + hour=0).days_in_month, 29) # def test_properties_minutely(self): @@ -478,6 +488,9 @@ def test_properties_minutely(self): assert_equal(t_date.dayofyear, 1) assert_equal(t_date.hour, 0) assert_equal(t_date.minute, 0) + assert_equal(t_date.days_in_month, 31) + assert_equal(Period(freq='D', year=2012, month=2, day=1, hour=0, + minute=0).days_in_month, 29) def test_properties_secondly(self): # Test properties on Periods with secondly frequency. @@ -493,13 +506,16 @@ def test_properties_secondly(self): assert_equal(s_date.hour, 0) assert_equal(s_date.minute, 0) assert_equal(s_date.second, 0) + assert_equal(s_date.days_in_month, 31) + assert_equal(Period(freq='Min', year=2012, month=2, day=1, hour=0, + minute=0, second=0).days_in_month, 29) def test_properties_nat(self): p_nat = Period('NaT', freq='M') t_nat = pd.Timestamp('NaT') # confirm Period('NaT') work identical with Timestamp('NaT') for f in ['year', 'month', 'day', 'hour', 'minute', 'second', - 'week', 'dayofyear', 'quarter']: + 'week', 'dayofyear', 'quarter', 'days_in_month']: self.assertTrue(np.isnan(getattr(p_nat, f))) self.assertTrue(np.isnan(getattr(t_nat, f))) @@ -2327,7 +2343,7 @@ def test_fields(self): def _check_all_fields(self, periodindex): fields = ['year', 'month', 'day', 'hour', 'minute', 'second', 'weekofyear', 'week', 'dayofweek', - 'weekday', 'dayofyear', 'quarter', 'qyear'] + 'weekday', 'dayofyear', 'quarter', 'qyear', 'days_in_month'] periods = list(periodindex) diff --git a/pandas/tseries/tests/test_timeseries.py b/pandas/tseries/tests/test_timeseries.py index b65ecd14d3fff..436a976c72e7e 100644 --- a/pandas/tseries/tests/test_timeseries.py +++ b/pandas/tseries/tests/test_timeseries.py @@ -937,7 +937,7 @@ def test_nat_vector_field_access(self): fields = ['year', 'quarter', 'month', 'day', 'hour', 'minute', 'second', 'microsecond', 'nanosecond', - 'week', 'dayofyear'] + 'week', 'dayofyear', 'days_in_month'] for field in fields: result = getattr(idx, field) expected = [getattr(x, field) if x is not NaT else np.nan @@ -947,7 +947,7 @@ def test_nat_vector_field_access(self): def test_nat_scalar_field_access(self): fields = ['year', 'quarter', 'month', 'day', 'hour', 'minute', 'second', 'microsecond', 'nanosecond', - 'week', 'dayofyear'] + 'week', 'dayofyear', 'days_in_month'] for field in fields: result = getattr(NaT, field) self.assertTrue(np.isnan(result)) @@ -1625,7 +1625,7 @@ def test_timestamp_fields(self): # extra fields from DatetimeIndex like quarter and week idx = tm.makeDateIndex(100) - fields = ['dayofweek', 'dayofyear', 'week', 'weekofyear', 'quarter', 'is_month_start', 'is_month_end', 'is_quarter_start', 'is_quarter_end', 'is_year_start', 'is_year_end'] + fields = ['dayofweek', 'dayofyear', 'week', 'weekofyear', 'quarter', 'days_in_month', 'is_month_start', 'is_month_end', 'is_quarter_start', 'is_quarter_end', 'is_year_start', 'is_year_end'] for f in fields: expected = getattr(idx, f)[-1] result = getattr(Timestamp(idx[-1]), f) @@ -2865,6 +2865,9 @@ def test_datetimeindex_accessors(self): self.assertEqual(dti.quarter[0], 1) self.assertEqual(dti.quarter[120], 2) + self.assertEqual(dti.days_in_month[0], 31) + self.assertEqual(dti.days_in_month[90], 30) + self.assertEqual(dti.is_month_start[0], True) self.assertEqual(dti.is_month_start[1], False) self.assertEqual(dti.is_month_start[31], True) @@ -2948,7 +2951,9 @@ def test_datetimeindex_accessors(self): (Timestamp('2013-06-28', offset='BQS-APR').is_quarter_end, 1), (Timestamp('2013-03-29', offset='BQS-APR').is_year_end, 1), (Timestamp('2013-11-01', offset='AS-NOV').is_year_start, 1), - (Timestamp('2013-10-31', offset='AS-NOV').is_year_end, 1)] + (Timestamp('2013-10-31', offset='AS-NOV').is_year_end, 1), + (Timestamp('2012-02-01').days_in_month, 29), + (Timestamp('2013-02-01').days_in_month, 28)] for ts, value in tests: self.assertEqual(ts, value) diff --git a/pandas/tslib.pyx b/pandas/tslib.pyx index f4cf711951f5e..eee72f268036a 100644 --- a/pandas/tslib.pyx +++ b/pandas/tslib.pyx @@ -390,6 +390,12 @@ class Timestamp(_Timestamp): def quarter(self): return self._get_field('q') + @property + def days_in_month(self): + return self._get_field('dim') + + daysinmonth = days_in_month + @property def freqstr(self): return getattr(self.offset, 'freqstr', self.offset) @@ -603,7 +609,7 @@ class NaTType(_NaT): fields = ['year', 'quarter', 'month', 'day', 'hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond', - 'week', 'dayofyear'] + 'week', 'dayofyear', 'days_in_month'] for field in fields: prop = property(fget=lambda self: np.nan) setattr(NaTType, field, prop) @@ -3188,6 +3194,14 @@ def get_date_field(ndarray[int64_t] dtindex, object field): out[i] = ((out[i] - 1) / 3) + 1 return out + elif field == 'dim': + for i in range(count): + if dtindex[i] == NPY_NAT: out[i] = -1; continue + + pandas_datetime_to_datetimestruct(dtindex[i], PANDAS_FR_ns, &dts) + out[i] = monthrange(dts.year, dts.month)[1] + return out + raise ValueError("Field %s not supported" % field)