english-to-cron 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a8dd87f218cf1f44ff1b8c48fa06dd08735742231ec60b46cde398642151489e
4
+ data.tar.gz: 0e2be7f5fcd12eb57313c98b7dc6aaa1124b096e9c7fe64ff6796bd28efaa521
5
+ SHA512:
6
+ metadata.gz: 74ee1e670e395566e373fcc319b95af53d7d1a6921d3653ed59084dbe671faada7104708319a0d439329f90c600b1ad9182d68cece6c87d4d9b5efa78871a807
7
+ data.tar.gz: f1327e5943f8ae87f71ada74d2d799a2092d6fcfa02ccccb307be1d50f6d9e0669aeadf52a74f0e3e92a5e821156131fe7be26a81baa6b8f7b3fcb2fbb2b6140
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2025-04-06
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Vlad Dyachenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # English to CronJob Syntax Converter
2
+
3
+
4
+
5
+ `english-to-cron` converts natural language into cron expressions, allowing developers to easily schedule cron jobs using English text.
6
+
7
+ ## Features
8
+
9
+ - Converts various English text descriptions into cron job syntax
10
+ - Supports complex patterns including specific days, time ranges, and more
11
+ - Handles multiple time formats including AM/PM and 24-hour notation
12
+ - Zero dependencies
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem 'english-to-cron'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ $ bundle install
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ $ gem install english-to-cron
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ Simply provide an English phrase describing the schedule, and the library will return the corresponding cron job syntax.
37
+
38
+ ```ruby
39
+ require 'english_to_cron'
40
+
41
+ # Basic usage
42
+ EnglishToCron.parse("every 15 seconds") # => "0/15 * * * * ? *"
43
+ EnglishToCron.parse("every minute") # => "0 * * * * ? *"
44
+ EnglishToCron.parse("every day at 4:00 pm") # => "0 0 16 */1 * ? *"
45
+ EnglishToCron.parse("at 10:00 am") # => "0 0 10 * * ? *"
46
+ EnglishToCron.parse("Run at midnight on the 1st and 15th of the month") # => "0 0 0 1,15 * ? *"
47
+ EnglishToCron.parse("on Sunday at 12:00") # => "0 0 12 ? * SUN *"
48
+ ```
49
+
50
+ ## Full List of Supported English Patterns
51
+
52
+ | English Phrase | CronJob Syntax |
53
+ |------------------------------------------------------------------ |---------------------------- |
54
+ | every 15 seconds | 0/15 * * * * ? * |
55
+ | run every minute | 0 * * * * ? * |
56
+ | fire every day at 4:00 pm | 0 0 16 */1 * ? * |
57
+ | at 10:00 am | 0 0 10 * * ? * |
58
+ | run at midnight on the 1st and 15th of the month | 0 0 0 1,15 * ? * |
59
+ | On Sunday at 12:00 | 0 0 12 ? * SUN * |
60
+ | 7pm every Thursday | 0 0 19 ? * THU * |
61
+ | midnight on Tuesdays | 0 0 0 ? * TUE * |
62
+
63
+ ## Error Handling
64
+
65
+ The library will raise errors for invalid or unparseable inputs:
66
+
67
+ ```ruby
68
+ begin
69
+ EnglishToCron.parse("invalid input")
70
+ rescue EnglishToCron::Error => e
71
+ puts "Error: #{e.message}"
72
+ end
73
+ ```
74
+
75
+ ## Contributing
76
+
77
+ Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration.
78
+
79
+ ## License
80
+
81
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,400 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "stack"
4
+
5
+ module EnglishToCron
6
+ module Actions
7
+ module Kind
8
+ FREQUENCY_WITH = :frequency_with
9
+ FREQUENCY_ONLY = :frequency_only
10
+ CLOCK_TIME = :clock_time
11
+ DAY = :day
12
+ SECOND = :second
13
+ MINUTE = :minute
14
+ HOUR = :hour
15
+ MONTH = :month
16
+ YEAR = :year
17
+ RANGE_START = :range_start
18
+ RANGE_END = :range_end
19
+ DAY_OF_MONTH = :day_of_month
20
+
21
+ ALL = [
22
+ FREQUENCY_WITH,
23
+ FREQUENCY_ONLY,
24
+ CLOCK_TIME,
25
+ DAY,
26
+ SECOND,
27
+ MINUTE,
28
+ HOUR,
29
+ MONTH,
30
+ YEAR,
31
+ RANGE_START,
32
+ RANGE_END,
33
+ DAY_OF_MONTH
34
+ ].freeze
35
+ end
36
+
37
+ def self.try_from_token(token)
38
+ Kind::ALL.each do |kind|
39
+ is_match = case kind
40
+ when Kind::FREQUENCY_WITH then frequency_with_match?(token)
41
+ when Kind::FREQUENCY_ONLY then frequency_only_match?(token)
42
+ when Kind::CLOCK_TIME then clock_time_match?(token)
43
+ when Kind::DAY then day_match?(token)
44
+ when Kind::SECOND then second_match?(token)
45
+ when Kind::MINUTE then minute_match?(token)
46
+ when Kind::HOUR then hour_match?(token)
47
+ when Kind::MONTH then month_match?(token)
48
+ when Kind::YEAR then year_match?(token)
49
+ when Kind::RANGE_START then range_start_match?(token)
50
+ when Kind::RANGE_END then range_end_match?(token)
51
+ when Kind::DAY_OF_MONTH then day_of_month_match?(token)
52
+ end
53
+ return kind if is_match
54
+ end
55
+ nil
56
+ end
57
+
58
+ def self.process(kind, token, cron)
59
+ case kind
60
+ when Kind::FREQUENCY_WITH then process_frequency_with(token, cron)
61
+ when Kind::FREQUENCY_ONLY
62
+ frequency = parse_number(token, "frequency_only")
63
+ process_frequency_only(frequency, cron)
64
+ when Kind::CLOCK_TIME then process_clock_time(token, cron)
65
+ when Kind::DAY then process_day(token, cron)
66
+ when Kind::SECOND then process_second(token, cron)
67
+ when Kind::MINUTE then process_minute(token, cron)
68
+ when Kind::HOUR then process_hour(token, cron)
69
+ when Kind::MONTH then process_month(token, cron)
70
+ when Kind::YEAR then process_year(token, cron)
71
+ when Kind::RANGE_START then process_range_start(cron)
72
+ when Kind::RANGE_END then process_range_end(cron)
73
+ when Kind::DAY_OF_MONTH then process_day_of_month(token, cron)
74
+ end
75
+ end
76
+
77
+ # Token matching methods
78
+ def self.frequency_with_match?(token)
79
+ token.match?(/\b(?:every|each)\b/i)
80
+ end
81
+
82
+ def self.frequency_only_match?(token)
83
+ token.match?(/\A\d+\z/)
84
+ end
85
+
86
+ def self.clock_time_match?(token)
87
+ token.match?(/(\d+:\d+|\d+(?:am|pm)|noon|midnight)/i)
88
+ end
89
+
90
+ def self.day_match?(token)
91
+ days = %w[monday tuesday wednesday thursday friday saturday sunday mon tue wed thu fri sat sun weekend]
92
+ token.match?(/\b(?:#{days.join('|')}|days?)\b/i)
93
+ end
94
+
95
+ def self.day_of_month_match?(token)
96
+ token.match?(/\b\d+(?:st|nd|rd|th)\b/i)
97
+ end
98
+
99
+ def self.second_match?(token)
100
+ token.match?(/\b(?:seconds?|secs?)\b/i)
101
+ end
102
+
103
+ def self.minute_match?(token)
104
+ token.match?(/\b(?:minutes?|mins?|min)\b/i)
105
+ end
106
+
107
+ def self.hour_match?(token)
108
+ token.match?(/\b(?:hours?|hrs?)\b/i)
109
+ end
110
+
111
+ def self.month_match?(token)
112
+ months = %w[january february march april may june july august september october november december jan feb mar apr may jun jul aug sep sept oct nov dec]
113
+ token.match?(/\b(?:#{months.join('|')}|months?)\b/i)
114
+ end
115
+
116
+ def self.year_match?(token)
117
+ token.match?(/\d{4}/)
118
+ end
119
+
120
+ def self.range_start_match?(token)
121
+ token.match?(/\b(?:between|starting|start)\b/i)
122
+ end
123
+
124
+ def self.range_end_match?(token)
125
+ token.match?(/\b(?:to|through|ending|end|and)\b/i)
126
+ end
127
+
128
+ # Processing methods
129
+ def self.parse_number(token, state)
130
+ Integer(token)
131
+ rescue ArgumentError
132
+ raise Error::ParseToNumber.new(state, token)
133
+ end
134
+
135
+ # Helper method to extract day number
136
+ def self.extract_day_number(token)
137
+ if token.match?(/\b(\d+)(?:st|nd|rd|th)\b/i)
138
+ token.match(/\b(\d+)(?:st|nd|rd|th)\b/i)[1].to_i
139
+ else
140
+ nil
141
+ end
142
+ end
143
+
144
+ # Implement process methods for each action type
145
+ def self.process_frequency_with(token, cron)
146
+ # Mark the current stack position for frequency handling
147
+ cron.stack << Stack.builder(Kind::FREQUENCY_WITH).build
148
+ true
149
+ end
150
+
151
+ def self.process_frequency_only(frequency, cron)
152
+ # Find the last FREQUENCY_WITH stack or add directly if none
153
+ frequency_with_stack = cron.stack.find { |s| s.owner == Kind::FREQUENCY_WITH }
154
+
155
+ if frequency_with_stack
156
+ frequency_with_stack.frequency = frequency
157
+ else
158
+ # Stack for frequency
159
+ stack = Stack.builder(Kind::FREQUENCY_ONLY)
160
+ .frequency(frequency)
161
+ .build
162
+
163
+ cron.stack << stack
164
+ end
165
+ end
166
+
167
+ def self.process_clock_time(token, cron)
168
+ min = 0
169
+ hour = 0
170
+
171
+ # Process special times like noon and midnight
172
+ if token.match?(/noon/i)
173
+ hour = 12
174
+ elsif token.match?(/midnight/i)
175
+ hour = 0
176
+ else
177
+ # Process normal clock times
178
+ if token.match?(/(am|pm)/i)
179
+ parts = token.gsub(/[^0-9:]/, '').split(':')
180
+ if parts.length == 1
181
+ hour = parts[0].to_i
182
+ hour = 0 if hour == 12
183
+ hour += 12 if token.match?(/pm/i) && hour != 12
184
+ elsif parts.length == 2
185
+ hour = parts[0].to_i
186
+ min = parts[1].to_i
187
+ hour = 0 if hour == 12 && token.match?(/am/i)
188
+ hour += 12 if token.match?(/pm/i) && hour != 12
189
+ end
190
+ else # 24-hour format
191
+ parts = token.split(':')
192
+ if parts.length == 2
193
+ hour = parts[0].to_i
194
+ min = parts[1].to_i
195
+ end
196
+ end
197
+ end
198
+
199
+ # Create a stack item for clock time
200
+ min_range = StartEnd.new(min)
201
+ hour_range = StartEnd.new(hour)
202
+
203
+ stack = Stack.builder(Kind::CLOCK_TIME)
204
+ .min(min_range)
205
+ .hour(hour_range)
206
+ .build
207
+
208
+ cron.stack << stack
209
+ true
210
+ end
211
+
212
+ def self.process_day(token, cron)
213
+ day_map = {
214
+ 'sunday' => 'SUN', 'sun' => 'SUN',
215
+ 'monday' => 'MON', 'mon' => 'MON',
216
+ 'tuesday' => 'TUE', 'tue' => 'TUE',
217
+ 'wednesday' => 'WED', 'wed' => 'WED',
218
+ 'thursday' => 'THU', 'thu' => 'THU',
219
+ 'friday' => 'FRI', 'fri' => 'FRI',
220
+ 'saturday' => 'SAT', 'sat' => 'SAT',
221
+ 'weekend' => 'SUN,SAT'
222
+ }
223
+
224
+ day_of_week = nil
225
+
226
+ day_map.each do |key, value|
227
+ if token.match?(/\b#{key}\b/i)
228
+ day_of_week = value
229
+ break
230
+ end
231
+ end
232
+
233
+ if day_of_week
234
+ stack = Stack.builder(Kind::DAY)
235
+ .day_of_week(day_of_week)
236
+ .build
237
+
238
+ cron.stack << stack
239
+ end
240
+
241
+ true
242
+ end
243
+
244
+ def self.process_day_of_month(token, cron)
245
+ day_num = extract_day_number(token)
246
+
247
+ if day_num
248
+ day_string = StartEndString.new(day_num.to_s)
249
+
250
+ stack = Stack.builder(Kind::DAY_OF_MONTH)
251
+ .day(day_string)
252
+ .build
253
+
254
+ cron.stack << stack
255
+ end
256
+
257
+ true
258
+ end
259
+
260
+ def self.process_second(token, cron)
261
+ # Look for a frequency in the last stack item
262
+ frequency_stack = cron.stack.reverse.find { |s| s.owner == Kind::FREQUENCY_WITH && s.frequency }
263
+ last_frequency = cron.stack.reverse.find { |s| s.owner == Kind::FREQUENCY_ONLY }
264
+
265
+ if frequency_stack && frequency_stack.frequency
266
+ stack = Stack.builder(Kind::SECOND)
267
+ .frequency(frequency_stack.frequency)
268
+ .build
269
+
270
+ cron.stack << stack
271
+ elsif last_frequency && last_frequency.frequency
272
+ stack = Stack.builder(Kind::SECOND)
273
+ .frequency(last_frequency.frequency)
274
+ .build
275
+
276
+ cron.stack << stack
277
+ else
278
+ stack = Stack.builder(Kind::SECOND).build
279
+ cron.stack << stack
280
+ end
281
+
282
+ true
283
+ end
284
+
285
+ def self.process_minute(token, cron)
286
+ # Look for a frequency in the last stack item
287
+ frequency_stack = cron.stack.reverse.find { |s| s.owner == Kind::FREQUENCY_WITH && s.frequency }
288
+ last_frequency = cron.stack.reverse.find { |s| s.owner == Kind::FREQUENCY_ONLY }
289
+
290
+ if frequency_stack && frequency_stack.frequency
291
+ stack = Stack.builder(Kind::MINUTE)
292
+ .frequency(frequency_stack.frequency)
293
+ .build
294
+
295
+ cron.stack << stack
296
+ elsif last_frequency && last_frequency.frequency
297
+ stack = Stack.builder(Kind::MINUTE)
298
+ .frequency(last_frequency.frequency)
299
+ .build
300
+
301
+ cron.stack << stack
302
+ else
303
+ stack = Stack.builder(Kind::MINUTE).build
304
+ cron.stack << stack
305
+ end
306
+
307
+ true
308
+ end
309
+
310
+ def self.process_hour(token, cron)
311
+ # Look for a frequency in the last stack item
312
+ frequency_stack = cron.stack.reverse.find { |s| s.owner == Kind::FREQUENCY_WITH && s.frequency }
313
+ last_frequency = cron.stack.reverse.find { |s| s.owner == Kind::FREQUENCY_ONLY }
314
+
315
+ if frequency_stack && frequency_stack.frequency
316
+ stack = Stack.builder(Kind::HOUR)
317
+ .frequency(frequency_stack.frequency)
318
+ .build
319
+
320
+ cron.stack << stack
321
+ elsif last_frequency && last_frequency.frequency
322
+ stack = Stack.builder(Kind::HOUR)
323
+ .frequency(last_frequency.frequency)
324
+ .build
325
+
326
+ cron.stack << stack
327
+ else
328
+ stack = Stack.builder(Kind::HOUR).build
329
+ cron.stack << stack
330
+ end
331
+
332
+ true
333
+ end
334
+
335
+ def self.process_month(token, cron)
336
+ month_map = {
337
+ 'january' => '1', 'jan' => '1',
338
+ 'february' => '2', 'feb' => '2',
339
+ 'march' => '3', 'mar' => '3',
340
+ 'april' => '4', 'apr' => '4',
341
+ 'may' => '5',
342
+ 'june' => '6', 'jun' => '6',
343
+ 'july' => '7', 'jul' => '7',
344
+ 'august' => '8', 'aug' => '8',
345
+ 'september' => '9', 'sep' => '9', 'sept' => '9',
346
+ 'october' => '10', 'oct' => '10',
347
+ 'november' => '11', 'nov' => '11',
348
+ 'december' => '12', 'dec' => '12'
349
+ }
350
+
351
+ month_value = nil
352
+
353
+ month_map.each do |key, value|
354
+ if token.match?(/\b#{key}\b/i)
355
+ month_value = value
356
+ break
357
+ end
358
+ end
359
+
360
+ if month_value
361
+ month_range = StartEndString.new(month_value)
362
+ stack = Stack.builder(Kind::MONTH)
363
+ .month(month_range)
364
+ .build
365
+
366
+ cron.stack << stack
367
+ end
368
+
369
+ true
370
+ end
371
+
372
+ def self.process_year(token, cron)
373
+ # We assume the token contains a year like "2024"
374
+ year = token.match(/(\d{4})/)&.[](1)&.to_i
375
+
376
+ if year
377
+ year_range = StartEnd.new(year)
378
+ stack = Stack.builder(Kind::YEAR)
379
+ .frequency(year)
380
+ .build
381
+
382
+ cron.stack << stack
383
+ end
384
+
385
+ true
386
+ end
387
+
388
+ def self.process_range_start(cron)
389
+ # Mark the current position for range processing
390
+ # Will be processed in process_stack
391
+ true
392
+ end
393
+
394
+ def self.process_range_end(cron)
395
+ # Process the range based on previous tokens
396
+ # Will be processed in process_stack
397
+ true
398
+ end
399
+ end
400
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "syntax"
4
+ require_relative "tokenizer"
5
+ require_relative "actions"
6
+
7
+ module EnglishToCron
8
+ class Cron
9
+ attr_accessor :syntax, :stack, :original_text
10
+
11
+ def initialize
12
+ @syntax = Syntax.new
13
+ @stack = []
14
+ @original_text = nil
15
+ end
16
+
17
+ def self.parse(text)
18
+ if text.nil? || text.to_s.strip.empty?
19
+ raise Error::InvalidInput
20
+ end
21
+
22
+ cron = new
23
+ cron.original_text = text.to_s.downcase
24
+
25
+ tokenizer = Tokenizer.new
26
+ tokens = tokenizer.run(text)
27
+
28
+ if tokens.empty?
29
+ raise Error::InvalidInput
30
+ end
31
+
32
+ tokens.each do |token|
33
+ if kind = Actions.try_from_token(token)
34
+ Actions.process(kind, token, cron)
35
+ end
36
+ end
37
+
38
+ cron.process_stack
39
+
40
+ cron.to_s
41
+ end
42
+
43
+ def process_stack
44
+ detect_day_patterns
45
+
46
+ process_seconds
47
+ process_minutes
48
+ process_hours
49
+ process_day_of_month
50
+ process_months
51
+ # process_day_of_week
52
+ process_years
53
+ end
54
+
55
+ def detect_day_patterns
56
+ if @original_text =~ /(every|each) day at/i
57
+ @syntax.day_of_month = "*/1"
58
+ end
59
+ end
60
+
61
+ def process_seconds
62
+ second_items = @stack.select { |s| s.owner == Actions::Kind::SECOND }
63
+ if second_items.any? { |s| s.frequency }
64
+ # For patterns like "every 15 seconds"
65
+ @syntax.seconds = "0/#{second_items.first.frequency}"
66
+ end
67
+ end
68
+
69
+ def process_minutes
70
+ minute_items = @stack.select { |s| s.owner == Actions::Kind::MINUTE }
71
+ frequency_items = @stack.select { |s| s.owner == Actions::Kind::FREQUENCY_WITH }
72
+
73
+ if has_minute_pattern? && !has_any_time_pattern?
74
+ # "every minute" pattern
75
+ @syntax.min = "*"
76
+ elsif minute_items.any? { |s| s.frequency }
77
+ # For patterns with specific minute frequencies
78
+ @syntax.min = "0/#{minute_items.first.frequency}"
79
+ elsif @stack.any? { |s| s.owner == Actions::Kind::CLOCK_TIME }
80
+ # Minutes are already set by clock time processing
81
+ elsif !@stack.any? { |s| s.owner == Actions::Kind::MINUTE }
82
+ # Keep default
83
+ end
84
+ end
85
+
86
+ def process_hours
87
+ hour_items = @stack.select { |s| s.owner == Actions::Kind::HOUR }
88
+ clock_items = @stack.select { |s| s.owner == Actions::Kind::CLOCK_TIME }
89
+
90
+ if hour_items.any? { |s| s.frequency }
91
+ # For patterns with specific hour frequencies
92
+ @syntax.hour = "0/#{hour_items.first.frequency}"
93
+ elsif clock_items.any?
94
+ # For patterns with specific clock times
95
+ clock_item = clock_items.first
96
+ if clock_item.hour && clock_item.hour.start
97
+ @syntax.hour = clock_item.hour.start.to_s
98
+ # Default minutes to 0 if not specified
99
+ if clock_item.min && clock_item.min.start
100
+ @syntax.min = clock_item.min.start.to_s
101
+ else
102
+ @syntax.min = "0"
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def process_day_of_month
109
+ day_items = @stack.select { |s| s.owner == Actions::Kind::DAY }
110
+ day_of_month_items = @stack.select { |s| s.owner == Actions::Kind::DAY_OF_MONTH }
111
+
112
+ # Process day of week first
113
+ if day_items.any? { |s| s.day_of_week }
114
+ @syntax.day_of_week = day_items.first.day_of_week
115
+ @syntax.day_of_month = "?"
116
+ return
117
+ end
118
+
119
+ # Process specific days of month
120
+ if day_of_month_items.any?
121
+ day_values = day_of_month_items.map { |s| s.day.start if s.day && s.day.start }.compact
122
+
123
+ if day_values.any?
124
+ # Handle multiple days like "1,15"
125
+ if @original_text && @original_text.match?(/1st.+15th|15th.+1st/)
126
+ @syntax.day_of_month = "1,15"
127
+ else
128
+ @syntax.day_of_month = day_values.join(',')
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ def process_months
135
+ # Handle months
136
+ month_items = @stack.select { |s| s.owner == Actions::Kind::MONTH }
137
+
138
+ if month_items.any? { |s| s.month && s.month.start }
139
+ month_values = month_items.map { |s| s.month.start if s.month }.compact
140
+ @syntax.month = month_values.join(',')
141
+ end
142
+ end
143
+
144
+ def process_years
145
+ year_items = @stack.select { |s| s.owner == Actions::Kind::YEAR }
146
+
147
+ if year_items.any? { |s| s.frequency }
148
+ @syntax.year = year_items.first.frequency.to_s
149
+ end
150
+ end
151
+
152
+ def has_minute_pattern?
153
+ # Check if we have a pattern like "every minute"
154
+ minute_pattern = @stack.any? { |s| s.owner == Actions::Kind::MINUTE }
155
+ frequency_pattern = @stack.any? { |s| s.owner == Actions::Kind::FREQUENCY_WITH }
156
+
157
+ frequency_pattern && minute_pattern
158
+ end
159
+
160
+ def has_any_time_pattern?
161
+ # Check if there's any time specification
162
+ @stack.any? { |s| s.owner == Actions::Kind::CLOCK_TIME || s.owner == Actions::Kind::HOUR }
163
+ end
164
+
165
+ def has_every_day_pattern?
166
+ # Check for patterns like "every day at X"
167
+ frequency_pattern = @stack.any? { |s| s.owner == Actions::Kind::FREQUENCY_WITH }
168
+ day_pattern = @stack.any? { |s|
169
+ s.owner == Actions::Kind::DAY &&
170
+ (s.day_of_week.nil? || @original_text&.match?(/every day|each day/i))
171
+ }
172
+
173
+ frequency_pattern && day_pattern
174
+ end
175
+
176
+ def to_s
177
+ @syntax.to_s
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnglishToCron
4
+ class Error < StandardError
5
+ class InvalidInput < Error
6
+ def initialize(msg = "Please enter human readable text")
7
+ super(msg)
8
+ end
9
+ end
10
+
11
+ class Capture < Error
12
+ attr_reader :state, :token
13
+
14
+ def initialize(state, token)
15
+ @state = state
16
+ @token = token
17
+ super("Could not capture: #{token} in state: #{state}")
18
+ end
19
+ end
20
+
21
+ class ParseToNumber < Error
22
+ attr_reader :state, :value
23
+
24
+ def initialize(state, value)
25
+ @state = state
26
+ @value = value
27
+ super("Could not parse: #{value} to number. state: #{state}")
28
+ end
29
+ end
30
+
31
+ class IncorrectValue < Error
32
+ attr_reader :state, :error_description
33
+
34
+ def initialize(state, error_description)
35
+ @state = state
36
+ @error_description = error_description
37
+ super("Value is invalid in state: #{state}. description: #{error_description}")
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnglishToCron
4
+ class StartEnd
5
+ attr_accessor :start, :end
6
+
7
+ def initialize(start = nil, end_val = nil)
8
+ @start = start
9
+ @end = end_val
10
+ end
11
+ end
12
+
13
+ class StartEndString
14
+ attr_accessor :start, :end
15
+
16
+ def initialize(start = nil, end_val = nil)
17
+ @start = start
18
+ @end = end_val
19
+ end
20
+ end
21
+
22
+ class Stack
23
+ attr_accessor :owner, :frequency, :frequency_end, :frequency_start,
24
+ :min, :hour, :day, :month, :year, :day_of_week
25
+
26
+ def initialize(owner)
27
+ @owner = owner
28
+ @frequency = nil
29
+ @frequency_end = nil
30
+ @frequency_start = nil
31
+ @min = nil
32
+ @hour = nil
33
+ @day = nil
34
+ @month = nil
35
+ @year = nil
36
+ @day_of_week = nil
37
+ end
38
+
39
+ def frequency_to_string
40
+ frequency ? frequency.to_s : "*"
41
+ end
42
+
43
+ class Builder
44
+ def initialize(owner)
45
+ @stack = Stack.new(owner)
46
+ end
47
+
48
+ def frequency(frequency)
49
+ @stack.frequency = frequency
50
+ self
51
+ end
52
+
53
+ def min(min)
54
+ @stack.min = min
55
+ self
56
+ end
57
+
58
+ def hour(hour)
59
+ @stack.hour = hour
60
+ self
61
+ end
62
+
63
+ def day(day)
64
+ @stack.day = day
65
+ self
66
+ end
67
+
68
+ def month(month)
69
+ @stack.month = month
70
+ self
71
+ end
72
+
73
+ def day_of_week(day_of_week)
74
+ @stack.day_of_week = day_of_week
75
+ self
76
+ end
77
+
78
+ def build
79
+ @stack
80
+ end
81
+ end
82
+
83
+ def self.builder(owner)
84
+ Builder.new(owner)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnglishToCron
4
+ class Syntax
5
+ attr_accessor :seconds, :min, :hour, :day_of_month, :month, :day_of_week, :year
6
+
7
+ def initialize
8
+ @seconds = "0"
9
+ @min = "*"
10
+ @hour = "*"
11
+ @day_of_month = "*"
12
+ @month = "*"
13
+ @day_of_week = "?"
14
+ @year = "*"
15
+ end
16
+
17
+ def to_s
18
+ "#{seconds} #{min} #{hour} #{day_of_month} #{month} #{day_of_week} #{year}"
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnglishToCron
4
+ class Tokenizer
5
+ TOKEN_PATTERN = /(?i)(?:seconds|second|secs|sec)|(?:hours?|hrs?)|(?:minutes?|mins?|min)|(?:months?|(?:january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sept|oct|nov|dec)(?: ?and)?,? ?)+|[0-9]+(?:th|nd|rd|st)|(?:[0-9]+:)?[0-9]+ ?(?:am|pm)|[0-9]+:[0-9]+|(?:noon|midnight)|(?:days?|(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|weekend|mon|tue|wed|thu|fri|sat|sun)(?: ?and)?,? ?)+|(?:[0-9]{4}[0-9]*(?: ?and)?,? ?)+|[0-9]+|(?:to|through|ending|end|and)|(?:between|starting|start)/
6
+
7
+ def initialize
8
+ @regex = TOKEN_PATTERN
9
+ end
10
+
11
+ def run(input_string)
12
+ input_string.scan(@regex).map { |token| token.strip.downcase }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EnglishToCron
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "english_to_cron/version"
4
+ require_relative "english_to_cron/error"
5
+ require_relative "english_to_cron/tokenizer"
6
+ require_relative "english_to_cron/syntax"
7
+ require_relative "english_to_cron/actions"
8
+ require_relative "english_to_cron/cron"
9
+
10
+ module EnglishToCron
11
+ def self.parse(input)
12
+ Cron.parse(input)
13
+ end
14
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: english-to-cron
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vlad Dyachenko
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-04-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.12'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.12'
41
+ description: A Ruby library that converts various English text descriptions into cron
42
+ job syntax.
43
+ email:
44
+ - gotta.go.vlad@gmail.com
45
+ executables: []
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - CHANGELOG.md
50
+ - LICENSE.txt
51
+ - README.md
52
+ - lib/english_to_cron.rb
53
+ - lib/english_to_cron/actions.rb
54
+ - lib/english_to_cron/cron.rb
55
+ - lib/english_to_cron/error.rb
56
+ - lib/english_to_cron/stack.rb
57
+ - lib/english_to_cron/syntax.rb
58
+ - lib/english_to_cron/tokenizer.rb
59
+ - lib/english_to_cron/version.rb
60
+ homepage: https://github.com/wowinter13/english-to-cron
61
+ licenses:
62
+ - MIT
63
+ metadata:
64
+ homepage_uri: https://github.com/wowinter13/english-to-cron
65
+ source_code_uri: https://github.com/wowinter13/english-to-cron
66
+ changelog_uri: https://github.com/wowinter13/english-to-cron/blob/main/CHANGELOG.md
67
+ post_install_message:
68
+ rdoc_options: []
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 3.0.0
76
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubygems_version: 3.2.33
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: English to Cron Job Syntax Converter
86
+ test_files: []