Skip to content

Commit bb9d35c

Browse files
committed
Merge pull request #9605 from josham/days_in_month
ENH: Add days_in_month property to Timestamp/DatetimeIndex/... (GH9572)
2 parents ed45e6e + e873169 commit bb9d35c

File tree

10 files changed

+72
-12
lines changed

10 files changed

+72
-12
lines changed

doc/source/whatsnew/v0.16.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ New features
6161
- Added ``StringMethods.ljust()`` and ``rjust()`` which behave as the same as standard ``str`` (:issue:`9352`)
6262
- ``StringMethods.pad()`` and ``center()`` now accept ``fillchar`` option to specify filling character (:issue:`9352`)
6363
- Added ``StringMethods.zfill()`` which behave as the same as standard ``str`` (:issue:`9387`)
64+
- Added ``days_in_month`` (compatibility alias ``daysinmonth``) property to ``Timestamp``, ``DatetimeIndex``, ``Period``, ``PeriodIndex``, and ``Series.dt`` (:issue:`9572`)
6465

6566
DataFrame Assign
6667
~~~~~~~~~~~~~~~~

pandas/src/period.pyx

+9
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ cdef extern from "period_helper.h":
9595
int phour(int64_t ordinal, int freq) except INT32_MIN
9696
int pminute(int64_t ordinal, int freq) except INT32_MIN
9797
int psecond(int64_t ordinal, int freq) except INT32_MIN
98+
int pdays_in_month(int64_t ordinal, int freq) except INT32_MIN
9899
char *c_strftime(date_info *dinfo, char *fmt)
99100
int get_yq(int64_t ordinal, int freq, int *quarter, int *year)
100101

@@ -427,6 +428,8 @@ cdef accessor _get_accessor_func(int code):
427428
return &pday_of_year
428429
elif code == 10:
429430
return &pweekday
431+
elif code == 11:
432+
return &pdays_in_month
430433
return NULL
431434

432435

@@ -925,6 +928,12 @@ cdef class Period(object):
925928
property qyear:
926929
def __get__(self):
927930
return self._field(1)
931+
property days_in_month:
932+
def __get__(self):
933+
return self._field(11)
934+
property daysinmonth:
935+
def __get__(self):
936+
return self.days_in_month
928937

929938
@classmethod
930939
def now(cls, freq=None):

pandas/src/period_helper.c

+10
Original file line numberDiff line numberDiff line change
@@ -1439,3 +1439,13 @@ int psecond(npy_int64 ordinal, int freq) {
14391439
return INT_ERR_CODE;
14401440
return (int)dinfo.second;
14411441
}
1442+
1443+
int pdays_in_month(npy_int64 ordinal, int freq) {
1444+
int days;
1445+
struct date_info dinfo;
1446+
if(get_date_info(ordinal, freq, &dinfo) == INT_ERR_CODE)
1447+
return INT_ERR_CODE;
1448+
1449+
days = days_in_month[dInfoCalc_Leapyear(dinfo.year, dinfo.calendar)][dinfo.month-1];
1450+
return days;
1451+
}

pandas/src/period_helper.h

+1
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ int pweek(npy_int64 ordinal, int freq);
160160
int phour(npy_int64 ordinal, int freq);
161161
int pminute(npy_int64 ordinal, int freq);
162162
int psecond(npy_int64 ordinal, int freq);
163+
int pdays_in_month(npy_int64 ordinal, int freq);
163164

164165
double getAbsTime(int freq, npy_int64 dailyDate, npy_int64 originalDate);
165166
char *c_strftime(struct date_info *dinfo, char *fmt);

pandas/tests/test_series.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def test_dt_namespace_accessor(self):
7979
# GH 7207
8080
# test .dt namespace accessor
8181

