Holidays


Description:
This class knows about a several holidays and can return date value representing the day that a holiday falls on for a given year. It can also detemine if a given date is a holiday.

1/27/2000: Added several more holidays to this class. I should now have every major American holiday.

5/4/2001: Fixed the date calculation for Tax Day.

10/22/2004: Added several Jewish holidays.
 
Code:
'--- clsHolidays

Option Explicit

' Hebrew_* Functions by Kees Couprie from
'  http://www.geocities.com/couprie/calmath/index.html

'The following properties are exposed:
' Count() As Long
' HolidayName(holiday As holidays) As String
' HolidayInYear(holiday As holidays, InYear As Date) As Date
' IsHoliday(DateQuery As Date) As Boolean
' HolidayNameOfDate(DateQuery As Date) As String

Public Enum Holidays
    NewYearsDay
    MartinLutherKingDay
    GroundhogDay
    ValentinesDay
    PresidentsDay
    SaintPatricksDay
    TaxDay
    Easter
    MothersDay
    MemorialDay
    FlagDay
    FathersDay
    IndependenceDay
    LaborDay
    ColumbusDay
    Halloween
    ElectionDay
    VeteransDay
    ThanksgivingDay
    ChristmasEve
    Christmas
    NewYearsEve
    RoshHashanah
    YomKippur
    Sukkot
    SheminiAtzeret
    SimchatTorah
    Chanukkah
    TuBShevat
    Purim
    PesachPassover
    LagBOmer
    Shavuot
    TishaBAv
    
    HolidayCount
End Enum

Private Const Hebrew_c_Gregorian = 1
Private Const Hebrew_c_Julian = 1

Public Property Get Count() As Long
    
    Count = HolidayCount
    
End Property

Public Property Get HolidayName(holiday As Holidays) As String
    
    Select Case holiday
        Case NewYearsDay
            HolidayName = "New Year's Day"
        Case MartinLutherKingDay
            HolidayName = "Martin Luther King Day"
        Case PresidentsDay
            HolidayName = "President's Day"
        Case Easter
            HolidayName = "Easter"
        Case MemorialDay
            HolidayName = "Memorial Day"
        Case IndependenceDay
            HolidayName = "Independence Day"
        Case LaborDay
            HolidayName = "Labor Day"
        Case ColumbusDay
            HolidayName = "Columbus Day"
        Case VeteransDay
            HolidayName = "Veterans' Day"
        Case ThanksgivingDay
            HolidayName = "Thanksgiving Day"
        Case Christmas
            HolidayName = "Christmas"
        Case NewYearsEve
            HolidayName = "New Years Eve"
        Case Halloween
            HolidayName = "Halloween"
        Case GroundhogDay
            HolidayName = "Groundhog Day"
        Case ValentinesDay
            HolidayName = "Valentine's Day"
        Case SaintPatricksDay
            HolidayName = "Saint Patrick's Day"
        Case TaxDay
            HolidayName = "Tax Day"
        Case MothersDay
            HolidayName = "Mother's Day"
        Case FlagDay
            HolidayName = "Flag Day"
        Case FathersDay
            HolidayName = "Father's Day"
        Case ElectionDay
            HolidayName = "Election Day"
        Case ChristmasEve
            HolidayName = "Christmas Eve"
        Case RoshHashanah
            HolidayName = "Rosh Hashanah"
        Case YomKippur
            HolidayName = "Yom Kippur"
        Case Sukkot
            HolidayName = "Sukkot"
        Case SheminiAtzeret
            HolidayName = "Shemini Atzeret"
        Case SimchatTorah
            HolidayName = "Simchat Torah"
        Case Chanukkah
            HolidayName = "Chanukkah"
        Case TuBShevat
            HolidayName = "Tu B'Shevat"
        Case Purim
            HolidayName = "Purim"
        Case PesachPassover
            HolidayName = "Pesach (Passover)"
        Case LagBOmer
            HolidayName = "Lag B'Omer"
        Case Shavuot
            HolidayName = "Shavu'ot"
        Case TishaBAv
            HolidayName = "Tisha B'Av"
        Case Else
            Debug.Assert False
            HolidayName = "Error."
    End Select
    
