Option Explicit On Public Const DEFER_DELIM As String = "::" Public Const ILLEGAL_DATE As Date = "01-JAN-1900 00:00" Private mError As String ''' ''' Establishes if a deferring instruction is present in the given (scenario) parameter, and if ''' so returns the date/time when the deferment should take place. ''' For instance if "scenario" is "My Test e-mail ::in 10 minutes" then NOW+10 minutes will ''' be returned (and used to defer the sending of an e-mail. ''' Various deferment types are supported, including: ''' ''' IN - For instance "Make coffee :: in 10 minutes" or "Person of interest, series 5 starts :: in 2 weeks" ''' ON - For instance "It's Christmas! :: on 31-Dec-15 at 12:01" ''' AT - For instance "Home time! :: at 16:00" ''' ''' scenario - String to test for a deferment (e.g. ":: in 10 minutes") ''' fromDate - Base date to use for a deferment ''' (normally this is NOW, but if we're testing it's useful to start from a constant time) ''' ''' ''' If a deferment (anything with ::) is present, the deferment time is returned ''' If an error is present (deferment can't be decoded for instance) then "01-JAN-1900" is returned ''' Public Function GetDeferredDate(inScenario As String, fromDate As Date) As Date Dim delimPos As Integer Dim arr() As String Dim d As Date Dim scenario As String ' Delims: ' ":: in 20 minutes" ' ":: on 23/3/15 at 14:00" ' ":: at 14:00" delimPos = InStr(inScenario, DEFER_DELIM) If delimPos <= 0 Then ' No special rules, so send straight away GetDeferredDate = fromDate Exit Function End If ' Extract whatever is after the "::" delimiter scenario = Trim(Mid(inScenario, delimPos + Len(DEFER_DELIM))) ' Convert into elements for processing arr = Split(scenario, " ") If IsToday(arr(0)) Or IsTomorrow(arr(0)) Or GetDayOfWeekNumber(arr(0)) > -1 Then ' TODAY or TOMORROW, either way just re-use what we already have: scenario = ":: on " & scenario d = GetDeferredDate(scenario, fromDate) Else ' Use the appropriate method given the type of deferment found Select Case UCase(arr(0)) Case "IN" : d = GetIn(scenario, arr, fromDate) Case "ON" : d = GetOnAt(scenario, arr, fromDate) Case "AT" : d = GetAt(scenario, arr, fromDate) End Select End If GetDeferredDate = d End Function ''' ''' Establishes the offset for the "IN" scenario (e.g. "Make coffee :: in 10 minutes") ''' ''' ''' On success returns the calculated offset ''' Private Function GetIn(scenario As String, options() As String, fromDate As Date) As Date ' "in 20 minutes" Dim amount As Integer Dim granularity As String Dim answer As Date amount = CInt(options(1)) granularity = GetDateGranularity(options(2)) answer = DateAdd(granularity, amount, fromDate) GetIn = answer End Function ''' ''' Establishes the offset for the "ON" scenario (e.g. "It's Christmas! :: on 31-Dec-15 at 12:01") ''' ''' ''' On success returns the calculated offset ''' Private Function GetAt(scenario As String, options() As String, fromDate As Date) As Date ' "at 14:00" Dim tmStr As String Dim answer As Date tmStr = options(1) answer = GetDateSerial(CStr(fromDate)) + GetTimeSerial(tmStr) If answer < fromDate Then ' Too late in the day for this date ... assuming tomorrow answer = DateAdd("d", 1, answer) End If GetAt = answer End Function ''' ''' Establishes the offset for the "AT" scenario (e.g. "Home time! :: at 16:00") ''' ''' ''' On success returns the calculated offset ''' Private Function GetOnAt(scenario As String, options() As String, fromDate As Date) As Date ' "on 23/3/15 at 14:00" OR ' "on Friday at 14:00" Dim onDateStr As String Dim targetDate As Date onDateStr = UCase(options(1)) If GetDayOfWeekNumber(onDateStr) > -1 Or IsToday(onDateStr) Or IsTomorrow(onDateStr) Then targetDate = GetOnDayAt(scenario, options, fromDate) Else targetDate = GetOnDateAt(scenario, options, fromDate) End If GetOnAt = targetDate End Function ''' ''' Establishes the offset for the "AT" scenario (e.g. "Home time! :: at 16:00") ''' ''' ''' On success returns the calculated offset ''' Private Function GetOnDateAt(scenario, options() As String, fromDate As Date) As Date ' "on 23/3/15 at 14:00" Dim tmStr As String Dim answer As Date Dim dateStr As String Dim d As Date dateStr = options(1) If UBound(options) = 1 Then ' No time provided, assuming current time tmStr = GetTimeSerial(Right(CStr(fromDate), 8)) Else ' Use provided time tmStr = options(3) End If d = CDate(dateStr) answer = GetDateSerial(CStr(d)) + GetTimeSerial(tmStr) If answer < fromDate Then mError = """" & scenario & """" & " is in the past ... move to tomorrow?" answer = ILLEGAL_DATE End If GetOnDateAt = answer End Function ''' ''' ''' ''' ''' On success returns the calculated offset ''' Private Function GetOnDayAt(scenario, options() As String, fromDate As Date) As Date ' "on Friday at 14:00" Dim answer As Date Dim dowNdx As Integer Dim dayStr As String Dim timeStr As String Dim currDate As Date dayStr = options(1) If UBound(options) = 1 Then ' No time component present, use current time (dervied from the fromDate - testing) timeStr = GetTimeSerial(Right(CStr(fromDate), 8)) Else ' Take time component as defined timeStr = options(3) End If ' Start from our offset currDate = fromDate If IsToday(dayStr) Then GetOnDayAt = GetDateSerial(CStr(fromDate)) + GetTimeSerial(timeStr) Exit Function End If If IsTomorrow(dayStr) Then ' Change to force "Tomorrow" as a result currDate = DateAdd("d", 1, currDate) dayStr = WeekdayName(Weekday(currDate, vbMonday), True, vbMonday) ElseIf Year(currDate) = Year(fromDate) And Month(currDate) = Month(fromDate) And Day(currDate) = Day(fromDate) Then ' Assume they mean "next Thursday" ' ... so move on a day currDate = DateAdd("d", 1, currDate) End If ' Loop around until we get a match on the DayName dowNdx = GetDayOfWeekNumber(dayStr) While Weekday(currDate, vbMonday) <> dowNdx currDate = DateAdd("d", 1, currDate) Wend answer = GetDateSerial(CStr(currDate)) + GetTimeSerial(timeStr) GetOnDayAt = answer End Function ''' ''' Helper method which converts the given string into the time component of ''' a date - this one however respects AM/PM settings ''' ''' ''' On success returns time portion of a date object ''' Private Function GetTimeSerial(timeInput As String) As Date Dim args() As String Dim hh As Integer, mm As Integer Dim t As Date Dim hasAm As Boolean, hasPm As Boolean Dim timeStr As String timeStr = UCase(timeInput) ' AM/PM specified? hasAm = InStr(timeStr, "AM") > 0 hasPm = InStr(timeStr, "PM") > 0 ' Remove the AM/PM to remove confusion later timeStr = Replace(Replace(timeStr, "AM", ""), "PM", "") args = Split(timeStr, ":") hh = CInt(args(0)) If UBound(args) >= 1 Then ' Have minutes mm = CInt(args(1)) End If If hasPm And hh < 12 Then hh = hh + 12 End If GetTimeSerial = TimeSerial(hh, mm, 0) End Function ''' Returns only the date portion Private Function GetDateSerial(dateStr As String) As Date Dim d As Date d = CDate(dateStr) GetDateSerial = DateSerial(Year(d), Month(d), Day(d)) End Function ''' ''' Helper method to convert an English date component (e.g. "year") into an "interval" that ''' can be used by the VB date methods (e.g. "yyyy" in "DateAdd" for year) ''' ''' ''' On success returns VB Date method equivalent of an English duration ''' Public Function GetDateGranularity(dateAddStr As String) As String Dim outStr As String dateAddStr = UCase(dateAddStr) If Left(dateAddStr, 4) = "YEAR" Then outStr = "yyyy" ElseIf Left(dateAddStr, 3) = "MIN" Then outStr = "n" ElseIf Left(dateAddStr, 4) = "WEEK" Then outStr = "ww" Else outStr = Left(dateAddStr, 1) End If GetDateGranularity = outStr End Function Private Function GetDayOfWeekNumber(dayName As String) As Integer Dim currDayName As String Dim n As Integer dayName = UCase(dayName) For n = 1 To 7 currDayName = UCase(WeekdayName(n, True, vbMonday)) If currDayName = Left(dayName, 3) Then GetDayOfWeekNumber = n Exit Function End If Next n ' No match GetDayOfWeekNumber = -1 End Function Private Function IsToday(dayStr As String) As Boolean IsToday = Left(UCase(dayStr), 3) = "TOD" End Function Private Function IsTomorrow(dayStr As String) As Boolean IsTomorrow = Left(UCase(dayStr), 3) = "TOM" End Function