philiprehberger-cron_parser 0.1.4 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32817318714cb93a510d59bfae5592462669488da4ee176d3985d484889f48d7
4
- data.tar.gz: ad3ebb67b51c6e6b93a1126779950b5097b04ba6c935d7c8406250c5f5637f4a
3
+ metadata.gz: d11c034aa656522a0e2ca5e145ca68f974b1b0eb9aeafad15797ee5a53d859f1
4
+ data.tar.gz: 874e7abb865fa66511d7a407e7e0cc4ea0dfd8e8a637ff75225cffc9db5f0206
5
5
  SHA512:
6
- metadata.gz: 04572d61d279af7901ad03bc56f041e0e0fc7083323b240639fb8fa4d3646ad340976c22051ed5e84a3d79471c63b8a39a983f245aa44ab276c2f88aeed0db6e
7
- data.tar.gz: 61d578083cf566b3add7df47b14203d8c3ef6815128d89fd3cf2a86c1d952702168a085ae175f1b1f1d75d38c509d460bec09852366a6c9ded1ed4cb1c275bd4
6
+ metadata.gz: c8053224dd61f4defd352fbdf106351f995de7b16b6f10b610a40e99c47f577c0b27d241587aa74cd4299da2d7a4601c6879844eb63d68ce47dfcaa383ce5847
7
+ data.tar.gz: f830772d6a686fec8dcf8f857a68b4be1f162bf70f0ebde4146f7228b44f5aadc18dbe567ce8570fcd4d5c5fa3125ab2ef56313978cec0320caa5b7796d458b5
data/CHANGELOG.md CHANGED
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.0] - 2026-04-09
11
+
12
+ ### Added
13
+ - Named cron aliases (`@hourly`, `@daily`, `@midnight`, `@weekly`, `@monthly`, `@yearly`, `@annually`) with case-insensitive matching
14
+ - `valid?` and `validate` now accept alias expressions
15
+
16
+ ## [0.2.0] - 2026-04-03
17
+
18
+ ### Added
19
+ - Named month support (JAN-DEC) in cron expressions
20
+ - Named weekday support (SUN-SAT) in cron expressions
21
+ - `valid?` method for non-raising expression validation
22
+ - `validate` method returning structured field-level errors
23
+ - `description` alias for `human_readable`
24
+
25
+ ## [0.1.5] - 2026-03-31
26
+
27
+ ### Added
28
+ - Add GitHub issue templates, dependabot config, and PR template
29
+
10
30
  ## [0.1.4] - 2026-03-31
11
31
 
12
32
  ### Changed
data/README.md CHANGED
@@ -56,6 +56,44 @@ 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
+ ### Named Aliases
73
+
74
+ Standard crontab aliases are supported (case-insensitive):
75
+
76
+ ```ruby
77
+ Philiprehberger::CronParser.new('@hourly') # => 0 * * * *
78
+ Philiprehberger::CronParser.new('@daily') # => 0 0 * * *
79
+ Philiprehberger::CronParser.new('@midnight') # alias for @daily
80
+ Philiprehberger::CronParser.new('@weekly') # => 0 0 * * 0
81
+ Philiprehberger::CronParser.new('@monthly') # => 0 0 1 * *
82
+ Philiprehberger::CronParser.new('@yearly') # => 0 0 1 1 *
83
+ Philiprehberger::CronParser.new('@annually') # alias for @yearly
84
+ ```
85
+
86
+ ### Validation
87
+
88
+ ```ruby
89
+ Philiprehberger::CronParser.valid?('*/5 * * * *') # => true
90
+ Philiprehberger::CronParser.valid?('60 * * * *') # => false
91
+
92
+ result = Philiprehberger::CronParser.validate('60 25 * * *')
93
+ result[:valid] # => false
94
+ result[:errors] # => ["minute field: Value 60 out of range (0-59)", "hour field: Value 25 out of range (0-23)"]
95
+ ```
96
+
59
97
  ### Supported Syntax
60
98
 
61
99
  Standard 5-field cron expressions (minute hour day month weekday):
@@ -66,6 +104,8 @@ Philiprehberger::CronParser.new('*/5 * * * *') # every 5 minutes
66
104
  Philiprehberger::CronParser.new('0 9-17 * * *') # hourly 9am-5pm
67
105
  Philiprehberger::CronParser.new('0 9,12,17 * * *') # specific hours
68
106
  Philiprehberger::CronParser.new('0 0 1 * *') # first of month
107
+ Philiprehberger::CronParser.new('0 0 1 JAN-MAR *') # named months
108
+ Philiprehberger::CronParser.new('0 9 * * MON-FRI') # named weekdays
69
109
  ```
70
110
 
71
111
  ## API
@@ -73,11 +113,14 @@ Philiprehberger::CronParser.new('0 0 1 * *') # first of month
73
113
  | Method | Description |
74
114
  |--------|-------------|
75
115
  | `CronParser.new(expr)` | Parse a 5-field cron expression |
116
+ | `CronParser.valid?(expr)` | Check if expression is valid (returns boolean) |
117
+ | `CronParser.validate(expr)` | Validate with structured field-level errors |
76
118
  | `Expression#next(from:)` | Calculate the next matching time |