End Property

Public Property Get HolidayInYear(holiday As Holidays, _
    InYear As Date) As Date
    
    Select Case holiday
        Case NewYearsDay
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 1, 1)
        Case MartinLutherKingDay
            HolidayInYear = DayOfWeek(InYear, 1, 3, vbMonday)
        Case PresidentsDay
            HolidayInYear = DayOfWeek(InYear, 2, 3, vbMonday)
        Case Easter
            HolidayInYear = CalcEaster(InYear)
        Case MemorialDay
            HolidayInYear = LastDayInMonth(InYear, 5, vbMonday)
        Case IndependenceDay
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 7, 4)
        Case LaborDay
            HolidayInYear = DayOfWeek(InYear, 9, 1, vbMonday)
        Case ColumbusDay
            HolidayInYear = DayOfWeek(InYear, 10, 2, vbMonday)
        Case VeteransDay
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 11, 11)
        Case ThanksgivingDay
            HolidayInYear = DayOfWeek(InYear, 11, 4, vbThursday)
        Case Christmas
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 12, 25)
        Case NewYearsEve
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 12, 31)
        Case Halloween
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 10, 31)
        Case GroundhogDay
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 2, 2)
        Case ValentinesDay
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 2, 14)
        Case SaintPatricksDay
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 3, 17)
        Case TaxDay
            HolidayInYear = CalcTaxDay(InYear)
        Case MothersDay
            HolidayInYear = DayOfWeek(InYear, 5, 2, vbSunday)
        Case FlagDay
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 6, 14)
        Case FathersDay
            HolidayInYear = DayOfWeek(InYear, 6, 3, vbSunday)
        Case ElectionDay
            HolidayInYear = DayOfWeek(InYear, 11, 1, vbTuesday)
        Case ChristmasEve
            HolidayInYear = DateSerial(DatePart("yyyy", InYear), 12, 24)
        Case RoshHashanah
            HolidayInYear = Hebrew_InYear(InYear, 1, 1)
        Case YomKippur
            HolidayInYear = Hebrew_InYear(InYear, 1, 10)
        Case Sukkot
            HolidayInYear = Hebrew_InYear(InYear, 1, 15)
        Case SheminiAtzeret
            HolidayInYear = Hebrew_InYear(InYear, 1, 22)
        Case SimchatTorah
            HolidayInYear = Hebrew_InYear(InYear, 1, 23)
        Case Chanukkah
            HolidayInYear = Hebrew_InYear(InYear, 3, 25)
        Case TuBShevat
            HolidayInYear = Hebrew_InYear(InYear, 5, 15)
        Case Purim
            HolidayInYear = Hebrew_InYear(InYear, 6, 14)
        Case PesachPassover
            HolidayInYear = Hebrew_InYear(InYear, 7, 15)
        Case LagBOmer
            HolidayInYear = Hebrew_InYear(InYear, 8, 18)
        Case Shavuot
            HolidayInYear = Hebrew_InYear(InYear, 9, 6)
        Case TishaBAv
            HolidayInYear = Hebrew_InYear(InYear, 11, 9)
        Case Else
            Debug.Assert False
    End Select
    
End Property

Private Function CalcTaxDay(InYear As Date) As Date

    CalcTaxDay = DateSerial(DatePart("yyyy", InYear), 4, 15)
    Do While DatePart("w", CalcTaxDay) = vbSunday Or _
       DatePart("w", CalcTaxDay) = vbSaturday
        CalcTaxDay = DateAdd("d", 1, CalcTaxDay)
    Loop

End Function

Private Function LastDayInMonth(dateYear As Date, nMonth As Long, _
        nDayOfWeek As VbDayOfWeek) As Date
    
    Dim dateValid As Date
    LastDayInMonth = DateSerial(DatePart("yyyy", dateYear), nMonth, 1)
    
    Do Until DatePart("m", LastDayInMonth) <> nMonth
        If DatePart("w", LastDayInMonth) = nDayOfWeek Then
            dateValid = LastDayInMonth
        End If
        
        LastDayInMonth = DateAdd("d", 1, LastDayInMonth)
    Loop
    
    LastDayInMonth = dateValid
    
End Function

