3 people like it.

cron scheduling

Parsing cron expression and calculating next launch time from schedule. v 1.1: bugs fixed for case */n + catch bad parsings

  1: 
  2: 
  3: 
  4: 
  5: 
  6: 
  7: 
  8: 
  9: 
 10: 
 11: 
 12: 
 13: 
 14: 
 15: 
 16: 
 17: 
 18: 
 19: 
 20: 
 21: 
 22: 
 23: 
 24: 
 25: 
 26: 
 27: 
 28: 
 29: 
 30: 
 31: 
 32: 
 33: 
 34: 
 35: 
 36: 
 37: 
 38: 
 39: 
 40: 
 41: 
 42: 
 43: 
 44: 
 45: 
 46: 
 47: 
 48: 
 49: 
 50: 
 51: 
 52: 
 53: 
 54: 
 55: 
 56: 
 57: 
 58: 
 59: 
 60: 
 61: 
 62: 
 63: 
 64: 
 65: 
 66: 
 67: 
 68: 
 69: 
 70: 
 71: 
 72: 
 73: 
 74: 
 75: 
 76: 
 77: 
 78: 
 79: 
 80: 
 81: 
 82: 
 83: 
 84: 
 85: 
 86: 
 87: 
 88: 
 89: 
 90: 
 91: 
 92: 
 93: 
 94: 
 95: 
 96: 
 97: 
 98: 
 99: 
100: 
101: 
102: 
103: 
104: 
105: 
106: 
107: 
108: 
109: 
110: 
111: 
112: 
113: 
114: 
115: 
116: 
117: 
118: 
119: 
120: 
121: 
122: 
123: 
124: 
125: 
126: 
127: 
128: 
module CronSchedule

open System
open System.Text.RegularExpressions

let cronRegex = 
    Regex(@"(?<=^|,)(?<rangeStart>(?:\d+|\*))(?:\-(?<rangeEnd>(?:\d+|\*)))?(?:\/(?<devided>[1-9]\d*))?(?=,|$)")

type CronSchedule = {
    minutes: Set<int>
    hours: Set<int>
    dayOfMonth: Set<int>
    months: Set<int>
    dayOfWeek: Set<int>
    fail: string
    original: string
} with 
    static member Fail(str, original) = 
        { minutes = Set.empty; hours = Set.empty; dayOfMonth = Set.empty; 
            months = Set.empty; dayOfWeek = Set.empty; fail = str; original = original}
    member x.IsTime(date: DateTime) = 
        x.minutes.Contains(date.Minute) && 
        x.hours.Contains(date.Hour) &&
        x.dayOfMonth.Contains(date.Day) &&
        x.months.Contains(date.Month) &&
        x.dayOfWeek.Contains(int date.DayOfWeek)
    member x.NextTime(startDate: DateTime, ?maxDate: DateTime) = 
        let maxDate = defaultArg maxDate (startDate.AddYears(4))
        let years = [startDate.Year..maxDate.Year]  
        let startMonthes, monthes = 
            if Set.isEmpty x.months then 
                [yield![startDate.Month..12];yield![1..startDate.Month-1]], [1..12]
            else x.months |> Set.toList |> fun x -> x,x
        let startDaysOfMonth, daysOfMonth = 
            if Set.isEmpty x.dayOfMonth then 
                [yield![startDate.Day..31];yield![1..startDate.Day-1]], [1..31]
            else x.dayOfMonth |> Set.toList |> fun x -> x,x 
        let startHours, hours = 
            if Set.isEmpty x.hours then 
                [yield![startDate.Hour..23];yield![0..startDate.Hour-1]], [0..23]
            else x.hours |> Set.toList |> fun x -> x,x
        let startMinutes, minutes = 
            if Set.isEmpty x.minutes then 
                [yield![startDate.Minute..59];yield![0..startDate.Minute-1]], [0..59]
            else x.minutes |> Set.toList |> fun x -> x,x
        let daysOfWeek = 
            if Set.isEmpty x.dayOfWeek then Set [0..7]
            else x.dayOfWeek
        let rec search = 
            function 
            //no more years to check
            | [], _, _, _, _ -> None
            //no more monthes to check -> go to next year
            | year::avYears, [], _, _, _ -> 
                search (avYears, monthes, daysOfMonth, hours, minutes)
            //no more days of monthes to check -> go to next month
            | avYears, month::avMonthes, [], _, _ -> 
                search (avYears, avMonthes, daysOfMonth, hours, minutes)
            // days count in this month year is less than specified day of month --> go to next month
            | (year::_ as avYears), month::avMonthes, dayOfMonth::_, _, _ 
                when DateTime.DaysInMonth(year, month) < dayOfMonth -> 
                search (avYears, avMonthes, daysOfMonth, hours, minutes)
            //no more hours -> go to next day
            | avYears, avMonthes, dayOfMonth::avDaysOfMonth, [], _ -> 
                search (avYears, avMonthes, avDaysOfMonth, hours, minutes)
            // no more minutes -> go to next hour
            | avYears, avMonthes, avDaysOfMonth, hour::avHours, [] -> 
                search (avYears, avMonthes, avDaysOfMonth, avHours, minutes)
            //build date and additional check
            | (year::_ as avYears), (month::_ as avMonthes), 
                (dayOfMonth::otherDaysOfMonth as avDaysOfMonth), 
                (hour::_ as avHours), minute::avMinutes ->
                let date = DateTime(year, month, dayOfMonth, hour, minute, 0)
                if date >= startDate && date <= maxDate then
                    if daysOfWeek.Contains(int date.DayOfWeek) then Some date
                    else search (avYears, avMonthes, otherDaysOfMonth, hours, minutes)
                else search (avYears, avMonthes, avDaysOfMonth, avHours, avMinutes)
        search (years, startMonthes, startDaysOfMonth, startHours, startMinutes)

