philiprehberger-cron_parser 0.1.3 → 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 +19 -0
- data/README.md +49 -2
- data/lib/philiprehberger/cron_parser/expression.rb +14 -4
- data/lib/philiprehberger/cron_parser/field.rb +34 -6
- 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,25 @@ 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
|
+
|
|
24
|
+
## [0.1.4] - 2026-03-31
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Standardize README badges, support section, and license format
|
|
28
|
+
|
|
10
29
|
## [0.1.3] - 2026-03-24
|
|
11
30
|
|
|
12
31
|
### Fixed
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/philiprehberger/rb-cron-parser/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/philiprehberger-cron_parser)
|
|
5
|
-
[](https://github.com/philiprehberger/rb-cron-parser/commits/main)
|
|
6
6
|
|
|
7
7
|
Cron expression parser for calculating next and previous occurrences
|
|
8
8
|
|
|
@@ -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
|
|
|
@@ -87,6 +116,24 @@ bundle exec rspec
|
|
|
87
116
|
bundle exec rubocop
|
|
88
117
|
```
|
|
89
118
|
|
|
119
|
+
## Support
|
|
120
|
+
|
|
121
|
+
If you find this project useful:
|
|
122
|
+
|
|
123
|
+
⭐ [Star the repo](https://github.com/philiprehberger/rb-cron-parser)
|
|
124
|
+
|
|
125
|
+
🐛 [Report issues](https://github.com/philiprehberger/rb-cron-parser/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
|
126
|
+
|
|
127
|
+
💡 [Suggest features](https://github.com/philiprehberger/rb-cron-parser/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement)
|
|
128
|
+
|
|
129
|
+
❤️ [Sponsor development](https://github.com/sponsors/philiprehberger)
|
|
130
|
+
|
|
131
|
+
🌐 [All Open Source Projects](https://philiprehberger.com/open-source-packages)
|
|
132
|
+
|
|
133
|
+
💻 [GitHub Profile](https://github.com/philiprehberger)
|
|
134
|
+
|
|
135
|
+
🔗 [LinkedIn Profile](https://www.linkedin.com/in/philiprehberger)
|
|
136
|
+
|
|
90
137
|
## License
|
|
91
138
|
|
|
92
|
-
MIT
|
|
139
|
+
[MIT](LICENSE)
|
|
@@ -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:)
|
|
@@ -113,7 +123,7 @@ module Philiprehberger
|
|
|
113
123
|
current += step
|
|
114
124
|
end
|
|
115
125
|
|
|
116
|
-
raise Error,
|
|
126
|
+
raise Error, 'No matching time found within 4 years'
|
|
117
127
|
end
|
|
118
128
|
|
|
119
129
|
def round_to_minute(time)
|
|
@@ -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,18 +48,29 @@ 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 '*'
|
|
41
65
|
(@min..@max).to_a
|
|
42
|
-
when
|
|
66
|
+
when %r{\A\*/(\d+)\z}
|
|
43
67
|
step = Regexp.last_match(1).to_i
|
|
44
68
|
raise Error, "Invalid step: #{step}" if step.zero?
|
|
45
69
|
|
|
46
70
|
(@min..@max).step(step).to_a
|
|
47
|
-
when
|
|
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
|
|
@@ -68,7 +96,7 @@ module Philiprehberger
|
|
|
68
96
|
end
|
|
69
97
|
|
|
70
98
|
def validate_value!(value)
|
|
71
|
-
return if value
|
|
99
|
+
return if value.between?(@min, @max)
|
|
72
100
|
|
|
73
101
|
raise Error, "Value #{value} out of range (#{@min}-#{@max})"
|
|
74
102
|
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
|