Private Function DayOfWeek(dateYear As Date, nMonth As Long, nWeeks As _
        Long, nDayOfWeek As VbDayOfWeek) As Date
    
    DayOfWeek = DateSerial(DatePart("yyyy", dateYear), nMonth, 1)
    
    If DatePart("w", DayOfWeek) > nDayOfWeek Then
        DayOfWeek = DateAdd("d", 7 - (DatePart("w", DayOfWeek) - _
                  nDayOfWeek), DayOfWeek)
    ElseIf DatePart("w", DayOfWeek) < nDayOfWeek Then
        DayOfWeek = DateAdd("d", (nDayOfWeek - DatePart("w", DayOfWeek)), _
                  DayOfWeek)
    End If
    
    DayOfWeek = DateAdd("d", ((nWeeks - 1) * 7), DayOfWeek)
    
End Function

Private Function CalcEaster(InYear As Date) As Date
    
    Dim nGolden As Long
    Dim nTillPaschal As Long
    Dim nDayPaschal As Long
    Dim nCentury As Long
    Dim nEpact As Long
    Dim nSundayPaschal As Long
    
    Dim nMonth As Long
    Dim nDay As Long
    Dim nYear As Long
    
    nYear = DatePart("yyyy", InYear)
    nGolden = nYear Mod 19
    nCentury = nYear \ 100
    nEpact = (nCentury - nCentury \ 4 - (8 * nCentury + 13) \ 25 + 19 * _
           nGolden + 15) Mod 30
    nTillPaschal = nEpact - (nEpact \ 28) * (1 - (nEpact \ 28) * (29 \ _
                 (nEpact + 1)) * ((21 - nGolden) \ 11))
    nDayPaschal = (nYear + nYear \ 4 + nTillPaschal + 2 - nCentury + _
                nCentury \ 4) Mod 7
    nSundayPaschal = nTillPaschal - nDayPaschal
    nMonth = 3 + (nSundayPaschal + 40) \ 44
    nDay = nSundayPaschal + 28 - 31 * (nMonth \ 4)
    
    CalcEaster = DateSerial(nYear, nMonth, nDay)
    
End Function

Public Property Get IsHoliday(DateQuery As Date) As Boolean
    
    Dim i As Long
    
    IsHoliday = False
    
    For i = 0 To HolidayCount - 1
        If Int(DateQuery) = Int(HolidayInYear(i, DateQuery)) Then
            IsHoliday = True
            Exit For
        End If
    Next
    
End Property

Public Property Get HolidayNameOfDate(DateQuery As Date) As String
    
    Dim i As Long
    
    HolidayNameOfDate = "None"
    
    For i = 0 To HolidayCount - 1
        If Int(DateQuery) = Int(HolidayInYear(i, DateQuery)) Then
            HolidayNameOfDate = HolidayName(i)
            Exit For
        End If
    Next
    
End Property

Private Function Hebrew_JulianToDate(ByVal iJulian As Long) As Date

    Hebrew_JulianToDate = CDate(iJulian - 2415019)

End Function

Private Sub Hebrew_JulianToHebrew(ByVal jdn As Long, _
               ByRef iyear As Integer, _
               ByRef iMonth As Integer, _
               ByRef iDay As Integer, _
               Optional ByVal monthcoding As Integer)
    Dim InputJDN As Long
    Dim tishri1 As Long
    Dim LeftOverDays As Long
    If jdn <= 347997 Then
        iyear = 0
        iMonth = 0
        iDay = 0
    Else
        InputJDN = jdn - 347997
        iyear = (InputJDN \ 365) + 1
        tishri1 = Hebrew_ElapsedCalendarDays(iyear)
        While (tishri1 > InputJDN)
            iyear = iyear - 1
            tishri1 = Hebrew_ElapsedCalendarDays(iyear)
        Wend
        iMonth = 1
        LeftOverDays = InputJDN - tishri1
        While (LeftOverDays >= Hebrew_LastDayOfMonth(iyear, iMonth))
            LeftOverDays = LeftOverDays - _
                Hebrew_LastDayOfMonth(iyear, iMonth)
            iMonth = iMonth + 1
        Wend
        If Sgn(monthcoding) = -1 Then
            If iMonth > 6 Then
                If Hebrew_LeapYear(iyear) Then
                    iMonth = iMonth - 14
                Else
                    iMonth = iMonth - 13
                End If
            End If
        End If
        iDay = LeftOverDays + 1
    End If
