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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32817318714cb93a510d59bfae5592462669488da4ee176d3985d484889f48d7
4
- data.tar.gz: ad3ebb67b51c6e6b93a1126779950b5097b04ba6c935d7c8406250c5f5637f4a
3
+ metadata.gz: eeb369c781ff5bf8c316b443bf97ee4786ed74e11842f7f5de81a9071418bdb2
4
+ data.tar.gz: ee7c9d64bae3aee0f9a7b617197e32c6af9816d09ad372c80031d748082f9ab2
5
5
  SHA512:
6
- metadata.gz: 04572d61d279af7901ad03bc56f041e0e0fc7083323b240639fb8fa4d3646ad340976c22051ed5e84a3d79471c63b8a39a983f245aa44ab276c2f88aeed0db6e
7
- data.tar.gz: 61d578083cf566b3add7df47b14203d8c3ef6815128d89fd3cf2a86c1d952702168a085ae175f1b1f1d75d38c509d460bec09852366a6c9ded1ed4cb1c275bd4
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
- 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',
@@ -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], **range)
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(\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.2.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,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.1.4
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-31 00:00:00.000000000 Z
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://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