Repository: pandas-dev/pandas

Issue:
ENH: add days_in_month to Timestamp/DatetimeIndex/.dt
http://stackoverflow.com/questions/28819470/numbers-of-day-in-month?nah=1#28821925
xref #4640 

This should be exposed as a vectorized method in tslib (e.g. take in an ndarray of i8 and return an ndarray)

```
pd.tslib.monthrange is an unadvertised / undocumented function that handles the days_in_month calculation (adjusting for leap years). This could/should prob be added as a property to Timestamp/DatetimeIndex (and to ``.dt``)

In [34]: df = DataFrame({'date' : pd.date_range('20120101',periods=15,freq='M') })

In [35]: df['year'] = df['date'].dt.year

In [36]: df['month'] = df['date'].dt.month

In [37]: df['days_in_month'] = df.apply(lambda x: pd.tslib.monthrange(x['year'],x['month'])[1], axis=1)

In [38]: df
Out[38]: 
         date  year  month  days_in_month
0  2012-01-31  2012      1             31
1  2012-02-29  2012      2             29
2  2012-03-31  2012      3             31
3  2012-04-30  2012      4             30
4  2012-05-31  2012      5             31
5  2012-06-30  2012      6             30
6  2012-07-31  2012      7             31
7  2012-08-31  2012      8             31
8  2012-09-30  2012      9             30
9  2012-10-31  2012     10             31
10 2012-11-30  2012     11             30
11 2012-12-31  2012     12             31
12 2013-01-31  2013      1             31
13 2013-02-28  2013      2             28
14 2013-03-31  2013      3             31
```



Discussion:

Resolving patch:
diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py
--- 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
--- 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)


Test patch:
diff --git a/pandas/tests/test_series.py b/pandas/tests/test_series.py
--- 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/tests/test_period.py b/pandas/tseries/tests/test_period.py
--- 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
--- 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)