cronify 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 +7 -0
- data/.DS_Store +0 -0
- data/.idea/.gitignore +10 -0
- data/.idea/copilot.data.migration.ask2agent.xml +6 -0
- data/.idea/cronify.iml +69 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/CHANGELOG.md +11 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +125 -0
- data/Rakefile +18 -0
- data/lib/cronify/cron_emitter.rb +49 -0
- data/lib/cronify/dispatcher.rb +20 -0
- data/lib/cronify/ir.rb +17 -0
- data/lib/cronify/matchers/base.rb +15 -0
- data/lib/cronify/matchers/daily.rb +17 -0
- data/lib/cronify/matchers/ordinal_weekday.rb +40 -0
- data/lib/cronify/matchers/simple_interval.rb +15 -0
- data/lib/cronify/matchers/time_bounded_interval.rb +23 -0
- data/lib/cronify/matchers/weekday_weekend.rb +27 -0
- data/lib/cronify/schedule.rb +65 -0
- data/lib/cronify/time_parser.rb +22 -0
- data/lib/cronify/version.rb +5 -0
- data/lib/cronify.rb +52 -0
- data/rbs_collection.lock.yaml +188 -0
- data/rbs_collection.yaml +19 -0
- data/sig/cronify/cron_emitter.rbs +15 -0
- data/sig/cronify/dispatcher.rbs +7 -0
- data/sig/cronify/ir.rbs +25 -0
- data/sig/cronify/matchers/base.rbs +9 -0
- data/sig/cronify/matchers/daily.rbs +9 -0
- data/sig/cronify/matchers/ordinal_weekday.rbs +11 -0
- data/sig/cronify/matchers/simple_interval.rbs +9 -0
- data/sig/cronify/matchers/time_bounded_interval.rbs +9 -0
- data/sig/cronify/matchers/weekday_weekend.rbs +10 -0
- data/sig/cronify/schedule.rbs +17 -0
- data/sig/cronify/time_parser.rbs +7 -0
- data/sig/cronify.rbs +18 -0
- metadata +113 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f34b1a2a4b9759aacc9c783af0ecd21cc43be73dc917511c23e0cbf69f800beb
|
|
4
|
+
data.tar.gz: 75f9ed65437e7b63061f70ef3f926320d333dce06afa0b2b58622ef4bf22d00b
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 46689461aedea3112bb5927c195b37b88473944409bdc21b6f0babdccc37cb886e3ad92b7d71a01caf9761fb0315179cf8abc6029b023e6b8e9917ea6836aed1
|
|
7
|
+
data.tar.gz: da907b78de358c9a2f1510886f74242e322d9096f2982c118a7ca8afff03955222da2734e8063d5ef9e90e89a14832543a1e0700a4ceb0d2442948d16e89970a
|
data/.DS_Store
ADDED
|
Binary file
|
data/.idea/.gitignore
ADDED
data/.idea/cronify.iml
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="RUBY_MODULE" version="4">
|
|
3
|
+
<component name="ModuleRunConfigurationManager">
|
|
4
|
+
<shared />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="NewModuleRootManager">
|
|
7
|
+
<content url="file://$MODULE_DIR$">
|
|
8
|
+
<sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
|
|
9
|
+
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
|
|
10
|
+
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
|
11
|
+
</content>
|
|
12
|
+
<orderEntry type="jdk" jdkName="mise: 3.4.8" jdkType="RUBY_SDK" />
|
|
13
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
14
|
+
<orderEntry type="library" scope="PROVIDED" name="addressable (v2.8.9, mise: 3.4.8) [gem]" level="application" />
|
|
15
|
+
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.3, mise: 3.4.8) [gem]" level="application" />
|
|
16
|
+
<orderEntry type="library" scope="PROVIDED" name="bigdecimal (v4.0.1, mise: 3.4.8) [gem]" level="application" />
|
|
17
|
+
<orderEntry type="library" scope="PROVIDED" name="bundler (v4.0.8, mise: 3.4.8) [gem]" level="application" />
|
|
18
|
+
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.6, mise: 3.4.8) [gem]" level="application" />
|
|
19
|
+
<orderEntry type="library" scope="PROVIDED" name="date (v3.5.1, mise: 3.4.8) [gem]" level="application" />
|
|
20
|
+
<orderEntry type="library" scope="PROVIDED" name="diff-lcs (v1.6.2, mise: 3.4.8) [gem]" level="application" />
|
|
21
|
+
<orderEntry type="library" scope="PROVIDED" name="docile (v1.4.1, mise: 3.4.8) [gem]" level="application" />
|
|
22
|
+
<orderEntry type="library" scope="PROVIDED" name="erb (v6.0.2, mise: 3.4.8) [gem]" level="application" />
|
|
23
|
+
<orderEntry type="library" scope="PROVIDED" name="et-orbi (v1.4.0, mise: 3.4.8) [gem]" level="application" />
|
|
24
|
+
<orderEntry type="library" scope="PROVIDED" name="fugit (v1.12.1, mise: 3.4.8) [gem]" level="application" />
|
|
25
|
+
<orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.2, mise: 3.4.8) [gem]" level="application" />
|
|
26
|
+
<orderEntry type="library" scope="PROVIDED" name="irb (v1.17.0, mise: 3.4.8) [gem]" level="application" />
|
|
27
|
+
<orderEntry type="library" scope="PROVIDED" name="json (v2.19.1, mise: 3.4.8) [gem]" level="application" />
|
|
28
|
+
<orderEntry type="library" scope="PROVIDED" name="json-schema (v6.2.0, mise: 3.4.8) [gem]" level="application" />
|
|
29
|
+
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.5, mise: 3.4.8) [gem]" level="application" />
|
|
30
|
+
<orderEntry type="library" scope="PROVIDED" name="lint_roller (v1.1.0, mise: 3.4.8) [gem]" level="application" />
|
|
31
|
+
<orderEntry type="library" scope="PROVIDED" name="logger (v1.7.0, mise: 3.4.8) [gem]" level="application" />
|
|
32
|
+
<orderEntry type="library" scope="PROVIDED" name="mcp (v0.8.0, mise: 3.4.8) [gem]" level="application" />
|
|
33
|
+
<orderEntry type="library" scope="PROVIDED" name="parallel (v1.27.0, mise: 3.4.8) [gem]" level="application" />
|
|
34
|
+
<orderEntry type="library" scope="PROVIDED" name="parser (v3.3.10.2, mise: 3.4.8) [gem]" level="application" />
|
|
35
|
+
<orderEntry type="library" scope="PROVIDED" name="pp (v0.6.3, mise: 3.4.8) [gem]" level="application" />
|
|
36
|
+
<orderEntry type="library" scope="PROVIDED" name="prettyprint (v0.2.0, mise: 3.4.8) [gem]" level="application" />
|
|
37
|
+
<orderEntry type="library" scope="PROVIDED" name="prism (v1.9.0, mise: 3.4.8) [gem]" level="application" />
|
|
38
|
+
<orderEntry type="library" scope="PROVIDED" name="psych (v5.3.1, mise: 3.4.8) [gem]" level="application" />
|
|
39
|
+
<orderEntry type="library" scope="PROVIDED" name="public_suffix (v7.0.5, mise: 3.4.8) [gem]" level="application" />
|
|
40
|
+
<orderEntry type="library" scope="PROVIDED" name="raabro (v1.4.0, mise: 3.4.8) [gem]" level="application" />
|
|
41
|
+
<orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, mise: 3.4.8) [gem]" level="application" />
|
|
42
|
+
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, mise: 3.4.8) [gem]" level="application" />
|
|
43
|
+
<orderEntry type="library" scope="PROVIDED" name="rake (v13.3.1, mise: 3.4.8) [gem]" level="application" />
|
|
44
|
+
<orderEntry type="library" scope="TEST" name="rbs (v3.10.3, mise: 3.4.8) [gem]" level="application" />
|
|
45
|
+
<orderEntry type="library" scope="PROVIDED" name="rdoc (v7.2.0, mise: 3.4.8) [gem]" level="application" />
|
|
46
|
+
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.11.3, mise: 3.4.8) [gem]" level="application" />
|
|
47
|
+
<orderEntry type="library" scope="PROVIDED" name="reline (v0.6.3, mise: 3.4.8) [gem]" level="application" />
|
|
48
|
+
<orderEntry type="library" scope="TEST" name="rspec (v3.13.2, mise: 3.4.8) [gem]" level="application" />
|
|
49
|
+
<orderEntry type="library" scope="PROVIDED" name="rspec-core (v3.13.6, mise: 3.4.8) [gem]" level="application" />
|
|
50
|
+
<orderEntry type="library" scope="PROVIDED" name="rspec-expectations (v3.13.5, mise: 3.4.8) [gem]" level="application" />
|
|
51
|
+
<orderEntry type="library" scope="PROVIDED" name="rspec-mocks (v3.13.8, mise: 3.4.8) [gem]" level="application" />
|
|
52
|
+
<orderEntry type="library" scope="PROVIDED" name="rspec-support (v3.13.7, mise: 3.4.8) [gem]" level="application" />
|
|
53
|
+
<orderEntry type="library" scope="TEST" name="rubocop (v1.85.1, mise: 3.4.8) [gem]" level="application" />
|
|
54
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.49.1, mise: 3.4.8) [gem]" level="application" />
|
|
55
|
+
<orderEntry type="library" scope="TEST" name="rubocop-rake (v0.7.1, mise: 3.4.8) [gem]" level="application" />
|
|
56
|
+
<orderEntry type="library" scope="TEST" name="rubocop-rspec (v3.9.0, mise: 3.4.8) [gem]" level="application" />
|
|
57
|
+
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, mise: 3.4.8) [gem]" level="application" />
|
|
58
|
+
<orderEntry type="library" scope="TEST" name="simplecov (v0.22.0, mise: 3.4.8) [gem]" level="application" />
|
|
59
|
+
<orderEntry type="library" scope="PROVIDED" name="simplecov-html (v0.13.2, mise: 3.4.8) [gem]" level="application" />
|
|
60
|
+
<orderEntry type="library" scope="PROVIDED" name="simplecov_json_formatter (v0.1.4, mise: 3.4.8) [gem]" level="application" />
|
|
61
|
+
<orderEntry type="library" scope="PROVIDED" name="stringio (v3.2.0, mise: 3.4.8) [gem]" level="application" />
|
|
62
|
+
<orderEntry type="library" scope="TEST" name="timecop (v0.9.10, mise: 3.4.8) [gem]" level="application" />
|
|
63
|
+
<orderEntry type="library" scope="PROVIDED" name="tsort (v0.2.0, mise: 3.4.8) [gem]" level="application" />
|
|
64
|
+
<orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.6, mise: 3.4.8) [gem]" level="application" />
|
|
65
|
+
<orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v3.2.0, mise: 3.4.8) [gem]" level="application" />
|
|
66
|
+
<orderEntry type="library" scope="PROVIDED" name="unicode-emoji (v4.2.0, mise: 3.4.8) [gem]" level="application" />
|
|
67
|
+
<orderEntry type="library" scope="TEST" name="yard (v0.9.38, mise: 3.4.8) [gem]" level="application" />
|
|
68
|
+
</component>
|
|
69
|
+
</module>
|
data/.idea/modules.xml
ADDED
data/.idea/vcs.xml
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Natural language schedule parser for common patterns (intervals, weekdays, ordinal weekdays)
|
|
5
|
+
- Cron string emitter compatible with Sidekiq, Whenever, and standard cron
|
|
6
|
+
- `next_occurrence` and `next_occurrences(n:)` for calculating future fire times
|
|
7
|
+
- Timezone support via TZInfo
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-03-12
|
|
10
|
+
|
|
11
|
+
- Initial release
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
"cronify" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
|
|
4
|
+
|
|
5
|
+
* Participants will be tolerant of opposing views.
|
|
6
|
+
* Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
|
|
7
|
+
* When interpreting the words and actions of others, participants should always assume good intentions.
|
|
8
|
+
* Behaviour which can be reasonably considered harassment will not be tolerated.
|
|
9
|
+
|
|
10
|
+
If you have any concerns about behaviour within this project, please contact us at ["andreas@christopoulos.me"](mailto:"andreas@christopoulos.me").
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andreas Christopoulos
|
|
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,125 @@
|
|
|
1
|
+
# Cronify
|
|
2
|
+
|
|
3
|
+
Parse human-friendly schedule descriptions into cron expressions and next-occurrence timestamps.
|
|
4
|
+
|
|
5
|
+
```ruby
|
|
6
|
+
Cronify.parse("every weekday at 9am")
|
|
7
|
+
# => #<Cronify::Schedule cron="0 9 * * 1-5" ...>
|
|
8
|
+
|
|
9
|
+
Cronify.parse("first Monday of each month at noon")
|
|
10
|
+
# => #<Cronify::Schedule cron="0 12 ? * 1#1" ...>
|
|
11
|
+
|
|
12
|
+
Cronify.parse("every 2 hours between 8am and 6pm")
|
|
13
|
+
# => #<Cronify::Schedule cron="0 8,10,12,14,16,18 * * *" ...>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Cronify fills the gap between raw cron syntax and heavy scheduling libraries. It outputs cron strings compatible with Sidekiq, Whenever, and similar tools, plus the next N fire times as Ruby `Time` objects.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bundle add cronify
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or add to your Gemfile manually:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem "cronify"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Parsing a schedule
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
schedule = Cronify.parse("every weekday at 9am")
|
|
36
|
+
|
|
37
|
+
schedule.cron # => "0 9 * * 1-5"
|
|
38
|
+
schedule.next_occurrence # => 2026-03-16 09:00:00 UTC
|
|
39
|
+
schedule.next_occurrences(n: 3) # => [2026-03-16 09:00:00 UTC, 2026-03-17 09:00:00 UTC, 2026-03-18 09:00:00 UTC]
|
|
40
|
+
schedule.original_input # => "every weekday at 9am"
|
|
41
|
+
schedule.timezone # => "UTC"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Timezones
|
|
45
|
+
|
|
46
|
+
Pass any valid [TZInfo identifier](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) via the `timezone:` keyword:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
schedule = Cronify.parse("every weekday at 9am", timezone: "Europe/Athens")
|
|
50
|
+
schedule.next_occurrence # => time expressed in Europe/Athens
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
An invalid timezone raises `Cronify::Error` immediately with a descriptive message.
|
|
54
|
+
|
|
55
|
+
## Supported patterns
|
|
56
|
+
|
|
57
|
+
| Input | Cron output |
|
|
58
|
+
|----------------------------------------------------------------------|--------------------------------|
|
|
59
|
+
| `"every N hours"` | `0 */N * * *` |
|
|
60
|
+
| `"every N minutes"` | `*/N * * * *` |
|
|
61
|
+
| `"every day at <time>"` | `0 H * * *` |
|
|
62
|
+
| `"every weekday at <time>"` | `0 H * * 1-5` |
|
|
63
|
+
| `"every weekend at <time>"` | `0 H * * 6,0` |
|
|
64
|
+
| `"first/second/third/fourth/last <weekday> of each month at <time>"` | `0 H ? * W#N` / `0 H ? * WL` |
|
|
65
|
+
| `"every N hours between <time> and <time>"` | `0 H1,H2,... * * *` |
|
|
66
|
+
|
|
67
|
+
**Accepted time formats:** `9am`, `5pm`, `9:30am`, `noon`, `midnight`
|
|
68
|
+
|
|
69
|
+
### Cron dialect
|
|
70
|
+
|
|
71
|
+
Cronify targets **Quartz/Sidekiq cron syntax**. This means:
|
|
72
|
+
|
|
73
|
+
- Ordinal weekday patterns use `#N` notation: `1#1` = first Monday, `5#3` = third Friday
|
|
74
|
+
- Last weekday of month uses `L` suffix: `5L` = last Friday
|
|
75
|
+
- The `?` wildcard is used in the day-of-month field when day-of-week is specified
|
|
76
|
+
|
|
77
|
+
This syntax is supported by [Sidekiq Pro](https://sidekiq.org), [sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron), and [Whenever](https://github.com/javan/whenever). It is **not** compatible with standard POSIX cron.
|
|
78
|
+
|
|
79
|
+
## Error handling
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
# Unrecognized input
|
|
83
|
+
Cronify.parse("whenever I feel like it")
|
|
84
|
+
# => raises Cronify::ParseError: Unrecognized schedule: 'whenever I feel like it'
|
|
85
|
+
|
|
86
|
+
# Invalid timezone
|
|
87
|
+
Cronify.parse("every day at 9am", timezone: "Not/Real")
|
|
88
|
+
# => raises Cronify::Error: Unknown timezone: 'Not/Real'. Use a valid TZInfo identifier...
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Exception hierarchy
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
StandardError
|
|
95
|
+
└── Cronify::Error
|
|
96
|
+
├── Cronify::ParseError # input string not recognized
|
|
97
|
+
└── Cronify::AmbiguousInputError # input matches multiple patterns
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Development
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
bin/setup # install dependencies
|
|
104
|
+
bundle exec rspec # run tests
|
|
105
|
+
bundle exec rubocop # lint
|
|
106
|
+
bin/console # interactive prompt
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Type signatures
|
|
110
|
+
|
|
111
|
+
Cronify ships with [RBS](https://github.com/ruby/rbs) type signatures in the `sig/` directory. To use them with your project's type checker:
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
bundle exec rbs collection install
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Signatures cover the full public API (`Cronify.parse`, `Schedule`, and all error classes) and are validated in CI.
|
|
118
|
+
|
|
119
|
+
## Contributing
|
|
120
|
+
|
|
121
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/achristopoulos/cronify.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT — see [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "rspec/core/rake_task"
|
|
5
|
+
|
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
|
7
|
+
|
|
8
|
+
require "rubocop/rake_task"
|
|
9
|
+
|
|
10
|
+
RuboCop::RakeTask.new
|
|
11
|
+
|
|
12
|
+
desc "Validate RBS type signatures"
|
|
13
|
+
task :rbs do
|
|
14
|
+
sh "bundle exec rbs collection install"
|
|
15
|
+
sh "bundle exec rbs validate"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
task default: %i[spec rubocop rbs]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronify
|
|
4
|
+
class CronEmitter
|
|
5
|
+
ORDINAL_NUMBERS = { first: 1, second: 2, third: 3, fourth: 4 }.freeze
|
|
6
|
+
|
|
7
|
+
def self.emit(ir)
|
|
8
|
+
case ir.type
|
|
9
|
+
when :interval then emit_interval(ir)
|
|
10
|
+
when :daily then emit_daily(ir)
|
|
11
|
+
when :weekday, :weekend then emit_weekday_weekend(ir)
|
|
12
|
+
when :ordinal_weekday then emit_ordinal_weekday(ir)
|
|
13
|
+
when :bounded_interval then emit_bounded_interval(ir)
|
|
14
|
+
else raise Cronify::Error, "Unknown IR type: #{ir.type}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private_class_method def self.emit_interval(ir)
|
|
19
|
+
case ir.unit
|
|
20
|
+
when :hour then "0 */#{ir.interval} * * *"
|
|
21
|
+
when :minute then "*/#{ir.interval} * * * *"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private_class_method def self.emit_daily(ir)
|
|
26
|
+
"#{ir.minute} #{ir.hour} * * *"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private_class_method def self.emit_weekday_weekend(ir)
|
|
30
|
+
days = ir.type == :weekday ? "1-5" : "6,0"
|
|
31
|
+
"#{ir.minute} #{ir.hour} * * #{days}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private_class_method def self.emit_ordinal_weekday(ir)
|
|
35
|
+
day_field = if ir.ordinal == :last
|
|
36
|
+
"#{ir.weekday}L"
|
|
37
|
+
else
|
|
38
|
+
"#{ir.weekday}##{ORDINAL_NUMBERS[ir.ordinal]}"
|
|
39
|
+
end
|
|
40
|
+
"#{ir.minute} #{ir.hour} ? * #{day_field}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private_class_method def self.emit_bounded_interval(ir)
|
|
44
|
+
start_hour, end_hour = ir.hour_range
|
|
45
|
+
hours = (start_hour..end_hour).step(ir.interval).to_a.join(",")
|
|
46
|
+
"#{ir.minute} #{hours} * * *"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronify
|
|
4
|
+
class Dispatcher
|
|
5
|
+
MATCHERS = [
|
|
6
|
+
Matchers::TimeBoundedInterval,
|
|
7
|
+
Matchers::OrdinalWeekday,
|
|
8
|
+
Matchers::WeekdayWeekend,
|
|
9
|
+
Matchers::Daily,
|
|
10
|
+
Matchers::SimpleInterval
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
def self.dispatch(input)
|
|
14
|
+
matcher = MATCHERS.map(&:new).find { |m| m.match?(input) }
|
|
15
|
+
raise Cronify::ParseError, "Unrecognized schedule: '#{input}'" unless matcher
|
|
16
|
+
|
|
17
|
+
matcher.parse(input)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/cronify/ir.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronify
|
|
4
|
+
# Intermediate Representation — the structured output of the parser,
|
|
5
|
+
# before any cron string is produced.
|
|
6
|
+
IR = Struct.new(
|
|
7
|
+
:type, # Symbol: :interval, :daily, :weekday, :weekend, :ordinal_weekday, :bounded_interval
|
|
8
|
+
:interval, # Integer — the N in "every N hours/minutes"
|
|
9
|
+
:unit, # Symbol: :hour or :minute
|
|
10
|
+
:hour, # Integer 0–23
|
|
11
|
+
:minute, # Integer 0–59
|
|
12
|
+
:days_of_week, # Array<Integer> — 0=Sun, 1=Mon, …, 6=Sat
|
|
13
|
+
:ordinal, # Symbol: :first, :second, :third, :fourth, :last
|
|
14
|
+
:weekday, # Integer 0–6 — used with ordinal
|
|
15
|
+
:hour_range # [start_hour, end_hour] — used by bounded interval
|
|
16
|
+
)
|
|
17
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronify
|
|
4
|
+
module Matchers
|
|
5
|
+
class Base
|
|
6
|
+
def match?(input)
|
|
7
|
+
self.class::PATTERN.match?(input.downcase.strip)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def parse(input)
|
|
11
|
+
raise NotImplementedError, "#{self.class} must implement #parse"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronify
|
|
4
|
+
module Matchers
|
|
5
|
+
class Daily < Base
|
|
6
|
+
include TimeParser
|
|
7
|
+
|
|
8
|
+
PATTERN = /\Aevery day at (?<time>\d{1,2}(?::\d{2})?(?:am|pm)|noon|midnight)\z/
|
|
9
|
+
|
|
10
|
+
def parse(input)
|
|
11
|
+
m = PATTERN.match(input.downcase.strip)
|
|
12
|
+
hour, minute = parse_time(m[:time])
|
|
13
|
+
IR.new(type: :daily, hour: hour, minute: minute)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronify
|
|
4
|
+
module Matchers
|
|
5
|
+
class OrdinalWeekday < Base
|
|
6
|
+
include TimeParser
|
|
7
|
+
|
|
8
|
+
PATTERN = /
|
|
9
|
+
\A
|
|
10
|
+
(?<ordinal>first|second|third|fourth|last)\s
|
|
11
|
+
(?<weekday>monday|tuesday|wednesday|thursday|friday|saturday|sunday)\s
|
|
12
|
+
of\s(?:each|every)\smonth\sat\s
|
|
13
|
+
(?<time>\d{1,2}(?::\d{2})?(?:am|pm)|noon|midnight)
|
|
14
|
+
\z
|
|
15
|
+
/x
|
|
16
|
+
|
|
17
|
+
ORDINALS = {
|
|
18
|
+
"first" => :first, "second" => :second,
|
|
19
|
+
"third" => :third, "fourth" => :fourth, "last" => :last
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
WEEKDAYS = {
|
|
23
|
+
"sunday" => 0, "monday" => 1, "tuesday" => 2,
|
|
24
|
+
"wednesday" => 3, "thursday" => 4, "friday" => 5, "saturday" => 6
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
def parse(input)
|
|
28
|
+
m = PATTERN.match(input.downcase.strip)
|
|
29
|
+
hour, minute = parse_time(m[:time])
|
|
30
|
+
IR.new(
|
|
31
|
+
type: :ordinal_weekday,
|
|
32
|
+
ordinal: ORDINALS[m[:ordinal]],
|
|
33
|
+
weekday: WEEKDAYS[m[:weekday]],
|
|
34
|
+
hour: hour,
|
|
35
|
+
minute: minute
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronify
|
|
4
|
+
module Matchers
|
|
5
|
+
class SimpleInterval < Base
|
|
6
|
+
PATTERN = /\Aevery (?<interval>\d+) (?<unit>hours?|minutes?)\z/
|
|
7
|
+
|
|
8
|
+
def parse(input)
|
|
9
|
+
m = PATTERN.match(input.downcase.strip)
|
|
10
|
+
unit = m[:unit].start_with?("hour") ? :hour : :minute
|
|
11
|
+
IR.new(type: :interval, interval: m[:interval].to_i, unit: unit, minute: 0)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronify
|
|
4
|
+
module Matchers
|
|
5
|
+
class TimeBoundedInterval < Base
|
|
6
|
+
include TimeParser
|
|
7
|
+
|
|
8
|
+
PATTERN = /\Aevery (?<interval>\d+) hours? between (?<from>\d{1,2}(?:am|pm)) and (?<to>\d{1,2}(?:am|pm))\z/
|
|
9
|
+
|
|
10
|
+
def parse(input)
|
|
11
|
+
m = PATTERN.match(input.downcase.strip)
|
|
12
|
+
from_hour, = parse_time(m[:from])
|
|
13
|
+
to_hour, = parse_time(m[:to])
|
|
14
|
+
IR.new(
|
|
15
|
+
type: :bounded_interval,
|
|
16
|
+
interval: m[:interval].to_i,
|
|
17
|
+
hour_range: [from_hour, to_hour],
|
|
18
|
+
minute: 0
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronify
|
|
4
|
+
module Matchers
|
|
5
|
+
class WeekdayWeekend < Base
|
|
6
|
+
include TimeParser
|
|
7
|
+
|
|
8
|
+
PATTERN = /\Aevery (?<scope>weekday|weekend) at (?<time>\d{1,2}(?::\d{2})?(?:am|pm)|noon|midnight)\z/
|
|
9
|
+
|
|
10
|
+
DAYS = {
|
|
11
|
+
"weekday" => [1, 2, 3, 4, 5],
|
|
12
|
+
"weekend" => [6, 0]
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def parse(input)
|
|
16
|
+
m = PATTERN.match(input.downcase.strip)
|
|
17
|
+
hour, minute = parse_time(m[:time])
|
|
18
|
+
IR.new(
|
|
19
|
+
type: m[:scope].to_sym,
|
|
20
|
+
hour: hour,
|
|
21
|
+
minute: minute,
|
|
22
|
+
days_of_week: DAYS[m[:scope]]
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fugit"
|
|
4
|
+
require "tzinfo"
|
|
5
|
+
|
|
6
|
+
module Cronify
|
|
7
|
+
# Represents a parsed schedule.
|
|
8
|
+
#
|
|
9
|
+
# Holds the cron expression produced by {Cronify.parse} and provides methods
|
|
10
|
+
# to calculate future fire times. Instances are immutable — all attributes
|
|
11
|
+
# are read-only after initialization.
|
|
12
|
+
class Schedule
|
|
13
|
+
# @return [String] the cron expression in Quartz/Sidekiq syntax
|
|
14
|
+
attr_reader :cron
|
|
15
|
+
|
|
16
|
+
# @return [String] the TZInfo timezone identifier used for this schedule
|
|
17
|
+
attr_reader :timezone
|
|
18
|
+
|
|
19
|
+
# @return [String] the original natural language input string
|
|
20
|
+
attr_reader :original_input
|
|
21
|
+
|
|
22
|
+
# @param cron [String] a valid cron expression in Quartz/Sidekiq syntax
|
|
23
|
+
# @param timezone [String] a TZInfo timezone identifier (e.g. "UTC", "Europe/Athens")
|
|
24
|
+
# @param original_input [String] the original input string, stored for reference
|
|
25
|
+
# @raise [Cronify::Error] if the timezone identifier is not recognized
|
|
26
|
+
def initialize(cron:, timezone:, original_input:)
|
|
27
|
+
@cron = cron
|
|
28
|
+
@timezone = validate_timezone!(timezone)
|
|
29
|
+
@original_input = original_input
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Returns the next time this schedule fires.
|
|
33
|
+
#
|
|
34
|
+
# @return [Time] the next occurrence
|
|
35
|
+
def next_occurrence
|
|
36
|
+
next_occurrences(n: 1).first
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Returns the next N times this schedule fires, in ascending order.
|
|
40
|
+
#
|
|
41
|
+
# @param n [Integer] number of occurrences to return (default:5)
|
|
42
|
+
# @return [Array<Time>] next N fire times
|
|
43
|
+
def next_occurrences(n: 5)
|
|
44
|
+
tz = TZInfo::Timezone.get(timezone)
|
|
45
|
+
fugit_cr = Fugit::Cron.parse(cron)
|
|
46
|
+
raise Cronify::Error, "Invalid cron expression: #{cron}" unless fugit_cr
|
|
47
|
+
|
|
48
|
+
now = tz.utc_to_local(Time.now.utc)
|
|
49
|
+
Array.new(n).each_with_object([]) do |_, times|
|
|
50
|
+
reference = times.last || now
|
|
51
|
+
times << fugit_cr.next_time(reference).to_t
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def validate_timezone!(tz)
|
|
58
|
+
TZInfo::Timezone.get(tz)
|
|
59
|
+
tz
|
|
60
|
+
rescue TZInfo::InvalidTimezoneIdentifier
|
|
61
|
+
raise Error,
|
|
62
|
+
"Unknown timezone: '#{tz}'. Use a valid TZInfo identifier (e.g. 'Europe/Athens', 'America/New_York')."
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Cronify
|
|
4
|
+
module TimeParser
|
|
5
|
+
TIME_PATTERN = /\A(?<hour>\d{1,2})(?::(?<min>\d{2}))?(?<period>am|pm)\z/
|
|
6
|
+
|
|
7
|
+
def parse_time(str)
|
|
8
|
+
return [0, 0] if str == "midnight"
|
|
9
|
+
return [12, 0] if str == "noon"
|
|
10
|
+
|
|
11
|
+
m = TIME_PATTERN.match(str)
|
|
12
|
+
raise Cronify::ParseError, "Unrecognized time format: #{str}" unless m
|
|
13
|
+
|
|
14
|
+
hour = m[:hour].to_i
|
|
15
|
+
minute = m[:min].to_i
|
|
16
|
+
hour += 12 if m[:period] == "pm" && hour != 12
|
|
17
|
+
hour = 0 if m[:period] == "am" && hour == 12
|
|
18
|
+
|
|
19
|
+
[hour, minute]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/cronify.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cronify/version"
|
|
4
|
+
require_relative "cronify/time_parser"
|
|
5
|
+
require_relative "cronify/ir"
|
|
6
|
+
require_relative "cronify/schedule"
|
|
7
|
+
require_relative "cronify/matchers/base"
|
|
8
|
+
require_relative "cronify/matchers/simple_interval"
|
|
9
|
+
require_relative "cronify/matchers/daily"
|
|
10
|
+
require_relative "cronify/matchers/weekday_weekend"
|
|
11
|
+
require_relative "cronify/matchers/ordinal_weekday"
|
|
12
|
+
require_relative "cronify/matchers/time_bounded_interval"
|
|
13
|
+
require_relative "cronify/dispatcher"
|
|
14
|
+
require_relative "cronify/cron_emitter"
|
|
15
|
+
|
|
16
|
+
# Top-level namespace for the Cronify gem.
|
|
17
|
+
#
|
|
18
|
+
# Provides a single entry point {Cronify.parse} that converts a natural language
|
|
19
|
+
# schedule description into a {Cronify::Schedule} object containing the cron
|
|
20
|
+
# expression and next-occurrence timestamps.
|
|
21
|
+
module Cronify
|
|
22
|
+
# Base error class for all Cronify exceptions.
|
|
23
|
+
class Error < StandardError; end
|
|
24
|
+
|
|
25
|
+
# Raised when the input string does not match any known schedule pattern.
|
|
26
|
+
class ParseError < Error; end
|
|
27
|
+
|
|
28
|
+
# Raised when the input string matches more than one pattern ambiguously.
|
|
29
|
+
class AmbiguousInputError < Error; end
|
|
30
|
+
|
|
31
|
+
# Parses a natural language schedule string into a {Schedule} object.
|
|
32
|
+
#
|
|
33
|
+
# @example Simple interval
|
|
34
|
+
# Cronify.parse("every 2 hours")
|
|
35
|
+
#
|
|
36
|
+
# @example Weekday schedule with timezone
|
|
37
|
+
# Cronify.parse("every weekday at 9am", timezone: "Europe/Athens")
|
|
38
|
+
#
|
|
39
|
+
# @example Ordinal weekday
|
|
40
|
+
# Cronify.parse("first Monday of each month at noon")
|
|
41
|
+
#
|
|
42
|
+
# @param input [String] a natural language schedule description
|
|
43
|
+
# @param timezone [String] a TZInfo timezone identifier (default: "UTC")
|
|
44
|
+
# @return [Schedule] the parsed schedule with cron expression and next occurrences
|
|
45
|
+
# @raise [Cronify::ParseError] if the input is not a recognized pattern
|
|
46
|
+
# @raise [Cronify::Error] if the timezone identifier is invalid
|
|
47
|
+
def self.parse(input, timezone: "UTC")
|
|
48
|
+
ir = Dispatcher.dispatch(input)
|
|
49
|
+
cron = CronEmitter.emit(ir)
|
|
50
|
+
Schedule.new(cron: cron, timezone: timezone, original_input: input)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
path: ".gem_rbs_collection"
|
|
3
|
+
gems:
|
|
4
|
+
- name: addressable
|
|
5
|
+
version: '2.8'
|
|
6
|
+
source:
|
|
7
|
+
type: git
|
|
8
|
+
name: ruby/gem_rbs_collection
|
|
9
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
10
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
11
|
+
repo_dir: gems
|
|
12
|
+
- name: ast
|
|
13
|
+
version: '2.4'
|
|
14
|
+
source:
|
|
15
|
+
type: git
|
|
16
|
+
name: ruby/gem_rbs_collection
|
|
17
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
18
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
19
|
+
repo_dir: gems
|
|
20
|
+
- name: bigdecimal
|
|
21
|
+
version: '4.0'
|
|
22
|
+
source:
|
|
23
|
+
type: git
|
|
24
|
+
name: ruby/gem_rbs_collection
|
|
25
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
26
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
27
|
+
repo_dir: gems
|
|
28
|
+
- name: concurrent-ruby
|
|
29
|
+
version: '1.1'
|
|
30
|
+
source:
|
|
31
|
+
type: git
|
|
32
|
+
name: ruby/gem_rbs_collection
|
|
33
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
34
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
35
|
+
repo_dir: gems
|
|
36
|
+
- name: date
|
|
37
|
+
version: '0'
|
|
38
|
+
source:
|
|
39
|
+
type: stdlib
|
|
40
|
+
- name: dbm
|
|
41
|
+
version: '0'
|
|
42
|
+
source:
|
|
43
|
+
type: stdlib
|
|
44
|
+
- name: diff-lcs
|
|
45
|
+
version: '1.5'
|
|
46
|
+
source:
|
|
47
|
+
type: git
|
|
48
|
+
name: ruby/gem_rbs_collection
|
|
49
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
50
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
51
|
+
repo_dir: gems
|
|
52
|
+
- name: erb
|
|
53
|
+
version: '0'
|
|
54
|
+
source:
|
|
55
|
+
type: stdlib
|
|
56
|
+
- name: fileutils
|
|
57
|
+
version: '0'
|
|
58
|
+
source:
|
|
59
|
+
type: stdlib
|
|
60
|
+
- name: io-console
|
|
61
|
+
version: '0'
|
|
62
|
+
source:
|
|
63
|
+
type: stdlib
|
|
64
|
+
- name: json
|
|
65
|
+
version: '0'
|
|
66
|
+
source:
|
|
67
|
+
type: stdlib
|
|
68
|
+
- name: logger
|
|
69
|
+
version: '0'
|
|
70
|
+
source:
|
|
71
|
+
type: stdlib
|
|
72
|
+
- name: monitor
|
|
73
|
+
version: '0'
|
|
74
|
+
source:
|
|
75
|
+
type: stdlib
|
|
76
|
+
- name: optparse
|
|
77
|
+
version: '0'
|
|
78
|
+
source:
|
|
79
|
+
type: stdlib
|
|
80
|
+
- name: parallel
|
|
81
|
+
version: '1.20'
|
|
82
|
+
source:
|
|
83
|
+
type: git
|
|
84
|
+
name: ruby/gem_rbs_collection
|
|
85
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
86
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
87
|
+
repo_dir: gems
|
|
88
|
+
- name: parser
|
|
89
|
+
version: '3.2'
|
|
90
|
+
source:
|
|
91
|
+
type: git
|
|
92
|
+
name: ruby/gem_rbs_collection
|
|
93
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
94
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
95
|
+
repo_dir: gems
|
|
96
|
+
- name: pp
|
|
97
|
+
version: '0'
|
|
98
|
+
source:
|
|
99
|
+
type: stdlib
|
|
100
|
+
- name: prettyprint
|
|
101
|
+
version: '0'
|
|
102
|
+
source:
|
|
103
|
+
type: stdlib
|
|
104
|
+
- name: prism
|
|
105
|
+
version: 1.9.0
|
|
106
|
+
source:
|
|
107
|
+
type: rubygems
|
|
108
|
+
- name: pstore
|
|
109
|
+
version: '0'
|
|
110
|
+
source:
|
|
111
|
+
type: stdlib
|
|
112
|
+
- name: psych
|
|
113
|
+
version: '0'
|
|
114
|
+
source:
|
|
115
|
+
type: stdlib
|
|
116
|
+
- name: rainbow
|
|
117
|
+
version: '3.0'
|
|
118
|
+
source:
|
|
119
|
+
type: git
|
|
120
|
+
name: ruby/gem_rbs_collection
|
|
121
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
122
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
123
|
+
repo_dir: gems
|
|
124
|
+
- name: rake
|
|
125
|
+
version: '13.0'
|
|
126
|
+
source:
|
|
127
|
+
type: git
|
|
128
|
+
name: ruby/gem_rbs_collection
|
|
129
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
130
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
131
|
+
repo_dir: gems
|
|
132
|
+
- name: rbs
|
|
133
|
+
version: 3.10.3
|
|
134
|
+
source:
|
|
135
|
+
type: rubygems
|
|
136
|
+
- name: rdoc
|
|
137
|
+
version: '0'
|
|
138
|
+
source:
|
|
139
|
+
type: stdlib
|
|
140
|
+
- name: regexp_parser
|
|
141
|
+
version: '2.8'
|
|
142
|
+
source:
|
|
143
|
+
type: git
|
|
144
|
+
name: ruby/gem_rbs_collection
|
|
145
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
146
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
147
|
+
repo_dir: gems
|
|
148
|
+
- name: ripper
|
|
149
|
+
version: '0'
|
|
150
|
+
source:
|
|
151
|
+
type: stdlib
|
|
152
|
+
- name: rubocop
|
|
153
|
+
version: '1.57'
|
|
154
|
+
source:
|
|
155
|
+
type: git
|
|
156
|
+
name: ruby/gem_rbs_collection
|
|
157
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
158
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
159
|
+
repo_dir: gems
|
|
160
|
+
- name: rubocop-ast
|
|
161
|
+
version: '1.46'
|
|
162
|
+
source:
|
|
163
|
+
type: git
|
|
164
|
+
name: ruby/gem_rbs_collection
|
|
165
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
166
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
167
|
+
repo_dir: gems
|
|
168
|
+
- name: tsort
|
|
169
|
+
version: '0'
|
|
170
|
+
source:
|
|
171
|
+
type: stdlib
|
|
172
|
+
- name: tzinfo
|
|
173
|
+
version: '2.0'
|
|
174
|
+
source:
|
|
175
|
+
type: git
|
|
176
|
+
name: ruby/gem_rbs_collection
|
|
177
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
178
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
179
|
+
repo_dir: gems
|
|
180
|
+
- name: yard
|
|
181
|
+
version: '0.9'
|
|
182
|
+
source:
|
|
183
|
+
type: git
|
|
184
|
+
name: ruby/gem_rbs_collection
|
|
185
|
+
revision: 9bf2eebb1c54b5d6f23f2acb65d4c36f195b4783
|
|
186
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
187
|
+
repo_dir: gems
|
|
188
|
+
gemfile_lock_path: Gemfile.lock
|
data/rbs_collection.yaml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Download sources
|
|
2
|
+
sources:
|
|
3
|
+
- type: git
|
|
4
|
+
name: ruby/gem_rbs_collection
|
|
5
|
+
remote: https://github.com/ruby/gem_rbs_collection.git
|
|
6
|
+
revision: main
|
|
7
|
+
repo_dir: gems
|
|
8
|
+
|
|
9
|
+
# You can specify local directories as sources also.
|
|
10
|
+
# - type: local
|
|
11
|
+
# path: path/to/your/local/repository
|
|
12
|
+
|
|
13
|
+
# A directory to install the downloaded RBSs
|
|
14
|
+
path: .gem_rbs_collection
|
|
15
|
+
|
|
16
|
+
# gems:
|
|
17
|
+
# # If you want to avoid installing rbs files for gems, you can specify them here.
|
|
18
|
+
# - name: GEM_NAME
|
|
19
|
+
# ignore: true
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Cronify
|
|
2
|
+
class CronEmitter
|
|
3
|
+
ORDINAL_NUMBERS: Hash[ordinal_type, Integer]
|
|
4
|
+
|
|
5
|
+
def self.emit: (IR ir) -> String
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def self.emit_interval: (IR ir) -> String?
|
|
10
|
+
def self.emit_daily: (IR ir) -> String
|
|
11
|
+
def self.emit_weekday_weekend: (IR ir) -> String
|
|
12
|
+
def self.emit_ordinal_weekday: (IR ir) -> String
|
|
13
|
+
def self.emit_bounded_interval: (IR ir) -> String
|
|
14
|
+
end
|
|
15
|
+
end
|
data/sig/cronify/ir.rbs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Cronify
|
|
2
|
+
class IR < Struct[untyped]
|
|
3
|
+
attr_accessor type: schedule_type?
|
|
4
|
+
attr_accessor interval: Integer?
|
|
5
|
+
attr_accessor unit: unit_type?
|
|
6
|
+
attr_accessor hour: Integer?
|
|
7
|
+
attr_accessor minute: Integer?
|
|
8
|
+
attr_accessor days_of_week: Array[Integer]?
|
|
9
|
+
attr_accessor ordinal: ordinal_type?
|
|
10
|
+
attr_accessor weekday: Integer?
|
|
11
|
+
attr_accessor hour_range: [Integer, Integer]?
|
|
12
|
+
|
|
13
|
+
def initialize: (
|
|
14
|
+
?type: schedule_type?,
|
|
15
|
+
?interval: Integer?,
|
|
16
|
+
?unit: unit_type?,
|
|
17
|
+
?hour: Integer?,
|
|
18
|
+
?minute: Integer?,
|
|
19
|
+
?days_of_week: Array[Integer]?,
|
|
20
|
+
?ordinal: ordinal_type?,
|
|
21
|
+
?weekday: Integer?,
|
|
22
|
+
?hour_range: [Integer, Integer]?
|
|
23
|
+
) -> void
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Cronify
|
|
2
|
+
class Schedule
|
|
3
|
+
attr_reader cron: String
|
|
4
|
+
attr_reader timezone: String
|
|
5
|
+
attr_reader original_input: String
|
|
6
|
+
|
|
7
|
+
def initialize: (cron: String, timezone: String, original_input: String) -> void
|
|
8
|
+
|
|
9
|
+
def next_occurrence: () -> Time
|
|
10
|
+
|
|
11
|
+
def next_occurrences: (?n: Integer) -> Array[Time]
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def validate_timezone!: (String tz) -> String
|
|
16
|
+
end
|
|
17
|
+
end
|
data/sig/cronify.rbs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Cronify
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
type schedule_type = :interval | :daily | :weekday | :weekend | :ordinal_weekday | :bounded_interval
|
|
5
|
+
type unit_type = :hour | :minute
|
|
6
|
+
type ordinal_type = :first | :second | :third | :fourth | :last
|
|
7
|
+
|
|
8
|
+
class Error < StandardError
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class ParseError < Error
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class AmbiguousInputError < Error
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.parse: (String input, ?timezone: String) -> Schedule
|
|
18
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cronify
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Andreas Christopoulos
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: fugit
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.11'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.11'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: tzinfo
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: Cronify parses human-friendly schedule descriptions (e.g. 'every weekday
|
|
41
|
+
at 9am', 'first Monday of each month') into standard cron strings and next-occurrence
|
|
42
|
+
timestamps. Designed for SaaS apps where users configure their own recurring schedules,
|
|
43
|
+
with output compatible with Sidekiq, Whenever, and similar tools.
|
|
44
|
+
email:
|
|
45
|
+
- andreas@christopoulos.me
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- ".DS_Store"
|
|
51
|
+
- ".idea/.gitignore"
|
|
52
|
+
- ".idea/copilot.data.migration.ask2agent.xml"
|
|
53
|
+
- ".idea/cronify.iml"
|
|
54
|
+
- ".idea/modules.xml"
|
|
55
|
+
- ".idea/vcs.xml"
|
|
56
|
+
- CHANGELOG.md
|
|
57
|
+
- CODE_OF_CONDUCT.md
|
|
58
|
+
- LICENSE.txt
|
|
59
|
+
- README.md
|
|
60
|
+
- Rakefile
|
|
61
|
+
- lib/cronify.rb
|
|
62
|
+
- lib/cronify/cron_emitter.rb
|
|
63
|
+
- lib/cronify/dispatcher.rb
|
|
64
|
+
- lib/cronify/ir.rb
|
|
65
|
+
- lib/cronify/matchers/base.rb
|
|
66
|
+
- lib/cronify/matchers/daily.rb
|
|
67
|
+
- lib/cronify/matchers/ordinal_weekday.rb
|
|
68
|
+
- lib/cronify/matchers/simple_interval.rb
|
|
69
|
+
- lib/cronify/matchers/time_bounded_interval.rb
|
|
70
|
+
- lib/cronify/matchers/weekday_weekend.rb
|
|
71
|
+
- lib/cronify/schedule.rb
|
|
72
|
+
- lib/cronify/time_parser.rb
|
|
73
|
+
- lib/cronify/version.rb
|
|
74
|
+
- rbs_collection.lock.yaml
|
|
75
|
+
- rbs_collection.yaml
|
|
76
|
+
- sig/cronify.rbs
|
|
77
|
+
- sig/cronify/cron_emitter.rbs
|
|
78
|
+
- sig/cronify/dispatcher.rbs
|
|
79
|
+
- sig/cronify/ir.rbs
|
|
80
|
+
- sig/cronify/matchers/base.rbs
|
|
81
|
+
- sig/cronify/matchers/daily.rbs
|
|
82
|
+
- sig/cronify/matchers/ordinal_weekday.rbs
|
|
83
|
+
- sig/cronify/matchers/simple_interval.rbs
|
|
84
|
+
- sig/cronify/matchers/time_bounded_interval.rbs
|
|
85
|
+
- sig/cronify/matchers/weekday_weekend.rbs
|
|
86
|
+
- sig/cronify/schedule.rbs
|
|
87
|
+
- sig/cronify/time_parser.rbs
|
|
88
|
+
homepage: https://github.com/achristop/cronify
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata:
|
|
92
|
+
allowed_push_host: https://rubygems.org
|
|
93
|
+
homepage_uri: https://github.com/achristop/cronify
|
|
94
|
+
source_code_uri: https://github.com/achristop/cronify
|
|
95
|
+
changelog_uri: https://github.com/achristop/cronify/blob/main/CHANGELOG.md
|
|
96
|
+
rdoc_options: []
|
|
97
|
+
require_paths:
|
|
98
|
+
- lib
|
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: 3.2.0
|
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0'
|
|
109
|
+
requirements: []
|
|
110
|
+
rubygems_version: 3.6.9
|
|
111
|
+
specification_version: 4
|
|
112
|
+
summary: Parse natural language schedules into cron expressions and next occurrences.
|
|
113
|
+
test_files: []
|