hron 0.5.1
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 +7 -0
- data/README.md +98 -0
- data/Rakefile +13 -0
- data/hron.gemspec +39 -0
- data/lib/hron/ast.rb +235 -0
- data/lib/hron/cron.rb +250 -0
- data/lib/hron/display.rb +166 -0
- data/lib/hron/error.rb +64 -0
- data/lib/hron/evaluator.rb +725 -0
- data/lib/hron/lexer.rb +253 -0
- data/lib/hron/parser.rb +617 -0
- data/lib/hron/schedule.rb +75 -0
- data/lib/hron/version.rb +5 -0
- data/lib/hron.rb +30 -0
- metadata +116 -0
data/lib/hron/display.rb
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ast"
|
|
4
|
+
|
|
5
|
+
module Hron
|
|
6
|
+
# Display module for converting schedules to canonical string representation
|
|
7
|
+
module Display
|
|
8
|
+
def self.display(schedule)
|
|
9
|
+
out = display_expr(schedule.expr)
|
|
10
|
+
|
|
11
|
+
unless schedule.except.empty?
|
|
12
|
+
parts = schedule.except.map do |exc|
|
|
13
|
+
case exc
|
|
14
|
+
when NamedException
|
|
15
|
+
"#{exc.month} #{exc.day}"
|
|
16
|
+
when IsoException
|
|
17
|
+
exc.date
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
out += " except #{parts.join(", ")}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
if schedule.until
|
|
24
|
+
case schedule.until
|
|
25
|
+
when IsoUntil
|
|
26
|
+
out += " until #{schedule.until.date}"
|
|
27
|
+
when NamedUntil
|
|
28
|
+
out += " until #{schedule.until.month} #{schedule.until.day}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
out += " starting #{schedule.anchor}" if schedule.anchor
|
|
33
|
+
|
|
34
|
+
out += " during #{schedule.during.join(", ")}" unless schedule.during.empty?
|
|
35
|
+
|
|
36
|
+
out += " in #{schedule.timezone}" if schedule.timezone
|
|
37
|
+
|
|
38
|
+
out
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.display_expr(expr)
|
|
42
|
+
case expr
|
|
43
|
+
when IntervalRepeat
|
|
44
|
+
out = "every #{expr.interval} #{unit_display(expr.interval, expr.unit)}"
|
|
45
|
+
out += " from #{expr.from_time} to #{expr.to_time}"
|
|
46
|
+
out += " on #{display_day_filter(expr.day_filter)}" if expr.day_filter
|
|
47
|
+
out
|
|
48
|
+
|
|
49
|
+
when DayRepeat
|
|
50
|
+
if expr.interval > 1
|
|
51
|
+
"every #{expr.interval} days at #{format_time_list(expr.times)}"
|
|
52
|
+
else
|
|
53
|
+
"every #{display_day_filter(expr.days)} at #{format_time_list(expr.times)}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
when WeekRepeat
|
|
57
|
+
day_str = expr.days.join(", ")
|
|
58
|
+
"every #{expr.interval} weeks on #{day_str} at #{format_time_list(expr.times)}"
|
|
59
|
+
|
|
60
|
+
when MonthRepeat
|
|
61
|
+
target_str = case expr.target
|
|
62
|
+
when DaysTarget
|
|
63
|
+
format_ordinal_day_specs(expr.target.specs)
|
|
64
|
+
when LastDayTarget
|
|
65
|
+
"last day"
|
|
66
|
+
else
|
|
67
|
+
"last weekday"
|
|
68
|
+
end
|
|
69
|
+
if expr.interval > 1
|
|
70
|
+
"every #{expr.interval} months on the #{target_str} at #{format_time_list(expr.times)}"
|
|
71
|
+
else
|
|
72
|
+
"every month on the #{target_str} at #{format_time_list(expr.times)}"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
when OrdinalRepeat
|
|
76
|
+
if expr.interval > 1
|
|
77
|
+
"#{expr.ordinal} #{expr.day} of every #{expr.interval} months at #{format_time_list(expr.times)}"
|
|
78
|
+
else
|
|
79
|
+
"#{expr.ordinal} #{expr.day} of every month at #{format_time_list(expr.times)}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
when SingleDateExpr
|
|
83
|
+
date_str = case expr.date
|
|
84
|
+
when NamedDate
|
|
85
|
+
"#{expr.date.month} #{expr.date.day}"
|
|
86
|
+
when IsoDate
|
|
87
|
+
expr.date.date
|
|
88
|
+
end
|
|
89
|
+
"on #{date_str} at #{format_time_list(expr.times)}"
|
|
90
|
+
|
|
91
|
+
when YearRepeat
|
|
92
|
+
target_str = case expr.target
|
|
93
|
+
when YearDateTarget
|
|
94
|
+
"#{expr.target.month} #{expr.target.day}"
|
|
95
|
+
when YearOrdinalWeekdayTarget
|
|
96
|
+
"the #{expr.target.ordinal} #{expr.target.weekday} of #{expr.target.month}"
|
|
97
|
+
when YearDayOfMonthTarget
|
|
98
|
+
"the #{expr.target.day}#{ordinal_suffix(expr.target.day)} of #{expr.target.month}"
|
|
99
|
+
when YearLastWeekdayTarget
|
|
100
|
+
"the last weekday of #{expr.target.month}"
|
|
101
|
+
end
|
|
102
|
+
if expr.interval > 1
|
|
103
|
+
"every #{expr.interval} years on #{target_str} at #{format_time_list(expr.times)}"
|
|
104
|
+
else
|
|
105
|
+
"every year on #{target_str} at #{format_time_list(expr.times)}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
else
|
|
109
|
+
raise "unknown expression type: #{expr.class}"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.display_day_filter(filter)
|
|
114
|
+
case filter
|
|
115
|
+
when DayFilterEvery
|
|
116
|
+
"day"
|
|
117
|
+
when DayFilterWeekday
|
|
118
|
+
"weekday"
|
|
119
|
+
when DayFilterWeekend
|
|
120
|
+
"weekend"
|
|
121
|
+
when DayFilterDays
|
|
122
|
+
filter.days.join(", ")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.format_time_list(times)
|
|
127
|
+
times.map(&:to_s).join(", ")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.format_ordinal_day_specs(specs)
|
|
131
|
+
parts = specs.map do |spec|
|
|
132
|
+
case spec
|
|
133
|
+
when SingleDay
|
|
134
|
+
"#{spec.day}#{ordinal_suffix(spec.day)}"
|
|
135
|
+
when DayRange
|
|
136
|
+
"#{spec.start}#{ordinal_suffix(spec.start)} to #{spec.end_day}#{ordinal_suffix(spec.end_day)}"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
parts.join(", ")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.ordinal_suffix(n)
|
|
143
|
+
mod100 = n % 100
|
|
144
|
+
return "th" if mod100.between?(11, 13)
|
|
145
|
+
|
|
146
|
+
case n % 10
|
|
147
|
+
when 1
|
|
148
|
+
"st"
|
|
149
|
+
when 2
|
|
150
|
+
"nd"
|
|
151
|
+
when 3
|
|
152
|
+
"rd"
|
|
153
|
+
else
|
|
154
|
+
"th"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.unit_display(interval, unit)
|
|
159
|
+
if unit == IntervalUnit::MIN
|
|
160
|
+
(interval == 1) ? "minute" : "min"
|
|
161
|
+
else
|
|
162
|
+
(interval == 1) ? "hour" : "hours"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
data/lib/hron/error.rb
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hron
|
|
4
|
+
# Span in source input for error reporting
|
|
5
|
+
Span = Data.define(:start, :end_pos) do # end_pos to avoid Ruby keyword
|
|
6
|
+
def length
|
|
7
|
+
end_pos - start
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Error kinds
|
|
12
|
+
module ErrorKind
|
|
13
|
+
LEX = :lex
|
|
14
|
+
PARSE = :parse
|
|
15
|
+
EVAL = :eval
|
|
16
|
+
CRON = :cron
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Main error class for hron
|
|
20
|
+
class HronError < StandardError
|
|
21
|
+
attr_reader :kind, :span, :input, :suggestion
|
|
22
|
+
|
|
23
|
+
def initialize(kind, message, span: nil, input: nil, suggestion: nil)
|
|
24
|
+
super(message)
|
|
25
|
+
@kind = kind
|
|
26
|
+
@span = span
|
|
27
|
+
@input = input
|
|
28
|
+
@suggestion = suggestion
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.lex(message, span, input)
|
|
32
|
+
new(ErrorKind::LEX, message, span: span, input: input)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.parse(message, span, input, suggestion: nil)
|
|
36
|
+
new(ErrorKind::PARSE, message, span: span, input: input, suggestion: suggestion)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.eval(message)
|
|
40
|
+
new(ErrorKind::EVAL, message)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.cron(message)
|
|
44
|
+
new(ErrorKind::CRON, message)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def display_rich
|
|
48
|
+
if [ErrorKind::LEX, ErrorKind::PARSE].include?(kind) && span && input
|
|
49
|
+
buf = []
|
|
50
|
+
buf << "error: #{message}"
|
|
51
|
+
buf << " #{input}"
|
|
52
|
+
padding = " " * (span.start + 2)
|
|
53
|
+
len = [span.length, 1].max
|
|
54
|
+
underline = "^" * len
|
|
55
|
+
line = "#{padding}#{underline}"
|
|
56
|
+
line += " try: \"#{suggestion}\"" if suggestion
|
|
57
|
+
buf << line
|
|
58
|
+
buf.join("\n")
|
|
59
|
+
else
|
|
60
|
+
"error: #{message}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|