hron 0.6.1 → 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 +11 -5
- 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 +370 -500
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8531585b861b8b39b203886814c4b21a7e3b2e9f800d0e01c259987840b1bdde
|
|
4
|
+
data.tar.gz: 3f7e82f141a0600150eccb91aa44d25de6d6b2d08415273f5744d788ff49f782
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 18352d886d95e080d250869a6cf3e0ef48ab43a732c538ca0b8021cd8e890de0f9323526c5570363453597bea2afca2adfd2cd3161e8e96c72f19ef33bf1c4e1
|
|
7
|
+
data.tar.gz: f325d9c26c288c2a166eaf29405e5f3908f7e7a062ce2a7d1dff81d26c244dbb5432bcde86abeb5b554777b01c3b5af8fb725fdc62fb8b11259679f5c4a93463
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# hron
|
|
2
2
|
|
|
3
|
-
**Human-readable cron** — scheduling expressions that
|
|
3
|
+
**Human-readable cron** — scheduling expressions that read like English and convert to and from cron.
|
|
4
4
|
|
|
5
5
|
```ruby
|
|
6
6
|
require 'hron'
|
|
@@ -28,17 +28,18 @@ require 'hron'
|
|
|
28
28
|
# Parse an expression
|
|
29
29
|
schedule = Hron::Schedule.parse("every weekday at 9:00 except dec 25, jan 1 in America/New_York")
|
|
30
30
|
|
|
31
|
-
# Get next occurrence
|
|
32
|
-
now = Time.
|
|
33
|
-
|
|
34
|
-
puts next_time
|
|
31
|
+
# Get the next occurrence, in the schedule's timezone
|
|
32
|
+
now = Time.new(2026, 2, 6, 21, 0, 0, "+09:00") # 07:00 in New York
|
|
33
|
+
puts schedule.next_from(now) # 2026-02-06 09:00:00 -0500
|
|
35
34
|
|
|
36
|
-
# Get next
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
# Get the next 3 occurrences
|
|
36
|
+
schedule.next_n_from(now, 3).each { |t| puts t }
|
|
37
|
+
# 2026-02-06 09:00:00 -0500
|
|
38
|
+
# 2026-02-09 09:00:00 -0500
|
|
39
|
+
# 2026-02-10 09:00:00 -0500
|
|
39
40
|
|
|
40
41
|
# Check if a time matches
|
|
41
|
-
schedule.matches(Time.new(2026, 2, 9, 9, 0, 0)) # true
|
|
42
|
+
schedule.matches(Time.new(2026, 2, 9, 9, 0, 0, "-05:00")) # true: 09:00 in New York
|
|
42
43
|
|
|
43
44
|
# Convert to/from cron
|
|
44
45
|
simple = Hron::Schedule.parse("every day at 9:00")
|
|
@@ -50,48 +51,152 @@ puts from_cron # "every 30 min from 00:00 to 23:59"
|
|
|
50
51
|
# Validate without exceptions
|
|
51
52
|
Hron::Schedule.validate("every day at 9:00") # true
|
|
52
53
|
Hron::Schedule.validate("invalid") # false
|
|
54
|
+
|
|
55
|
+
# Build from parts, checked as parse checks text
|
|
56
|
+
data = Hron::ScheduleData.new(
|
|
57
|
+
expression: Hron::DayRepeat.new(1, Hron::DayFilterWeekday.new, [Hron::TimeOfDay.new(9, 0)]),
|
|
58
|
+
timezone: "america/new_york"
|
|
59
|
+
)
|
|
60
|
+
puts Hron::Schedule.new(data) # every weekday at 09:00 in America/New_York
|
|
61
|
+
begin
|
|
62
|
+
Hron::Schedule.new(data.with(timezone: "EST"))
|
|
63
|
+
rescue Hron::HronError => e
|
|
64
|
+
puts e.message # timezone must be UTC or an Area/Location name such as America/New_York, got EST
|
|
65
|
+
end
|
|
53
66
|
```
|
|
54
67
|
|
|
55
68
|
## Expression Syntax
|
|
56
69
|
|
|
57
|
-
See the full [expression reference](https://github.com/
|
|
70
|
+
See the full [expression reference](https://github.com/simpllyf/hron#expression-syntax).
|
|
58
71
|
|
|
59
72
|
## API
|
|
60
73
|
|
|
61
74
|
### `Hron::Schedule.parse(input) -> Schedule`
|
|
62
|
-
Parse an hron expression string.
|
|
75
|
+
Parse an hron expression string. Raises `Hron::HronError` with `kind` `:lex` or `:parse` when it is invalid (see [Errors](#errors)), and `TypeError` unless `input` is a `String`.
|
|
63
76
|
|
|
64
77
|
### `Hron::Schedule.from_cron(cron_expr) -> Schedule`
|
|
65
|
-
Convert a 5-field cron expression to a Schedule.
|
|
78
|
+
Convert a 5-field cron expression to a Schedule that fires at the same times. This ignores the timezone and DST transitions, where cron schedulers differ. Raises `Hron::HronError` with `kind` `:cron` for invalid cron, for crons that restrict both the day of month and the day of week (`0 9 15 * 1`), and for more than 24 times a day, unless they are evenly spaced on days an interval can carry (`*/7 * * * *` fires 216 times at uneven gaps). Raises `TypeError` unless `cron_expr` is a `String`.
|
|
66
79
|
|
|
67
80
|
### `Hron::Schedule.validate(input) -> Boolean`
|
|
68
|
-
|
|
81
|
+
`true` when `parse` accepts `input`, `false` when it raises `Hron::HronError`. Raises `TypeError` unless `input` is a `String`.
|
|
82
|
+
|
|
83
|
+
### `Hron::Schedule.new(data) -> Schedule`
|
|
84
|
+
Build a schedule from a `Hron::ScheduleData` (see [Building a schedule](#building-a-schedule)), checked by the rules `parse` applies, so it evaluates, displays and converts as a parsed one does. Raises `Hron::HronError` with `kind` `:eval` and no `span`, `input` or `suggestion` for the first part that breaks a rule, with the message and in the order of the [spec](https://github.com/simpllyf/hron/blob/main/spec/README.md#schedules-built-in-code), and `TypeError` for a part of the wrong type. The schedule keeps a frozen copy of `data`, so changing `data`'s lists or strings afterwards does not change it.
|
|
69
85
|
|
|
70
86
|
### `schedule.next_from(now) -> Time | nil`
|
|
71
87
|
Compute the next occurrence after `now`.
|
|
72
88
|
|
|
89
|
+
### `schedule.previous_from(now) -> Time | nil`
|
|
90
|
+
Compute the most recent occurrence before `now`.
|
|
91
|
+
|
|
73
92
|
### `schedule.next_n_from(now, n) -> Array<Time>`
|
|
74
|
-
Compute
|
|
93
|
+
Compute up to `n` occurrences after `now`: none when `n <= 0`, and every one through the end of the supported range when there are fewer than `n`. Raises `TypeError` when `n` is not an `Integer`.
|
|
75
94
|
|
|
76
95
|
### `schedule.matches(time) -> Boolean`
|
|
77
96
|
Check if a time matches this schedule.
|
|
78
97
|
|
|
98
|
+
### `schedule.occurrences(from) -> Enumerator::Lazy<Time>`
|
|
99
|
+
Every occurrence after `from`, computed as it is taken.
|
|
100
|
+
|
|
101
|
+
### `schedule.between(from, to) -> Enumerator::Lazy<Time>`
|
|
102
|
+
Every occurrence after `from` and up to and including `to`.
|
|
103
|
+
|
|
104
|
+
### Times
|
|
105
|
+
|
|
106
|
+
Every method takes a `Time` in any zone or offset; only its instant counts, and the methods never change it. Anything else, such as a `String`, `Date`, `DateTime` or `nil`, raises `TypeError`. Every `Time` returned is in the schedule's timezone, with that `TZInfo::Timezone` as its `zone`, or UTC when the schedule has none.
|
|
107
|
+
|
|
79
108
|
### `schedule.to_cron -> String`
|
|
80
|
-
Convert to a 5-field cron expression. Raises `Hron::HronError`
|
|
109
|
+
Convert to a 5-field cron expression that fires at the same times. Yearly schedules, ordinal weekdays and partial-day intervals convert (`every 15 min from 09:00 to 17:45 on weekday` is `*/15 9-17 * * 1-5`). Raises `Hron::HronError` with `kind` `:cron` when no cron fires at the same times, as for `except`, `until`, `starting`, ISO dates, repeats every `n > 1` days, weeks, months or years, a directional nearest weekday, a `during` that excludes a yearly or named date's month, and times that are not every combination of their minutes and hours (`at 09:00, 17:30`). The timezone is not part of the cron: run it in the schedule's timezone. The [spec](https://github.com/simpllyf/hron/blob/main/spec/README.md#cron-conversion) has the full rules and every error message.
|
|
81
110
|
|
|
82
111
|
### `schedule.to_s -> String`
|
|
83
112
|
Render as the canonical string form (roundtrip-safe).
|
|
84
113
|
|
|
85
|
-
###
|
|
86
|
-
|
|
114
|
+
### Getters
|
|
115
|
+
|
|
116
|
+
Each getter returns one part of the schedule, with the classes under [Building a schedule](#building-a-schedule). What they return is frozen, so it cannot change the schedule.
|
|
117
|
+
|
|
118
|
+
| Getter | Returns |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `schedule.expression` | The expression: an `IntervalRepeat`, `DayRepeat`, `WeekRepeat`, `MonthRepeat`, `SingleDateExpr` or `YearRepeat`. |
|
|
121
|
+
| `schedule.timezone` | The IANA timezone name with its canonical capitalization (`in utc` gives `"UTC"`), or `nil`. |
|
|
122
|
+
| `schedule.except` | An `Array` of `NamedException` and `IsoException` in the order written; empty without an `except` clause. |
|
|
123
|
+
| `schedule.until` | An `IsoUntil` or `NamedUntil`, or `nil`. |
|
|
124
|
+
| `schedule.starting` | The `starting` date as a `"YYYY-MM-DD"` `String`, or `nil`. |
|
|
125
|
+
| `schedule.during` | An `Array` of month `Symbol`s in the order written; empty without a `during` clause. |
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
schedule = Hron::Schedule.parse("every weekday at 9:00 except dec 25 starting 2026-01-05 during jan, feb")
|
|
129
|
+
schedule.expression # #<data Hron::DayRepeat interval=1, days=#<data Hron::DayFilterWeekday>, times=[#<data Hron::TimeOfDay hour=9, minute=0>]>
|
|
130
|
+
schedule.except # [#<data Hron::NamedException month=:dec, day=25>]
|
|
131
|
+
schedule.until # nil
|
|
132
|
+
schedule.starting # "2026-01-05"
|
|
133
|
+
schedule.during # [:jan, :feb]
|
|
134
|
+
schedule.timezone # nil
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `schedule.data -> ScheduleData`
|
|
138
|
+
All the parts at once, frozen, with the timezone in its IANA capitalization. `Hron::Schedule.new(schedule.data.with(timezone: "UTC"))` builds a changed copy.
|
|
139
|
+
|
|
140
|
+
### `schedule == other`, `schedule.eql?(other)` and `schedule.hash`
|
|
141
|
+
Schedules are equal when their parts are, with lists compared in order and duplicates included, and equal schedules have equal hashes, so a schedule works as a `Hash` key. `Hron::Schedule.parse("every day at 9:00")` equals `Hron::Schedule.parse("every day at 09:00")` and the schedule built from the same parts. Comparing a schedule with `nil` or anything but a schedule gives `false`.
|
|
142
|
+
|
|
143
|
+
### Usage errors
|
|
144
|
+
|
|
145
|
+
A value of the wrong type where an argument goes raises `TypeError`, never `Hron::HronError`, and `validate` never returns `false` for it: an input to `parse`, `validate` or `from_cron` that is not a `String` (`nil` included), a time that is not a `Time`, an `n` that is not an `Integer`, a part of the wrong type in `Hron::Schedule.new`, and a message, span, input or suggestion of the wrong type in the [`Hron::HronError` factories](#errors).
|
|
146
|
+
|
|
147
|
+
## Building a schedule
|
|
148
|
+
|
|
149
|
+
`Hron::ScheduleData.new(expression:, timezone: nil, except: [], until: nil, starting: nil, during: [])` holds a schedule's parts; every class here is a `Data` and takes its fields in this order, positionally or by name. `Hron::Schedule.new` checks them.
|
|
150
|
+
|
|
151
|
+
| Field | Value |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `expression` | One expression below. |
|
|
154
|
+
| `timezone` | `"UTC"` or an IANA `Area/Location` name in any case, or `nil` for UTC. |
|
|
155
|
+
| `except` | An `Array` of `NamedException(month, day)` and `IsoException(date)`; empty means no `except` clause. |
|
|
156
|
+
| `until` | `NamedUntil(month, day)`, which needs `starting`, `IsoUntil(date)`, or `nil`. |
|
|
157
|
+
| `starting` | The `starting` date, or `nil`. |
|
|
158
|
+
| `during` | An `Array` of months; empty means no `during` clause. |
|
|
159
|
+
|
|
160
|
+
| Expression | Fields |
|
|
161
|
+
|---|---|
|
|
162
|
+
| `IntervalRepeat` | `interval`, `unit` (`:min` or `:hours`), `from_time`, `to_time`, `day_filter` (a day filter or `nil`) |
|
|
163
|
+
| `DayRepeat` | `interval`, `days` (a day filter, `DayFilterEvery` when `interval` is above 1), `times` |
|
|
164
|
+
| `WeekRepeat` | `interval`, `days` (an `Array` of weekdays), `times` |
|
|
165
|
+
| `MonthRepeat` | `interval`, `target`: `DaysTarget(specs)` of `SingleDay(day)` and `DayRange(start, end_day)`, `LastDayTarget`, `LastWeekdayTarget`, `NearestWeekdayTarget(day, direction)` with `direction` `nil`, `:next` or `:previous`, or `OrdinalWeekdayTarget(ordinal, weekday)`; `times` |
|
|
166
|
+
| `SingleDateExpr` | `date`: `NamedDate(month, day)` or `IsoDate(date)`; `times` |
|
|
167
|
+
| `YearRepeat` | `interval`, `target`: `YearDateTarget(month, day)`, `YearOrdinalWeekdayTarget(ordinal, weekday, month)`, `YearDayOfMonthTarget(day, month)` or `YearLastWeekdayTarget(month)`; `times` |
|
|
168
|
+
|
|
169
|
+
- `interval`, `day` and the `hour` and `minute` of `TimeOfDay(hour, minute)` are `Integer`s, `times` is an `Array` of `TimeOfDay`, and a date is a `"YYYY-MM-DD"` `String`.
|
|
170
|
+
- A day filter is `DayFilterEvery.new`, `DayFilterWeekday.new`, `DayFilterWeekend.new` or `DayFilterDays(days)`, with `days` an `Array` of weekdays.
|
|
171
|
+
- Names are `Symbol`s: weekdays `:monday` to `:sunday` (`Hron::Weekday::ALL`), months `:jan` to `:dec` (`Hron::MonthName::ALL`), and ordinals `:first` to `:fifth` and `:last` (`Hron::OrdinalPosition::ALL`). `Hron::OrdinalPosition.to_n` gives 1 to 5 for `:first` to `:fifth` and -1 for `:last`.
|
|
172
|
+
- A name of another type, such as `"monday"`, is a `TypeError`; an unknown `Symbol`, such as `:january`, is an `:eval` error (`unknown month :january`), as is any value where an expression, day filter, day spec, target, date, exception or until goes that is none of its classes.
|
|
173
|
+
|
|
174
|
+
## Errors
|
|
175
|
+
|
|
176
|
+
Every hron error is a `Hron::HronError`, a `StandardError` with:
|
|
177
|
+
|
|
178
|
+
| Member | Value |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `kind` | `:lex` or `:parse` from `parse`, `:eval` from `Hron::Schedule.new` (see [above](#hronschedulenewdata---schedule)), `:cron` from `from_cron` and `to_cron`; the values of `Hron::ErrorKind`. |
|
|
181
|
+
| `message` | The exact message of the [spec](https://github.com/simpllyf/hron/blob/main/spec/README.md#error-message-format). |
|
|
182
|
+
| `span` | A `Hron::Span` with `start` and `end_pos` for `:lex` and `:parse`, otherwise `nil`. |
|
|
183
|
+
| `input` | The input as given for `:lex` and `:parse`, otherwise `nil`. |
|
|
184
|
+
| `suggestion` | A suggested fix for some `:parse` errors, otherwise `nil`. |
|
|
185
|
+
| `display_rich` | `error: {message}`, and for `:lex` and `:parse` the input with carets under the span and any suggestion. |
|
|
186
|
+
| `Hron::HronError.lex(message, span, input)`, `.parse(message, span, input, suggestion: nil)`, `.eval(message)`, `.cron(message)` | Build an error of each kind. Each raises `TypeError` for a `message` or `input` that is not a `String`, a `span` that is not a `Hron::Span`, and a `suggestion` that is neither a `String` nor `nil`. |
|
|
187
|
+
|
|
188
|
+
```text
|
|
189
|
+
error: until dec 31 has no year: add a starting date, or use an ISO date
|
|
190
|
+
every weekday at 09:00 until dec 31
|
|
191
|
+
^^^^^^^^^^^^ try: "until dec 31 starting YYYY-MM-DD"
|
|
192
|
+
```
|
|
87
193
|
|
|
88
|
-
|
|
89
|
-
The underlying schedule expression AST.
|
|
194
|
+
`span.start` and `span.end_pos` mark `[start, end_pos)` in code points, which for a valid UTF-8 string are Ruby's own character indices: `error.input[error.span.start...error.span.end_pos]` is the text the error points at. Each byte of invalid UTF-8 counts as one code point and is reported as `unexpected character U+FFFD`. A binary (`ASCII-8BIT`) string is read as UTF-8 bytes, and a string in any other encoding is converted to UTF-8 first, so a lone UTF-16 surrogate is also U+FFFD.
|
|
90
195
|
|
|
91
196
|
## Requirements
|
|
92
197
|
|
|
93
|
-
- Ruby >=
|
|
94
|
-
- TZInfo gem for timezone support
|
|
198
|
+
- Ruby >= 4.0
|
|
199
|
+
- TZInfo gem for timezone support. TZInfo generates DST rules only about 100 years ahead of the current year; past that it keeps the zone's last offset, which is summer time for a southern-hemisphere zone such as Australia/Sydney.
|
|
95
200
|
|
|
96
201
|
## License
|
|
97
202
|
|
data/hron.gemspec
CHANGED
|
@@ -6,19 +6,19 @@ Gem::Specification.new do |spec|
|
|
|
6
6
|
spec.name = "hron"
|
|
7
7
|
spec.version = Hron::VERSION
|
|
8
8
|
spec.authors = ["Prasanna Venkataraman"]
|
|
9
|
-
spec.email = ["
|
|
9
|
+
spec.email = ["pras@simpllyf.io"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "Human-readable cron —
|
|
11
|
+
spec.summary = "Human-readable cron — scheduling expressions that read like English and convert to and from cron"
|
|
12
12
|
spec.description = "hron (human-readable cron) is a scheduling expression language " \
|
|
13
|
-
"that is designed to be easy to read, write, and understand. It
|
|
14
|
-
"
|
|
15
|
-
spec.homepage = "https://
|
|
13
|
+
"that is designed to be easy to read, write, and understand. It converts to and from cron " \
|
|
14
|
+
"exactly where both can express a schedule, and expresses schedules cron cannot."
|
|
15
|
+
spec.homepage = "https://hron.io"
|
|
16
16
|
spec.license = "MIT"
|
|
17
17
|
spec.required_ruby_version = ">= 4.0.0"
|
|
18
18
|
|
|
19
19
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
20
|
-
spec.metadata["source_code_uri"] = "https://github.com/
|
|
21
|
-
spec.metadata["changelog_uri"] = "https://github.com/
|
|
20
|
+
spec.metadata["source_code_uri"] = "https://github.com/simpllyf/hron"
|
|
21
|
+
spec.metadata["changelog_uri"] = "https://github.com/simpllyf/hron/releases"
|
|
22
22
|
spec.metadata["rubygems_mfa_required"] = "true"
|
|
23
23
|
|
|
24
24
|
spec.files = Dir.chdir(__dir__) do
|
|
@@ -32,8 +32,4 @@ Gem::Specification.new do |spec|
|
|
|
32
32
|
spec.require_paths = ["lib"]
|
|
33
33
|
|
|
34
34
|
spec.add_dependency "tzinfo", "~> 2.0"
|
|
35
|
-
|
|
36
|
-
spec.add_development_dependency "minitest", "~> 5.20"
|
|
37
|
-
spec.add_development_dependency "rake", "~> 13.0"
|
|
38
|
-
spec.add_development_dependency "standard", "~> 1.43"
|
|
39
35
|
end
|
data/lib/hron/ast.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Hron
|
|
4
|
-
# Weekday enumeration (ISO 8601: Monday=1, Sunday=7)
|
|
5
4
|
module Weekday
|
|
6
5
|
MONDAY = :monday
|
|
7
6
|
TUESDAY = :tuesday
|
|
@@ -37,10 +36,12 @@ module Hron
|
|
|
37
36
|
"sunday" => SUNDAY, "sun" => SUNDAY
|
|
38
37
|
}.freeze
|
|
39
38
|
|
|
39
|
+
# ISO 8601: Monday=1, Sunday=7.
|
|
40
40
|
def self.number(day)
|
|
41
41
|
NUMBERS[day]
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Sunday=0 … Saturday=6.
|
|
44
45
|
def self.cron_dow(day)
|
|
45
46
|
CRON_DOW[day]
|
|
46
47
|
end
|
|
@@ -58,7 +59,6 @@ module Hron
|
|
|
58
59
|
end
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
# Month name enumeration
|
|
62
62
|
module MonthName
|
|
63
63
|
JAN = :jan
|
|
64
64
|
FEB = :feb
|
|
@@ -101,6 +101,15 @@ module Hron
|
|
|
101
101
|
NUMBERS[month]
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# A named date has no year, so February has 29.
|
|
105
|
+
def self.max_day(month)
|
|
106
|
+
case month
|
|
107
|
+
when FEB then 29
|
|
108
|
+
when APR, JUN, SEP, NOV then 30
|
|
109
|
+
else 31
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
104
113
|
def self.from_number(n)
|
|
105
114
|
NUMBER_TO_MONTH[n]
|
|
106
115
|
end
|
|
@@ -114,17 +123,17 @@ module Hron
|
|
|
114
123
|
end
|
|
115
124
|
end
|
|
116
125
|
|
|
117
|
-
# Interval unit (minutes or hours)
|
|
118
126
|
module IntervalUnit
|
|
119
127
|
MIN = :min
|
|
120
128
|
HOURS = :hours
|
|
121
129
|
|
|
130
|
+
ALL = [MIN, HOURS].freeze
|
|
131
|
+
|
|
122
132
|
def self.to_s(unit)
|
|
123
133
|
unit.to_s
|
|
124
134
|
end
|
|
125
135
|
end
|
|
126
136
|
|
|
127
|
-
# Ordinal position (first, second, etc.)
|
|
128
137
|
module OrdinalPosition
|
|
129
138
|
FIRST = :first
|
|
130
139
|
SECOND = :second
|
|
@@ -133,10 +142,13 @@ module Hron
|
|
|
133
142
|
FIFTH = :fifth
|
|
134
143
|
LAST = :last
|
|
135
144
|
|
|
145
|
+
ALL = [FIRST, SECOND, THIRD, FOURTH, FIFTH, LAST].freeze
|
|
146
|
+
|
|
136
147
|
TO_N = {
|
|
137
|
-
FIRST => 1, SECOND => 2, THIRD => 3, FOURTH => 4, FIFTH => 5
|
|
148
|
+
FIRST => 1, SECOND => 2, THIRD => 3, FOURTH => 4, FIFTH => 5, LAST => -1
|
|
138
149
|
}.freeze
|
|
139
150
|
|
|
151
|
+
# 1-5 for first to fifth, and -1 for last.
|
|
140
152
|
def self.to_n(ord)
|
|
141
153
|
TO_N[ord]
|
|
142
154
|
end
|
|
@@ -146,64 +158,48 @@ module Hron
|
|
|
146
158
|
end
|
|
147
159
|
end
|
|
148
160
|
|
|
149
|
-
# Time of day (hour and minute)
|
|
150
161
|
TimeOfDay = Data.define(:hour, :minute) do
|
|
151
162
|
def to_s
|
|
152
163
|
format("%02d:%02d", hour, minute)
|
|
153
164
|
end
|
|
154
165
|
end
|
|
155
166
|
|
|
156
|
-
# --- Day filter variants ---
|
|
157
|
-
|
|
158
167
|
DayFilterEvery = Data.define
|
|
159
168
|
DayFilterWeekday = Data.define
|
|
160
169
|
DayFilterWeekend = Data.define
|
|
161
|
-
DayFilterDays = Data.define(:days)
|
|
162
|
-
|
|
163
|
-
# --- Day of month spec ---
|
|
170
|
+
DayFilterDays = Data.define(:days)
|
|
164
171
|
|
|
165
172
|
SingleDay = Data.define(:day)
|
|
166
173
|
DayRange = Data.define(:start, :end_day) # end_day to avoid Ruby keyword
|
|
167
174
|
|
|
168
|
-
# --- Direction for nearest weekday ---
|
|
169
|
-
|
|
170
175
|
module NearestDirection
|
|
171
176
|
NEXT = :next
|
|
172
177
|
PREVIOUS = :previous
|
|
173
|
-
end
|
|
174
178
|
|
|
175
|
-
|
|
179
|
+
ALL = [NEXT, PREVIOUS].freeze
|
|
180
|
+
end
|
|
176
181
|
|
|
177
|
-
DaysTarget = Data.define(:specs)
|
|
182
|
+
DaysTarget = Data.define(:specs)
|
|
178
183
|
LastDayTarget = Data.define
|
|
179
184
|
LastWeekdayTarget = Data.define
|
|
180
|
-
|
|
185
|
+
# A nil direction stays in the month, as cron W does; a direction can cross into the adjacent month.
|
|
186
|
+
NearestWeekdayTarget = Data.define(:day, :direction)
|
|
181
187
|
OrdinalWeekdayTarget = Data.define(:ordinal, :weekday)
|
|
182
188
|
|
|
183
|
-
# --- Year target variants ---
|
|
184
|
-
|
|
185
189
|
YearDateTarget = Data.define(:month, :day)
|
|
186
190
|
YearOrdinalWeekdayTarget = Data.define(:ordinal, :weekday, :month)
|
|
187
191
|
YearDayOfMonthTarget = Data.define(:day, :month)
|
|
188
192
|
YearLastWeekdayTarget = Data.define(:month)
|
|
189
193
|
|
|
190
|
-
# --- Date spec variants ---
|
|
191
|
-
|
|
192
194
|
NamedDate = Data.define(:month, :day)
|
|
193
|
-
IsoDate = Data.define(:date)
|
|
194
|
-
|
|
195
|
-
# --- Exception spec variants ---
|
|
195
|
+
IsoDate = Data.define(:date)
|
|
196
196
|
|
|
197
197
|
NamedException = Data.define(:month, :day)
|
|
198
198
|
IsoException = Data.define(:date)
|
|
199
199
|
|
|
200
|
-
# --- Until spec variants ---
|
|
201
|
-
|
|
202
200
|
IsoUntil = Data.define(:date)
|
|
203
201
|
NamedUntil = Data.define(:month, :day)
|
|
204
202
|
|
|
205
|
-
# --- Schedule expression variants ---
|
|
206
|
-
|
|
207
203
|
IntervalRepeat = Data.define(:interval, :unit, :from_time, :to_time, :day_filter)
|
|
208
204
|
DayRepeat = Data.define(:interval, :days, :times)
|
|
209
205
|
WeekRepeat = Data.define(:interval, :days, :times)
|
|
@@ -211,33 +207,11 @@ module Hron
|
|
|
211
207
|
SingleDateExpr = Data.define(:date, :times)
|
|
212
208
|
YearRepeat = Data.define(:interval, :target, :times)
|
|
213
209
|
|
|
214
|
-
#
|
|
215
|
-
|
|
216
|
-
ScheduleData = Data.define(:
|
|
217
|
-
def initialize(
|
|
210
|
+
# The parts of a schedule, for Schedule.new to check. starting is a YYYY-MM-DD String; an
|
|
211
|
+
# empty except or during list is no clause.
|
|
212
|
+
ScheduleData = Data.define(:expression, :timezone, :except, :until, :starting, :during) do
|
|
213
|
+
def initialize(expression:, timezone: nil, except: [], until: nil, starting: nil, during: [])
|
|
218
214
|
super
|
|
219
215
|
end
|
|
220
216
|
end
|
|
221
|
-
|
|
222
|
-
# --- Helper functions ---
|
|
223
|
-
|
|
224
|
-
def self.expand_day_spec(spec)
|
|
225
|
-
case spec
|
|
226
|
-
when SingleDay
|
|
227
|
-
[spec.day]
|
|
228
|
-
when DayRange
|
|
229
|
-
(spec.start..spec.end_day).to_a
|
|
230
|
-
else
|
|
231
|
-
[]
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def self.expand_month_target(target)
|
|
236
|
-
case target
|
|
237
|
-
when DaysTarget
|
|
238
|
-
target.specs.flat_map { |spec| expand_day_spec(spec) }
|
|
239
|
-
else
|
|
240
|
-
[]
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
217
|
end
|