philiprehberger-cron_parser 0.1.4 → 0.2.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/CHANGELOG.md +14 -0
- data/README.md +29 -0
- data/lib/philiprehberger/cron_parser/expression.rb +13 -3
- data/lib/philiprehberger/cron_parser/field.rb +32 -4
- data/lib/philiprehberger/cron_parser/version.rb +1 -1
- data/lib/philiprehberger/cron_parser.rb +37 -0
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eeb369c781ff5bf8c316b443bf97ee4786ed74e11842f7f5de81a9071418bdb2
|
|
4
|
+
data.tar.gz: ee7c9d64bae3aee0f9a7b617197e32c6af9816d09ad372c80031d748082f9ab2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c95125f661ce1799d954dc8766c494b784db4fcc7ccd1ad37ca638b4cd3a6a807560fa9640868d2a9eb91e89e8323604868a3f27fecf797324571f7d6d341b48
|
|
7
|
+
data.tar.gz: c1464aa92d9df184418b6e211864f2ea30c7444822c955c5772482e2c7e24d10c5dc395de909c4baf7e17436cee4aba87c20a7ff72fd3241ab3c5049a3781ba2
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-04-03
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Named month support (JAN-DEC) in cron expressions
|
|
14
|
+
- Named weekday support (SUN-SAT) in cron expressions
|
|
15
|
+
- `valid?` method for non-raising expression validation
|
|
16
|
+
- `validate` method returning structured field-level errors
|
|
17
|
+
- `description` alias for `human_readable`
|
|
18
|
+
|
|
19
|
+
## [0.1.5] - 2026-03-31
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- Add GitHub issue templates, dependabot config, and PR template
|
|
23
|
+
|
|
10
24
|
## [0.1.4] - 2026-03-31
|
|
11
25
|
|
|
12
26
|
### Changed
|
data/README.md
CHANGED
|
@@ -56,6 +56,30 @@ cron = Philiprehberger::CronParser.new('30 9 * * 1-5')
|
|
|
56
56
|
cron.human_readable # => "at minute 30, at hour 9, on weekday 1,2,3,4,5"
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
### Named Months and Weekdays
|
|
60
|
+
|
|
61
|
+
Use named months (JAN-DEC) and weekdays (SUN-SAT) in cron expressions. Names are case-insensitive and work in ranges and lists.
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
Philiprehberger::CronParser.new('0 0 1 JAN *') # first of January
|
|
65
|
+
Philiprehberger::CronParser.new('0 0 1 JAN-MAR *') # first of Jan-Mar
|
|
66
|
+
Philiprehberger::CronParser.new('0 0 * * MON') # every Monday
|
|
67
|
+
Philiprehberger::CronParser.new('0 0 * * MON-FRI') # weekdays
|
|
68
|
+
Philiprehberger::CronParser.new('0 0 * * MON,WED,FRI') # specific days
|
|
69
|
+
Philiprehberger::CronParser.new('0 0 * * 1,WED,5') # mixed numeric and named
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Validation
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
Philiprehberger::CronParser.valid?('*/5 * * * *') # => true
|
|
76
|
+
Philiprehberger::CronParser.valid?('60 * * * *') # => false
|
|
77
|
+
|
|
78
|
+
result = Philiprehberger::CronParser.validate('60 25 * * *')
|
|
79
|
+
result[:valid] # => false
|
|
80
|
+
result[:errors] # => ["minute field: Value 60 out of range (0-59)", "hour field: Value 25 out of range (0-23)"]
|
|
81
|
+
```
|
|
82
|
+
|
|
59
83
|
### Supported Syntax
|
|
60
84
|
|
|
61
85
|
Standard 5-field cron expressions (minute hour day month weekday):
|
|
@@ -66,6 +90,8 @@ Philiprehberger::CronParser.new('*/5 * * * *') # every 5 minutes
|
|
|
66
90
|
Philiprehberger::CronParser.new('0 9-17 * * *') # hourly 9am-5pm
|
|
67
91
|
Philiprehberger::CronParser.new('0 9,12,17 * * *') # specific hours
|
|
68
92
|
Philiprehberger::CronParser.new('0 0 1 * *') # first of month
|
|
93
|
+
Philiprehberger::CronParser.new('0 0 1 JAN-MAR *') # named months
|
|
94
|
+
Philiprehberger::CronParser.new('0 9 * * MON-FRI') # named weekdays
|
|
69
95
|
```
|
|
70
96
|
|
|
71
97
|
## API
|
|
@@ -73,11 +99,14 @@ Philiprehberger::CronParser.new('0 0 1 * *') # first of month
|
|
|
73
99
|
| Method | Description |
|
|
74
100
|
|--------|-------------|
|
|
75
101
|
| `CronParser.new(expr)` | Parse a 5-field cron expression |
|
|
102
|
+
| `CronParser.valid?(expr)` | Check if expression is valid (returns boolean) |
|
|
103
|
+
| `CronParser.validate(expr)` | Validate with structured field-level errors |
|
|
76
104
|
| `Expression#next(from:)` | Calculate the next matching time |
|
|
77
105
|
| `Expression#prev(from:)` | Calculate the previous matching time |
|
|
78
106
|
| `Expression#next_n(n, from:)` | Calculate the next N matching times |
|
|
79
107
|
| `Expression#matches?(time)` | Check if a time matches the expression |
|
|
80
108
|
| `Expression#human_readable` | Human-readable description of the expression |
|
|
109
|
+
| `Expression#description` | Alias for `human_readable` |
|
|
81
110
|
|
|
82
111
|
## Development
|
|
83
112
|
|
|
@@ -14,8 +14,13 @@ module Philiprehberger
|
|
|
14
14
|
weekday: { min: 0, max: 6 }
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
FIELD_NAMES_MAP = {
|
|
18
|
+
minute: nil,
|
|
19
|
+
hour: nil,
|
|
20
|
+
day: nil,
|
|
21
|
+
month: Field::MONTH_NAMES,
|
|
22
|
+
weekday: Field::WEEKDAY_NAMES
|
|
23
|
+
}.freeze
|
|
19
24
|
|
|
20
25
|
HUMAN_LABELS = {
|
|
21
26
|
minute: 'minute',
|
|
@@ -37,7 +42,7 @@ module Philiprehberger
|
|
|
37
42
|
|
|
38
43
|
@fields = {}
|
|
39
44
|
FIELD_RANGES.each_with_index do |(name, range), index|
|
|
40
|
-
@fields[name] = Field.new(parts[index],
|
|
45
|
+
@fields[name] = Field.new(parts[index], min: range[:min], max: range[:max], names: FIELD_NAMES_MAP[name])
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
|
|
@@ -100,6 +105,11 @@ module Philiprehberger
|
|
|
100
105
|
parts.compact.join(', ')
|
|
101
106
|
end
|
|
102
107
|
|
|
108
|
+
# Alias for human_readable
|
|
109
|
+
#
|
|
110
|
+
# @return [String]
|
|
111
|
+
alias description human_readable
|
|
112
|
+
|
|
103
113
|
private
|
|
104
114
|
|
|
105
115
|
def find_occurrence(from, direction:)
|
|
@@ -4,16 +4,29 @@ module Philiprehberger
|
|
|
4
4
|
module CronParser
|
|
5
5
|
# Parses a single cron field (minute, hour, day, month, weekday)
|
|
6
6
|
class Field
|
|
7
|
+
MONTH_NAMES = {
|
|
8
|
+
'JAN' => 1, 'FEB' => 2, 'MAR' => 3, 'APR' => 4,
|
|
9
|
+
'MAY' => 5, 'JUN' => 6, 'JUL' => 7, 'AUG' => 8,
|
|
10
|
+
'SEP' => 9, 'OCT' => 10, 'NOV' => 11, 'DEC' => 12
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
WEEKDAY_NAMES = {
|
|
14
|
+
'SUN' => 0, 'MON' => 1, 'TUE' => 2, 'WED' => 3,
|
|
15
|
+
'THU' => 4, 'FRI' => 5, 'SAT' => 6
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
7
18
|
# @return [Array<Integer>] the expanded set of valid values
|
|
8
19
|
attr_reader :values
|
|
9
20
|
|
|
10
21
|
# @param expr [String] the field expression (e.g. "*/5", "1,3,5", "1-10")
|
|
11
22
|
# @param min [Integer] the minimum valid value
|
|
12
23
|
# @param max [Integer] the maximum valid value
|
|
24
|
+
# @param names [Hash, nil] optional name-to-value mapping for this field
|
|
13
25
|
# @raise [Error] if the expression is invalid
|
|
14
|
-
def initialize(expr, min:, max:)
|
|
26
|
+
def initialize(expr, min:, max:, names: nil)
|
|
15
27
|
@min = min
|
|
16
28
|
@max = max
|
|
29
|
+
@names = names
|
|
17
30
|
@values = parse(expr).sort.freeze
|
|
18
31
|
end
|
|
19
32
|
|
|
@@ -35,6 +48,17 @@ module Philiprehberger
|
|
|
35
48
|
result.uniq
|
|
36
49
|
end
|
|
37
50
|
|
|
51
|
+
def resolve_name(token)
|
|
52
|
+
return token.to_i if token.match?(/\A\d+\z/)
|
|
53
|
+
|
|
54
|
+
raise Error, "Invalid cron field expression: #{token}" unless @names
|
|
55
|
+
|
|
56
|
+
value = @names[token.upcase]
|
|
57
|
+
raise Error, "Invalid name '#{token}' for this field" unless value
|
|
58
|
+
|
|
59
|
+
value
|
|
60
|
+
end
|
|
61
|
+
|
|
38
62
|
def parse_part(part)
|
|
39
63
|
case part
|
|
40
64
|
when '*'
|
|
@@ -44,9 +68,9 @@ module Philiprehberger
|
|
|
44
68
|
raise Error, "Invalid step: #{step}" if step.zero?
|
|
45
69
|
|
|
46
70
|
(@min..@max).step(step).to_a
|
|
47
|
-
when %r{\A(
|
|
48
|
-
range_start = Regexp.last_match(1)
|
|
49
|
-
range_end = Regexp.last_match(2)
|
|
71
|
+
when %r{\A([a-zA-Z0-9]+)-([a-zA-Z0-9]+)(?:/(\d+))?\z}
|
|
72
|
+
range_start = resolve_name(Regexp.last_match(1))
|
|
73
|
+
range_end = resolve_name(Regexp.last_match(2))
|
|
50
74
|
step = Regexp.last_match(3)&.to_i || 1
|
|
51
75
|
validate_range!(range_start, range_end)
|
|
52
76
|
raise Error, "Invalid step: #{step}" if step.zero?
|
|
@@ -56,6 +80,10 @@ module Philiprehberger
|
|
|
56
80
|
value = part.to_i
|
|
57
81
|
validate_value!(value)
|
|
58
82
|
[value]
|
|
83
|
+
when /\A[a-zA-Z]+\z/
|
|
84
|
+
value = resolve_name(part)
|
|
85
|
+
validate_value!(value)
|
|
86
|
+
[value]
|
|
59
87
|
else
|
|
60
88
|
raise Error, "Invalid cron field expression: #{part}"
|
|
61
89
|
end
|
|
@@ -8,6 +8,8 @@ module Philiprehberger
|
|
|
8
8
|
module CronParser
|
|
9
9
|
class Error < StandardError; end
|
|
10
10
|
|
|
11
|
+
FIELD_ORDER = %i[minute hour day month weekday].freeze
|
|
12
|
+
|
|
11
13
|
# Parse a cron expression and return an Expression instance
|
|
12
14
|
#
|
|
13
15
|
# @param expr [String] a 5-field cron expression (minute hour day month weekday)
|
|
@@ -16,5 +18,40 @@ module Philiprehberger
|
|
|
16
18
|
def self.new(expr)
|
|
17
19
|
Expression.new(expr)
|
|
18
20
|
end
|
|
21
|
+
|
|
22
|
+
# Check if a cron expression is valid without raising
|
|
23
|
+
#
|
|
24
|
+
# @param expr [String] a 5-field cron expression
|
|
25
|
+
# @return [Boolean]
|
|
26
|
+
def self.valid?(expr)
|
|
27
|
+
Expression.new(expr)
|
|
28
|
+
true
|
|
29
|
+
rescue Error
|
|
30
|
+
false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Validate a cron expression and return structured errors
|
|
34
|
+
#
|
|
35
|
+
# @param expr [String] a 5-field cron expression
|
|
36
|
+
# @return [Hash] { valid: true/false, errors: [String] }
|
|
37
|
+
def self.validate(expr)
|
|
38
|
+
errors = []
|
|
39
|
+
stripped = expr.strip
|
|
40
|
+
parts = stripped.split(/\s+/)
|
|
41
|
+
|
|
42
|
+
unless parts.size == 5
|
|
43
|
+
return { valid: false, errors: ["Expected 5 fields, got #{parts.size}"] }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
FIELD_ORDER.each_with_index do |name, index|
|
|
47
|
+
range = Expression::FIELD_RANGES[name]
|
|
48
|
+
names_map = Expression::FIELD_NAMES_MAP[name]
|
|
49
|
+
Field.new(parts[index], min: range[:min], max: range[:max], names: names_map)
|
|
50
|
+
rescue Error => e
|
|
51
|
+
errors << "#{name} field: #{e.message}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
{ valid: errors.empty?, errors: errors }
|
|
55
|
+
end
|
|
19
56
|
end
|
|
20
57
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: philiprehberger-cron_parser
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Philip Rehberger
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03
|
|
11
|
+
date: 2026-04-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Parse standard 5-field cron expressions and calculate next/previous occurrences,
|
|
14
14
|
match times against patterns, and generate human-readable descriptions.
|
|
@@ -25,11 +25,11 @@ files:
|
|
|
25
25
|
- lib/philiprehberger/cron_parser/expression.rb
|
|
26
26
|
- lib/philiprehberger/cron_parser/field.rb
|
|
27
27
|
- lib/philiprehberger/cron_parser/version.rb
|
|
28
|
-
homepage: https://
|
|
28
|
+
homepage: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-cron_parser
|
|
29
29
|
licenses:
|
|
30
30
|
- MIT
|
|
31
31
|
metadata:
|
|
32
|
-
homepage_uri: https://
|
|
32
|
+
homepage_uri: https://philiprehberger.com/open-source-packages/ruby/philiprehberger-cron_parser
|
|
33
33
|
source_code_uri: https://github.com/philiprehberger/rb-cron-parser
|
|
34
34
|
changelog_uri: https://github.com/philiprehberger/rb-cron-parser/blob/main/CHANGELOG.md
|
|
35
35
|
bug_tracker_uri: https://github.com/philiprehberger/rb-cron-parser/issues
|