Skip to content

ENH: Add days_in_month property to Timestamp/DatetimeIndex/... (GH9572) #9605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 6, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v0.16.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~~~~
Expand Down
9 changes: 9 additions & 0 deletions pandas/src/period.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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):
Expand Down
10 changes: 10 additions & 0 deletions pandas/src/period_helper.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
1 change: 1 addition & 0 deletions pandas/src/period_helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion pandas/tests/test_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
4 changes: 3 additions & 1 deletion pandas/tseries/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)")
Expand Down
6 changes: 4 additions & 2 deletions pandas/tseries/period.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down
22 changes: 19 additions & 3 deletions pandas/tseries/tests/test_period.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
#
Expand All @@ -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.
Expand All @@ -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):
Expand All @@ -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.
Expand All @@ -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)))

Expand Down Expand Up @@ -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)

Expand Down
13 changes: 9 additions & 4 deletions pandas/tseries/tests/test_timeseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
16 changes: 15 additions & 1 deletion pandas/tslib.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)


Expand Down