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.
@@ -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