cron_describer 0.1.0 → 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: a72eb20b1b1637fff6bf409289667313e77d8d6524315099b8b4620ab4f0c72c
4
- data.tar.gz: 5c4da5ac5775ecb2d9b439ecfc8a1f0ada6ccde97a75acc924366a9b6f4a9c0f
3
+ metadata.gz: c483e3538e13ede9eaafdcae76e23a7e7f86d43a541aedc31d7062dec2d2b5b1
4
+ data.tar.gz: 65dec9f33dd2a62eb12c849e00cfc1edbe20e29c7966f5ec77590aaf5b1f854d
5
5
  SHA512:
6
- metadata.gz: be4cc3cd5ff45fb9a8865b070ff1a831c9c709573d077731fb5c26ed6729b0879791c7943dcaceb7c9ea94bf0d6727a9948a3bfcf5c84608c2d15a389ba14c2c
7
- data.tar.gz: daae3b954bcc707b8dab8228bb2749d6e7a019d779ae05999bfe182f3aa56f7b94dee588dc1115f31232fde4b9f8b64887d533ad9b154e273318cd28b3147108
6
+ metadata.gz: 0eea700d69154e7dd5fff5de279d335b1294c8029852fd57d599e6134a85435337762f5519c53770ec4d11253f1933691526a387ff647cd7ade867bff7ca1c7b
7
+ data.tar.gz: 0b1854d853eb6a94c709d2f4437f141df0a006944da79fa8377b1cd25028072d579d73339f80af858df42785bee924f11d92361794ba20a413a45f8475525689
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # CronDescriber
2
2
 
3
- CronDescriber is a simple Ruby gem that converts cron schedule strings into human-readable time descriptions. It can help you describe complex cron schedules in a way that is easy to understand.
3
+ CronDescriber is a Ruby gem that converts cron schedule strings into human-readable time descriptions. It supports the full cron syntax including ranges, lists, step values, and provides comprehensive input validation with meaningful error messages.
4
4
 
5
5
  ## Installation
6
6
 
@@ -22,21 +22,117 @@ Or install it yourself as:
22
22
  $ gem install cron_describer
23
23
  ```
24
24
 
25
-
26
25
  ## Usage
27
26
 
28
- Here is a basic example of how to use the CronDescriber gem:
27
+ ### Basic Examples
29
28
 
30
29
  ```ruby
31
30
  require 'cron_describer'
32
31
 
