;; 
;;=====================================================================================----- 
;; 
;;FUNCTION       NextRunDate() 
;; 
;;ACTION         Calculates next possible run date/time for a given index, weekday, & Time 
;; 
;;AUTHOR         Glenn Barnas 
;; 
;;VERSION        1.1a - 2016/03/04 
;; 
;;HISTORY        1.0  - 2013/12/20 - Initial Release 
;;               1.1  - 2014/03/16 - bugfix: incorrect "next" weekday calculation 
;;                                   (returned prior date) 
;;               1.1a - 2016/03/04 - bugfix: did not correctly set the target day when 
;;                                   selecting the next day occurrence when the target time 
;;                                   had already passed. (select next, Friday, 1am at 8am on 
;;                                   a Friday returned current date instead of next Friday) 
;; 
;;SYNTAX         NextRunDate(Array) 
;; 
;;PARAMETERS     Array - REQUIRED - Array 
;;               - a 3 element array containing the following values: 
;;               * Element 0 - Index: A value of 1-5 representing one of 
;;               "First", "Second", "Third", "Fourth", or "Last" weeks in the month 
;;               A value of zero represents the "next available" Weekday occurrence 
;; 
;;               * Element 1 - Weekday: A value of 1-7 representing weekdays 
;;               Monday - Sunday Note that "Sunday" is 7, not 0 as is commonly 
;;               defined. A value of zero will target the next valid run time, 
;;               the current or next day. This is valid ONLY when Element 0 
;;               is also zero. 
;; 
;;               * Element 2 - Time: The desired time in 24-hour format 
;; 
;;REMARKS        A process must often run on a specific schedule. For a specific 
;;               date/time, this is not difficult, but for a "Third Tuesday at 
;;               15:00" specification, the calculations become more complex. This 
;;               collection of functions, along with our two standard time 
;;               calculation functions, accomplish this easily. The Date portion 
;;               of the values are used in the calculation, the Time value is used 
;;               only when determining if the target date/time has passed, and is 
;;               returned in the string so that the entire string can be used in a 
;;               calculation of when to perform a task. 
;; 
;;               The resulting string can then be used to schedule a task at a 
;;               precise date/time using a scheduler program, but is often used 
;;               in internal calculations to determine how long to wait until a 
;;               specific point in time occurs, as shown in the example below. 
;; 
;;RETURNS        String - "YYYY/MM/DD HH:MM" format 
;;               A date/time string in "YYYY/MM/DD HH:MM" format. The Time part 
;;               is returned as provided or calculated and may or may not contain 
;;               the seconds part. 
;; 
;;DEPENDENCIES   GetRunDay() 
;;               - in this file; calculates the date of the month based on Index and Weekday 
;;               Weekday() 
;;               - External; returns a weekday index (1-7) for a given date for Mon-Sun.  
;;               TimeConvert() 
;;               - External; Converts between cTime value and "Date Time" string & back again. 
;;               TimeDiff() 
;;               - External; Calculates the difference between to "Date Time" strings. 
;; 
;;TESTED WITH    W2K3, W2K8, W2K12, WXP, Vista, Win7, Win8 
;; 
;;EXAMPLES       ; Wait for the next occurrence of 22:00 on the Second Saturday and run a command 
;;		  
;;               $aSched = 2, 6, '22:00'			; second Saturday, 10pm 
;;               $Tag = 1 
;;                
;;               While $Tag 
;;                 $TargetDate = NextRunDate($aSched)	; get the next run Date/Time string 
;;                
;;                 ; Sleep for 1 day, then 12, 4, and 1 hour increments until execution time 
;;                 ; This method minimizes script overhead and provides +1/-0 second accuracy. 
;;                 $Seconds = TimeDiff('Now', $TargetDate) 
;;                 Select 
;;                  Case $Seconds > 86400 
;;                   Sleep 86400				; 1 day 
;;                  Case $Seconds > 43200 
;;                   Sleep 43200				; 12 hours 
;;                  Case $Seconds > 14400 
;;                   Sleep 14400				; 4 hours 
;;                  Case $Seconds > 3600 
;;                   Sleep 3600				; 1 hour 
;;                  Case $Seconds > 600 
;;                   Sleep 600				; 10 minutes 
;;                  Case $Seconds > 60 
;;                   Sleep 60				; 1 minute 
;;                  Case 1 
;;                   Sleep $Seconds			; remaining seconds 
;;                   RunCommand()			; run the desired command 
;;                   Sleep 66				; delay to prevent retriggering 
;;                   ; $Tag = 0				; optional - clear Tag to prevent auto-rescheduling 
;;                 EndSelect 
;;               Loop					; run forever or set $Tag=0 to exit loop 
; 
Function NextRunDate($_aData)
 
  Dim $_Year, $_Month, $_Date				; date values 
  Dim $_TDate						; Target date 
  Dim $_Secs						; seconds until T-Time 
  Dim $_CDow, $_DInc					; vars for "next" process calculations 
 
  ; Verify data passed to the function - must be an array, contain 3 elements, none may be blank, 
  ; and can't have a zero/nonzero condition in the first two elements. 
  If VarType($_aData) < 8192
    $NextRunDate = 'INVALID: Not An Array'
    Exit 87						; not an array 
  EndIf
 
  If UBound($_aData) <> 2
    $NextRunDate = 'INVALID: Array Format'
    Exit 87						; not 3 elements 
  EndIf
 
  If Len(CStr($_aData[0])) = 0 Or
    Len(CStr($_aData[1])) = 0 Or
    Len(CStr($_aData[2])) = 0
    $NextRunDate = 'INVALID: Missing Data'
    Exit 87						; data missing 
  EndIf
 
  If Val($_aData[0]) > 5 Or
    Val($_aData[1]) > 7
    $NextRunDate = 'INVALID: Bad Data Value'
    Exit 87						; data missing 
  EndIf
 
  ; If $_aData[1] = 0 and $_aData[0] <> 0, an error is returned 
  If $_aData[1] = 0 And $_aData[0] <> 0
    $NextRunDate = 'INVALID: Next Anyday!'
    Exit 87						; invalid condition 
  EndIf
 
  ; reformate HH:MM to HH:MM:SS with zero seconds value 
  If Len($_aData[2]) = 5 $_aData[2] = $_aData[2] + ':00' EndIf
 
  ; If $_aData[0] = 0 then the T-Day is the NEXT DAY defined by $_aData[1] 
  If Val($_aData[0]) = 0
    ; If $_aData[1] = 0 then the T-Day is the next occurrence of the TIME 
    If ($_aData[1]) = 0					; Run today or tomorrow 
      If TimeDiff('Now', @DATE + ' ' + $_aData[2]) < 0	; target time for today passed? 
        $NextRunDate = TimeConvert(TimeConvert(@DATE + ' ' + $_aData[2]) + 86400)
      Else						; schedule for today 
        $NextRunDate = @DATE + ' ' + $_aData[2]
      EndIf
    Else						; run on next DAY occurrence 
      ; Get the current DOW index 
      $_CDow = WeekDay(@DATE)
      ; subtract from the Target DOW index (OffsetDays) 
      $_DInc = $_aData[1] - $_CDow
      If $_DInc < 0 $_DInc = 7 + $_DInc EndIf
      ; NextRunDate is Today's TargetTime + (OffsetDays * 86400) 
      $NextRunDate = TimeConvert(@DATE + ' ' + $_aData[2]) + ($_DInc * 86400)
      ; BUT - if NextRunDate is prior to the current time, it has to occur 1 week later! 
      If $NextRunDate < TimeConvert(@DATE + ' ' + @TIME)
        $NextRunDate = $NextRunDate + 604800
      EndIf
      $NextRunDate = TimeConvert($NextRunDate)
    EndIf
    Exit 0
  EndIf
 
 
  ; Not an "immediate" process - perform the complete Next Date/Time calculation 
 
  $_Year  = SubStr(@DATE, 1, 4)
  $_Month = SubStr(@DATE, 6, 2)
  $_Date  = '' + $_Year + '/' + $_Month + '/01'		; first of current month 
 
  $_TDate = '' + $_Year + '/' + $_Month + '/' + GetRunDay($_Date, $_aData)
 
  $_Secs = TimeDiff('Now', $_TDate + ' ' + $_aData[2])
 
  ; Secs will be negative if the target date/time has passed for the current month. Adjust 
  ; for the next month (and year if month is December!) 
  If $_Secs < 0	
    $_Month = Val($_Month) + 1
    If $_Month > 12					; did we cross a year boundary? 
      $_Month = '01'					; reset month to 1 and increase year 
      $_Year = Val($_Year) + 1
    Else
      $_Month = Right('00' + $_Month, 2)		; format month to 2-digits 
    EndIf
    $_Date  = '' + $_Year + '/' + $_Month + '/01'	; first of new target date 
    $_TDate = '' + $_Year + '/' + $_Month + '/' + GetRunDay($_Date, $_aData)
  EndIf
 
  ; return the Date Time format string 
  $NextRunDate = $_TDate + ' ' + $_aData[2]
 
  Exit 0
 