82-
ok_for_base = ['year','month','day','hour','minute','second','weekofyear','week','dayofweek','weekday','dayofyear','quarter','freq']
82+
ok_for_base = ['year','month','day','hour','minute','second','weekofyear','week','dayofweek','weekday','dayofyear','quarter','freq','days_in_month','daysinmonth']
8383
ok_for_period = ok_for_base + ['qyear']
8484
ok_for_dt = ok_for_base + ['date','time','microsecond','nanosecond', 'is_month_start', 'is_month_end', 'is_quarter_start',
8585
'is_quarter_end', 'is_year_start', 'is_year_end', 'tz']

pandas/tseries/index.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def _join_i8_wrapper(joinf, **kwargs):
187187
_comparables = ['name','freqstr','tz']
188188
_attributes = ['name','freq','tz']
189189
_datetimelike_ops = ['year','month','day','hour','minute','second',
190-
'weekofyear','week','dayofweek','weekday','dayofyear','quarter',
190+
'weekofyear','week','dayofweek','weekday','dayofyear','quarter', 'days_in_month', 'daysinmonth',
191191
'date','time','microsecond','nanosecond','is_month_start','is_month_end',
192192
'is_quarter_start','is_quarter_end','is_year_start','is_year_end','tz','freq']
193193
_is_numeric_dtype = False
@@ -1401,6 +1401,8 @@ def _set_freq(self, value):
14011401
weekday = dayofweek
14021402
dayofyear = _field_accessor('dayofyear', 'doy', "The ordinal day of the year")
14031403
quarter = _field_accessor('quarter', 'q', "The quarter of the date")
1404+
days_in_month = _field_accessor('days_in_month', 'dim', "The number of days in the month")
1405+
daysinmonth = days_in_month
14041406
is_month_start = _field_accessor('is_month_start', 'is_month_start', "Logical indicating if first day of month (defined by frequency)")
14051407
is_month_end = _field_accessor('is_month_end', 'is_month_end', "Logical indicating if last day of month (defined by frequency)")
14061408
is_quarter_start = _field_accessor('is_quarter_start', 'is_quarter_start', "Logical indicating if first day of quarter (defined by frequency)")

pandas/tseries/period.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ class PeriodIndex(DatetimeIndexOpsMixin, Int64Index):
150150
_typ = 'periodindex'
151151
_attributes = ['name','freq']
152152
_datetimelike_ops = ['year','month','day','hour','minute','second',
153-
'weekofyear','week','dayofweek','weekday','dayofyear','quarter', 'qyear', 'freq']
153+
'weekofyear','week','dayofweek','weekday','dayofyear','quarter', 'qyear', 'freq', 'days_in_month', 'daysinmonth']
154154
_is_numeric_dtype = False
155155
freq = None
156156

@@ -385,7 +385,9 @@ def to_datetime(self, dayfirst=False):
385385
dayofyear = day_of_year = _field_accessor('dayofyear', 9, "The ordinal day of the year")
386386
quarter = _field_accessor('quarter', 2, "The quarter of the date")
387387
qyear = _field_accessor('qyear', 1)
388-
388+
days_in_month = _field_accessor('days_in_month', 11, "The number of days in the month")
389+
daysinmonth = days_in_month
390+
389391
def _get_object_array(self):
390392
freq = self.freq
391393
return np.array([ Period._from_ordinal(ordinal=x, freq=freq) for x in self.values], copy=False)

pandas/tseries/tests/test_period.py

