Date functions are a pain, months and years don't have a standard length and do we want to include both beginning and end dates when figuring out time periods? the further you go into the details of date ranges the more knotty the calculations become. That said a few well placed vba functions can help - to that end here's a bunch to get you started and can be tweaked for a number of purposes:
' wraps the DateDiff function for use on a worksheet Public Function GetDateDiff(ByVal DuratType As String, ByVal StartDate As Date, ByVal EndDate As Date _ , Optional FirstDayOfWeek As VbDayOfWeek = vbSunday _ , Optional FirstWeekOfYear As VbFirstWeekOfYear = vbFirstJan1) As Integer GetDateDiff = DateDiff(DuratType, StartDate, EndDate, FirstDayOfWeek, FirstWeekOfYear) End Function ' wraps the DateAdd function for use on a worksheet Public Function GetDateAdd(interval As String, initialDate As Date, Value As Double) As Date GetDateAdd = DateAdd(interval, Value, initialDate) End Function Function IsLeapYear(ByVal Year As Long) As Boolean ' Check if divisible by 4, not divisible by 100, or divisible by 400 IsLeapYear = Year Mod 4 = 0 And (Year Mod 100 <> 0 Or Year Mod 400 = 0) End Function ' Helper function for leap year days Function GetLeapYearDays(ByVal StartDate As Date, ByVal EndDate As Date) As Integer ' Ensure StartDate is earlier than EndDate If StartDate > EndDate Then Dim tempDate As Date tempDate = StartDate StartDate = EndDate EndDate = tempDate End If Dim years As Integer ' calculate years and remove the start and end years, we'll test those later years = Year(EndDate) - Year(StartDate) ' grab the intervening years For i = 0 To years If IsLeapYear(Year(StartDate) + i) Then GetLeapYearDays = GetLeapYearDays + 1 End If Next i If Month(StartDate) > 2 And IsLeapYear(Year(StartDate)) Then GetLeapYearDays = GetLeapYearDays - 1 If (Month(EndDate) < 2 Or (Month(EndDate) = 2 And Day(EndDate) < 29)) _ And IsLeapYear(Year(EndDate)) Then GetLeapYearDays = GetLeapYearDays - 1 End Function ' returns number of days for given month/year Function DaysInMonth(ByVal Month As Integer, ByVal Year As Long) As Integer ' Validate month number If Month < 1 Or Month > 12 Then DaysInMonth = -1 ' Indicate invalid month Exit Function End If ' Array for days in non-leap years Dim daysPerMonth(1 To 12) As Integer For i = 1 To 12 Select Case i Case 1, 3, 5, 7, 8, 10, 12 daysPerMonth(i) = 31 Case 4, 6, 9, 11 daysPerMonth(i) = 30 Case 2 daysPerMonth(i) = 28 End Select Next i ' Add extra day for February in leap years DaysInMonth = daysPerMonth(Month) + IIf(IsLeapYear(Year) And Month = 2, 1, 0) End Function ' Gives a specific function to enable a period presented as a year decimal to be added to a date Public Function DateAddYrDec(initial As Date, yeardecimal As Double) As Date DateAddYrDec = DateAdd("M", yeardecimal * 12, initial) End Function ' Returns a decimal to represent a time period based on years and days/365 Public Function GetYrsDaysDecimal(ByVal StartDate As Date, ByVal EndDate As Date, _ Optional RoundTo As Integer = 4) As Double Dim Yrs As Integer Dim Days As Integer Yrs = GetCompleteYears(StartDate, EndDate) Days = DateDiff("d", DateAdd("yyyy", Yrs, StartDate), EndDate) - 1 Dim out As Double out = Yrs + (Days / 365) GetYrsDaysDecimal = Round(out, RoundTo) End Function ' Returns a year decimal based on complete years and months between two dates Public Function GetYrsMthDecimal(ByVal StartDate As Date, ByVal EndDate As Date, _ Optional RoundTo As Integer = 4, Optional Absolute As Boolean = False) As Double Dim res As Double res = Round((GetCompleteMonths(StartDate, EndDate)) / 12 _ , RoundTo) If Absolute And EndDate <= StartDate Then res = res * -1 GetYrsMthDecimal = res End Function ' Returns a year decimal based on the years, months and month percentage between two dates Public Function GetYrsMthPercDecimal(ByVal StartDate As Date, ByVal EndDate As Date, _ Optional RoundTo As Integer = 4) As Double Dim res As Double res = Round((DateDiff("M", StartDate, EndDate) + MonthDiffDec(StartDate, EndDate)) / 12 _ , RoundTo) If EndDate <= StartDate Then res = res * -1 GetYrsMthPercDecimal = res End Function ' Returns a year decimal based on the actual time between two dates Public Function GetAccYrDecimal(ByVal StartDate As Date, ByVal EndDate As Date, _ Optional RoundTo As Integer = 4) As Double Dim res As Double res = Round((DateDiff("M", StartDate, EndDate) + MonthDiffDec(StartDate, EndDate)) / 12 _ , RoundTo) If EndDate <= StartDate Then res = res * -1 GetAccYrDecimal = res End Function ' Returns a number representing the percentage of month between two given dates, ignoring years Public Function MonthDiffDec(varDateFrom As Variant, Optional varDateTo As Variant) As Variant Dim intMonths As Integer Dim IntDays As Integer If Not IsNull(varDateFrom) Then If IsMissing(varDateTo) Then varDateTo = VBA.Date intMonths = DateDiff("m", varDateFrom, varDateTo) - (GetAge(varDateFrom, varDateTo) * 12) If Day(varDateFrom) > Day(varDateTo) Then intMonths = intMonths - 1 IntDays = varDateTo - DateSerial(Year(varDateTo), Month(varDateTo) - 1, Day(varDateFrom)) Else IntDays = varDateTo - DateSerial(Year(varDateTo), Month(varDateTo), Day(varDateFrom)) End If MonthDiffDec = intMonths + IntDays / Day(DateSerial(Year(varDateTo), Month(varDateTo) + 1, 0)) End If End Function ' Returns an integer of complete years for the passed dates Public Function GetAge(varDoB As Variant, Optional varAgeAt As Variant) As Integer If IsMissing(varAgeAt) Then varAgeAt = VBA.Date GetAge = DateDiff("yyyy", varDoB, varAgeAt) - _ IIf(Format(varAgeAt, "mmdd") < Format(varDoB, "mmdd"), 1, 0) End Function ' returns the month end for the date given Public Function GetMonthEnd(PassDate As Date) As Date Dim carry As Date carry = DateSerial(Year(PassDate), Month(PassDate), 1) carry = DateAdd("M", 1, carry) carry = DateAdd("D", -1, carry) GetMonthEnd = carry End Function ' returns complete months, correcting for whole months rather than using month separators (counting the first) Public Function GetCompleteMonths(StartDate As Date, EndDate As Date) As Integer Dim tempDate As Date ' ensure start is before end If StartDate > EndDate Then tempDate = StartDate StartDate = EndDate EndDate = tempDate End If Dim Yrs As Integer Dim Mths As Integer Yrs = GetCompleteYears(StartDate, EndDate) tempDate = DateAdd("Y", Yrs, StartDate) Mths = Yrs * 12 If Year(tempDate) = Year(EndDate) Then Mths = Mths + Month(EndDate) - Month(StartDate) Else Mths = Mths + (12 - Month(StartDate)) + Month(EndDate) End If If Day(StartDate) > Day(EndDate) Then Mths = Mths - 1 GetCompleteMonths = Mths End Function ' returns a month integer (1-12) based on an initally passed month and a number of months to add ' to get to whether a year has been added use the following: year = year + (MonthsToAdd \ 12) Function CalculateMonth(ByVal StartMonth As Integer, ByVal MonthsToAdd As Integer) As Integer ' Validate input month If StartMonth < 1 Or StartMonth > 12 Then CalculateMonth = 0 ' Indicate an invalid input month Exit Function End If ' Calculate the new month with potential year rollover Dim totalMonths As Integer totalMonths = StartMonth + MonthsToAdd Dim resultMonth As Integer resultMonth = ((totalMonths - 1) Mod 12) + 1 CalculateMonth = resultMonth End Function ' Returns the number of whole years between two dates ' If a 29/2 is passed as the start then the next 28/2 will not count as a full year Public Function GetCompleteYears(StartDate As Date, EndDate As Date) As Integer ' Ensure StartDate is earlier than EndDate If StartDate > EndDate Then Dim tempDate As Date tempDate = StartDate StartDate = EndDate EndDate = tempDate End If ' Extract year, month, and day components Dim startYear As Long, StartMonth As Integer, startDay As Integer Dim endYear As Long, endMonth As Integer, endDay As Integer startYear = Year(StartDate): StartMonth = Month(StartDate): startDay = Day(StartDate) endYear = Year(EndDate): endMonth = Month(EndDate): endDay = Day(EndDate) Dim FullYrs, YrDiff As Integer ' get the number of years difference YrDiff = endYear - startYear If StartMonth < endMonth Or _ (StartMonth = endMonth And startDay <= endDay) Then ' if the start date was before or equal to the end date in months and days ' full years is fine FullYrs = YrDiff Else ' otherwise we're cutting, minus one FullYrs = YrDiff - 1 End If GetCompleteYears = FullYrs End Function ' Returns an accurate number of days between two given dates Function DaysBetweenDates(ByVal StartDate As Date, ByVal EndDate As Date) As Long Dim tempDate As Date ' Ensure StartDate is earlier than EndDate If StartDate > EndDate Then tempDate = StartDate StartDate = EndDate EndDate = tempDate End If ' Extract year, month, and day components Dim startYear As Long, StartMonth As Integer, startDay As Integer Dim endYear As Long, endMonth As Integer, endDay As Integer startYear = Year(StartDate): StartMonth = Month(StartDate): startDay = Day(StartDate) endYear = Year(EndDate): endMonth = Month(EndDate): endDay = Day(EndDate) Dim days As Long, months As Integer, years As Integer ' Calculate difference in years and set a tempdate years = GetCompleteYears(StartDate, EndDate) tempDate = DateSerial(startYear + years, StartMonth, startDay) ' add the years days to our running total so that they're out of the way, only grab leap year days days = (years * 365) + GetLeapYearDays(StartDate, tempDate) ' if we're in the same month and year just add the days and let's get out If (Year(tempDate) = endYear) And endMonth = StartMonth Then DaysBetweenDates = days + endDay - startDay ' + 1 here to include start day Exit Function End If ' see if there are any months intervening and grab the days for them For i = 0 To GetCompleteMonths(tempDate, EndDate) - 1 days = days + DaysInMonth(CalculateMonth(StartMonth, i), Year(tempDate) + (i \ 12)) ' set the tempDate to the date at the end of the period processed here tempDate = DateSerial((Year(tempDate) + ((i + 1) \ 12)), CalculateMonth(StartMonth, i + 1), startDay) Next i ' if we've landed in the same month then grab the day difference and return it If endMonth = Month(tempDate) Then DaysBetweenDates = days + endDay - startDay ' + 1 here to include start day Exit Function End If ' count up the remaining days in the start month month and add the days in the end month days = days + DaysInMonth(Month(tempDate), Year(tempDate)) - startDay + endDay DaysBetweenDates = days End Function ' Returns the number of complete tax years between two dates Public Function GetCompleteTaxYears(StartDate As Date, EndDate As Date) As Integer If StartDate >= EndDate Then GetCompleteTaxYears = 0 Exit Function End If Dim TaxYrs As Integer ' get the number of years difference TaxYrs = DatePart("yyyy", EndDate) - DatePart("yyyy", StartDate) If DatePart("m", StartDate) > 4 Or _ (DatePart("m", StartDate) = 4 And DatePart("d", StartDate) > 6) Then ' we started after the start of the tax year, take one off TaxYrs = TaxYrs - 1 End If If DatePart("m", EndDate) < 4 Or _ (DatePart("m", EndDate) = 4 And DatePart("d", EndDate) < 5) Then ' we ended before the end of the tax year, take one off TaxYrs = TaxYrs - 1 End If GetCompleteTaxYears = TaxYrs End Function ' Returns the number of occurences of a day/month between two dates Public Function GetDateOccurrences(Day As Integer, Month As Integer, StartDate As Date, EndDate As Date, _ Optional inclusiveStart As Boolean = True, Optional inclusiveEnd As Boolean = True) As Integer If StartDate > EndDate Then GetDateOccurrences = 0 Exit Function ElseIf StartDate = EndDate And (inclusiveStart Or inclusiveEnd) Then GetDateOccurrences = 1 Exit Function End If ' if we're looking for leap year days then just pass that If Day = 29 And Month = 2 Then GetDateOccurrences = GetLeapYearDays(StartDate, EndDate) Exit Function End If Dim AnnivYrs, YrDiff As Integer ' Extract year, month, and day components Dim startYear As Long, StartMonth As Integer, startDay As Integer Dim endYear As Long, endMonth As Integer, endDay As Integer startYear = Year(StartDate): StartMonth = Month(StartDate): startDay = Day(StartDate) endYear = Year(EndDate): endMonth = Month(EndDate): endDay = Day(EndDate) ' get the number of years difference YrDiff = endYear - startYear Dim startBefore, endAfter As Boolean If inclusiveStart Then startBefore = StartMonth < Month Or _ (StartMonth = Month And startDay <= Day) Else startBefore = StartMonth < Month Or _ (StartMonth = Month And startDay < Day) End If If inclusiveEnd Then endAfter = endMonth > Month Or _ (endMonth = Month And endDay >= Day) Else endAfter = endMonth > Month Or _ (endMonth = Month And endDay > Day) End If If startBefore <> endAfter Then AnnivYrs = YrDiff ElseIf startBefore Then AnnivYrs = YrDiff + 1 Else AnnivYrs = YrDiff - 1 End If GetDateOccurrences = AnnivYrs End Function ' Returns the date of the preceeding 6th of april from the passed date Public Function GetPreceeding64(TestDate As Date) As Date If Month(TestDate) < 4 Or _ (Month(TestDate) = 4 And Day(TestDate) < 6) Then GetPreceeding64 = DateSerial(Year(TestDate) - 1, 4, 6) Else GetPreceeding64 = DateSerial(Year(TestDate), 4, 6) End If End Function ' Returns the date of the preceeding passed anniversary for the passed date Public Function GetPreceedingAnniversary(pDay As Integer, pMonth As Integer, TestDate As Date) As Date If pMonth = 2 And pDay = 29 Then ' we're looking for a leap year Dim YearDiv As Double Dim WholeYearDiv As Integer YearDiv = Year(TestDate) / 4 WholeYearDiv = Year(TestDate) \ 4 If Month(TestDate) < pMonth Or _ (Month(TestDate) = pMonth And Day(TestDate) <= pDay) Then ' Earlier than/on anniv, looking for preceeding leap year If YearDiv = WholeYearDiv Then ' We're in a leap year, get the one before it! GetPreceedingAnniversary = DateSerial((YearDiv - 1) * 4, pMonth, pDay) Else ' this isn't a leap year, get 4 * YearDiv GetPreceedingAnniversary = DateSerial(YearDiv * 4, pMonth, pDay) End If Else ' passed anniv in this year, is this a leap year? If YearDiv = WholeYearDiv Then ' We're in a leap year, get this one! GetPreceedingAnniversary = DateSerial(Year(TestDate), pMonth, pDay) Else ' this isn't a leap year, get the one before it (get 4 * YearDiv) GetPreceedingAnniversary = DateSerial(YearDiv * 4, pMonth, pDay) End If End If ElseIf Month(TestDate) < pMonth Or _ (Month(TestDate) = pMonth And Day(TestDate) <= pDay) Then ' less than or = to anniversary, get last year GetPreceedingAnniversary = DateSerial(Year(TestDate) - 1, pMonth, pDay) Else ' Have passed the anniversary this year, get this year GetPreceedingAnniversary = DateSerial(Year(TestDate), pMonth, pDay) End If End Function