EndFunction
 
 
; Supporting Function - determines the day of the month to run given a date and the index/weekday/time array 
Function GetRunDay($_Date, $_aData)
 
  Dim $_FDay						; first weekday of month 
  Dim $_Offset						; # of days offset from start of month 
  Dim $_CTime						; time vallue 
  Dim $_FTDay, $_RDay					; First Target and Run day values 
  Dim $_Month						; calendar month being processed 
  Dim $_FebLast						; last day of Feb in the target year 
 
  ; Calculate the date of the day before March 1 of the current year 
  $_FebLast = SubStr(TimeConvert(TimeConvert('' + @YEAR + '/03/01 12:00:00') - 86400), 9, 2)
 
  $_Month = SubStr($_Date, 6, 2)
 
  ; find weekday of first of month 
  $_FDay = WeekDay($_Date)
 
  ; Find Offset will be zero or negative 
  $_Offset = Val($_aData[1]) - $_FDay
 
  ; Get current CTime with Offset Days 
  $_CTime = TimeConvert($_Date + ' 00:00:00') + (86400 * $_Offset)
 
  ; adjust if necessary, moving back into the current month 
  If $_Offset < 0
    $_CTime = $_CTime + (7 * 86400)
  EndIf
 
  ; Now have date of first target "day" 
  $_FTDay = Val(SubStr(TimeConvert($_CTime), 9, 2))
 
  $_RDay = $_FTDay + (7 * (Val($_aData[0]) - 1))
 
  ; Insure that "Last" does not point past the end of the month 
  Select
   Case $_Month = 2
    If $_RDay > $_FebLast $_RDay = $_RDay - 7 EndIf
   Case InStr('~4~6~9~11~', '~' + Int($_Month) + '~')
    If $_RDay > 30 $_RDay = $_RDay - 7 EndIf
   Case 1
    If $_RDay > 31 $_RDay = $_RDay - 7 EndIf
  EndSelect
 
  $_RDay = Right('0' + $_RDay, 2)
 
  $GetRunDay = $_RDay
 
  Exit 0
 
EndFunction