+19-3
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,9 @@ def test_properties_weekly(self):
432432
assert_equal(w_date.month, 1)
433433
assert_equal(w_date.week, 1)
434434
assert_equal((w_date - 1).week, 52)
435-
435+
assert_equal(w_date.days_in_month, 31)
436+
assert_equal(Period(freq='WK', year=2012, month=2, day=1).days_in_month, 29)
437+
436438
def test_properties_daily(self):
437439
# Test properties on Periods with daily frequency.
438440
b_date = Period(freq='B', year=2007, month=1, day=1)
@@ -443,6 +445,8 @@ def test_properties_daily(self):
443445
assert_equal(b_date.day, 1)
444446
assert_equal(b_date.weekday, 0)
445447
assert_equal(b_date.dayofyear, 1)
448+
assert_equal(b_date.days_in_month, 31)
449+
assert_equal(Period(freq='B', year=2012, month=2, day=1).days_in_month, 29)
446450
#
447451
d_date = Period(freq='D', year=2007, month=1, day=1)
448452
#
@@ -452,6 +456,9 @@ def test_properties_daily(self):
452456
assert_equal(d_date.day, 1)
453457
assert_equal(d_date.weekday, 0)
454458
assert_equal(d_date.dayofyear, 1)
459+
assert_equal(d_date.days_in_month, 31)
460+
assert_equal(Period(freq='D', year=2012, month=2,
461+
day=1).days_in_month, 29)
455462

456463
def test_properties_hourly(self):
457464
# Test properties on Periods with hourly frequency.
@@ -464,6 +471,9 @@ def test_properties_hourly(self):
464471
assert_equal(h_date.weekday, 0)
465472
assert_equal(h_date.dayofyear, 1)
466473
assert_equal(h_date.hour, 0)
474+
assert_equal(h_date.days_in_month, 31)
475+
assert_equal(Period(freq='H', year=2012, month=2, day=1,
476+
hour=0).days_in_month, 29)
467477
#
468478

469479
def test_properties_minutely(self):
@@ -478,6 +488,9 @@ def test_properties_minutely(self):
478488
assert_equal(t_date.dayofyear, 1)
479489
assert_equal(t_date.hour, 0)
480490
assert_equal(t_date.minute, 0)
491+
assert_equal(t_date.days_in_month, 31)
492+
assert_equal(Period(freq='D', year=2012, month=2, day=1, hour=0,
493+
minute=0).days_in_month, 29)
481494

482495
def test_properties_secondly(self):
483496
# Test properties on Periods with secondly frequency.
@@ -493,13 +506,16 @@ def test_properties_secondly(self):
493506
assert_equal(s_date.hour, 0)
494507
assert_equal(s_date.minute, 0)
495508
assert_equal(s_date.second, 0)
509+
assert_equal(s_date.days_in_month, 31)
510+
assert_equal(Period(freq='Min', year=2012, month=2, day=1, hour=0,
511+
minute=0, second=0).days_in_month, 29)
496512

497513
def test_properties_nat(self):
498514
p_nat = Period('NaT', freq='M')
499515
t_nat = pd.Timestamp('NaT')
500516
# confirm Period('NaT') work identical with Timestamp('NaT')
501517
for f in ['year', 'month', 'day', 'hour', 'minute', 'second',
502-
'week', 'dayofyear', 'quarter']:
518+
'week', 'dayofyear', 'quarter', 'days_in_month']:
503519
self.assertTrue(np.isnan(getattr(p_nat, f)))
504520
self.assertTrue(np.isnan(getattr(t_nat, f)))
505521

@@ -2327,7 +2343,7 @@ def test_fields(self):
23272343
def _check_all_fields(self, periodindex):
23282344
fields = ['year', 'month', 'day', 'hour', 'minute',
23292345
'second', 'weekofyear', 'week', 'dayofweek',
2330-
'weekday', 'dayofyear', 'quarter', 'qyear']
2346+
'weekday', 'dayofyear', 'quarter', 'qyear', 'days_in_month']
23312347

23322348
periods = list(periodindex)
23332349

pandas/tseries/tests/test_timeseries.py

+9-4
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,7 @@ def test_nat_vector_field_access(self):
937937

