Repository: pandas-dev/pandas

Issue:
AbstractHolidayCalendar: suggestion to update documentation with correct usage
``` python
    from pandas.tseries.holiday import Holiday, nearest_workday, get_calendar,    HolidayCalendarFactory, USPresidentsDay, \
USMemorialDay, USLaborDay, USThanksgivingDay, AbstractHolidayCalendar, USFederalHolidayCalendar

    from pandas.util.print_versions import show_versions
    >>> show_versions()

INSTALLED VERSIONS
------------------
commit: None
python: 3.4.3.final.0
python-bits: 64
OS: Linux
OS-release: 3.13.0-24-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: en_US.UTF-8
LANG: en_US.UTF-8

pandas: 0.16.2
nose: 1.3.4
Cython: None
numpy: 1.9.1
scipy: None
statsmodels: None
IPython: 4.0.0
sphinx: None
patsy: None
dateutil: 2.2
pytz: 2013.9
bottleneck: None
tables: None
numexpr: None
matplotlib: 1.3.1
openpyxl: 2.1.4
xlrd: None
xlwt: None
xlsxwriter: None
lxml: 3.4.4
bs4: 4.3.1
html5lib: None
httplib2: None
apiclient: None
sqlalchemy: 0.9.7
pymysql: None
psycopg2: 2.4.6 (dt dec pq3 ext)

    >>> class MyCalendar(AbstractHolidayCalendar):
    rules = [
        Holiday("New Years Day", month=1, day=1, observance=nearest_workday),
        USPresidentsDay,
        USMemorialDay,
        Holiday("Independence Day", month=7, day=4, observance=nearest_workday),
        USLaborDay,
        Holiday("Veterans Day", month=11, day=11, observance=nearest_workday),
        USThanksgivingDay,
        Holiday("Christmas", month=12, day=25, observance=nearest_workday)
    ]

    >>> cal = MyCalendar()
    >>> cal.rules

[Holiday: New Years Day (month=1, day=1, observance=<function nearest_workday at 0x7f6dbbfd5bf8>),
 Holiday: Presidents Day (month=2, day=1, offset=<DateOffset: kwds={'weekday': MO(+3)}>),
 Holiday: MemorialDay (month=5, day=24, offset=<DateOffset: kwds={'weekday': MO(+1)}>),
 Holiday: Independence Day (month=7, day=4, observance=<function nearest_workday at 0x7f6dbbfd5bf8>),
 Holiday: Labor Day (month=9, day=1, offset=<DateOffset: kwds={'weekday': MO(+1)}>),
 Holiday: Veterans Day (month=11, day=11, observance=<function nearest_workday at 0x7f6dbbfd5bf8>),
 Holiday: Thanksgiving (month=11, day=1, offset=<DateOffset: kwds={'weekday': TH(+4)}>),
 Holiday: Christmas (month=12, day=25, observance=<function nearest_workday at 0x7f6dbbfd5bf8>)]

#this works as expected, but isn't in the documentation:

    >>> cal.holidays(start='11/5/2014', end='11/4/2015')

DatetimeIndex(['2014-11-11', '2014-11-27', '2014-12-25', '2015-01-01',
               '2015-02-16', '2015-05-25', '2015-07-03', '2015-09-07'],
              dtype='datetime64[ns]', freq=None, tz=None)

#this is what the documentation says to do, and the results make no sense:

    from pandas.tseries.offsets import CDay #docs don't actually say where CDay comes from, I had to dig that up myself

    >>> oneyear = pd.DatetimeIndex(start='11/5/2014', end='11/4/2015', freq=CDay(calendar=cal)).to_pydatetime()
    >>> oneyear

array([datetime.datetime(2014, 11, 5, 0, 0),
       datetime.datetime(2014, 11, 6, 0, 0),
       datetime.datetime(2014, 11, 7, 0, 0),
       datetime.datetime(2014, 11, 10, 0, 0),
       datetime.datetime(2014, 11, 11, 0, 0),
       datetime.datetime(2014, 11, 12, 0, 0),
       datetime.datetime(2014, 11, 13, 0, 0),
       datetime.datetime(2014, 11, 14, 0, 0),
       datetime.datetime(2014, 11, 17, 0, 0),
       datetime.datetime(2014, 11, 18, 0, 0),
       datetime.datetime(2014, 11, 19, 0, 0),
       datetime.datetime(2014, 11, 20, 0, 0),
       datetime.datetime(2014, 11, 21, 0, 0),
       datetime.datetime(2014, 11, 24, 0, 0),
       datetime.datetime(2014, 11, 25, 0, 0),
       datetime.datetime(2014, 11, 26, 0, 0),
       datetime.datetime(2014, 11, 27, 0, 0),
       datetime.datetime(2014, 11, 28, 0, 0), etc.


```



