Repository: pandas-dev/pandas

Issue:
API: add is_beg_month/year, is_end_month/year accessors for a DateTimeIndex/Timestamp
```
In [110]: x = Timestamp('20130101')
```

Last day of month

```
In [111]: pd.tslib.monthrange(x.year,x.month)[1]==x.day
Out[111]: False
```

First day of month

```
In [112]: x.day==1
Out[112]: True
```

First day of year

```
In [113]: x.day==1 & x.month==1
Out[113]: True
```

http://stackoverflow.com/questions/18233107/pandas-convert-datetime-to-end-of-month/18233876?noredirect=1#comment26732170_18233876



Discussion:

Resolving patch:
diff --git a/pandas/core/base.py b/pandas/core/base.py
--- a/pandas/core/base.py
+++ b/pandas/core/base.py
@@ -336,3 +336,9 @@ def nunique(self):
     dayofyear = _field_accessor('dayofyear', "The ordinal day of the year")
     quarter = _field_accessor('quarter', "The quarter of the date")
     qyear = _field_accessor('qyear')
+    is_month_start = _field_accessor('is_month_start', "Logical indicating if first day of month (defined by frequency)")
+    is_month_end = _field_accessor('is_month_end', "Logical indicating if last day of month (defined by frequency)")
+    is_quarter_start = _field_accessor('is_quarter_start', "Logical indicating if first day of quarter (defined by frequency)")
+    is_quarter_end = _field_accessor('is_quarter_end', "Logical indicating if last day of quarter (defined by frequency)")
+    is_year_start = _field_accessor('is_year_start', "Logical indicating if first day of year (defined by frequency)")
+    is_year_end = _field_accessor('is_year_end', "Logical indicating if last day of year (defined by frequency)")
diff --git a/pandas/tseries/index.py b/pandas/tseries/index.py
--- a/pandas/tseries/index.py
+++ b/pandas/tseries/index.py
@@ -14,7 +14,7 @@
 from pandas.compat import u
 from pandas.tseries.frequencies import (
     infer_freq, to_offset, get_period_alias,
-    Resolution, get_reso_string)
+    Resolution, get_reso_string, get_offset)
 from pandas.tseries.offsets import DateOffset, generate_range, Tick, CDay
 from pandas.tseries.tools import parse_time_string, normalize_date
 from pandas.util.decorators import cache_readonly
@@ -28,6 +28,7 @@
 import pandas.algos as _algos
 import pandas.index as _index
 
+from pandas.tslib import isleapyear
 
 def _utc():
     import pytz
@@ -43,7 +44,14 @@ def f(self):
             utc = _utc()
             if self.tz is not utc:
                 values = self._local_timestamps()
-        return tslib.get_date_field(values, field)
+        if field in ['is_month_start', 'is_month_end',
+                    'is_quarter_start', 'is_quarter_end',
+                    'is_year_start', 'is_year_end']:
+            month_kw = self.freq.kwds.get('startingMonth', self.freq.kwds.get('month', 12)) if self.freq else 12
+            freqstr = self.freqstr if self.freq else None
+            return tslib.get_start_end_field(values, field, freqstr, month_kw)
+        else:
+            return tslib.get_date_field(values, field)
     f.__name__ = name
     f.__doc__ = docstring
     return property(f)
@@ -1439,6 +1447,12 @@ def freqstr(self):
     _weekday = _dayofweek
     _dayofyear = _field_accessor('dayofyear', 'doy')
     _quarter = _field_accessor('quarter', 'q')
+    _is_month_start = _field_accessor('is_month_start', 'is_month_start')
+    _is_month_end = _field_accessor('is_month_end', 'is_month_end')
+    _is_quarter_start = _field_accessor('is_quarter_start', 'is_quarter_start')
+    _is_quarter_end = _field_accessor('is_quarter_end', 'is_quarter_end')
+    _is_year_start = _field_accessor('is_year_start', 'is_year_start')
+    _is_year_end = _field_accessor('is_year_end', 'is_year_end')
 
     @property
     def _time(self):