let (|Cron|_|) (rangeStart, rangeEnd) str = 
    let res = cronRegex.Matches(str)
    if res.Count > 0 then 
        [ 
            for m in res do
                let rangeStartGroup = m.Groups.["rangeStart"]
                let rangeEndGroup = m.Groups.["rangeEnd"]
                let stepGroup = m.Groups.["devided"]
                let step = 
                    if stepGroup.Success then
                        Int32.Parse(stepGroup.Value)
                    else 1 //defaultStep
                match rangeStartGroup.Success, rangeEndGroup.Success with
                | true, false when rangeStartGroup.Value = "*" && step=1 -> ()
                | true, false when rangeStartGroup.Value = "*" -> 
                    for i in rangeStart..step..rangeEnd -> i
                | true, false ->  yield Int32.Parse(rangeStartGroup.Value)
                | true, true when rangeStartGroup.Value = "*" && rangeEndGroup.Value = "*" -> ()
                | true, true ->
                    let rangeStart = 
                        if rangeStartGroup.Value = "*" then rangeStart
                        else max rangeStart (Int32.Parse(rangeStartGroup.Value))
                    let rangeEnd = 
                        if rangeEndGroup.Value = "*" then rangeEnd
                        else min rangeEnd (Int32.Parse(rangeEndGroup.Value))
                    for i in rangeStart..step..rangeEnd do
                        yield i
                | _, _ -> ()
        ] |> Set.ofList |> Some
    else None

let create(expression: string) = 
    let parts = expression.Split() 
    try
    match parts with
    | [| Cron (0,59) minutes; Cron (0,23) hours; Cron (1,31) days; 
            Cron (1,12) months; Cron (0,7) daysOfWeek |] -> 
        { minutes = minutes; hours = hours; dayOfMonth = days; months = months; 
            dayOfWeek = (if Set.contains 7 daysOfWeek then Set.add 0 daysOfWeek else daysOfWeek); 
            fail = ""; original = expression}
    | _ -> CronSchedule.Fail("Wrong expression format. Expression should contain 5 parts", expression)
    with _ -> CronSchedule.Fail("Wrong expression format. Expression should contain 5 parts", expression)

