hron 1.0.0 → 2.0.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/README.md +126 -21
- data/hron.gemspec +7 -11
- data/lib/hron/ast.rb +28 -54
- data/lib/hron/cron.rb +369 -652
- data/lib/hron/display.rb +6 -4
- data/lib/hron/error.rb +54 -17
- data/lib/hron/eval/calendar.rb +120 -0
- data/lib/hron/eval/wall_clock.rb +65 -0
- data/lib/hron/evaluator.rb +339 -1010
- data/lib/hron/lexer.rb +84 -94
- data/lib/hron/parser.rb +369 -502
- data/lib/hron/parts.rb +257 -0
- data/lib/hron/schedule.rb +81 -19
- data/lib/hron/version.rb +1 -1
- data/lib/hron.rb +13 -3
- metadata +14 -53
data/lib/hron/parts.rb
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
require_relative "ast"
|
|
5
|
+
require_relative "error"
|
|
6
|
+
require_relative "display"
|
|
7
|
+
require_relative "parser"
|
|
8
|
+
|
|
9
|
+
module Hron
|
|
10
|
+
# The checks of spec/README.md, "Schedules built in code", in its order. A part of the wrong
|
|
11
|
+
# Ruby type is a TypeError, a usage error rather than a HronError. Names are Symbols, so a
|
|
12
|
+
# name of another type is a TypeError too; the AST's unions (expression, day filter, ...)
|
|
13
|
+
# have no Ruby type of their own, so any value outside one is an unknown value of its kind.
|
|
14
|
+
# checked builds every part anew as its Hron class, so a part of a subclass gives a schedule
|
|
15
|
+
# equal to the parsed one.
|
|
16
|
+
module Parts
|
|
17
|
+
INTERVAL_MAX = 2_147_483_647
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
def checked(data)
|
|
22
|
+
raise TypeError, "data must be a Hron::ScheduleData, not #{data.class}" unless data.is_a?(ScheduleData)
|
|
23
|
+
|
|
24
|
+
expr = check_expression(data.expression)
|
|
25
|
+
except = list(data.except, "except").map { |exception| check_exception(exception) }
|
|
26
|
+
until_spec = data.until.nil? ? nil : check_until(data.until)
|
|
27
|
+
starting = data.starting.nil? ? nil : check_iso_date(string(data.starting, "starting"))
|
|
28
|
+
during = list(data.during, "during").map { |month| check_name(month, MonthName::ALL, "month") }
|
|
29
|
+
timezone = data.timezone.nil? ? nil : check_timezone(string(data.timezone, "timezone"))
|
|
30
|
+
if until_spec.is_a?(NamedUntil) && starting.nil?
|
|
31
|
+
raise error("until #{until_spec.month} #{until_spec.day} has no year: add a starting date, or use an ISO date")
|
|
32
|
+
end
|
|
33
|
+
ScheduleData.new(expression: expr, timezone:, except:, until: until_spec, starting:, during:)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def check_expression(expr)
|
|
37
|
+
case expr
|
|
38
|
+
when IntervalRepeat
|
|
39
|
+
interval = check_interval(expr.interval)
|
|
40
|
+
unit = check_name(expr.unit, IntervalUnit::ALL, "interval unit")
|
|
41
|
+
from = check_time(expr.from_time)
|
|
42
|
+
to = check_time(expr.to_time)
|
|
43
|
+
if minute_of_day(from) > minute_of_day(to)
|
|
44
|
+
raise error("time window must not run backwards: #{from} to #{to} (a window cannot cross midnight)")
|
|
45
|
+
end
|
|
46
|
+
day_filter = expr.day_filter.nil? ? nil : check_day_filter(expr.day_filter)
|
|
47
|
+
IntervalRepeat.new(interval, unit, from, to, day_filter)
|
|
48
|
+
when DayRepeat
|
|
49
|
+
interval = check_interval(expr.interval)
|
|
50
|
+
# `every 2 days` has no place for a day filter, so other days would not survive display.
|
|
51
|
+
raise error("days must be every day when the interval is above 1") if interval > 1 && !expr.days.is_a?(DayFilterEvery)
|
|
52
|
+
|
|
53
|
+
DayRepeat.new(interval, check_day_filter(expr.days), check_times(expr.times))
|
|
54
|
+
when WeekRepeat
|
|
55
|
+
interval = check_interval(expr.interval)
|
|
56
|
+
WeekRepeat.new(interval, check_weekdays(expr.days), check_times(expr.times))
|
|
57
|
+
when MonthRepeat
|
|
58
|
+
interval = check_interval(expr.interval)
|
|
59
|
+
MonthRepeat.new(interval, check_month_target(expr.target), check_times(expr.times))
|
|
60
|
+
when SingleDateExpr
|
|
61
|
+
SingleDateExpr.new(check_date(expr.date), check_times(expr.times))
|
|
62
|
+
when YearRepeat
|
|
63
|
+
interval = check_interval(expr.interval)
|
|
64
|
+
YearRepeat.new(interval, check_year_target(expr.target), check_times(expr.times))
|
|
65
|
+
else
|
|
66
|
+
raise unknown("expression", expr)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def check_interval(interval)
|
|
71
|
+
integer(interval, "interval")
|
|
72
|
+
raise error("interval must be 1-#{INTERVAL_MAX}, got #{interval}") unless interval.between?(1, INTERVAL_MAX)
|
|
73
|
+
|
|
74
|
+
interval
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def check_times(times)
|
|
78
|
+
raise error("times must not be empty") if list(times, "times").empty?
|
|
79
|
+
|
|
80
|
+
times.map { |time| check_time(time) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def check_time(time)
|
|
84
|
+
raise TypeError, "a time must be a Hron::TimeOfDay, not #{time.class}" unless time.is_a?(TimeOfDay)
|
|
85
|
+
|
|
86
|
+
integer(time.hour, "hour")
|
|
87
|
+
integer(time.minute, "minute")
|
|
88
|
+
raise error("time must be 00:00-23:59, got #{time}") unless time.hour.between?(0, 23) && time.minute.between?(0, 59)
|
|
89
|
+
|
|
90
|
+
TimeOfDay.new(time.hour, time.minute)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def minute_of_day(time)
|
|
94
|
+
(time.hour * 60) + time.minute
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def check_day_filter(filter)
|
|
98
|
+
case filter
|
|
99
|
+
when DayFilterEvery then DayFilterEvery.new
|
|
100
|
+
when DayFilterWeekday then DayFilterWeekday.new
|
|
101
|
+
when DayFilterWeekend then DayFilterWeekend.new
|
|
102
|
+
when DayFilterDays then DayFilterDays.new(check_weekdays(filter.days))
|
|
103
|
+
else raise unknown("day filter", filter)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def check_weekdays(days)
|
|
108
|
+
raise error("days must not be empty") if list(days, "days").empty?
|
|
109
|
+
|
|
110
|
+
days.map { |day| check_name(day, Weekday::ALL, "weekday") }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def check_month_target(target)
|
|
114
|
+
case target
|
|
115
|
+
when DaysTarget
|
|
116
|
+
raise error("days must not be empty") if list(target.specs, "days").empty?
|
|
117
|
+
|
|
118
|
+
DaysTarget.new(target.specs.map { |spec| check_day_spec(spec) })
|
|
119
|
+
when LastDayTarget then LastDayTarget.new
|
|
120
|
+
when LastWeekdayTarget then LastWeekdayTarget.new
|
|
121
|
+
when NearestWeekdayTarget
|
|
122
|
+
direction = target.direction.nil? ? nil : check_name(target.direction, NearestDirection::ALL, "direction")
|
|
123
|
+
check_day(target.day, suffixed: true)
|
|
124
|
+
NearestWeekdayTarget.new(target.day, direction)
|
|
125
|
+
when OrdinalWeekdayTarget
|
|
126
|
+
ordinal = check_name(target.ordinal, OrdinalPosition::ALL, "ordinal")
|
|
127
|
+
OrdinalWeekdayTarget.new(ordinal, check_name(target.weekday, Weekday::ALL, "weekday"))
|
|
128
|
+
else
|
|
129
|
+
raise unknown("month target", target)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def check_day_spec(spec)
|
|
134
|
+
case spec
|
|
135
|
+
when SingleDay
|
|
136
|
+
check_day(spec.day, suffixed: true)
|
|
137
|
+
SingleDay.new(spec.day)
|
|
138
|
+
when DayRange
|
|
139
|
+
start = check_day(spec.start, suffixed: true)
|
|
140
|
+
last = check_day(spec.end_day, suffixed: true)
|
|
141
|
+
raise error("day range must not run backwards: #{start} to #{last}") if spec.start > spec.end_day
|
|
142
|
+
|
|
143
|
+
DayRange.new(spec.start, spec.end_day)
|
|
144
|
+
else
|
|
145
|
+
raise unknown("day spec", spec)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def check_year_target(target)
|
|
150
|
+
case target
|
|
151
|
+
when YearDateTarget then YearDateTarget.new(*check_named_date(target.month, target.day))
|
|
152
|
+
when YearOrdinalWeekdayTarget
|
|
153
|
+
ordinal = check_name(target.ordinal, OrdinalPosition::ALL, "ordinal")
|
|
154
|
+
weekday = check_name(target.weekday, Weekday::ALL, "weekday")
|
|
155
|
+
YearOrdinalWeekdayTarget.new(ordinal, weekday, check_name(target.month, MonthName::ALL, "month"))
|
|
156
|
+
when YearDayOfMonthTarget
|
|
157
|
+
month = check_name(target.month, MonthName::ALL, "month")
|
|
158
|
+
shown = check_day(target.day, suffixed: true)
|
|
159
|
+
check_day_in_month(target.day, month, shown)
|
|
160
|
+
YearDayOfMonthTarget.new(target.day, month)
|
|
161
|
+
when YearLastWeekdayTarget then YearLastWeekdayTarget.new(check_name(target.month, MonthName::ALL, "month"))
|
|
162
|
+
else raise unknown("year target", target)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def check_date(date)
|
|
167
|
+
case date
|
|
168
|
+
when NamedDate then NamedDate.new(*check_named_date(date.month, date.day))
|
|
169
|
+
when IsoDate then IsoDate.new(check_iso_date(string(date.date, "date")))
|
|
170
|
+
else raise unknown("date", date)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def check_exception(exception)
|
|
175
|
+
case exception
|
|
176
|
+
when NamedException then NamedException.new(*check_named_date(exception.month, exception.day))
|
|
177
|
+
when IsoException then IsoException.new(check_iso_date(string(exception.date, "date")))
|
|
178
|
+
else raise unknown("exception", exception)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def check_until(until_spec)
|
|
183
|
+
case until_spec
|
|
184
|
+
when NamedUntil then NamedUntil.new(*check_named_date(until_spec.month, until_spec.day))
|
|
185
|
+
when IsoUntil then IsoUntil.new(check_iso_date(string(until_spec.date, "date")))
|
|
186
|
+
else raise unknown("until", until_spec)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def check_named_date(month, day)
|
|
191
|
+
check_name(month, MonthName::ALL, "month")
|
|
192
|
+
shown = check_day(day, suffixed: false)
|
|
193
|
+
check_day_in_month(day, month, shown)
|
|
194
|
+
[month, day]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def check_day(day, suffixed:)
|
|
198
|
+
integer(day, "day")
|
|
199
|
+
shown = suffixed ? "#{day}#{Display.ordinal_suffix(day)}" : day.to_s
|
|
200
|
+
raise error("day must be 1-31, got #{shown}") unless day.between?(1, 31)
|
|
201
|
+
|
|
202
|
+
shown
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def check_day_in_month(day, month, shown)
|
|
206
|
+
max = MonthName.max_day(month)
|
|
207
|
+
raise error("day must be 1-#{max} for #{month}, got #{shown}") if day > max
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Date.iso8601 alone would also read `20260206`, `+002026-02-06` and fullwidth digits. The
|
|
211
|
+
# encoding must be ASCII-compatible, or ten bytes that read as a date are other characters.
|
|
212
|
+
def check_iso_date(date)
|
|
213
|
+
if date.encoding.ascii_compatible? && date.b.match?(/\A\d{4}-\d{2}-\d{2}\z/)
|
|
214
|
+
year, month, day = date.b.split("-").map(&:to_i)
|
|
215
|
+
return date if year >= 1 && Date.valid_date?(year, month, day, Date::GREGORIAN)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
raise error("date must be a calendar date from 0001-01-01 to 9999-12-31, got #{Utf8.convert(date)}")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def check_timezone(name)
|
|
222
|
+
Parser.iana_timezone(name) or
|
|
223
|
+
raise error("timezone must be UTC or an Area/Location name such as America/New_York, got #{Utf8.convert(name)}")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def check_name(value, names, kind)
|
|
227
|
+
raise TypeError, "#{kind} must be a Symbol, not #{value.class}" unless value.is_a?(Symbol)
|
|
228
|
+
raise unknown(kind, value) unless names.include?(value)
|
|
229
|
+
|
|
230
|
+
value
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def integer(value, what)
|
|
234
|
+
raise TypeError, "#{what} must be an Integer, not #{value.class}" unless value.is_a?(Integer)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def string(value, what)
|
|
238
|
+
raise TypeError, "#{what} must be a String, not #{value.class}" unless value.is_a?(String)
|
|
239
|
+
|
|
240
|
+
String.new(value)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def list(value, what)
|
|
244
|
+
raise TypeError, "#{what} must be an Array, not #{value.class}" unless value.is_a?(Array)
|
|
245
|
+
|
|
246
|
+
value
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def unknown(kind, value)
|
|
250
|
+
error("unknown #{kind} #{value.inspect}")
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def error(message)
|
|
254
|
+
HronError.eval(message)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
data/lib/hron/schedule.rb
CHANGED
|
@@ -4,65 +4,96 @@ require_relative "parser"
|
|
|
4
4
|
require_relative "evaluator"
|
|
5
5
|
require_relative "display"
|
|
6
6
|
require_relative "cron"
|
|
7
|
+
require_relative "parts"
|
|
7
8
|
|
|
8
9
|
module Hron
|
|
9
|
-
#
|
|
10
|
+
# Each method that takes a time takes a Time in any zone and reads only its instant, and
|
|
11
|
+
# raises TypeError for anything else. Each Time returned is in the schedule's timezone, with
|
|
12
|
+
# that TZInfo::Timezone as its zone, or in UTC when the schedule has none.
|
|
10
13
|
class Schedule
|
|
14
|
+
# The frozen ScheduleData this schedule was built from, with the timezone in its IANA
|
|
15
|
+
# capitalization.
|
|
11
16
|
attr_reader :data
|
|
12
17
|
|
|
18
|
+
# Builds a schedule from a ScheduleData, checked by the rules parse applies. Raises HronError
|
|
19
|
+
# of kind :eval for the first part that breaks one, in the order of spec/README.md,
|
|
20
|
+
# "Schedules built in code", and TypeError for a part of the wrong type. Keeps a frozen
|
|
21
|
+
# copy, so later changes to data's lists and strings do not change the schedule.
|
|
13
22
|
def initialize(data)
|
|
14
|
-
@data = data
|
|
23
|
+
@data = Ractor.make_shareable(Parts.checked(data))
|
|
15
24
|
end
|
|
16
25
|
|
|
17
|
-
#
|
|
26
|
+
# For parse and from_cron, whose parts already keep every rule and are their own.
|
|
27
|
+
def self.from_valid(data)
|
|
28
|
+
allocate.tap { |schedule| schedule.instance_variable_set(:@data, Ractor.make_shareable(data)) }
|
|
29
|
+
end
|
|
30
|
+
private_class_method :from_valid
|
|
31
|
+
|
|
32
|
+
# Raises HronError if the expression is invalid, and TypeError unless input is a String.
|
|
18
33
|
def self.parse(input)
|
|
19
|
-
|
|
34
|
+
require_string(input, "input")
|
|
35
|
+
from_valid(Parser.parse(input))
|
|
20
36
|
end
|
|
21
37
|
|
|
22
|
-
#
|
|
38
|
+
# Converts a 5-field cron expression to a Schedule that fires at the same times. Raises
|
|
39
|
+
# HronError of kind :cron when it is not valid cron or has no exact hron equivalent, and
|
|
40
|
+
# TypeError unless cron_expr is a String.
|
|
23
41
|
def self.from_cron(cron_expr)
|
|
24
|
-
|
|
42
|
+
require_string(cron_expr, "cron_expr")
|
|
43
|
+
from_valid(Cron.from_cron(cron_expr))
|
|
25
44
|
end
|
|
26
45
|
|
|
27
|
-
#
|
|
46
|
+
# True when parse accepts input, false when it raises HronError. Raises TypeError unless
|
|
47
|
+
# input is a String.
|
|
28
48
|
def self.validate(input)
|
|
29
|
-
|
|
49
|
+
require_string(input, "input")
|
|
50
|
+
Parser.parse(input)
|
|
30
51
|
true
|
|
31
52
|
rescue HronError
|
|
32
53
|
false
|
|
33
54
|
end
|
|
34
55
|
|
|
35
|
-
#
|
|
56
|
+
# A usage error, never a HronError or false (spec/README.md, "Timestamps and counts").
|
|
57
|
+
def self.require_string(value, name)
|
|
58
|
+
raise TypeError, "#{name} must be a String, not #{value.class}" unless value.is_a?(String)
|
|
59
|
+
end
|
|
60
|
+
private_class_method :require_string
|
|
61
|
+
|
|
62
|
+
# Returns the next occurrence strictly after now, or nil if there is none.
|
|
36
63
|
def next_from(now)
|
|
37
64
|
Evaluator.next_from(@data, now)
|
|
38
65
|
end
|
|
39
66
|
|
|
40
|
-
#
|
|
67
|
+
# Returns an Array of up to n occurrences strictly after now, empty when n <= 0. Raises
|
|
68
|
+
# TypeError unless n is an Integer.
|
|
41
69
|
def next_n_from(now, n)
|
|
42
70
|
Evaluator.next_n_from(@data, now, n)
|
|
43
71
|
end
|
|
44
72
|
|
|
45
|
-
#
|
|
73
|
+
# Returns the most recent occurrence strictly before now, or nil if there is none.
|
|
46
74
|
def previous_from(now)
|
|
47
75
|
Evaluator.previous_from(@data, now)
|
|
48
76
|
end
|
|
49
77
|
|
|
50
|
-
#
|
|
78
|
+
# True when the minute containing dt, on the schedule's wall clock, is an occurrence.
|
|
51
79
|
def matches(dt)
|
|
52
80
|
Evaluator.matches(@data, dt)
|
|
53
81
|
end
|
|
54
82
|
|
|
55
|
-
# Returns a lazy Enumerator of occurrences
|
|
83
|
+
# Returns a lazy Enumerator of occurrences strictly after from. Unbounded for repeating
|
|
84
|
+
# schedules unless an until clause or the end of the supported range ends them.
|
|
56
85
|
def occurrences(from)
|
|
57
86
|
Evaluator.occurrences(@data, from)
|
|
58
87
|
end
|
|
59
88
|
|
|
60
|
-
# Returns a lazy Enumerator of occurrences where from < occurrence <= to
|
|
89
|
+
# Returns a lazy Enumerator of occurrences where from < occurrence <= to, empty when either
|
|
90
|
+
# bound is outside the supported range.
|
|
61
91
|
def between(from, to)
|
|
62
92
|
Evaluator.between(@data, from, to)
|
|
63
93
|
end
|
|
64
94
|
|
|
65
|
-
#
|
|
95
|
+
# Converts to a 5-field cron expression that fires at the same times, leaving out the
|
|
96
|
+
# timezone. Raises HronError of kind :cron when no cron does.
|
|
66
97
|
def to_cron
|
|
67
98
|
Cron.to_cron(@data)
|
|
68
99
|
end
|
|
@@ -72,19 +103,50 @@ module Hron
|
|
|
72
103
|
Display.display(@data)
|
|
73
104
|
end
|
|
74
105
|
|
|
75
|
-
# Get inspect representation
|
|
76
106
|
def inspect
|
|
77
107
|
"Schedule(\"#{self}\")"
|
|
78
108
|
end
|
|
79
109
|
|
|
80
|
-
#
|
|
110
|
+
# Schedules are equal when their parts are.
|
|
111
|
+
def ==(other)
|
|
112
|
+
other.is_a?(Schedule) && data == other.data
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def eql?(other)
|
|
116
|
+
other.is_a?(Schedule) && data.eql?(other.data)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def hash
|
|
120
|
+
data.hash
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Returns the IANA timezone name with its canonical capitalization, or nil if none was given.
|
|
81
124
|
def timezone
|
|
82
125
|
@data.timezone
|
|
83
126
|
end
|
|
84
127
|
|
|
85
|
-
# Get the schedule expression
|
|
86
128
|
def expression
|
|
87
|
-
@data.
|
|
129
|
+
@data.expression
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The except dates, in the order written; empty without an except clause.
|
|
133
|
+
def except
|
|
134
|
+
@data.except
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# An IsoUntil or NamedUntil, or nil without an until clause.
|
|
138
|
+
def until
|
|
139
|
+
@data.until
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The starting date as a YYYY-MM-DD String, or nil without a starting clause.
|
|
143
|
+
def starting
|
|
144
|
+
@data.starting
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The during months as Symbols, in the order written; empty without a during clause.
|
|
148
|
+
def during
|
|
149
|
+
@data.during
|
|
88
150
|
end
|
|
89
151
|
end
|
|
90
152
|
end
|
data/lib/hron/version.rb
CHANGED
data/lib/hron.rb
CHANGED
|
@@ -8,21 +8,31 @@ require_relative "hron/parser"
|
|
|
8
8
|
require_relative "hron/evaluator"
|
|
9
9
|
require_relative "hron/display"
|
|
10
10
|
require_relative "hron/cron"
|
|
11
|
+
require_relative "hron/parts"
|
|
11
12
|
require_relative "hron/schedule"
|
|
12
13
|
|
|
13
14
|
module Hron
|
|
15
|
+
# Parser, Evaluator, Display and Cron take or return a schedule's parts without the checks
|
|
16
|
+
# of Schedule.new, Parts is those checks, and Lexer gives Parser its tokens, so only code
|
|
17
|
+
# inside Hron uses them (spec/README.md, "Schedules built in code").
|
|
18
|
+
private_constant :Lexer, :Parser, :Evaluator, :Display, :Cron, :Parts
|
|
19
|
+
|
|
14
20
|
class << self
|
|
15
|
-
#
|
|
21
|
+
# Parses a hron expression into a Schedule. Raises HronError if it is invalid, and TypeError
|
|
22
|
+
# unless input is a String.
|
|
16
23
|
def parse_schedule(input)
|
|
17
24
|
Schedule.parse(input)
|
|
18
25
|
end
|
|
19
26
|
|
|
20
|
-
#
|
|
27
|
+
# True when parse accepts input, false when it raises HronError. Raises TypeError unless
|
|
28
|
+
# input is a String.
|
|
21
29
|
def validate(input)
|
|
22
30
|
Schedule.validate(input)
|
|
23
31
|
end
|
|
24
32
|
|
|
25
|
-
#
|
|
33
|
+
# Converts a 5-field cron expression to a Schedule that fires at the same times. Raises
|
|
34
|
+
# HronError of kind :cron when it is not valid cron or has no exact hron equivalent, and
|
|
35
|
+
# TypeError unless cron_expr is a String.
|
|
26
36
|
def from_cron(cron_expr)
|
|
27
37
|
Schedule.from_cron(cron_expr)
|
|
28
38
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hron
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Prasanna Venkataraman
|
|
@@ -23,53 +23,11 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '2.0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: minitest
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '5.20'
|
|
33
|
-
type: :development
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '5.20'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: rake
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '13.0'
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '13.0'
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: standard
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '1.43'
|
|
61
|
-
type: :development
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '1.43'
|
|
68
26
|
description: hron (human-readable cron) is a scheduling expression language that is
|
|
69
|
-
designed to be easy to read, write, and understand. It
|
|
70
|
-
|
|
27
|
+
designed to be easy to read, write, and understand. It converts to and from cron
|
|
28
|
+
exactly where both can express a schedule, and expresses schedules cron cannot.
|
|
71
29
|
email:
|
|
72
|
-
-
|
|
30
|
+
- pras@simpllyf.io
|
|
73
31
|
executables: []
|
|
74
32
|
extensions: []
|
|
75
33
|
extra_rdoc_files: []
|
|
@@ -82,18 +40,21 @@ files:
|
|
|
82
40
|
- lib/hron/cron.rb
|
|
83
41
|
- lib/hron/display.rb
|
|
84
42
|
- lib/hron/error.rb
|
|
43
|
+
- lib/hron/eval/calendar.rb
|
|
44
|
+
- lib/hron/eval/wall_clock.rb
|
|
85
45
|
- lib/hron/evaluator.rb
|
|
86
46
|
- lib/hron/lexer.rb
|
|
87
47
|
- lib/hron/parser.rb
|
|
48
|
+
- lib/hron/parts.rb
|
|
88
49
|
- lib/hron/schedule.rb
|
|
89
50
|
- lib/hron/version.rb
|
|
90
|
-
homepage: https://
|
|
51
|
+
homepage: https://hron.io
|
|
91
52
|
licenses:
|
|
92
53
|
- MIT
|
|
93
54
|
metadata:
|
|
94
|
-
homepage_uri: https://
|
|
95
|
-
source_code_uri: https://github.com/
|
|
96
|
-
changelog_uri: https://github.com/
|
|
55
|
+
homepage_uri: https://hron.io
|
|
56
|
+
source_code_uri: https://github.com/simpllyf/hron
|
|
57
|
+
changelog_uri: https://github.com/simpllyf/hron/releases
|
|
97
58
|
rubygems_mfa_required: 'true'
|
|
98
59
|
rdoc_options: []
|
|
99
60
|
require_paths:
|
|
@@ -109,8 +70,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
109
70
|
- !ruby/object:Gem::Version
|
|
110
71
|
version: '0'
|
|
111
72
|
requirements: []
|
|
112
|
-
rubygems_version: 4.0.
|
|
73
|
+
rubygems_version: 4.0.20
|
|
113
74
|
specification_version: 4
|
|
114
|
-
summary: Human-readable cron —
|
|
115
|
-
|
|
75
|
+
summary: Human-readable cron — scheduling expressions that read like English and convert
|
|
76
|
+
to and from cron
|
|
116
77
|
test_files: []
|