End Sub

Private Sub Hebrew_GregorianToHebrew(ByRef iyear As Integer, _
                 ByRef iMonth As Integer, _
                 ByRef iDay As Integer)
    Call Hebrew_JulianToHebrew(Hebrew_JulianToGregorian(iyear, _
                              iMonth, _
                              iDay), _
                    iyear, iMonth, iDay)
End Sub

Private Function Hebrew_MonthName(ByVal iyear, _
                                  ByVal iMonth) As String
        
    If ((iMonth > 6) And (Not (Hebrew_LeapYear(iyear)))) Then
       iMonth = iMonth + 1
    End If
    Select Case iMonth
    Case 1
        Hebrew_MonthName = "Tishri"     ' or "Tishrey"
    Case 2
        Hebrew_MonthName = "Heshvan"
    Case 3
        Hebrew_MonthName = "Kislev"
    Case 4
        Hebrew_MonthName = "Teveth"     ' or "Tevet"
    Case 5
        Hebrew_MonthName = "Shevat"
    Case 6
        If (Not (Hebrew_LeapYear(iyear))) Then
            Hebrew_MonthName = "Adar"
        Else
            Hebrew_MonthName = "Adar I"
        End If
    Case 7
        Hebrew_MonthName = "Adar II"
    Case 8
        Hebrew_MonthName = "Nisan"
    Case 9
        Hebrew_MonthName = "Iyyar"
    Case 10
        Hebrew_MonthName = "Sivan"
    Case 11
        Hebrew_MonthName = "Tammuz"
    Case 12
        Hebrew_MonthName = "Av"
    Case 13
        Hebrew_MonthName = "Elul"
    Case Else
        Hebrew_MonthName = ""
    End Select
End Function

Private Function Hebrew_HebrewToJulian(ByVal iyear, _
    ByVal iMonth, ByVal iDay) As Long
    
    Dim jdn As Long
    Dim counter As Integer
    If iMonth < 0 Then
        If Hebrew_LeapYear(iyear) Then
            iMonth = 14 + iMonth
        Else
            iMonth = 13 + iMonth
        End If
    End If
    jdn = Hebrew_ElapsedCalendarDays(iyear)
    For counter = 1 To (iMonth - 1) Step 1
        jdn = jdn + Hebrew_LastDayOfMonth(iyear, counter)
    Next counter
    Hebrew_HebrewToJulian = jdn + (iDay - 1 + 347997)
End Function

Private Function Hebrew_DateInOrAfterCivilYear( _
              ByVal civilYear As Integer, _
              ByVal HebrewMonth As Integer, _
              ByVal HebrewDay As Integer, _
              Optional ByVal calendarType As Integer = _
                Hebrew_c_Gregorian) As Long
    Dim jdnJanuary1 As Long
    Dim jdnHoliday As Long
    Dim hebrewYear As Integer
    Dim dummy1 As Integer
    Dim dummy2 As Integer
    jdnJanuary1 = Hebrew_CivilToJulian(civilYear, 1, 1, calendarType)
    Hebrew_JulianToHebrew jdnJanuary1, hebrewYear, dummy1, dummy2
    jdnHoliday = Hebrew_HebrewToJulian(hebrewYear, _
            HebrewMonth, HebrewDay)
    If jdnHoliday < jdnJanuary1 Then
        ' Oops! Wrong civil Year. Use next hebrewYear's in stead.
        jdnHoliday = Hebrew_HebrewToJulian(hebrewYear + 1, _
                HebrewMonth, HebrewDay)
    End If
    Hebrew_DateInOrAfterCivilYear = jdnHoliday
End Function