(*Examples:  
let a = CronSchedule.create "40-59/3,15-20,24,28-35/2 11-17 * * 0,3,5"
let b = CronSchedule.create "*/5 * * * *"
let fromTime = DateTime.UtcNow
let nextLaunch = a.NextTime(fromTime)
*)
module CronSchedule
namespace System
namespace System.Text
namespace System.Text.RegularExpressions
val cronRegex : Regex

Full name: CronSchedule.cronRegex
Multiple items
type Regex =
  new : pattern:string -> Regex + 1 overload
  member GetGroupNames : unit -> string[]
  member GetGroupNumbers : unit -> int[]
  member GroupNameFromNumber : i:int -> string
  member GroupNumberFromName : name:string -> int
  member IsMatch : input:string -> bool + 1 overload
  member Match : input:string -> Match + 2 overloads
  member Matches : input:string -> MatchCollection + 1 overload
  member Options : RegexOptions
  member Replace : input:string * replacement:string -> string + 5 overloads
  ...

Full name: System.Text.RegularExpressions.Regex

--------------------
Regex(pattern: string) : unit
Regex(pattern: string, options: RegexOptions) : unit
type CronSchedule =
  {minutes: Set<int>;
   hours: Set<int>;
   dayOfMonth: Set<int>;
   months: Set<int>;
   dayOfWeek: Set<int>;
   fail: string;
   original: string;}
  member IsTime : date:DateTime -> bool
  member NextTime : startDate:DateTime * ?maxDate:DateTime -> DateTime option
  static member Fail : str:string * original:string -> CronSchedule

Full name: CronSchedule.CronSchedule
CronSchedule.minutes: Set<int>
Multiple items
module Set

from Microsoft.FSharp.Collections

--------------------
type Set<'T (requires comparison)> =
  interface IComparable
  interface IEnumerable
  interface IEnumerable<'T>
  interface ICollection<'T>
  new : elements:seq<'T> -> Set<'T>
  member Add : value:'T -> Set<'T>
  member Contains : value:'T -> bool
  override Equals : obj -> bool
  member IsProperSubsetOf : otherSet:Set<'T> -> bool
  member IsProperSupersetOf : otherSet:Set<'T> -> bool
  ...

Full name: Microsoft.FSharp.Collections.Set<_>

--------------------
new : elements:seq<'T> -> Set<'T>
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
CronSchedule.hours: Set<int>
CronSchedule.dayOfMonth: Set<int>
CronSchedule.months: Set<int>
CronSchedule.dayOfWeek: Set<int>
CronSchedule.fail: string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
CronSchedule.original: string
static member CronSchedule.Fail : str:string * original:string -> CronSchedule

Full name: CronSchedule.CronSchedule.Fail
val str : string
val original : string
val empty<'T (requires comparison)> : Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.empty
val x : CronSchedule
member CronSchedule.IsTime : date:DateTime -> bool

Full name: CronSchedule.CronSchedule.IsTime
val date : DateTime
Multiple items
type DateTime =
  struct
    new : ticks:int64 -> DateTime + 10 overloads
    member Add : value:TimeSpan -> DateTime
    member AddDays : value:float -> DateTime
    member AddHours : value:float -> DateTime
    member AddMilliseconds : value:float -> DateTime
    member AddMinutes : value:float -> DateTime
    member AddMonths : months:int -> DateTime
    member AddSeconds : value:float -> DateTime
    member AddTicks : value:int64 -> DateTime
    member AddYears : value:int -> DateTime
    ...
  end

Full name: System.DateTime

--------------------
DateTime()
   (+0 other overloads)
DateTime(ticks: int64) : unit
   (+0 other overloads)
DateTime(ticks: int64, kind: DateTimeKind) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, calendar: Globalization.Calendar) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, kind: DateTimeKind) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, calendar: Globalization.Calendar) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int) : unit
   (+0 other overloads)
DateTime(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, kind: DateTimeKind) : unit
   (+0 other overloads)
