1 ! Copyright (C) 2018 John Benediktsson
2 ! See https://factorcode.org/license.txt for BSD license
4 USING: accessors arrays ascii assocs assocs.extras calendar
5 calendar.english calendar.format calendar.parser
6 calendar.private circular combinators combinators.short-circuit
7 io kernel literals math math.order math.parser prettyprint
8 random ranges sequences sets sorting splitting ;
12 ERROR: invalid-cronentry value ;
14 TUPLE: cronentry minutes hours days months days-of-week command ;
18 :: parse-range ( from/f to/f quot: ( input -- value ) seq -- from to )
19 from/f [ seq first ] quot if-empty
20 to/f [ seq last ] quot if-empty ; inline
22 :: parse-value ( input quot: ( input -- value ) seq -- value )
24 { [ dup "*" = ] [ drop seq ] }
25 { [ CHAR: , over member? ] [
26 "," split [ quot seq parse-value ] map concat ] }
27 { [ CHAR: / over member? ] [
29 quot seq parse-value dup length 1 =
30 [ seq swap first seq index seq length ]
31 [ 0 over length ] if 1 -
32 ] dip string>number <range> swap nths ] }
33 { [ CHAR: - over member? ] [
34 "-" split1 quot seq parse-range [a..b] ] }
35 { [ CHAR: ~ over member? ] [
36 "~" split1 quot seq parse-range [a..b] random 1array ] }
38 } cond members sort ; inline recursive
40 : parse-day ( str -- n )
41 [ string>number dup 7 = [ drop 0 ] when ] [
42 >lower $[ day-abbreviations3 [ >lower ] map ] index
45 : parse-month ( str -- n )
47 >lower $[ month-abbreviations [ >lower ] map ] index
51 { "@yearly" "0 0 1 1 *" }
52 { "@annually" "0 0 1 1 *" }
53 { "@monthly" "0 0 1 * *" }
54 { "@weekly" "0 0 * * 0" }
55 { "@daily" "0 0 * * *" }
56 { "@midnight" "0 0 * * *" }
57 { "@hourly" "0 * * * *" }
60 : check-cronentry ( cronentry -- cronentry )
62 [ days-of-week>> [ 0 6 between? ] all? ]
63 [ months>> [ 1 12 between? ] all? ]
65 [ days>> 1 ] [ months>> ] bi [
66 { 0 31 29 31 30 31 30 31 31 30 31 30 31 } nth
67 ] map maximum [ between? ] 2curry all?
69 [ minutes>> [ 0 59 between? ] all? ]
70 [ hours>> [ 0 23 between? ] all? ]
71 } 1&& [ invalid-cronentry ] unless ;
75 : parse-cronentry ( entry -- cronentry )
76 " " split1 [ aliases ?at drop ] dip " " glue
77 " " split1 " " split1 " " split1 " " split1 " " split1 {
78 [ [ string>number ] T{ range f 0 60 1 } parse-value ]
79 [ [ string>number ] T{ range f 0 24 1 } parse-value ]
80 [ [ string>number ] T{ range f 1 31 1 } parse-value ]
81 [ [ parse-month ] T{ range f 1 12 1 } parse-value ]
82 [ [ parse-day ] T{ circular f T{ range f 0 7 1 } 1 } parse-value ]
84 } spread cronentry boa check-cronentry ;
88 : ?parse-cronentry ( entry -- cronentry )
89 dup cronentry? [ parse-cronentry ] unless ;
91 :: (next-time-after) ( cronentry timestamp -- )
93 f ! should we keep searching for a matching time
95 timestamp month>> :> month
96 cronentry months>> [ month >= ] find nip
97 dup month = [ drop ] [
98 [ cronentry months>> first timestamp 1 +year drop ] unless*
99 timestamp 1 >>day 0 >>hour 0 >>minute month<< drop t
102 timestamp day-of-week :> weekday
103 cronentry days-of-week>> [ weekday >= ] find nip [
104 cronentry days-of-week>> first 7 +
105 ] unless* weekday - :> days-to-weekday
107 timestamp day>> :> day
108 cronentry days>> [ day >= ] find nip [
109 cronentry days>> first timestamp days-in-month +
110 ] unless* day - :> days-to-day
112 cronentry days-of-week>> length 7 =
113 cronentry days>> length 31 = 2array
115 { { f t } [ days-to-weekday ] }
116 { { t f } [ days-to-day ] }
117 [ drop days-to-weekday days-to-day min ]
119 timestamp 0 >>hour 0 >>minute swap +day 2drop t
122 timestamp hour>> :> hour
123 cronentry hours>> [ hour >= ] find nip
124 dup hour = [ drop ] [
125 [ cronentry hours>> first timestamp 1 +day drop ] unless*
126 timestamp 0 >>minute hour<< drop t
129 timestamp minute>> :> minute
130 cronentry minutes>> [ minute >= ] find nip
131 dup minute = [ drop ] [
132 [ cronentry minutes>> first timestamp 1 +hour drop ] unless*
133 timestamp minute<< drop t
136 [ cronentry timestamp (next-time-after) ] when ;
140 : next-time-after ( cronentry timestamp -- timestamp )
141 [ ?parse-cronentry ] dip 1 minutes time+ 0 >>second
142 [ (next-time-after) ] keep ;
144 : next-time ( cronentry -- timestamp )
145 now next-time-after ;
147 : next-times-after ( cronentry n timestamp -- timestamps )
148 swap [ dupd next-time-after dup ] replicate 2nip ;
150 : next-times-from-until ( cronentry from-timestamp until-timestamp -- timestamps )
151 [ dup second>> 0 = [ 1 minutes time- ] when ] dip
152 '[ dupd next-time-after dup dup _ before? ] [ ] produce 3nip ;
154 : next-times-until ( cronentry timestamp -- timestamps )
155 [ now start-of-minute ] dip next-times-from-until ;
157 : next-times ( cronentry n -- timestamps )
158 now next-times-after ;
160 : read-crontab ( -- entries )
161 read-lines harvest [ parse-cronentry ] map ;
163 : group-crons ( cronstrings from-timestamp until-timestamp -- entries )
164 '[ _ _ next-times-from-until [ timestamp>unix-time ] map ] zip-with
165 [ first2 [ 2array ] with map ] map concat
166 [ nip ] collect-key-by sort-keys ;
168 : group-crons-for-duration-from ( cronstrings duration from-timestamp -- entries )
169 tuck time+ group-crons ;
171 : group-crons-for-duration ( cronstrings duration -- entries )
172 now group-crons-for-duration-from ;
174 : crons-for-minute ( cronstrings timestamp -- entries )
175 start-of-minute dup end-of-minute group-crons ;
177 : crons-for-hour ( cronstrings timestamp -- entries )
178 start-of-hour dup end-of-hour group-crons ;
180 : crons-for-day ( cronstrings timestamp -- entries )
181 start-of-day dup end-of-day group-crons ;
183 : crons-for-week ( cronstrings timestamp -- entries )
184 start-of-week dup end-of-week group-crons ;
186 : crons-for-month ( cronstrings timestamp -- entries )
187 start-of-month dup end-of-month group-crons ;
189 : crons-for-year ( cronstrings timestamp -- entries )
190 start-of-year dup end-of-year group-crons ;
192 : crons-for-decade ( cronstrings timestamp -- entries )
193 start-of-decade dup end-of-decade group-crons ;
195 : crons-this-minute ( cronstrings -- entries ) now crons-for-minute ;
196 : crons-this-hour ( cronstrings -- entries ) now crons-for-hour ;
197 : crons-this-day ( cronstrings -- entries ) now crons-for-day ;
198 ALIAS: crons-today crons-this-day
199 : crons-yesterday ( cronstrings -- entries ) 1 days ago crons-for-day ;
200 : crons-tomorrow ( cronstrings -- entries ) 1 days hence crons-for-day ;
201 : crons-this-week ( cronstrings -- entries ) now crons-for-week ;
202 : crons-this-month ( cronstrings -- entries ) now crons-for-month ;
203 : crons-this-year ( cronstrings -- entries ) now crons-for-year ;
204 : crons-this-decade ( cronstrings -- entries ) now crons-for-decade ;
206 : keys-unix-to-rfc822 ( assoc -- assoc' )
207 [ unix-time>timestamp timestamp>rfc822 ] map-keys ;
209 : keys-rfc822-to-unix ( assoc -- assoc' )
210 [ rfc822>timestamp timestamp>unix-time ] map-keys ;
212 : grouped-crons. ( assoc -- )
213 keys-unix-to-rfc822 [ first2 [ write bl ] [ ... ] bi* ] each ;