philiprehberger-cron_kit 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -1
- data/README.md +35 -11
- data/lib/philiprehberger/cron_kit/expression.rb +32 -6
- data/lib/philiprehberger/cron_kit/parser.rb +45 -1
- data/lib/philiprehberger/cron_kit/scheduler.rb +52 -0
- data/lib/philiprehberger/cron_kit/version.rb +1 -1
- data/lib/philiprehberger/cron_kit.rb +2 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f5662dedc928394f24b8af5ab47de3d35b4a9847f5c49bc5b10795317048b791
|
|
4
|
+
data.tar.gz: cf11c8cc20f88143e0b55f22f4f2aa8e35384b7ad558ce08eed1033265621bef
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a0c427360b5546e8e732d4bf20097f8ec8c0265b3a026758282b0c069ea196bb9f7d229dd305e222c248aac60bc9df9e0872d6c069ed95b05fc7d8afec3ec47
|
|
7
|
+
data.tar.gz: 5629f8e7aed24666f3e4b657d6760a6ce1e06ca457f6f4fa1ab146554bb45d17ff64c09f9f49beae50ca0c733f5328ed59b4561390db0fc736a8d07a46c218c0
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.0] - 2026-07-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Named month tokens (`JAN`–`DEC`) and weekday tokens (`SUN`–`SAT`) in the month and day-of-week fields, case-insensitive, in single values, lists, and ranges (e.g. `MON-FRI`, `JAN,JUN,DEC`)
|
|
14
|
+
- `7` accepted as an alias for Sunday in the day-of-week field (Vixie/crontab interop)
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Day-of-month / day-of-week now follow standard (Vixie) cron OR-semantics: when both fields are restricted, a time matches when either matches (e.g. `0 0 13 * 5` fires on the 13th or any Friday); previously the fields were ANDed, causing under-firing. Applies to `#match?`, `#next_at`, `#next_runs`, and `#previous_run`
|
|
18
|
+
|
|
19
|
+
## [0.8.1] - 2026-06-14
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Added package card image to README
|
|
23
|
+
- Added YARD doc comments to Scheduler public methods
|
|
24
|
+
- Made job-name examples consistent in README
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Replaced bare RuntimeError raises in Expression parser with Error class
|
|
28
|
+
- Scheduler#every now validates a block is provided at registration time
|
|
29
|
+
|
|
10
30
|
## [0.8.0] - 2026-05-13
|
|
11
31
|
|
|
12
32
|
### Added
|
|
@@ -154,7 +174,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
154
174
|
- `Philiprehberger::CronKit.parse` convenience method
|
|
155
175
|
- `Philiprehberger::CronKit.new` convenience method for creating a Scheduler
|
|
156
176
|
|
|
157
|
-
[Unreleased]: https://github.com/philiprehberger/rb-cron-kit/compare/v0.
|
|
177
|
+
[Unreleased]: https://github.com/philiprehberger/rb-cron-kit/compare/v0.9.0...HEAD
|
|
178
|
+
[0.9.0]: https://github.com/philiprehberger/rb-cron-kit/compare/v0.8.1...v0.9.0
|
|
179
|
+
[0.8.1]: https://github.com/philiprehberger/rb-cron-kit/compare/v0.8.0...v0.8.1
|
|
158
180
|
[0.8.0]: https://github.com/philiprehberger/rb-cron-kit/compare/v0.7.0...v0.8.0
|
|
159
181
|
[0.7.0]: https://github.com/philiprehberger/rb-cron-kit/compare/v0.6.0...v0.7.0
|
|
160
182
|
[0.6.0]: https://github.com/philiprehberger/rb-cron-kit/compare/v0.5.1...v0.6.0
|
data/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
[](https://rubygems.org/gems/philiprehberger-cron_kit)
|
|
5
5
|
[](https://github.com/philiprehberger/rb-cron-kit/commits/main)
|
|
6
6
|
|
|
7
|
+

|
|
8
|
+
|
|
7
9
|
Cron expression parser and scheduler
|
|
8
10
|
|
|
9
11
|
## Requirements
|
|
@@ -183,14 +185,14 @@ scheduler.running_jobs # => 2
|
|
|
183
185
|
```ruby
|
|
184
186
|
scheduler = Philiprehberger::CronKit.new
|
|
185
187
|
|
|
186
|
-
scheduler.every("0 9 * * 1-5", name: "
|
|
188
|
+
scheduler.every("0 9 * * 1-5", name: "morning_report") do
|
|
187
189
|
generate_report
|
|
188
190
|
end
|
|
189
191
|
|
|
190
|
-
scheduler.job_names # => ["
|
|
191
|
-
scheduler.job?("
|
|
192
|
-
scheduler.job?("
|
|
193
|
-
scheduler.remove("
|
|
192
|
+
scheduler.job_names # => ["morning_report"]
|
|
193
|
+
scheduler.job?("morning_report") # => true
|
|
194
|
+
scheduler.job?("does_not_exist") # => false
|
|
195
|
+
scheduler.remove("morning_report")
|
|
194
196
|
```
|
|
195
197
|
|
|
196
198
|
### Manual Trigger
|
|
@@ -199,12 +201,12 @@ Trigger a registered job by name without restarting the scheduler — useful for
|
|
|
199
201
|
or operator-driven re-runs.
|
|
200
202
|
|
|
201
203
|
```ruby
|
|
202
|
-
scheduler.every("0 9 * * 1-5", name:
|
|
204
|
+
scheduler.every("0 9 * * 1-5", name: "morning_report", timeout: 30) do
|
|
203
205
|
generate_report
|
|
204
206
|
end
|
|
205
207
|
|
|
206
|
-
scheduler.run_now(
|
|
207
|
-
scheduler.run_now(
|
|
208
|
+
scheduler.run_now("morning_report") # => return value of the block
|
|
209
|
+
scheduler.run_now("missing") # raises KeyError
|
|
208
210
|
```
|
|
209
211
|
|
|
210
212
|
`run_now` honors `timeout:` (raises `Timeout::Error`) and respects `overlap: false`
|
|
@@ -214,7 +216,28 @@ scheduler.run_now(:missing) # raises KeyError
|
|
|
214
216
|
|
|
215
217
|
```ruby
|
|
216
218
|
scheduler.next_runs(from: Time.now)
|
|
217
|
-
# => { "
|
|
219
|
+
# => { "morning_report" => 2026-03-13 09:00:00 ... }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Named Tokens and Sunday Interop
|
|
223
|
+
|
|
224
|
+
Three-letter month names (`JAN`–`DEC`) and weekday names (`SUN`–`SAT`) are accepted in the month and day-of-week fields, case-insensitive, in single values, lists, and ranges. In the day-of-week field, `7` is also accepted as an alias for Sunday (`0`), matching Vixie/crontab.
|
|
225
|
+
|
|
226
|
+
```ruby
|
|
227
|
+
Philiprehberger::CronKit.parse("0 9 * * MON-FRI") # weekdays at 09:00
|
|
228
|
+
Philiprehberger::CronKit.parse("0 0 1 JAN,JUN,DEC *") # 1st of Jan, Jun, Dec
|
|
229
|
+
Philiprehberger::CronKit.parse("* * * * 7") # every minute on Sunday (7 == 0)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### Day-of-Month / Day-of-Week Matching
|
|
233
|
+
|
|
234
|
+
Following standard (Vixie) cron, when **both** the day-of-month and day-of-week fields are restricted (neither is `*`), a time matches when **either** field matches. When only one is restricted, only that field must match; when both are `*`, the day is unconstrained.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# Fires on the 13th OR any Friday
|
|
238
|
+
expr = Philiprehberger::CronKit.parse("0 0 13 * 5")
|
|
239
|
+
expr.match?(Time.new(2026, 3, 6)) # => true (Friday, not the 13th)
|
|
240
|
+
expr.match?(Time.new(2026, 4, 13)) # => true (13th, not a Friday)
|
|
218
241
|
```
|
|
219
242
|
|
|
220
243
|
### Supported Syntax
|
|
@@ -226,6 +249,7 @@ scheduler.next_runs(from: Time.now)
|
|
|
226
249
|
| Range | `1-5` | Values from 1 through 5 |
|
|
227
250
|
| Step | `*/5` | Every 5th value |
|
|
228
251
|
| List | `1,3,5` | Values 1, 3, and 5 |
|
|
252
|
+
| Name | `MON-FRI` | Month/weekday names |
|
|
229
253
|
| Alias | `@daily` | Non-standard shorthand |
|
|
230
254
|
|
|
231
255
|
### Fields
|
|
@@ -235,8 +259,8 @@ scheduler.next_runs(from: Time.now)
|
|
|
235
259
|
| 1 | Minute | 0-59 |
|
|
236
260
|
| 2 | Hour | 0-23 |
|
|
237
261
|
| 3 | Day of month | 1-31 |
|
|
238
|
-
| 4 | Month | 1-12
|
|
239
|
-
| 5 | Day of week | 0-6
|
|
262
|
+
| 4 | Month | 1-12 (or `JAN`-`DEC`) |
|
|
263
|
+
| 5 | Day of week | 0-6 (or `SUN`-`SAT`; `7` = Sunday) |
|
|
240
264
|
|
|
241
265
|
## API
|
|
242
266
|
|
|
@@ -30,12 +30,14 @@ module Philiprehberger
|
|
|
30
30
|
@timezone = timezone
|
|
31
31
|
@utc_offset = Timezone.utc_offset_for(timezone)
|
|
32
32
|
@fields = parse_expression(@raw)
|
|
33
|
+
parts = @raw.split(/\s+/)
|
|
34
|
+
@dom_restricted = parts[2] != '*'
|
|
35
|
+
@dow_restricted = parts[4] != '*'
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
def match?(time)
|
|
36
39
|
time = coerce_time(time)
|
|
37
|
-
|
|
38
|
-
@fields.each_with_index.all? { |set, i| set.include?(values[i]) }
|
|
40
|
+
values_match?([time.min, time.hour, time.day, time.month, time.wday])
|
|
39
41
|
end
|
|
40
42
|
|
|
41
43
|
# Return true if any Time in the given enumerable matches the expression.
|
|
@@ -95,7 +97,7 @@ module Philiprehberger
|
|
|
95
97
|
|
|
96
98
|
time += 60
|
|
97
99
|
end
|
|
98
|
-
raise 'no matching time found within 4 years'
|
|
100
|
+
raise Error, 'no matching time found within 4 years'
|
|
99
101
|
end
|
|
100
102
|
|
|
101
103
|
def scan_backward(time)
|
|
@@ -104,12 +106,36 @@ module Philiprehberger
|
|
|
104
106
|
|
|
105
107
|
time -= 60
|
|
106
108
|
end
|
|
107
|
-
raise 'no matching time found within 4 years'
|
|
109
|
+
raise Error, 'no matching time found within 4 years'
|
|
108
110
|
end
|
|
109
111
|
|
|
110
112
|
def field_match?(time)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
values_match?([time.min, time.hour, time.day, time.month, time.wday])
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Minute, hour, and month must always match. Standard (Vixie) cron ORs the
|
|
117
|
+
# day-of-month and day-of-week fields when both are restricted, so
|
|
118
|
+
# `0 0 13 * 5` fires on the 13th OR any Friday.
|
|
119
|
+
def values_match?(values)
|
|
120
|
+
return false unless @fields[0].include?(values[0]) # minute
|
|
121
|
+
return false unless @fields[1].include?(values[1]) # hour
|
|
122
|
+
return false unless @fields[3].include?(values[3]) # month
|
|
123
|
+
|
|
124
|
+
day_of_week_match?(values[2], values[4])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def day_of_week_match?(day, wday)
|
|
128
|
+
dom = @fields[2].include?(day)
|
|
129
|
+
dow = @fields[4].include?(wday)
|
|
130
|
+
if @dom_restricted && @dow_restricted
|
|
131
|
+
dom || dow
|
|
132
|
+
elsif @dom_restricted
|
|
133
|
+
dom
|
|
134
|
+
elsif @dow_restricted
|
|
135
|
+
dow
|
|
136
|
+
else
|
|
137
|
+
true
|
|
138
|
+
end
|
|
113
139
|
end
|
|
114
140
|
end
|
|
115
141
|
end
|
|
@@ -15,6 +15,20 @@ module Philiprehberger
|
|
|
15
15
|
|
|
16
16
|
FIELD_NAMES = %w[minute hour day-of-month month day-of-week].freeze
|
|
17
17
|
|
|
18
|
+
# Three-letter month names (case-insensitive) mapped to their numeric value.
|
|
19
|
+
MONTH_NAMES = {
|
|
20
|
+
'JAN' => 1, 'FEB' => 2, 'MAR' => 3, 'APR' => 4, 'MAY' => 5, 'JUN' => 6,
|
|
21
|
+
'JUL' => 7, 'AUG' => 8, 'SEP' => 9, 'OCT' => 10, 'NOV' => 11, 'DEC' => 12
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# Three-letter weekday names (case-insensitive) mapped to their numeric value.
|
|
25
|
+
DAY_NAMES = {
|
|
26
|
+
'SUN' => 0, 'MON' => 1, 'TUE' => 2, 'WED' => 3, 'THU' => 4, 'FRI' => 5, 'SAT' => 6
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
# Sunday may be written as 7 in the day-of-week field (Vixie cron interop).
|
|
30
|
+
DOW_SUNDAY_ALIAS = 7
|
|
31
|
+
|
|
18
32
|
def parse_expression(expression)
|
|
19
33
|
parts = expression.split(/\s+/)
|
|
20
34
|
|
|
@@ -28,12 +42,42 @@ module Philiprehberger
|
|
|
28
42
|
private
|
|
29
43
|
|
|
30
44
|
def parse_field(field, range, name)
|
|
31
|
-
|
|
45
|
+
normalized = normalize_names(field, name)
|
|
46
|
+
parse_range = parse_range_for(name, range)
|
|
47
|
+
values = normalized.split(',').flat_map { |token| parse_token(token, parse_range, name) }
|
|
32
48
|
raise ParseError, "empty field for #{name}" if values.empty?
|
|
33
49
|
|
|
50
|
+
values = values.map { |value| value == DOW_SUNDAY_ALIAS ? 0 : value } if name == 'day-of-week'
|
|
34
51
|
values.uniq.sort
|
|
35
52
|
end
|
|
36
53
|
|
|
54
|
+
# Replace three-letter month/weekday names (case-insensitive) with their
|
|
55
|
+
# numeric value before numeric parsing. Names are supported in single
|
|
56
|
+
# values, lists, and ranges (e.g. MON-FRI, JAN,JUN,DEC).
|
|
57
|
+
def normalize_names(field, name)
|
|
58
|
+
table = name_table_for(name)
|
|
59
|
+
return field unless table
|
|
60
|
+
|
|
61
|
+
field.gsub(/[A-Za-z]+/) do |word|
|
|
62
|
+
table.fetch(word.upcase) do
|
|
63
|
+
raise ParseError, "invalid name #{word.inspect} in #{name} field"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def name_table_for(name)
|
|
69
|
+
case name
|
|
70
|
+
when 'month' then MONTH_NAMES
|
|
71
|
+
when 'day-of-week' then DAY_NAMES
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The day-of-week field accepts 7 as an alias for Sunday, so parsing must
|
|
76
|
+
# tolerate 7 (mapped to 0 afterwards) even though the field range is 0..6.
|
|
77
|
+
def parse_range_for(name, range)
|
|
78
|
+
name == 'day-of-week' ? (range.min..DOW_SUNDAY_ALIAS) : range
|
|
79
|
+
end
|
|
80
|
+
|
|
37
81
|
def parse_token(token, range, name)
|
|
38
82
|
try_wildcard(token, range) ||
|
|
39
83
|
try_step(token, range, name) ||
|
|
@@ -18,17 +18,43 @@ module Philiprehberger
|
|
|
18
18
|
@job_threads = {}
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Register a callback invoked when a job raises an error.
|
|
22
|
+
#
|
|
23
|
+
# Called with the failing +Job+ and the +StandardError+ raised. With no
|
|
24
|
+
# block, returns the currently registered callback (or +nil+).
|
|
25
|
+
#
|
|
26
|
+
# @yieldparam job [Job] the job that failed
|
|
27
|
+
# @yieldparam error [StandardError] the error raised by the job's block
|
|
28
|
+
# @return [Proc, nil] the registered callback when called without a block
|
|
21
29
|
def on_error(&block)
|
|
22
30
|
return @on_error unless block
|
|
23
31
|
|
|
24
32
|
@on_error = block
|
|
25
33
|
end
|
|
26
34
|
|
|
35
|
+
# Number of currently executing job threads.
|
|
36
|
+
#
|
|
37
|
+
# @return [Integer] count of live job threads
|
|
27
38
|
def running_jobs
|
|
28
39
|
@mutex.synchronize { @running_threads.count(&:alive?) }
|
|
29
40
|
end
|
|
30
41
|
|
|
42
|
+
# Register a cron job to be executed when +expression+ matches.
|
|
43
|
+
#
|
|
44
|
+
# Accepts either a cron expression string or a pre-built +Expression+.
|
|
45
|
+
# Raises +ArgumentError+ immediately if no block is provided, surfacing
|
|
46
|
+
# configuration mistakes at registration time rather than at tick time.
|
|
47
|
+
#
|
|
48
|
+
# @param expression [String, Expression] cron expression or pre-built Expression
|
|
49
|
+
# @param name [Symbol, String, nil] optional job name for lookup/removal/triggering
|
|
50
|
+
# @param timeout [Numeric, nil] kill the job if it exceeds this many seconds
|
|
51
|
+
# @param overlap [Boolean] when +false+, skip the tick if a previous run is still active
|
|
52
|
+
# @yieldparam time [Time] the tick time the job fires at
|
|
53
|
+
# @return [self]
|
|
54
|
+
# @raise [ArgumentError] if no block is provided
|
|
31
55
|
def every(expression, name: nil, timeout: nil, overlap: true, &block)
|
|
56
|
+
raise ArgumentError, 'block required' unless block
|
|
57
|
+
|
|
32
58
|
expr = expression.is_a?(Expression) ? expression : Expression.new(expression)
|
|
33
59
|
|
|
34
60
|
@mutex.synchronize do
|
|
@@ -38,6 +64,11 @@ module Philiprehberger
|
|
|
38
64
|
self
|
|
39
65
|
end
|
|
40
66
|
|
|
67
|
+
# Names of all registered named jobs.
|
|
68
|
+
#
|
|
69
|
+
# Anonymous jobs (registered without +name:+) are omitted.
|
|
70
|
+
#
|
|
71
|
+
# @return [Array<Symbol, String>] list of registered job names
|
|
41
72
|
def job_names
|
|
42
73
|
@mutex.synchronize { @jobs.map(&:name).compact }
|
|
43
74
|
end
|
|
@@ -54,6 +85,10 @@ module Philiprehberger
|
|
|
54
85
|
@mutex.synchronize { @jobs.any? { |j| j.name == name } }
|
|
55
86
|
end
|
|
56
87
|
|
|
88
|
+
# Remove a registered job by name.
|
|
89
|
+
#
|
|
90
|
+
# @param name [Symbol, String] the job name to remove
|
|
91
|
+
# @return [Boolean] +true+ if a matching job was removed, +false+ otherwise
|
|
57
92
|
def remove(name)
|
|
58
93
|
@mutex.synchronize do
|
|
59
94
|
initial_size = @jobs.size
|
|
@@ -80,6 +115,12 @@ module Philiprehberger
|
|
|
80
115
|
execute_now(job)
|
|
81
116
|
end
|
|
82
117
|
|
|
118
|
+
# Upcoming run times for every named job.
|
|
119
|
+
#
|
|
120
|
+
# Anonymous jobs are omitted.
|
|
121
|
+
#
|
|
122
|
+
# @param from [Time] starting point for the lookahead (defaults to +Time.now+)
|
|
123
|
+
# @return [Hash{Symbol, String => Time}] map of job name to the next matching time
|
|
83
124
|
def next_runs(from: Time.now)
|
|
84
125
|
jobs = @mutex.synchronize { @jobs.dup }
|
|
85
126
|
|
|
@@ -90,6 +131,11 @@ module Philiprehberger
|
|
|
90
131
|
end
|
|
91
132
|
end
|
|
92
133
|
|
|
134
|
+
# Start the scheduler loop in a background thread.
|
|
135
|
+
#
|
|
136
|
+
# Idempotent — calling +start+ on an already-running scheduler is a no-op.
|
|
137
|
+
#
|
|
138
|
+
# @return [self]
|
|
93
139
|
def start
|
|
94
140
|
@mutex.synchronize do
|
|
95
141
|
return self if @running
|
|
@@ -103,6 +149,9 @@ module Philiprehberger
|
|
|
103
149
|
self
|
|
104
150
|
end
|
|
105
151
|
|
|
152
|
+
# Stop the scheduler loop and wait briefly for the background thread to exit.
|
|
153
|
+
#
|
|
154
|
+
# @return [self]
|
|
106
155
|
def stop
|
|
107
156
|
@mutex.synchronize { @running = false }
|
|
108
157
|
@thread&.join(5)
|
|
@@ -111,6 +160,9 @@ module Philiprehberger
|
|
|
111
160
|
self
|
|
112
161
|
end
|
|
113
162
|
|
|
163
|
+
# Whether the scheduler loop is currently running.
|
|
164
|
+
#
|
|
165
|
+
# @return [Boolean]
|
|
114
166
|
def running?
|
|
115
167
|
@mutex.synchronize { @running }
|
|
116
168
|
end
|
|
@@ -10,6 +10,8 @@ require_relative 'cron_kit/scheduler'
|
|
|
10
10
|
|
|
11
11
|
module Philiprehberger
|
|
12
12
|
module CronKit
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
|
|
13
15
|
# Parse a 5-field cron expression and return an Expression instance.
|
|
14
16
|
# Supports non-standard aliases (@hourly, @daily, etc.) and optional timezone.
|
|
15
17
|
def self.parse(expression, timezone: nil)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-cron_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-16 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: A zero-dependency Ruby gem for parsing 5-field cron expressions and running
|
|
14
14
|
an in-process scheduler. Supports wildcards, ranges, steps, and lists.
|