Discussion:
You are right, your last example does not make any sense.  It should skip 11/11 (just like the documentation example skips 7/4).  Seems like something is not working as expected.

As far as .holidays(startDate, endDate) I think we could add an example to the docs but this should probably go into the API documentation.

The CDay import is suppressed by the [docs](https://raw.githubusercontent.com/pydata/pandas/master/doc/source/timeseries.rst) -- from pandas.tseries.offsets import *.  I don't see any harm in making this explicit.

I do not see the same behavior as you not skipping 11/11 on pandas 0.17.0.  Can you try and see if you see the same thing under 0.17.0?

```
oneyear = pd.DatetimeIndex(start='11/5/2014', end='11/4/2015', freq=CDay(calendar=cal)).to_pydatetime()
oneyear
Out[10]: 
array([datetime.datetime(2014, 11, 5, 0, 0),
       datetime.datetime(2014, 11, 6, 0, 0),
       datetime.datetime(2014, 11, 7, 0, 0),
       datetime.datetime(2014, 11, 10, 0, 0),
       datetime.datetime(2014, 11, 12, 0, 0),
       datetime.datetime(2014, 11, 13, 0, 0),
       datetime.datetime(2014, 11, 14, 0, 0),
       datetime.datetime(2014, 11, 17, 0, 0),
       datetime.datetime(2014, 11, 18, 0, 0),
       datetime.datetime(2014, 11, 19, 0, 0),
       datetime.datetime(2014, 11, 20, 0, 0),
       datetime.datetime(2014, 11, 21, 0, 0),
       datetime.datetime(2014, 11, 24, 0, 0),
       datetime.datetime(2014, 11, 25, 0, 0),
       datetime.datetime(2014, 11, 26, 0, 0),
       datetime.datetime(2014, 11, 28, 0, 0),
```

Skipping 11/11 was not the point I was making. I think it probably should skip that day (Veterans Day). I was saying I expected it to give me back _only_ the dates from the calendar, and instead it gave me back what looks like every day _except_ the dates from the calendar. 

CDay is custom business day so it supposed to give business days skipping holidays/weekends.

"The CDay or CustomBusinessDay class provides a parametric BusinessDay class which can be used to create customized business day calendars which account for local holidays and local weekend conventions."

that's good. The docs need some better descriptions of when and how to actually use a calendar object. It's especially bad to introduce things like CDay that live elsewhere in the code without first defining them and/or linking to them. 


Resolving patch:
diff --git a/pandas/tseries/holiday.py b/pandas/tseries/holiday.py
--- a/pandas/tseries/holiday.py
+++ b/pandas/tseries/holiday.py
@@ -3,6 +3,7 @@
 from datetime import datetime, timedelta
 from dateutil.relativedelta import MO, TU, WE, TH, FR, SA, SU
 from pandas.tseries.offsets import Easter, Day
+import numpy as np
 
 
 def next_monday(dt):
@@ -156,8 +157,8 @@ class from pandas.tseries.offsets
         self.month = month
         self.day = day
         self.offset = offset
-        self.start_date = start_date
-        self.end_date = end_date
+        self.start_date = Timestamp(start_date) if start_date is not None else start_date
+        self.end_date = Timestamp(end_date) if end_date is not None else end_date
         self.observance = observance
         assert (days_of_week is None or type(days_of_week) == tuple)
         self.days_of_week = days_of_week
@@ -179,7 +180,7 @@ def __repr__(self):
 
     def dates(self, start_date, end_date, return_name=False):
         """
-        Calculate holidays between start date and end date
+        Calculate holidays observed between start date and end date
 
         Parameters
         ----------
@@ -189,6 +190,12 @@ def dates(self, start_date, end_date, return_name=False):
             If True, return a series that has dates and holiday names.
             False will only return dates.
         """
+        start_date = Timestamp(start_date)
+        end_date = Timestamp(end_date)
+        
+        filter_start_date = start_date 
+        filter_end_date = end_date
+        
         if self.year is not None:
             dt = Timestamp(datetime(self.year, self.month, self.day))
             if return_name:
@@ -196,40 +203,57 @@ def dates(self, start_date, end_date, return_name=False):
             else:
                 return [dt]
 
-        if self.start_date is not None:
-            start_date = self.start_date
-
-        if self.end_date is not None:
-            end_date = self.end_date
-
-        start_date = Timestamp(start_date)
-        end_date = Timestamp(end_date)
-
-        year_offset = DateOffset(years=1)
-        base_date = Timestamp(
-            datetime(start_date.year, self.month, self.day),
-            tz=start_date.tz,
-        )
-        dates = DatetimeIndex(start=base_date, end=end_date, freq=year_offset)
+        dates = self._reference_dates(start_date, end_date)
         holiday_dates = self._apply_rule(dates)
         if self.days_of_week is not None:
-            holiday_dates = list(filter(lambda x: x is not None and
-                                                  x.dayofweek in self.days_of_week,
-                                                  holiday_dates))
-        else:
-            holiday_dates = list(filter(lambda x: x is not None, holiday_dates))
+            holiday_dates = holiday_dates[np.in1d(holiday_dates.dayofweek,
+                                                  self.days_of_week)]
+            
+        if self.start_date is not None:
+            filter_start_date = max(self.start_date.tz_localize(filter_start_date.tz), filter_start_date)
+        if self.end_date is not None:
+            filter_end_date = min(self.end_date.tz_localize(filter_end_date.tz), filter_end_date)
+        holiday_dates = holiday_dates[(holiday_dates >= filter_start_date) & 
+                                      (holiday_dates <= filter_end_date)]
         if return_name:
             return Series(self.name, index=holiday_dates)
         return holiday_dates
+        
+        
+    def _reference_dates(self, start_date, end_date):
+        """
+        Get reference dates for the holiday.
+        
+        Return reference dates for the holiday also returning the year
+        prior to the start_date and year following the end_date.  This ensures
+        that any offsets to be applied will yield the holidays within
+        the passed in dates.
+        """
+        if self.start_date is not None:
+            start_date = self.start_date.tz_localize(start_date.tz)
+
+        if self.end_date is not None:
+            end_date = self.end_date.tz_localize(start_date.tz)
+
+        year_offset = DateOffset(years=1)
+        reference_start_date = Timestamp(
+            datetime(start_date.year-1, self.month, self.day))
+        
+        reference_end_date = Timestamp(
+            datetime(end_date.year+1, self.month, self.day))
+        # Don't process unnecessary holidays
+        dates = DatetimeIndex(start=reference_start_date, end=reference_end_date, 
+                              freq=year_offset, tz=start_date.tz)
+        
+        return dates
 
     def _apply_rule(self, dates):
         """
-        Apply the given offset/observance to an
-        iterable of dates.
+        Apply the given offset/observance to a DatetimeIndex of dates.
 
         Parameters
         ----------
-        dates : array-like
+        dates : DatetimeIndex
             Dates to apply the given offset/observance rule
 
         Returns
@@ -237,7 +261,7 @@ def _apply_rule(self, dates):
         Dates with rules applied
         """
         if self.observance is not None:
-            return map(lambda d: self.observance(d), dates)
+            return dates.map(lambda d: self.observance(d))
 
         if self.offset is not None:
             if not isinstance(self.offset, list):
@@ -245,7 +269,7 @@ def _apply_rule(self, dates):
             else:
                 offsets = self.offset
             for offset in offsets:
-                dates = list(map(lambda d: d + offset, dates))
+                dates += offset
         return dates
 
 holiday_calendars = {}
@@ -303,6 +327,13 @@ def __init__(self, name=None, rules=None):
 
         if rules is not None:
             self.rules = rules
+            
+    def rule_from_name(self, name):
+        for rule in self.rules:
+            if rule.name == name:
+                return rule
+            
+        return None
 
     def holidays(self, start=None, end=None, return_name=False):
         """


Test patch:
diff --git a/pandas/tseries/tests/test_holiday.py b/pandas/tseries/tests/test_holiday.py
--- a/pandas/tseries/tests/test_holiday.py
+++ b/pandas/tseries/tests/test_holiday.py
@@ -1,15 +1,17 @@
 
 from datetime import datetime
 import pandas.util.testing as tm
+from pandas import compat
 from pandas import DatetimeIndex
 from pandas.tseries.holiday import (
-    USFederalHolidayCalendar, USMemorialDay, USThanksgivingDay,
+    USFederalHolidayCalendar, USMemorialDay, USThanksgivingDay, 
     nearest_workday, next_monday_or_tuesday, next_monday,
     previous_friday, sunday_to_monday, Holiday, DateOffset,
     MO, Timestamp, AbstractHolidayCalendar, get_calendar,
     HolidayCalendarFactory, next_workday, previous_workday,
     before_nearest_workday, EasterMonday, GoodFriday,
-    after_nearest_workday, weekend_to_monday)
+    after_nearest_workday, weekend_to_monday, USLaborDay,
+    USColumbusDay, USMartinLutherKingJr, USPresidentsDay)
 from pytz import utc
 import nose
 
@@ -72,7 +74,20 @@ def __init__(self, name=None, rules=None):
             jan2.holidays(),
             DatetimeIndex(['02-Jan-2015'])
         )
-
+        
+    def test_calendar_observance_dates(self):
+        # Test for issue 11477
+        USFedCal = get_calendar('USFederalHolidayCalendar')
+        holidays0 = USFedCal.holidays(datetime(2015,7,3), datetime(2015,7,3)) # <-- same start and end dates
+        holidays1 = USFedCal.holidays(datetime(2015,7,3), datetime(2015,7,6)) # <-- different start and end dates
+        holidays2 = USFedCal.holidays(datetime(2015,7,3), datetime(2015,7,3)) # <-- same start and end dates
+        
+        tm.assert_index_equal(holidays0, holidays1)
+        tm.assert_index_equal(holidays0, holidays2)
+        
+    def test_rule_from_name(self):
+        USFedCal = get_calendar('USFederalHolidayCalendar')
+        self.assertEqual(USFedCal.rule_from_name('Thanksgiving'), USThanksgivingDay)
 
 class TestHoliday(tm.TestCase):
 
@@ -193,6 +208,52 @@ def test_usthanksgivingday(self):
                 datetime(2020, 11, 26),
             ],
         )
+        
+    def test_holidays_within_dates(self):
+        # Fix holiday behavior found in #11477
+        # where holiday.dates returned dates outside start/end date
+        # or observed rules could not be applied as the holiday
+        # was not in the original date range (e.g., 7/4/2015 -> 7/3/2015)
+        start_date = datetime(2015, 7, 1)
+        end_date = datetime(2015, 7, 1)
+         
+        calendar = get_calendar('USFederalHolidayCalendar')
+        new_years = calendar.rule_from_name('New Years Day')
+        july_4th = calendar.rule_from_name('July 4th')
+        veterans_day = calendar.rule_from_name('Veterans Day')
+        christmas = calendar.rule_from_name('Christmas')
+         
+        # Holiday: (start/end date, holiday)
+        holidays = {USMemorialDay:          ("2015-05-25", "2015-05-25"),
+                    USLaborDay:             ("2015-09-07", "2015-09-07"),
+                    USColumbusDay:          ("2015-10-12", "2015-10-12"),
+                    USThanksgivingDay:      ("2015-11-26", "2015-11-26"),
+                    USMartinLutherKingJr:   ("2015-01-19", "2015-01-19"),
+                    USPresidentsDay:        ("2015-02-16", "2015-02-16"),
+                    GoodFriday:             ("2015-04-03", "2015-04-03"),
+                    EasterMonday:           [("2015-04-06", "2015-04-06"),
+                                             ("2015-04-05", [])],
+                    new_years:              [("2015-01-01", "2015-01-01"),
+                                             ("2011-01-01", []),
+                                             ("2010-12-31", "2010-12-31")],
+                    july_4th:               [("2015-07-03", "2015-07-03"),
+                                             ("2015-07-04", [])],
+                    veterans_day:           [("2012-11-11", []),
+                                             ("2012-11-12", "2012-11-12")],
+                    christmas:              [("2011-12-25", []),
+                                             ("2011-12-26", "2011-12-26")]}
+         
+        for rule, dates in compat.iteritems(holidays):
+            empty_dates = rule.dates(start_date, end_date)
+            self.assertEqual(empty_dates.tolist(), [])
+             
+            if isinstance(dates, tuple):
+                dates = [dates]
+                
+            for start, expected in dates:
+                if len(expected):
+                    expected = [Timestamp(expected)]
+                self.check_results(rule, start, start, expected)
 
     def test_argument_types(self):
         holidays = USThanksgivingDay.dates(self.start_date,
@@ -206,8 +267,8 @@ def test_argument_types(self):
                         Timestamp(self.start_date),
                         Timestamp(self.end_date))
 
-        self.assertEqual(holidays, holidays_1)
-        self.assertEqual(holidays, holidays_2)
+        self.assert_index_equal(holidays, holidays_1)
+        self.assert_index_equal(holidays, holidays_2)
 
     def test_special_holidays(self):
         base_date = [datetime(2012, 5, 28)]