@@ -1774,6 +1788,7 @@ def to_julian_date(self):
                              self.nanosecond/3600.0/1e+9
                             )/24.0)
 
+
 def _generate_regular_range(start, end, periods, offset):
     if isinstance(offset, Tick):
         stride = offset.nanos
@@ -1831,7 +1846,7 @@ def date_range(start=None, end=None, periods=None, freq='D', tz=None,
         Frequency strings can have multiples, e.g. '5H'
     tz : string or None
         Time zone name for returning localized DatetimeIndex, for example
-        Asia/Hong_Kong
+    Asia/Hong_Kong
     normalize : bool, default False
         Normalize start/end dates to midnight before generating date range
     name : str, default None
diff --git a/vb_suite/timeseries.py b/vb_suite/timeseries.py
--- a/vb_suite/timeseries.py
+++ b/vb_suite/timeseries.py
@@ -303,3 +303,14 @@ def date_range(start=None, end=None, periods=None, freq=None):
 
 timeseries_custom_bmonthend_incr_n = \
     Benchmark("date + 10 * cme",setup)
+
+#----------------------------------------------------------------------
+# month/quarter/year start/end accessors
+
+setup = common_setup + """
+N = 10000
+rng = date_range('1/1/1', periods=N, freq='B')
+"""
+
+timeseries_is_month_start = Benchmark('rng.is_month_start', setup,
+                                  start_date=datetime(2014, 4, 1))


Test patch:
diff --git a/pandas/tests/test_base.py b/pandas/tests/test_base.py
--- a/pandas/tests/test_base.py
+++ b/pandas/tests/test_base.py
@@ -214,7 +214,7 @@ def test_value_counts_unique_nunique(self):
                 # freq must be specified because repeat makes freq ambiguous
                 o = klass(np.repeat(values, range(1, len(o) + 1)), freq=o.freq)
             else:
-                o = klass(np.repeat(values, range(1, len(o) + 1)))                
+                o = klass(np.repeat(values, range(1, len(o) + 1)))
 
             expected_s = Series(range(10, 0, -1), index=values[::-1], dtype='int64')
             tm.assert_series_equal(o.value_counts(), expected_s)
@@ -246,7 +246,7 @@ def test_value_counts_unique_nunique(self):
                 if isinstance(o, PeriodIndex):
                     o = klass(np.repeat(values, range(1, len(o) + 1)), freq=o.freq)
                 else:
-                    o = klass(np.repeat(values, range(1, len(o) + 1)))   
+                    o = klass(np.repeat(values, range(1, len(o) + 1)))
 
                 if isinstance(o, DatetimeIndex):
                     # DatetimeIndex: nan is casted to Nat and included
@@ -278,7 +278,7 @@ def test_value_counts_inferred(self):
             s = klass(s_values)
             expected = Series([4, 3, 2, 1], index=['b', 'a', 'd', 'c'])
             tm.assert_series_equal(s.value_counts(), expected)
-            
+
             self.assert_numpy_array_equal(s.unique(), np.unique(s_values))
             self.assertEquals(s.nunique(), 4)
             # don't sort, have to sort after the fact as not sorting is platform-dep
@@ -410,7 +410,7 @@ def setUp(self):
 
     def test_ops_properties(self):
         self.check_ops_properties(['year','month','day','hour','minute','second','weekofyear','week','dayofweek','dayofyear','quarter'])
-        self.check_ops_properties(['date','time','microsecond','nanosecond'], lambda x: isinstance(x,DatetimeIndex))
+        self.check_ops_properties(['date','time','microsecond','nanosecond', 'is_month_start', 'is_month_end', 'is_quarter_start', 'is_quarter_end', 'is_year_start', 'is_year_end'], lambda x: isinstance(x,DatetimeIndex))
 
 class TestPeriodIndexOps(Ops):
     _allowed = '_allow_period_index_ops'
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
@@ -1473,7 +1473,7 @@ def test_timestamp_fields(self):
         # extra fields from DatetimeIndex like quarter and week
         idx = tm.makeDateIndex(100)
 
-        fields = ['dayofweek', 'dayofyear', 'week', 'weekofyear', 'quarter']
+        fields = ['dayofweek', 'dayofyear', 'week', 'weekofyear', 'quarter', '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)
@@ -2192,7 +2192,7 @@ def test_join_with_period_index(self):
 
 class TestDatetime64(tm.TestCase):
     """
-    Also test supoprt for datetime64[ns] in Series / DataFrame
+    Also test support for datetime64[ns] in Series / DataFrame
     """
 
     def setUp(self):
@@ -2202,37 +2202,115 @@ def setUp(self):
 
     def test_datetimeindex_accessors(self):
         dti = DatetimeIndex(
-            freq='Q-JAN', start=datetime(1997, 12, 31), periods=100)
+            freq='D', start=datetime(1998, 1, 1), periods=365)
 
         self.assertEquals(dti.year[0], 1998)
         self.assertEquals(dti.month[0], 1)
-        self.assertEquals(dti.day[0], 31)
+        self.assertEquals(dti.day[0], 1)
         self.assertEquals(dti.hour[0], 0)
         self.assertEquals(dti.minute[0], 0)
         self.assertEquals(dti.second[0], 0)
         self.assertEquals(dti.microsecond[0], 0)
-        self.assertEquals(dti.dayofweek[0], 5)
+        self.assertEquals(dti.dayofweek[0], 3)
 
-        self.assertEquals(dti.dayofyear[0], 31)
-        self.assertEquals(dti.dayofyear[1], 120)
+        self.assertEquals(dti.dayofyear[0], 1)
+        self.assertEquals(dti.dayofyear[120], 121)
 
-        self.assertEquals(dti.weekofyear[0], 5)
-        self.assertEquals(dti.weekofyear[1], 18)
+        self.assertEquals(dti.weekofyear[0], 1)
+        self.assertEquals(dti.weekofyear[120], 18)
 
         self.assertEquals(dti.quarter[0], 1)
-        self.assertEquals(dti.quarter[1], 2)
-
-        self.assertEquals(len(dti.year), 100)
-        self.assertEquals(len(dti.month), 100)
-        self.assertEquals(len(dti.day), 100)
-        self.assertEquals(len(dti.hour), 100)
-        self.assertEquals(len(dti.minute), 100)
-        self.assertEquals(len(dti.second), 100)
-        self.assertEquals(len(dti.microsecond), 100)
-        self.assertEquals(len(dti.dayofweek), 100)
-        self.assertEquals(len(dti.dayofyear), 100)
-        self.assertEquals(len(dti.weekofyear), 100)
-        self.assertEquals(len(dti.quarter), 100)
+        self.assertEquals(dti.quarter[120], 2)
+
+        self.assertEquals(dti.is_month_start[0], True)
+        self.assertEquals(dti.is_month_start[1], False)
+        self.assertEquals(dti.is_month_start[31], True)
+        self.assertEquals(dti.is_quarter_start[0], True)
+        self.assertEquals(dti.is_quarter_start[90], True)
+        self.assertEquals(dti.is_year_start[0], True)
+        self.assertEquals(dti.is_year_start[364], False)
+        self.assertEquals(dti.is_month_end[0], False)
+        self.assertEquals(dti.is_month_end[30], True)
+        self.assertEquals(dti.is_month_end[31], False)
+        self.assertEquals(dti.is_month_end[364], True)
+        self.assertEquals(dti.is_quarter_end[0], False)
+        self.assertEquals(dti.is_quarter_end[30], False)
+        self.assertEquals(dti.is_quarter_end[89], True)
+        self.assertEquals(dti.is_quarter_end[364], True)
+        self.assertEquals(dti.is_year_end[0], False)
+        self.assertEquals(dti.is_year_end[364], True)
+
+        self.assertEquals(len(dti.year), 365)
+        self.assertEquals(len(dti.month), 365)
+        self.assertEquals(len(dti.day), 365)
+        self.assertEquals(len(dti.hour), 365)
+        self.assertEquals(len(dti.minute), 365)
+        self.assertEquals(len(dti.second), 365)
+        self.assertEquals(len(dti.microsecond), 365)
+        self.assertEquals(len(dti.dayofweek), 365)
+        self.assertEquals(len(dti.dayofyear), 365)
+        self.assertEquals(len(dti.weekofyear), 365)
+        self.assertEquals(len(dti.quarter), 365)
+        self.assertEquals(len(dti.is_month_start), 365)
+        self.assertEquals(len(dti.is_month_end), 365)
+        self.assertEquals(len(dti.is_quarter_start), 365)
+        self.assertEquals(len(dti.is_quarter_end), 365)
+        self.assertEquals(len(dti.is_year_start), 365)
+        self.assertEquals(len(dti.is_year_end), 365)
+
+        dti = DatetimeIndex(
+            freq='BQ-FEB', start=datetime(1998, 1, 1), periods=4)
+
+        self.assertEquals(sum(dti.is_quarter_start), 0)
+        self.assertEquals(sum(dti.is_quarter_end), 4)
+        self.assertEquals(sum(dti.is_year_start), 0)
+        self.assertEquals(sum(dti.is_year_end), 1)
+
+        # Ensure is_start/end accessors throw ValueError for CustomBusinessDay, CBD requires np >= 1.7
+        if not _np_version_under1p7:
+            bday_egypt = offsets.CustomBusinessDay(weekmask='Sun Mon Tue Wed Thu')
+            dti = date_range(datetime(2013, 4, 30), periods=5, freq=bday_egypt)
+            self.assertRaises(ValueError, lambda: dti.is_month_start)
+
+        dti = DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-03'])
+
+        self.assertEquals(dti.is_month_start[0], 1)
+
+        tests = [
+            (Timestamp('2013-06-01', offset='M').is_month_start, 1),
+            (Timestamp('2013-06-01', offset='BM').is_month_start, 0),
+            (Timestamp('2013-06-03', offset='M').is_month_start, 0),
+            (Timestamp('2013-06-03', offset='BM').is_month_start, 1),
+            (Timestamp('2013-02-28', offset='Q-FEB').is_month_end, 1),
+            (Timestamp('2013-02-28', offset='Q-FEB').is_quarter_end, 1),
+            (Timestamp('2013-02-28', offset='Q-FEB').is_year_end, 1),
+            (Timestamp('2013-03-01', offset='Q-FEB').is_month_start, 1),
+            (Timestamp('2013-03-01', offset='Q-FEB').is_quarter_start, 1),
+            (Timestamp('2013-03-01', offset='Q-FEB').is_year_start, 1),
+            (Timestamp('2013-03-31', offset='QS-FEB').is_month_end, 1),
+            (Timestamp('2013-03-31', offset='QS-FEB').is_quarter_end, 0),
+            (Timestamp('2013-03-31', offset='QS-FEB').is_year_end, 0),
+            (Timestamp('2013-02-01', offset='QS-FEB').is_month_start, 1),
+            (Timestamp('2013-02-01', offset='QS-FEB').is_quarter_start, 1),
+            (Timestamp('2013-02-01', offset='QS-FEB').is_year_start, 1),
+            (Timestamp('2013-06-30', offset='BQ').is_month_end, 0),
+            (Timestamp('2013-06-30', offset='BQ').is_quarter_end, 0),
+            (Timestamp('2013-06-30', offset='BQ').is_year_end, 0),
+            (Timestamp('2013-06-28', offset='BQ').is_month_end, 1),
+            (Timestamp('2013-06-28', offset='BQ').is_quarter_end, 1),
+            (Timestamp('2013-06-28', offset='BQ').is_year_end, 0),
+            (Timestamp('2013-06-30', offset='BQS-APR').is_month_end, 0),
+            (Timestamp('2013-06-30', offset='BQS-APR').is_quarter_end, 0),
+            (Timestamp('2013-06-30', offset='BQS-APR').is_year_end, 0),
+            (Timestamp('2013-06-28', offset='BQS-APR').is_month_end, 1),
+            (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)]
+
+        for ts, value in tests:
+            self.assertEquals(ts, value)
+
 
     def test_nanosecond_field(self):
         dti = DatetimeIndex(np.arange(10))