77
119
  | `Expression#prev(from:)` | Calculate the previous matching time |
78
120
  | `Expression#next_n(n, from:)` | Calculate the next N matching times |
79
121
  | `Expression#matches?(time)` | Check if a time matches the expression |
80
122
  | `Expression#human_readable` | Human-readable description of the expression |
123
+ | `Expression#description` | Alias for `human_readable` |
81
124
 
82
125
  ## Development
83
126
 
@@ -14,8 +14,13 @@ module Philiprehberger
14
14
  weekday: { min: 0, max: 6 }
15
15
  }.freeze
16
16
 
17
- WEEKDAY_NAMES = { 'minute' => nil, 'hour' => nil, 'day' => nil, 'month' => nil,
18
- 'weekday' => nil }.freeze
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',
@@ -25,19 +30,33 @@ module Philiprehberger
25
30
  weekday: 'day of week'
26
31
  }.freeze
27
32
 
28
- # @return [String] the original expression
33
+ ALIASES = {
34
+ '@yearly' => '0 0 1 1 *',
35
+ '@annually' => '0 0 1 1 *',
36
+ '@monthly' => '0 0 1 * *',
37
+ '@weekly' => '0 0 * * 0',
38
+ '@daily' => '0 0 * * *',
39
+ '@midnight' => '0 0 * * *',
40
+ '@hourly' => '0 * * * *'
41
+ }.freeze
42
+
43
+ # @return [String] the original (post-alias expansion) expression
29
44
  attr_reader :expression
30
45
 
31
- # @param expr [String] a 5-field cron expression
46
+ # @param expr [String] a 5-field cron expression or named alias (e.g. "@daily")
32
47
  # @raise [Error] if the expression is invalid
33
48
  def initialize(expr)
34
- @expression = expr.strip
49
+ stripped = expr.strip
50
+ stripped = ALIASES[stripped.downcase] if stripped.start_with?('@')
51
+ raise Error, "Unknown cron alias: #{expr.strip}" if stripped.nil?
52
+
53
+ @expression = stripped
35
54
  parts = @expression.split(/\s+/)
36
55
  raise Error, "Expected 5 fields, got #{parts.size}: #{@expression}" unless parts.size == 5
37
56
 
38
57
  @fields = {}
39
58
  FIELD_RANGES.each_with_index do |(name, range), index|
40
- @fields[name] = Field.new(parts[index], **range)
59
+ @fields[name] = Field.new(parts[index], min: range[:min], max: range[:max], names: FIELD_NAMES_MAP[name])
41
60
  end
42
61
  end
43
62
 
@@ -100,6 +119,11 @@ module Philiprehberger
100
119
  parts.compact.join(', ')
101
120
  end
102
121
 
122
+ # Alias for human_readable
123
+ #
124
+ # @return [String]
125
+ alias description human_readable
126
+
103
127
  private
104
128
 
105
129
  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(\d+)-(\d+)(?:/(\d+))?\z}
48
- range_start = Regexp.last_match(1).to_i
49
- range_end = Regexp.last_match(2).to_i
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Philiprehberger
4
4
  module CronParser
5
- VERSION = '0.1.4'
5
+ VERSION = '0.3.0'
6
6
  end
7
7
  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,46 @@ 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
+ if stripped.start_with?('@')
41
+ alias_expanded = Expression::ALIASES[stripped.downcase]
42
+ return { valid: false, errors: ["Unknown cron alias: #{stripped}"] } if alias_expanded.nil?
43
+
44
+ stripped = alias_expanded
45
+ end
46
+ parts = stripped.split(/\s+/)
47
+
48
+ unless parts.size == 5
49
+ return { valid: false, errors: ["Expected 5 fields, got #{parts.size}"] }
50
+ end
51
+
52
+ FIELD_ORDER.each_with_index do |name, index|
53
+ range = Expression::FIELD_RANGES[name]
54
+ names_map = Expression::FIELD_NAMES_MAP[name]
55
+ Field.new(parts[index], min: range[:min], max: range[:max], names: names_map)
56
+ rescue Error => e
57
+ errors << "#{name} field: #{e.message}"
58
+ end
59
+
60
+ { valid: errors.empty?, errors: errors }
61
+ end
19
62
  end
20
63
  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.1.4
4
+ version: 0.3.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-31 00:00:00.000000000 Z
11
+ date: 2026-04-09 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://github.com/philiprehberger/rb-cron-parser
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://github.com/philiprehberger/rb-cron-parser
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