938938
fields = ['year', 'quarter', 'month', 'day', 'hour',
939939
'minute', 'second', 'microsecond', 'nanosecond',
940-
'week', 'dayofyear']
940+
'week', 'dayofyear', 'days_in_month']
941941
for field in fields:
942942
result = getattr(idx, field)
943943
expected = [getattr(x, field) if x is not NaT else np.nan
@@ -947,7 +947,7 @@ def test_nat_vector_field_access(self):
947947
def test_nat_scalar_field_access(self):
948948
fields = ['year', 'quarter', 'month', 'day', 'hour',
949949
'minute', 'second', 'microsecond', 'nanosecond',
950-
'week', 'dayofyear']
950+
'week', 'dayofyear', 'days_in_month']
951951
for field in fields:
952952
result = getattr(NaT, field)
953953
self.assertTrue(np.isnan(result))
@@ -1625,7 +1625,7 @@ def test_timestamp_fields(self):
16251625
# extra fields from DatetimeIndex like quarter and week
16261626
idx = tm.makeDateIndex(100)
16271627

1628-
fields = ['dayofweek', 'dayofyear', 'week', 'weekofyear', 'quarter', 'is_month_start', 'is_month_end', 'is_quarter_start', 'is_quarter_end', 'is_year_start', 'is_year_end']
1628+
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']
16291629
for f in fields:
16301630
expected = getattr(idx, f)[-1]
16311631
result = getattr(Timestamp(idx[-1]), f)
@@ -2865,6 +2865,9 @@ def test_datetimeindex_accessors(self):
28652865
self.assertEqual(dti.quarter[0], 1)
28662866
self.assertEqual(dti.quarter[120], 2)
28672867

2868+
self.assertEqual(dti.days_in_month[0], 31)
2869+
self.assertEqual(dti.days_in_month[90], 30)
2870+
28682871
self.assertEqual(dti.is_month_start[0], True)
28692872
self.assertEqual(dti.is_month_start[1], False)
28702873
self.assertEqual(dti.is_month_start[31], True)
@@ -2948,7 +2951,9 @@ def test_datetimeindex_accessors(self):
29482951
(Timestamp('2013-06-28', offset='BQS-APR').is_quarter_end, 1),
29492952
(Timestamp('2013-03-29', offset='BQS-APR').is_year_end, 1),
29502953
(Timestamp('2013-11-01', offset='AS-NOV').is_year_start, 1),
2951-
(Timestamp('2013-10-31', offset='AS-NOV').is_year_end, 1)]
2954+
(Timestamp('2013-10-31', offset='AS-NOV').is_year_end, 1),
2955+
(Timestamp('2012-02-01').days_in_month, 29),
2956+
(Timestamp('2013-02-01').days_in_month, 28)]
29522957

29532958
for ts, value in tests:
29542959
self.assertEqual(ts, value)

pandas/tslib.pyx

+15-1
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,12 @@ class Timestamp(_Timestamp):
390390
def quarter(self):
391391
return self._get_field('q')
392392

393+
@property
394+
def days_in_month(self):
395+
return self._get_field('dim')
396+
397+
daysinmonth = days_in_month
398+
393399
@property
394400
def freqstr(self):
395401
return getattr(self.offset, 'freqstr', self.offset)
@@ -603,7 +609,7 @@ class NaTType(_NaT):
603609

604610
fields = ['year', 'quarter', 'month', 'day', 'hour',
605611
'minute', 'second', 'millisecond', 'microsecond', 'nanosecond',
606-
'week', 'dayofyear']
612+
'week', 'dayofyear', 'days_in_month']
607613
for field in fields:
608614
prop = property(fget=lambda self: np.nan)
609615
setattr(NaTType, field, prop)
@@ -3188,6 +3194,14 @@ def get_date_field(ndarray[int64_t] dtindex, object field):
31883194
out[i] = ((out[i] - 1) / 3) + 1
31893195
return out
31903196

3197+
elif field == 'dim':
3198+
for i in range(count):
3199+
if dtindex[i] == NPY_NAT: out[i] = -1; continue
3200+
3201+
pandas_datetime_to_datetimestruct(dtindex[i], PANDAS_FR_ns, &dts)
3202+
out[i] = monthrange(dts.year, dts.month)[1]
3203+
return out
3204+
31913205
raise ValueError("Field %s not supported" % field)
31923206

31933207

0 commit comments

Comments
 (0)