Private Function Hebrew_DateInOrBeforeCivilYear( _
              ByVal civilYear As Integer, _
              ByVal HebrewMonth As Integer, _
              ByVal HebrewDay As Integer, _
              Optional ByVal calendarType As Integer = _
                  Hebrew_c_Gregorian) As Long
    Dim jdnDecember31 As Long
    Dim jdnHoliday As Long
    Dim hebrewYear As Integer
    Dim dummy1 As Integer
    Dim dummy2 As Integer
    jdnDecember31 = Hebrew_CivilToJulian(civilYear, _
            12, 31, calendarType)
    Call Hebrew_JulianToHebrew(jdnDecember31, hebrewYear, _
            dummy1, dummy2)
    jdnHoliday = Hebrew_HebrewToJulian(hebrewYear, _
            HebrewMonth, HebrewDay)
    If jdnHoliday > jdnDecember31 Then
        ' Oops! Wrong civil Year. Use last hebrewYear's in stead.
        jdnHoliday = Hebrew_HebrewToJulian(hebrewYear - 1, _
                HebrewMonth, HebrewDay)
    End If
    Hebrew_DateInOrBeforeCivilYear = jdnHoliday
End Function

Private Function Hebrew_ElapsedCalendarDays(ByVal iyear) As Long
    Dim MonthsElapsed As Long
    Dim PartsElapsed As Long
    Dim HoursElapsed As Long
    Dim ConjunctionDay As Long
    Dim ConjunctionParts As Long
    Dim AlternativeDay As Long

    MonthsElapsed = (235 * (((iyear - 1) \ 19))) + _
                    (12 * ((iyear - 1) Mod 19)) + _
                    (7 * ((iyear - 1) Mod 19) + 1) \ 19
    PartsElapsed = 204 + 793 * (MonthsElapsed Mod 1080)
    HoursElapsed = 5 + 12 * MonthsElapsed + _
                   793 * ((MonthsElapsed \ 1080)) + _
                   PartsElapsed \ 1080
    ConjunctionDay = 1 + 29 * MonthsElapsed + HoursElapsed \ 24
    ConjunctionParts = (1080 * (HoursElapsed Mod 24)) + _
                       PartsElapsed Mod 1080
    If ((ConjunctionParts >= 19440) Or _
        (((ConjunctionDay Mod 7) = 2) And _
        (ConjunctionParts >= 9924) And _
        (Not (Hebrew_LeapYear(iyear)))) Or _
       (((ConjunctionDay Mod 7) = 1) And _
        (ConjunctionParts >= 16789) And _
        (Hebrew_LeapYear(iyear - 1)))) _
    Then
        AlternativeDay = ConjunctionDay + 1
    Else
        AlternativeDay = ConjunctionDay
    End If
    If (((AlternativeDay Mod 7) = 0) Or _
        ((AlternativeDay Mod 7) = 3) Or _
        ((AlternativeDay Mod 7) = 5)) _
    Then
        AlternativeDay = AlternativeDay + 1
    End If
    Hebrew_ElapsedCalendarDays = AlternativeDay
End Function


Private Function Hebrew_LastDayOfMonth(ByVal iyear, _
    ByVal iMonth) As Integer
    
    If ((iMonth > 6) And (Not (Hebrew_LeapYear(iyear)))) Then
       iMonth = iMonth + 1
    End If
    Select Case iMonth
    Case 2
        If Hebrew_LongHeshvan(iyear) Then
            Hebrew_LastDayOfMonth = 30
        Else
            Hebrew_LastDayOfMonth = 29
        End If
    Case 3
        If Hebrew_ShortKislev(iyear) Then
            Hebrew_LastDayOfMonth = 29
        Else
            Hebrew_LastDayOfMonth = 30
        End If
    Case 6
        If Hebrew_LeapYear(iyear) Then
            Hebrew_LastDayOfMonth = 30
        Else
            Hebrew_LastDayOfMonth = 29
        End If
    Case 4, 7, 9, 11, 13
        Hebrew_LastDayOfMonth = 29
    Case Else
        Hebrew_LastDayOfMonth = 30
    End Select
End Function

Private Function Hebrew_LeapYear(ByVal iyear) As Boolean
    If ((((7 * iyear) + 1) Mod 19) < 7) Then
       Hebrew_LeapYear = True
    Else
       Hebrew_LeapYear = False
    End If
End Function

