/// cronttab parsing + = code_version:"V01.009" // parsing and tokenizing utilities sqz:{x@&~&/'x=" "} / no blank rows cut:{1_'(&y=x)_ y:x,y} / cut y on x glu:{1_,/x,'y} / glue y's with x deb:{x@&~0&':" "=x} / delete extra blanks dlb:{((x=" ")?0)_ x} / delete leading blanks dtb:|dlb@|: / delete trailing blanks trim:dlb dtb deb@ / delete leading, trailing, extra blanks db:{x _dv" "} / delete blanks // errors error:{':[-3=4:y;y;5:y],": ",x} // tokenize NUL:_ci 255 BLA:" " STR:"*" QUE:"?" COM:"," SKP:"/" RNG:"-" HSH:"#" DIG:"0123456789" NUM:DIG,"lwdm." TWO:SKP,RNG,HSH ZER:STR,QUE CHR:_ci&~(!256)_lin _ic BLA,NUM,ZER,COM,TWO TRANS:" @ @ BLA ZER NUM TWO COM CHR - - bla zer num two com chr bla bla bla zer num two com chr zer zer bla tok tok two com chr num num bla tok num_ two com chr num num_ bla tok num_ two com chr com com bla tok num tok tok chr two two bla tok num tok tok chr err tok tok tok tok tok tok tok err chr chr chr chr chr chr chr " / TRANS -> tokenizer trans:{ f:{{.[x;(;y);:;z]}/[((#x),256)#-1;y;+x]} / trans matrix -> n x 256 scan matrix a:`$cut[" "]'sqz cut["\n"]deb x;b:1_-1_ a[;0];c:1_-1_ a[;1];s:c@=b / dissect TRANS A:?b;C:0+\1_#:'s;M:(,/s)?/:/:2_'1_ a;F:f[M]_ic .:'2_*a / -> automata, tokenizer (A;C;F;M)} / tokenize token:{[s]_dv[tokens[Cut;s]tok[Mat;s]chr[Mat;s]1_0 Fsm\_ic s;,BLA]} / tokenize tokens:{[c;s;t](&(1=c[k]-c k-1)|(~=)':0,k:c _binl t)_ s} / chop into tokens automata:{[s]1_ Aut Cut _binl 0 Fsm\_ic s,""} / automaton of each token / errors chr:{:[(#z)>e:z?-1+#x;err[y;e-1]"bad character";z]} / bad character? tok:{:[(#z)>e:z?-2+#x;err[y;e-1]"bad token";z]} / bad transition? / general tokenizer, automata @[_d;`Aut`Cut`Fsm`Mat;:;trans TRANS]; // shift-reduce parser / operator table, shift-reduce table, valences OPR:(,COM;TWO;,NUL) / ,2Zc / shift-reduce table: s -> right assoc, r -> left assoc SRT:("rsrc" / , "rrrc" / / - # "ssac") / Z / , 2 Z c VAL:2 2 0 0 / parse parse:{**-1#act/("";x,,NUL;,NUL;"")} / construct parse structure act:{t:SRT . cl'x[2 1;0];trace[t]. x;H[`$t]. x} / action step T:0;trace:{[t;j;i;o;v]if[T;`0:,_dv[t,(T$j)," | ",((-T)$1_,/BLA,'i);NUL]]} / trace, e.g. T:15 / parser transition states H.a:{[j;i;o;v]:[1=#v;(j;i;o;v);error[j]"syntax error"]} / accept H.c:{[j;i;o;v](j,BLA,*i;1_ i;o;(,*i),v)} / constant -> v-stack H.r:{[j;i;o;v]:[0 o-stack / parser subfunctions cl:{(|/'(*x)_in/:OPR)?1} / class rn:{[j;i;o;v;n](j;i;(-n)_ o;v)} / reduce without result rp:{[j;i;o;v;n](j;i;1_ o;(,o[0],|n#v),n _ v)} / reduce with result / preparser functions parts:{:[6=#r:cut[BLA]trim sub flat x;r;5=#r;r,,,"*";error[x]"cronspec must be "]} / string -> list sub:{_ssr/[_ssr/[_ssr/[lc x;"@",'$!K;K[]];$!M;$M[]];$!D;$D[]]} / substitute S-M-D keywords lc:{_ci@[!256;(_ic "A")+!26;:;(_ic "a")+!26]_ic x} / lowercase flat:{:[-3=4:x;x;,/{" ",:[(t:4:x)_in 3 -3;x;@x;$x;1_,/",",'$x]}'x]} / list -> string // evaluation / ranges R[`m`h`D`M`d`P]:(!60;!24;1+!31;1+!12;!7;!0) R_[`m`h`D`M`d`P]:("minute";"hour";"day of month";"month";"day of week";"periodicity") / keywords M[`jan`feb`mar`apr`may`jun`jul`aug`sep`oct`nov`dec]:R`M D[`sun`mon`tue`wed`thu`fri`sat]:R`d K[`monthly`weekly`daily`hourly]:("0 0 1 * * *";"0 0 * * 0 *";"0 0 * * * *";"0 * * * * *") K[`midnight`teatime]:(K`daily;"0 16 * * * *") / eval skp:{if[@y;y _:R x];y@&~(!#y)!z} / e.g. */3 = every 3rd of, or 1/3 com:{y,z} / e.g. 3,4 rng:{y _!1+z} / e.g. 5-8 hsh:{(`h;y;z)} / e.g. fri#3 = 3rd friday E:SKP,COM,RNG,HSH / cron special characters F:(skp;com;rng;hsh) / cron lambdas eval:{nub'(!R)eval_'star x} / eval parts star:{@[x;:[QUE _in a:*:'x 2 4;!0;~s:+/b:STR=a;!0;2=s;4;2 4 b?1];:[;,QUE]]} / * -> ? eval_:{:[~4:y;F[E?*y][x]. _f[x]'1 _ y;STR~*y;R x;QUE~*y;!0;"."_in y;per . cut["."]y;lwn y]} / recursive evaluation eval_:{:[~4:y;F[E?*y][x]. _f[x]'1 _ y;STR~*y;R x;QUE~*y;!0;"."_in y;cut["."]y;lwn y]} / recursive evaluation nub:{{:[4:x;x@0)&~x _in R`D;error[($x),"L"]"out of range: ",R_`D]} / last day of the month (3L = last day of march) Dw:{if[(x>0)&~x _in R`D;error[($x),"W"]"out of range: ",R_`D]} / weekday nearest given day dl:{if[(x>0)&~x _in R`d;error[($x),"L"]"out of range: ",R_`d]} / last day of given month (5L = last friday of D) dh:{if[(x>0)&~(x _in R`d)&y _in 1+!5;error[($x),"#",$y]"must be 0-6#1-5"]} / yth x-day of given month (5#3 = third friday of D) e1:{error[y]"out of range: ",R_ x} / out of range error e2:{error[y]($*y)," cannot be used for ",R_ x} / special character error pny:{:[0=#x;x;~3=#x;e3 x;(f1;f2;f3)@'x]} / check periodicity e3:{error[x]"periodicity must be {d/w/m}.n.yyyymmdd"} / wrong structure e31:{error[x]"periodicity type must one of {d/w/m}"} / bad type e32:{error[x]"periodicity number must be a positive integer"} / bad number e33:{error[x]"periodicity start date must be a valid date"} / bad date f1:{if[~(1=#x)&(*x)_in"dwmDWM";e31 x]} / check type f2:{if[(~#x)|~&/x _lin DIG;e32 x]} / check number f3:{:[(~#x)|~&/x _lin DIG;e33 x;0N~_dj _jd 0$x;e33 x]} / check date // tokenize, parse, eval, check, map map:{[m;h;D;M;d;P].+(`M`D`h`m`d`P;(M;D;h;m;d;per P))} per:{:[#x;(`$lc x 0;0$x 1;0$x 2);x]} tab:{map . check eval@(parse token@)'parts x} // generate schedule from year + crontab object + time object sched:{[c;y]?,/days[dd y]'d2 c} / entry point dd:{d:*-1#+a:x{(x;y;1+!din[x]y)}'M[];.+(`D`d;(d;(1+_jd 100_sv'a)!7))} / days, day#s by month din:{[y;m]:[m=2;leap y;0]+31 28 31 30 31 30 31 31 30 31 30 31 m-1} / days in D[year]month leap:{[y]t:0=4 25 4 _vs y;t[2]&t[0]|~t 1} / leap year? days:{[D;c]:[#c`d;daysof[D;c`d];daysin[D;c`D]]c`M} / month day# | month day di:{[x;D;y;z]D[*x;y]@&D[x 1;y-:1]_lin(),z};dn:di[`D`d];nd:di[`d`D] / day <-> day# ld:{[D;m]*-1#D[`D]m-1} / last day of month daysof:{[D;d;m]:[`l~a:*d;lastof[D;d 1]'m;`h~a;nthof[D;d 1;-1+d 2]'m;,/of[D;d]'m]} / month day# lastof:{[D;n;m](m;*-1#dn[D;m]n)} / last day# nthof:{[D;n;k;m]:[k<#r:dn[D;m]n;(m;r k);error[($(!M)m-1),":",($n),"#",$k+1]"no such day"]} / nth day# of:{[D;n;m]+(m;dn[D;m]n)} / day#'s daysin:{[D;d;m]:[`l~a:*d;lastin[D]m;`w~a;weekday[D;d 1]'m;,/in[D;d]'m]} / month day weekday:{[D;d;m](m;:[0=n:*nd[D;m]d;d+1;6=n;d+-1 2 d=1;d])} / weekday of day lastin:{[D;m]+(m;ld[D]'m)} / last day in:{[D;d;m]+(m;d@&~d>ld[D]m)} / days d2:{[c]:[&/0<#:'c`D`d;c{@[x;y;:;!0]}/:`d`D;,c]} / split D + d // application / replaces .cron.nextstartfrom_localtime nextstartfrom_localtime:{nextstartfrom[x;y+z*3600]-z*3600%86400.} / replaces .cron.nextstartfrom nextstartfrom:{*nextruns[x;y;1]`nextruns_t} / replaces .cron.nextruns nextruns:{:["="~*x;eq[x]y;nextruns_[x;y]z]} / cron-spec|=-list / yyyy.mm.ddThh:mm -> yyyymmdd hhmm eq:{return[x]veq[t]tmap@fut[y]@k4't:cut[" "]trim 1_ x} / list of (valid) k4 date-times veq:{:[#i:&0N=y`nextruns_t;error[glu[","]x i]"bad timespec(s)",;y]} / trap invalid k4 date-times k4:{:[*r:@[{100_sv/:0$".:"cut'cut["T"]x,":00"};x;:];error[x]r 1;r 1]} / "yyyy.mm.ddThh:mm" -> (yyyymmdd;hhmm) fut:{a:100_sv/:0 3_ year[x],time[x][],0;y@:&~y[;0]<*a;:[#y;y@:&~(y[;0]=*a)&y[;1]0;m:sched[c]Y+:1;d:0]] / loop until we have z times return[x]tmap@,/r} / :time-map / run s from f to t inclusive (cron-spec,fromts,tots) runsfromto:{[s;f;t] k:{nextruns[s;x;y]`nextruns_t} :[_n~f:*k[f]1;!0;{x@&x){*-1#k[x]2}\f]} / year of _t, return, timemap, conversion return:{:[#y`ymd;y;error[x]"generates an empty schedule"]} / check for empty tmap:{.+(`nextruns_t`gtimes`ymd;(t;date'[x;t:tcnv'x];*+x))} / construct time map tfmt:{[y;r;m;d](100_sv y,m,d),/:100*100_sv+r} / ymd hms <- time:{.+(`M`D`h`m;1_-1_,/@[_gtime x;_n;0 100 100_vs])} / _t object tT:-_- 86400* / convert T -> t year:*0 100 100_vs*_gtime / year of _t tcnv:{0 24 60 60_sv _jd[*x],100 100 100_vs x 1} / yyyymmdd hhmmss -> _t tdt:{[d;t]0 24 60 60_sv(_jd d),100 100 100_vs t} / _t <- d t ldom:{tdt[t@*|*=*0 100_vs t:_dj _jd[*_gtime x]+!31;235959]} / 23:59:59 of last day of month x / periodicity filter filter:{[P;r] if[~(#P)&#r;:r] / no dates or filter period a:r[;0];n:!#b:fromto[P 2;*-1#a] / dates, filter c:b@&:[`d=*P;~n!P 1 / daily `w=*P;n _lin,/i@&~(!#i:(?0,&0=(_jd b)!7)_ n)!P 1 / weekly `m=*P;n _lin,/i@&~(!#i:(&(~=)':0,(0 100 100_vs b)1)_ n)!P 1] / monthly r@&a _lin c} / dates in filter fromto:{[f;t]:[f>t;!0;_dj a+!1+_jd[t]-a:_jd f]} / days from ymd to ymd date:{ r:@["yyyy/mm/dd hh:mm:ss";0 1 2 3 5 6 8 9 11 12 14 15 17 18;:;($*x),,/-2#'"0",'$0 100 100_vs x 1] ($`Mon`Tue`Wed`Thu`Fri`Sat`Sun(_ y%86400)!7)," ",r} format3:date / testing routines runs:{nextruns[x;_t;30]} run:{nextruns[x;_t;1]} \ / everything below here is for testing and should NOT be loaded into production code runsfromto["45 10 14 * *";_t;ldom _t] / tomorrow runsfromto["45 10 12 * *";_t;ldom _t] / yesterday scheds:{`0:,(3$#S),":",x;r:.[nextruns;(x;_t;5);:];if[~*r;`0:r[1]`gtimes;0:`]} / golda bug: scheds"15 8 6-10 * 1-5" scheds"=2016.04.20T10:30 2017.04.26T11:30" scheds"2016.04.20T10:30" scheds"15 10 * * * d.5.20170301" scheds"15 10 * * 5 w.2.20160101" scheds"15 10 * * 5#3 m.5.20150601" scheds"0 0 * * 3#4" / fourth wednesday scheds"* * * * *" / at all times scheds"* * * * 4L" / last thursday of every month scheds"* * * * 5#3" / third friday of every month scheds"* * 15W * *" / nearest weekday to the 15th of every month scheds"0 4 L * *" / at 4:00 AM on the last day of each month scheds"0 4 * 2-4 6L" / at 4:00 AM on the last friday of feb-apr scheds"15 10 * * 6#3" / at 10:15 AM on the third Friday of every month scheds"22 10 15W * *" / at 10:22 AM on the nearest weekeday to the 15th of every month scheds"30 4 1,15 * 5" / run at 4:30 am on the 1st and 15th of each month AND every Friday. scheds"0 12 * * *" / at 12:00 PM (noon) every day scheds"* * * * *" / every time scheds"0 12 * * *" / at 12:00 PM (noon) every day scheds"0 12 * * *" / at 12:00 PM (noon) every day scheds"15 10 * * *" / at 10:15 AM every day scheds"15 10 * * *" / at 10:15 AM every day during the year scheds"* 14 * * *" / every minute 2:00 PM - 2:59 PM, every day scheds"0 4 2-4 * 6L" / at 4:00 AM on the last friday of feb-apr scheds"0 4 L * *" / at 4:00 AM on the last day of each month scheds"15 10 * * 6#3" / at 10:15 AM on the third Friday of every month scheds"0 12 * * *" / at 12:00 PM (noon) every day scheds"15 10 * * *" / at 10:15 AM every day scheds"15 10 * * *" / at 10:15 AM every day scheds"15 10 * * *" / at 10:15 AM every day scheds"15 10 * * *" / at 10:15 AM every day during the year 2016 scheds"* 14 * * *" / every minute 2:00 PM - 2:59 PM, every day scheds"*/5 14 * * *" / every 5 minutes 2:00 PM - 2:55 PM, every day scheds"*/5 14,18 * * *" / every 5 minutes 2:00 PM - 2:55 PM AND every 5 minutes 6:00 PM - 6:55 PM, every day scheds"0-5 14 * * *" / every minute 2:00 PM - 2:05 PM, every day scheds"10,44 14 * 3 WED" / at 2:10 PM and at 2:44 PM every Wednesday in the month of March scheds"15 10 * * MON-FRI" / at 10:15 AM every Monday, Tuesday, Wednesday, Thursday and Friday scheds"15 10 15 * *" / at 10:15 AM on the 15th day of every month scheds"15 10 L * *" / at 10:15 AM on the last day of every month scheds"15 10 * * 6L" / at 10:15 AM on the last Friday of every month scheds"15 10 * * 6L" / at 10:15 AM on the last Friday of every month scheds"15 10 * * 6L" / at 10:15 AM on every last friday of every month during 2015-2016 scheds"15 10 * * 6#3" / at 10:15 AM on the third Friday of every month scheds"0 12 1/5 * *" / at 12 PM (noon) every 5 days every month, starting on the first day of the month scheds"11 11 11 11 *" / every November 11 at 11:11 AM scheds"0 0,12 1 */2 *" scheds"0 2 2-4 * *" scheds"0 4 * 2-4 6L" scheds"@monthly" scheds"0 0 * */2 3#3" N:0 nextstartfrom:{ T:_T;t:tT T a:_t;do[N|1;r:.cron.nextruns[x;T;5];r:r _di`nextruns_T];if[N>0;`0:,"old: ",$_t-a] do[N|1;s:nextruns[x;t;5]];if[N>0;`0:,"new: ",$_t-a] b:0N;if[N=0;`0:,$b:r~s _di`ymd] if[~b;`0:,"**** bad"] (b;r;s)} S:() scheds:{[a]i:0;do[#a;`0:,(3$#S),":",flat b:a i;S,:,nextstartfrom b;i+:1]} A:1:`cron.l scheds A a:nextstartfrom[( 2; 23; "*"; "*"; "*")] /23:02 every day b:nextstartfrom[( 2; 23; "*"; "*"; 5)] /23:02 every friday c:nextstartfrom[( "0-59/2"; "*"; "*"; "*"; "*")] /every two minutes d:nextstartfrom[( "0-59/2"; "*"; 3 4 5; 10 11 12; "*")] /every two minutes 3rd-5th oct-dec \ https://en.wikipedia.org/wiki/Cron nextruns takes either: a crontab spec x, an _T y, and z the desired number of times >= y which satisfy x or: a string of the form "= ... yyyy.mm.ddThh:mm ..." specifying date-times in k4 format and returns: dictionary of times in three formats -- nextruns_T, nextruns_t, and gtimes -- and a fourth element ymd of yyyymmdd dates. the crontab spec can be a string or a list of strings and integer atoms or vectors. the following documentation may be used to supplement and clarify the information contained in the wikipedia article referenced above. the crontab spec consists of either five parts or six parts (the sixth part is optional): m minutes 0-59 * , - / h hours 0-23 * , - / D day of month 1-31 * , - / ? L W (case insensitive) M month 1-12 or JAN-DEC (case insensitive) * , - / d day of week 0-6 or SUN-SAT (case insensitive) * , - / ? L # (case insensitive) P periodicity n.d.p = number.{d/w/m}.yyyymmdd ranges and lists: ranges: a-b lists: a,b,..,c can be combined: a-b,c,..,d-e non-standard characters: L in the d field used as follows: 0L .. 6L = last sunday .. last saturday in the D field used only as L = last day of each month in M W used only in the D field, as follows: if 1W then if the 1st is a saturday or sunday, then forward to monday the 3rd if kW then if the kth is a saturday then backward to friday the k-1st if the kth is a sunday then forward to monday the k+1st # used only in the d field, as follows: d#n means: the n-th d-day of the month specified in M, e.g. M = 3-5 d = 5#2 means the second friday of march, april, and may. ? used to indicate blank in the d or D fields. see below for the ?* logic. / used to specify step-values, e.g. X/n = every n of the X's. X may be any expression which resolves to a list, e.g. */5 0-15,45-59/2 NB: see the note from the wikipedia article on the logic of valid frequencies. * in m, h, and M fields * means all. the logical interactions of the D and d fields are as follows: in the D field, * means all if d = ?, else * means none in the d field, * means all if D = ?, else * means none if both D and d are *, then * means all in D and none in d if D is explicitly specified, then * in d means none if d is explicitly specified, then * in D means none examples: D d -> D d - - - - * ? all none ? * none all * * all none k * some none * k none some k l some some in which case the result combines d and D. n.p.d number of periods from anchor-date d. no reset on week/month/year turnover. examples: 15 10 * * * d.5.20150301 at 10:15 every five days starting 20150301 15 10 * * 5 w.2.20160101 at 10:15 every friday every two weeks from 20160101 15 10 * * 5#3 m.5.20150601 at 10:15 every third friday of every five months from 20150601 keywords (case-insensitive): @monthly 0 0 1 * * @weekly 0 0 * * 0 @daily 0 0 * * * = @midnight @hourly 0 * * * * coding logic: the script consists of the following functional blocks: crontab object: dictionary <- string or list tab:{map . check eval@(parse token@)'parts x} time object: dictionary <- _t or _T time:{.+(`M`D`h`m;1_-1_,/@[_gtime:[2=4:x;_ x*86400;x];_n;0 100 100_vs])} crontab interpretation: list of month-day pairs <- crontab object, year sched:{[c;y]?,/days[dd y]'d2 c} application: nextruns:{[crontab;time;#schedules] (see script for details) } nextstartfrom:{nextruns[x;y;1]`nextruns_T} nextstartfrom_localtime:{nextstartfrom[x;y+z*3600]-z*3600%86400.} expanded code for nextruns_: / (OMG! A STINKING NESTED LOOP!) nextruns_:{ f:100_sv g:24 60_sv c:tab x t:time y Y:year y m:sched[c]Y m@:0;m:sched[c]Y+:1;d:0]] r:return[x]tmap@,/r r}