Module: OpenLoam::Cron
- Defined in:
- lib/open_loam/cron.rb
Overview
A small, dependency-free "next run" calculator for a standard 5-field cron
expression ("min hour dom month dow") plus an "interval:N" (every N seconds)
form — same stdlib-only spirit as OpenLoam::Totp / OpenLoam::Base32. Timezone-aware;
always returns UTC. Supports *, lists (1,15), ranges (1-5), and steps
(*/15, 0-30/10), and the classic cron day-of-month / day-of-week OR quirk
(when BOTH are restricted, a match on either fires).
Constant Summary collapse
- RANGES =
{ min: 0..59, hour: 0..23, dom: 1..31, mon: 1..12, dow: 0..6 }.freeze
- MAX_HORIZON =
years to search before giving up (covers Feb-29 schedules)
4
Class Method Summary collapse
- .day_matches?(time, dom, dow, dom_restricted, dow_restricted) ⇒ Boolean
-
.next_after(expr, from:, zone: "UTC") ⇒ Object
The next run strictly after
from, evaluated inzone, returned in UTC. - .next_cron(expr, from, zone) ⇒ Object
-
.parse_field(field, range, dow: false) ⇒ Object
Parse one field into the set of matching integers.
Class Method Details
.day_matches?(time, dom, dow, dom_restricted, dow_restricted) ⇒ Boolean
58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/open_loam/cron.rb', line 58 def day_matches?(time, dom, dow, dom_restricted, dow_restricted) if dom_restricted && dow_restricted dom.include?(time.day) || dow.include?(time.wday) # OR quirk elsif dom_restricted dom.include?(time.day) elsif dow_restricted dow.include?(time.wday) else true end end |
.next_after(expr, from:, zone: "UTC") ⇒ Object
The next run strictly after from, evaluated in zone, returned in UTC.
17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/open_loam/cron.rb', line 17 def next_after(expr, from:, zone: "UTC") expr = expr.to_s.strip if expr.start_with?("interval:") seconds = expr.split(":", 2).last.to_i raise ArgumentError, "interval must be a positive number of seconds: #{expr.inspect}" unless seconds.positive? return (from + seconds).utc end next_cron(expr, from, zone) end |
.next_cron(expr, from, zone) ⇒ Object
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 |
# File 'lib/open_loam/cron.rb', line 30 def next_cron(expr, from, zone) fields = expr.split(/\s+/) raise ArgumentError, "cron must have 5 fields (min hour dom month dow): #{expr.inspect}" unless fields.size == 5 min = parse_field(fields[0], RANGES[:min]) hour = parse_field(fields[1], RANGES[:hour]) dom = parse_field(fields[2], RANGES[:dom]) mon = parse_field(fields[3], RANGES[:mon]) dow = parse_field(fields[4], RANGES[:dow], dow: true) dom_restricted = fields[2] != "*" dow_restricted = fields[4] != "*" time = from.in_time_zone(zone).change(sec: 0) + 60 limit = time + MAX_HORIZON.years while time < limit unless mon.include?(time.month) && day_matches?(time, dom, dow, dom_restricted, dow_restricted) time = (time + 1.day).change(hour: 0, min: 0) # skip the whole non-matching day next end return time.utc if hour.include?(time.hour) && min.include?(time.min) time += 60 end raise ArgumentError, "no cron match within #{MAX_HORIZON} years for #{expr.inspect}" end |
.parse_field(field, range, dow: false) ⇒ Object
Parse one field into the set of matching integers.
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 |
# File 'lib/open_loam/cron.rb', line 71 def parse_field(field, range, dow: false) values = Set.new field.to_s.split(",").each do |part| base, step = part.split("/", 2) step = step ? Integer(step) : 1 raise ArgumentError, "bad step in #{part.inspect}" unless step.positive? if base == "*" lo, hi = range.min, range.max elsif base.include?("-") lo, hi = base.split("-", 2).map { |n| Integer(n) } else lo = hi = Integer(base) end (lo..hi).step(step) do |value| value = 0 if dow && value == 7 # both 0 and 7 mean Sunday raise ArgumentError, "value #{value} out of range for #{field.inspect}" unless range.include?(value) values << value end end values end |