Private Function Hebrew_JulianToGregorian(ByVal iyear As Integer, _
                    ByVal iMonth As Integer, _
                    ByVal iDay As Integer) As Long
    Dim lYear As Long
    Dim lMonth As Long
    Dim lDay As Long

    lYear = CLng(iyear)
    lMonth = CLng(iMonth)
    lDay = CLng(iDay)

    Hebrew_JulianToGregorian = 367 * lYear - _
            ((7 * (lYear + 5001 + ((lMonth - 9) \ 7))) \ 4) _
            + ((275 * lMonth) \ 9) + lDay + 1729777

End Function


Private Function Hebrew_CivilToJulian(ByVal iyear As Integer, _
                   ByVal iMonth As Integer, _
                   ByVal iDay As Integer, _
                   Optional ByVal calendarType As Integer _
                       = Hebrew_c_Gregorian) As Long
    Dim lYear As Long
    Dim lMonth As Long
    Dim lDay As Long

    If calendarType = Hebrew_c_Gregorian And ((iyear > 1582) Or _
        ((iyear = 1582) And (iMonth > 10)) Or _
        ((iyear = 1582) And (iMonth = 10) And (iDay > 14))) _
    Then
        lYear = CLng(iyear)
        lMonth = CLng(iMonth)
        lDay = CLng(iDay)
        Hebrew_CivilToJulian = ((1461 * (lYear + 4800 + _
            ((lMonth - 14) \ 12))) \ 4) _
            + ((367 * (lMonth - 2 - 12 * _
            (((lMonth - 14) \ 12)))) \ 12) _
            - ((3 * (((lYear + 4900 + _
            ((lMonth - 14) \ 12)) \ 100))) \ 4) _
            + lDay - 32075
    Else
        Hebrew_CivilToJulian = Hebrew_JulianToGregorian(iyear, _
                iMonth, iDay)
    End If

End Function

Private Function Hebrew_LongHeshvan(ByVal iyear) As Boolean
    Hebrew_LongHeshvan = ((Hebrew_DaysInYear(iyear) Mod 10) = 5)
End Function

Private Function Hebrew_ShortKislev(ByVal iyear) As Boolean
    Hebrew_ShortKislev = ((Hebrew_DaysInYear(iyear) Mod 10) = 3)
End Function

Private Function Hebrew_DaysInYear(ByVal iyear) As Boolean
    Hebrew_DaysInYear = Hebrew_HebrewToJulian(iyear + 1, 1, 1) - _
            Hebrew_HebrewToJulian(iyear, 1, 1)
End Function

Private Function Hebrew_InYear(InYear As Date, ByVal nMonth As Integer, nDay As Integer)

    Dim nMonthOrig As Integer
    Dim nDayOrig As Integer
    Dim nYearOrig As Integer
    Dim nYear As Integer
    
    nMonthOrig = nMonth
    nDayOrig = nDay

    nYear = DatePart("yyyy", InYear)
    nYearOrig = nYear

    Hebrew_InYear = Hebrew_JulianToDate(Hebrew_DateInOrBeforeCivilYear( _
        nYear, nMonth, nDay))
        
    nYear = DatePart("yyyy", Hebrew_InYear)
    nMonth = DatePart("m", Hebrew_InYear)
    nDay = DatePart("d", Hebrew_InYear)
    
    Hebrew_GregorianToHebrew nYear, nMonth, nDay
    
    If Hebrew_LeapYear(nYear) And nMonthOrig >= 7 Then
        Hebrew_InYear = Hebrew_JulianToDate(Hebrew_DateInOrBeforeCivilYear( _
            nYearOrig, nMonthOrig + 1, nDayOrig))
    End If

End Function

'--- End of clsHolidays
 
Sample Usage:
 
    Dim objHolidays As clsHolidays
    Dim i As Long
    Set objHolidays = New clsHolidays
    
    For i = 0 To objHolidays.Count - 1
        Debug.Print objHolidays.HolidayName(i) & " occurs on " & _
            Format(objHolidays.HolidayInYear(i, Now), "mmmm dd, yyyy")
    Next

    If objHolidays.IsHoliday(Now) Then
        Debug.Print "Today is " & objHolidays.HolidayNameOfDate(Now)
    Else
        Debug.Print "Today is not a holiday"
    End If