33
- cron_schedule = "30 6 1 */3 *"
34
- description = CronDescriber.parse(cron_schedule)
35
- puts description
36
- # Output: "At 6:30 AM, on day 1 of the month, every 3 months"
32
+ # Simple time
33
+ CronDescriber.parse("30 6 * * *")
34
+ # => "At 6:30 AM"
35
+
36
+ # Every day at midnight
37
+ CronDescriber.parse("0 0 * * *")
38
+ # => "At 12:00 AM"
39
+
40
+ # Afternoon time
41
+ CronDescriber.parse("15 14 * * *")
42
+ # => "At 2:15 PM"
43
+ ```
44
+
45
+ ### Advanced Cron Patterns
46
+
47
+ #### Step Values
48
+ ```ruby
49
+ # Every 5 minutes
50
+ CronDescriber.parse("*/5 * * * *")
51
+ # => "Every 5 minutes"
52
+
53
+ # Every 2 hours
54
+ CronDescriber.parse("0 */2 * * *")
55
+ # => "Every 2 hours"
56
+
57
+ # Every 3 months on the 1st
58
+ CronDescriber.parse("0 0 1 */3 *")
59
+ # => "At 12:00 AM, on day 1 of the month, every 3 months"
60
+ ```
61
+
62
+ #### Ranges
63
+ ```ruby
64
+ # Weekdays (Monday through Friday)
65
+ CronDescriber.parse("0 9 * * 1-5")
66
+ # => "At 9:00 AM, only on Monday through Friday"
67
+
68
+ # Summer months
69
+ CronDescriber.parse("0 12 1 6-8 *")
70
+ # => "At 12:00 PM, on day 1 of the month, June through August"
71
+ ```
72
+
73
+ #### Lists
74
+ ```ruby
75
+ # Specific days of the week
76
+ CronDescriber.parse("0 9 * * 1,3,5")
77
+ # => "At 9:00 AM, only on Monday, Wednesday, Friday"
78
+
79
+ # Quarter hours
80
+ CronDescriber.parse("0,15,30,45 * * * *")
81
+ # => "At 0, 15, 30, 45 minutes past the hour"
82
+
83
+ # Multiple specific hours
84
+ CronDescriber.parse("0 9,13,17 * * *")
85
+ # => "At 9:00 AM, 1:00 PM, 5:00 PM"
86
+ ```
87
+
88
+ #### Wildcard Patterns
89
+ ```ruby
90
+ # Every minute
91
+ CronDescriber.parse("* * * * *")
92
+ # => "Every minute"
93
+
94
+ # Every minute during 9 AM
95
+ CronDescriber.parse("* 9 * * *")
96
+ # => "Every minute during 9:00 AM"
37
97
  ```
38
98
 
39
- You can pass any valid cron schedule string to the describe_cron method to get a human-readable description.
99
+ ## Supported Cron Features
100
+
101
+ - ✅ **Standard 5-field format**: `minute hour day_of_month month day_of_week`
102
+ - ✅ **Wildcards**: `*` for any value
103
+ - ✅ **Step values**: `*/5` (every 5), `10-50/5` (every 5 from 10 to 50)
104
+ - ✅ **Ranges**: `1-5` (1 through 5), `MON-FRI`
105
+ - ✅ **Lists**: `1,3,5` (values 1, 3, and 5)
106
+ - ✅ **Day of week**: Both `0` and `7` supported for Sunday
107
+ - ✅ **12-hour time format**: Proper AM/PM conversion
108
+ - ✅ **Input validation**: Comprehensive field validation with helpful error messages
109
+ - ✅ **Error handling**: Graceful handling of malformed cron expressions
110
+
111
+ ## Field Ranges
112
+
113
+ - **Minute**: 0-59
114
+ - **Hour**: 0-23 (converted to 12-hour AM/PM format in output)
115
+ - **Day of Month**: 1-31
116
+ - **Month**: 1-12
117
+ - **Day of Week**: 0-7 (0 and 7 both represent Sunday)
118
+
119
+ ## Error Handling
120
+
121
+ The gem provides detailed error messages for invalid input:
122
+
123
+ ```ruby
124
+ # Invalid minute value
125
+ CronDescriber.parse("60 12 * * *")
126
+ # => CronDescriber::Error: Invalid minute field: 60
127
+
128
+ # Wrong number of fields
129
+ CronDescriber.parse("30 6 1")
130
+ # => CronDescriber::Error: Invalid cron format. Expected 5 fields (minute hour day_of_month month day_of_week)
131
+
132
+ # Empty input
133
+ CronDescriber.parse("")
134
+ # => CronDescriber::Error: Cron schedule cannot be nil or empty
135
+ ```
40
136
 
41
137
  ## Development
42
138
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CronDescriber
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -5,22 +5,178 @@ require_relative "cron_describer/version"
5
5
  module CronDescriber
6
6
  class Error < StandardError; end
7
7
 
8
- def self.parse(cron_schedule)
9
- days_of_week = {0 => "Sunday", 1 => "Monday", 2 => "Tuesday", 3 => "Wednesday", 4 => "Thursday", 5 => "Friday", 6 => "Saturday"}
8
+ def self.ordinalize(number)
9
+ num = number.to_i
10
+ case num % 100
11
+ when 11, 12, 13
12
+ "#{num}th"
13
+ else
14
+ case num % 10
15
+ when 1
16
+ "#{num}st"
17
+ when 2
18
+ "#{num}nd"
19
+ when 3
20
+ "#{num}rd"
21
+ else
22
+ "#{num}th"
23
+ end
24
+ end
25
+ end
26
+
27
+ def self.parse_field(field, field_type)
28
+ return "*" if field == "*"
29
+
30
+ # Handle step values (e.g., "*/5", "10-30/5")
31
+ if field.include?("/")
32
+ base, step = field.split("/")
33
+ step_val = step.to_i
34
+
35
+ if base == "*"
36
+ case field_type
37
+ when :minute
38
+ return "every #{step_val} minutes"
39
+ when :hour
40
+ return "every #{step_val} hours"
41
+ when :day_of_month
42
+ return "every #{step_val} days"
43
+ when :month
44
+ return "every #{step_val} months"
45
+ when :day_of_week
46
+ return "every #{step_val} days of the week"
47
+ end
48
+ elsif base.include?("-")
49
+ start_val, end_val = base.split("-").map(&:to_i)
50
+ case field_type
51
+ when :minute
52
+ return "every #{step_val} minutes from #{start_val} to #{end_val}"
53
+ when :hour
54
+ return "every #{step_val} hours from #{start_val} to #{end_val}"
55
+ else
56
+ return "every #{step_val} from #{start_val} to #{end_val}"
57
+ end
58
+ end
59
+ end
60
+
61
+ # Handle ranges (e.g., "1-5")
62
+ if field.include?("-") && !field.include?("/")
63
+ start_val, end_val = field.split("-").map(&:to_i)
64
+ case field_type
65
+ when :day_of_week
66
+ days = {0 => "Sunday", 1 => "Monday", 2 => "Tuesday", 3 => "Wednesday", 4 => "Thursday", 5 => "Friday", 6 => "Saturday", 7 => "Sunday"}
67
+ return "#{days[start_val]} through #{days[end_val]}"
68
+ when :month
69
+ months = {1 => "January", 2 => "February", 3 => "March", 4 => "April", 5 => "May", 6 => "June",
70
+ 7 => "July", 8 => "August", 9 => "September", 10 => "October", 11 => "November", 12 => "December"}
71
+ return "#{months[start_val]} through #{months[end_val]}"
72
+ else
73
+ return "#{start_val}-#{end_val}"
74
+ end
75
+ end
76
+
77
+ # Handle lists (e.g., "0,15,30,45")
78
+ if field.include?(",")
79
+ values = field.split(",").map(&:to_i)
80
+ case field_type
81
+ when :day_of_week
82
+ days = {0 => "Sunday", 1 => "Monday", 2 => "Tuesday", 3 => "Wednesday", 4 => "Thursday", 5 => "Friday", 6 => "Saturday", 7 => "Sunday"}
83
+ day_names = values.map { |v| days[v] }.compact
84
+ return day_names.join(", ")
85
+ when :hour
86
+ return values.map { |v|
87
+ display_hour = v > 12 ? v - 12 : (v == 0 ? 12 : v)
88
+ "#{display_hour}:00 #{v >= 12 ? 'PM' : 'AM'}"
89
+ }.join(", ")
90
+ when :minute
91
+ return values.join(", ")
92
+ else
93
+ return values.join(", ")
94
+ end
95
+ end
96
+
97
+ field
98
+ end
10
99
 
11
- minute, hour, day_of_month, month, day_of_week = cron_schedule.split(" ")
100
+ def self.validate_cron_field(field, min_val, max_val, field_name)
101
+ return true if field == "*"
102
+
103
+ # Handle step values
104
+ if field.include?("/")
105
+ base, step = field.split("/")
106
+ return false if step.to_i <= 0
107
+ return validate_cron_field(base, min_val, max_val, field_name) if base != "*"
108
+ return true
109
+ end
110
+
111
+ # Handle ranges
112
+ if field.include?("-")
113
+ start_val, end_val = field.split("-").map(&:to_i)
114
+ return false if start_val < min_val || end_val > max_val || start_val > end_val
115
+ return true
116
+ end
117
+
118
+ # Handle lists
119
+ if field.include?(",")
120
+ values = field.split(",").map(&:to_i)
121
+ return values.all? { |v| v >= min_val && v <= max_val }
122
+ end
123
+
124
+ # Handle single values
125
+ val = field.to_i
126
+ val >= min_val && val <= max_val
127
+ end
128
+
129
+ def self.parse(cron_schedule)
130
+ raise Error, "Cron schedule cannot be nil or empty" if cron_schedule.nil? || cron_schedule.strip.empty?
131
+
132
+ fields = cron_schedule.strip.split(/\s+/)
133
+ raise Error, "Invalid cron format. Expected 5 fields (minute hour day_of_month month day_of_week)" unless fields.length == 5
134
+
135
+ minute, hour, day_of_month, month, day_of_week = fields
136
+
137
+ # Validate each field
138
+ raise Error, "Invalid minute field: #{minute}" unless validate_cron_field(minute, 0, 59, "minute")
139
+ raise Error, "Invalid hour field: #{hour}" unless validate_cron_field(hour, 0, 23, "hour")
140
+ raise Error, "Invalid day of month field: #{day_of_month}" unless validate_cron_field(day_of_month, 1, 31, "day_of_month")
141
+ raise Error, "Invalid month field: #{month}" unless validate_cron_field(month, 1, 12, "month")
142
+ raise Error, "Invalid day of week field: #{day_of_week}" unless validate_cron_field(day_of_week, 0, 7, "day_of_week")
143
+
144
+ days_of_week = {0 => "Sunday", 1 => "Monday", 2 => "Tuesday", 3 => "Wednesday", 4 => "Thursday", 5 => "Friday", 6 => "Saturday", 7 => "Sunday"}
12
145
 
13
146
  time_string = ""
14
147
  day_string = ""
15
148
  month_string = ""
16
149
 
17
150
  # For time string
18
- if minute == "0" && hour != "*"
19
- time_string = "At #{hour.to_i}:00 AM"
20
- elsif minute != "*" && hour != "*"
151
+ if minute == "0" && hour != "*" && !hour.include?(",") && !hour.include?("-") && !hour.include?("/")
21
152
  hour_val = hour.to_i
22
- time_string = "At #{hour_val > 12 ? hour_val - 12 : hour_val}:#{minute.to_i.to_s.rjust(2, '0')} #{hour_val >= 12 ? 'PM' : 'AM'}"
153
+ time_string = "At #{hour_val > 12 ? hour_val - 12 : (hour_val == 0 ? 12 : hour_val)}:00 #{hour_val >= 12 ? 'PM' : 'AM'}"
154
+ elsif minute != "*" && hour != "*" && !minute.include?(",") && !hour.include?(",") && !hour.include?("-") && !hour.include?("/") && !minute.include?("-") && !minute.include?("/")
155
+ hour_val = hour.to_i
156
+ display_hour = hour_val > 12 ? hour_val - 12 : (hour_val == 0 ? 12 : hour_val)
157
+ time_string = "At #{display_hour}:#{minute.to_i.to_s.rjust(2, '0')} #{hour_val >= 12 ? 'PM' : 'AM'}"
23
158
  elsif minute != "*" && hour == "*"
159
+ parsed_minute = parse_field(minute, :minute)
160
+ if parsed_minute.include?("every")
161
+ time_string = "#{parsed_minute.capitalize}"
162
+ elsif minute.include?(",")
163
+ time_string = "At #{parsed_minute} minutes past the hour"
164
+ else
165
+ time_string = "At #{minute.to_i} minutes past the hour"
166
+ end
167
+ elsif minute == "*" && hour != "*" && !hour.include?(",") && !hour.include?("-") && !hour.include?("/")
168
+ hour_val = hour.to_i
169
+ display_hour = hour_val > 12 ? hour_val - 12 : (hour_val == 0 ? 12 : hour_val)
170
+ time_string = "Every minute during #{display_hour}:00 #{hour_val >= 12 ? 'PM' : 'AM'}"
171
+ elsif minute == "*" && hour == "*"
172
+ time_string = "Every minute"
173
+ elsif minute == "0" && hour.include?("/")
174
+ parsed_hour = parse_field(hour, :hour)
175
+ time_string = "#{parsed_hour.capitalize}"
176
+ elsif minute == "0" && hour.include?(",")
177
+ parsed_hour = parse_field(hour, :hour)
178
+ time_string = "At #{parsed_hour}"
179
+ elsif minute != "*" && hour.include?("-") && !minute.include?(",") && !minute.include?("-") && !minute.include?("/")
24
180
  time_string = "At #{minute.to_i} minutes past the hour"
25
181
  end
26
182
 
@@ -30,15 +186,25 @@ module CronDescriber
30
186
  elsif day_of_week =~ /\d#(\d)/
31
187
  week_num = $1
32
188
  day_num = day_of_week[0].to_i
33
- day_string = ", on the #{week_num.ordinalize} #{days_of_week[day_num]} of the month"
189
+ day_string = ", on the #{ordinalize(week_num)} #{days_of_week[day_num]} of the month"
34
190
  elsif day_of_week != "*"
35
- day_num = day_of_week.to_i
36
- day_string = ", only on #{days_of_week[day_num]}"
191
+ parsed_dow = parse_field(day_of_week, :day_of_week)
192
+ if parsed_dow.include?(",") || parsed_dow.include?("through")
193
+ day_string = ", only on #{parsed_dow}"
194
+ else
195
+ day_num = day_of_week.to_i
196
+ day_string = ", only on #{days_of_week[day_num]}"
197
+ end
37
198
  end
38
199
 
39
200
  # For month string
40
- if month != "*" && month != "*/1"
41
- month_string = ", every #{month.gsub('*/','')} months"
201
+ if month != "*"
202
+ parsed_month = parse_field(month, :month)
203
+ if parsed_month != month && parsed_month != "*"
204
+ month_string = ", #{parsed_month}"
205
+ elsif month.include?("/") && month != "*/1"
206
+ month_string = ", #{parsed_month}"
207
+ end
42
208
  end
43
209
 
44
210
  return "#{time_string}#{day_string}#{month_string}"
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cron_describer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksander Lopez Yazikov
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2023-09-08 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: rspec
@@ -37,7 +36,6 @@ files:
37
36
  - LICENSE.txt
38
37
  - README.md
39
38
  - Rakefile
40
- - cron_describer.gemspec
41
39
  - lib/cron_describer.rb
42
40
  - lib/cron_describer/version.rb
43
41
  - sig/cron_describer.rbs
@@ -48,7 +46,6 @@ metadata:
48
46
  homepage_uri: https://github.com/logicalgroove/cron_describer
49
47
  source_code_uri: https://github.com/logicalgroove/cron_describer
50
48
  changelog_uri: https://github.com/logicalgroove/cron_describer
51
- post_install_message:
52
49
  rdoc_options: []
53
50
  require_paths:
54
51
  - lib
@@ -63,8 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
63
60
  - !ruby/object:Gem::Version
64
61
  version: '0'
65
62
  requirements: []
66
- rubygems_version: 3.2.32
67
- signing_key:
63
+ rubygems_version: 3.6.9
68
64
  specification_version: 4
69
65
  summary: A gem to convert cron schedule strings into human-readable time
70
66
  test_files: []
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/cron_describer/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "cron_describer"
7
- spec.version = CronDescriber::VERSION
8
- spec.authors = ["Aleksander Lopez Yazikov"]
9
- spec.email = ["webodessa@gmail.com"]
10
-
11
- spec.summary = "A gem to convert cron schedule strings into human-readable time"
12
- spec.description = "A Ruby gem that takes a cron schedule string and converts it into a human-readable time description."
13
- spec.homepage = "https://github.com/logicalgroove/cron_describer"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 2.6.0"
16
-
17
- spec.metadata["homepage_uri"] = spec.homepage
18
- spec.metadata["source_code_uri"] = "https://github.com/logicalgroove/cron_describer"
19
- spec.metadata["changelog_uri"] = "https://github.com/logicalgroove/cron_describer"
20
-
21
- # Specify which files should be added to the gem when it is released.
22
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
- spec.files = Dir.chdir(__dir__) do
24
- `git ls-files -z`.split("\x0").reject do |f|
25
- (File.expand_path(f) == __FILE__) ||
26
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
27
- end
28
- end
29
- spec.bindir = "exe"
30
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
- spec.require_paths = ["lib"]
32
-
33
- spec.add_development_dependency "rspec", "~> 3.0"
34
- end