member Set.Contains : value:'T -> bool
property DateTime.Minute: int
property DateTime.Hour: int
property DateTime.Day: int
property DateTime.Month: int
property DateTime.DayOfWeek: DayOfWeek
member CronSchedule.NextTime : startDate:DateTime * ?maxDate:DateTime -> DateTime option

Full name: CronSchedule.CronSchedule.NextTime
val startDate : DateTime
val maxDate : DateTime option
val maxDate : DateTime
val defaultArg : arg:'T option -> defaultValue:'T -> 'T

Full name: Microsoft.FSharp.Core.Operators.defaultArg
DateTime.AddYears(value: int) : DateTime
val years : int list
property DateTime.Year: int
val startMonthes : int list
val monthes : int list
val isEmpty : set:Set<'T> -> bool (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.isEmpty
val toList : set:Set<'T> -> 'T list (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.toList
val x : int list
val startDaysOfMonth : int list
val daysOfMonth : int list
val startHours : int list
val hours : int list
val startMinutes : int list
val minutes : int list
val daysOfWeek : Set<int>
val search : (int list * int list * int list * int list * int list -> DateTime option)
union case Option.None: Option<'T>
val year : int
val avYears : int list
val month : int
val avMonthes : int list
val dayOfMonth : int
DateTime.DaysInMonth(year: int, month: int) : int
val avDaysOfMonth : int list
val hour : int
val avHours : int list
val otherDaysOfMonth : int list
val minute : int
val avMinutes : int list
union case Option.Some: Value: 'T -> Option<'T>
val rangeStart : int
val rangeEnd : int
val res : MatchCollection
Regex.Matches(input: string) : MatchCollection
Regex.Matches(input: string, startat: int) : MatchCollection
property MatchCollection.Count: int
val m : Match
val rangeStartGroup : Group
property Match.Groups: GroupCollection
val rangeEndGroup : Group
val stepGroup : Group
val step : int
property Group.Success: bool
type Int32 =
  struct
    member CompareTo : value:obj -> int + 1 overload
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member GetTypeCode : unit -> TypeCode
    member ToString : unit -> string + 3 overloads
    static val MaxValue : int
    static val MinValue : int
    static member Parse : s:string -> int + 3 overloads
    static member TryParse : s:string * result:int -> bool + 1 overload
  end

Full name: System.Int32
Int32.Parse(s: string) : int
Int32.Parse(s: string, provider: IFormatProvider) : int
Int32.Parse(s: string, style: Globalization.NumberStyles) : int
Int32.Parse(s: string, style: Globalization.NumberStyles, provider: IFormatProvider) : int
property Capture.Value: string
val i : int
val max : e1:'T -> e2:'T -> 'T (requires comparison)

Full name: Microsoft.FSharp.Core.Operators.max
val min : e1:'T -> e2:'T -> 'T (requires comparison)

Full name: Microsoft.FSharp.Core.Operators.min
val ofList : elements:'T list -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.ofList
val create : expression:string -> CronSchedule

Full name: CronSchedule.create
val expression : string
val parts : string []
String.Split([<ParamArray>] separator: char []) : string []
String.Split(separator: string [], options: StringSplitOptions) : string []
String.Split(separator: char [], options: StringSplitOptions) : string []
String.Split(separator: char [], count: int) : string []
String.Split(separator: string [], count: int, options: StringSplitOptions) : string []
String.Split(separator: char [], count: int, options: StringSplitOptions) : string []
active recognizer Cron: int * int -> string -> Set<int> option

Full name: CronSchedule.( |Cron|_| )
val minutes : Set<int>
val hours : Set<int>
val days : Set<int>
val months : Set<int>
val contains : element:'T -> set:Set<'T> -> bool (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.contains
val add : value:'T -> set:Set<'T> -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Collections.Set.add
static member CronSchedule.Fail : str:string * original:string -> CronSchedule

More information

Link:http://fssnip.net/qd
Posted:2 years ago
Author:dvitel
Tags: cron , regex , scheduling