cronv-rb 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.
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in cronv-rb.gemspec
8
+ gemspec
9
+
10
+ gem 'fugit', '~> 1.2'
11
+ gem 'logger'
12
+ gem 'rspec', '~> 3.0'
13
+ gem 'rubocop'
14
+ gem 'rubocop-rspec'
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2024 Masato Yamashita
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,72 @@
1
+ # cronv-rb
2
+
3
+ A Ruby implementation of [cronv](https://github.com/takumakanari/cronv) (originally written in Go) that visualizes cron schedules from crontab as an interactive HTML timeline.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'cronv-rb'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle install
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install cronv-rb
20
+
21
+ ## Usage
22
+
23
+ Pipe your crontab into `cronv` to generate an HTML timeline:
24
+
25
+ ```sh
26
+ crontab -l | bundle exec ruby exe/cronv -o crontab.html
27
+ ```
28
+
29
+ ### Options
30
+
31
+ | Option | Description | Default |
32
+ |---|---|---|
33
+ | `-o`, `--output PATH` | Path to output HTML file | `./crontab.html` |
34
+ | `-d`, `--duration DURATION` | Duration to visualize (`1d` / `6h` / `30m`) | `6h` |
35
+ | `--from-date DATE` | Start date (`YYYY/MM/DD`) | Current date (UTC) |
36
+ | `--from-time TIME` | Start time (`HH:MM`) | Current time (UTC) |
37
+ | `-t`, `--title TITLE` | Title label of output | `Cron Tasks` |
38
+ | `-w`, `--width WIDTH` | Table width of output | `100` |
39
+
40
+ ### Examples
41
+
42
+ ```sh
43
+ # Visualize the next 24 hours starting from a specific date
44
+ crontab -l | bundle exec ruby exe/cronv \
45
+ -o crontab.html \
46
+ --from-date 2025/01/01 \
47
+ --from-time 00:00 \
48
+ -d 1d
49
+
50
+ # Visualize the next 30 minutes with a custom title
51
+ crontab -l | bundle exec ruby exe/cronv \
52
+ -o crontab.html \
53
+ -d 30m \
54
+ -t "My Cron Jobs"
55
+ ```
56
+
57
+ ## Development
58
+
59
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
60
+
61
+ ```sh
62
+ bundle install
63
+ bundle exec rspec
64
+ ```
65
+
66
+ ## Contributing
67
+
68
+ Bug reports and pull requests are welcome on GitHub at https://github.com/M-Yamashita01/cronv-rb.
69
+
70
+ ## License
71
+
72
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
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
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Layout/LeadingCommentSpace
4
+ #!/usr/bin/env ruby
5
+ # rubocop:enable Layout/LeadingCommentSpace
6
+
7
+ require 'bundler/setup'
8
+ require 'cronv/rb'
9
+
10
+ # You can add fixtures and/or initialization code here to make experimenting
11
+ # with your gem easier. You can also use a different console, if you like.
12
+
13
+ # (If you use this, don't forget to add pry to your Gemfile!)
14
+ # require "pry"
15
+ # Pry.start
16
+
17
+ require 'irb'
18
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/cronv-rb.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'cronv_rb/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'cronv-rb'
9
+ spec.version = CronvRb::VERSION
10
+ spec.authors = ['Masato Yamashita']
11
+ spec.email = ['4653960+M-Yamashita01@users.noreply.github.com']
12
+
13
+ spec.summary = 'Visualize your cron schedules in crontab by Ruby'
14
+ spec.description = 'A gem that rewrites cronv, originally written in Go, in Ruby to visualize your cron schedules in crontab.'
15
+ spec.homepage = 'https://github.com/M-Yamashita01/cronv-rb'
16
+ spec.license = 'MIT'
17
+
18
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
19
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
22
+
23
+ spec.metadata['homepage_uri'] = spec.homepage
24
+ spec.metadata['source_code_uri'] = 'https://github.com/M-Yamashita01/cronv-rb'
25
+ spec.metadata['changelog_uri'] = 'https://github.com/M-Yamashita01/cronv-rb'
26
+ else
27
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
28
+ 'public gem pushes.'
29
+ end
30
+
31
+ spec.required_ruby_version = '>= 3.2'
32
+ # Specify which files should be added to the gem when it is released.
33
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
34
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
35
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
36
+ end
37
+ spec.bindir = 'exe'
38
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
39
+ spec.require_paths = ['lib']
40
+
41
+ spec.add_development_dependency 'bundler', '>= 2.5'
42
+ spec.add_development_dependency 'rake', '~> 13.4'
43
+ spec.add_development_dependency 'rspec', '~> 3.0'
44
+ end
data/docs/release.md ADDED
@@ -0,0 +1,136 @@
1
+ # Release Flow
2
+
3
+ ## Overview
4
+
5
+ This project uses an automated release flow driven by [release-please](https://github.com/googleapis/release-please)
6
+ and Conventional Commits. There is no manual tagging.
7
+
8
+ The pipeline is:
9
+
10
+ ```
11
+ Conventional Commits merged to main
12
+ |
13
+ v
14
+ release-please keeps a Release PR up to date (version bump + CHANGELOG)
15
+ |
16
+ v (the Release PR is merged: see "How the Release PR is merged" below)
17
+ release-please creates the tag and the GitHub Release
18
+ |
19
+ v
20
+ the publish job builds the gem and pushes it to RubyGems (Trusted Publishing)
21
+ ```
22
+
23
+ ## How the Release PR is merged
24
+
25
+ Patch releases ship automatically; larger releases need a human.
26
+
27
+ - Patch bump (`x.y.Z`): the `auto-merge-patch` job runs weekly (Mondays 00:00 UTC
28
+ / 09:00 JST) and merges the open Release PR automatically when it is a patch-only
29
+ bump. This ships accumulated `fix:` / `fix(deps):` changes without manual action.
30
+ You can also trigger it on demand from the Actions tab (Run workflow).
31
+ - Minor bump (`x.Y.0`, from `feat:`) or major bump: the Release PR is left open for
32
+ a human to review and merge, since these are larger changes.
33
+
34
+ ## Conventional Commits
35
+
36
+ release-please derives the next version from commit messages, so the prefix matters:
37
+
38
+ | Prefix | Effect on version (while in 0.x) |
39
+ |--------|----------------------------------|
40
+ | `feat:` | MINOR bump (new feature); shown in release notes |
41
+ | `fix:` | PATCH bump (bug fix); shown in release notes |
42
+ | `fix(deps):` | PATCH bump (dependency update, used by Renovate); shown in release notes |
43
+ | `perf:` | PATCH bump (performance improvement); shown in release notes |
44
+ | `feat!:` / `BREAKING CHANGE:` | MINOR bump (breaking changes stay MINOR until 1.0.0) |
45
+ | `docs:`, `refactor:`, `chore:` | No version bump; hidden from release notes |
46
+
47
+ Note on how release-please decides the bump: internally, only `feat` and
48
+ breaking changes map to MINOR/MAJOR; every other releasable type maps to PATCH.
49
+ To keep `docs:` / `refactor:` / `chore:` from cutting a pointless release, they
50
+ are marked hidden in `release-please-config.json`, which also removes them from
51
+ the release notes. There is no release-please setting that both shows a type in
52
+ the notes and skips the version bump; that is why these types are hidden entirely.
53
+
54
+ If nothing since the last release bumps the version, no Release PR is created,
55
+ so weeks with only `docs:` / `refactor:` / `chore:` changes produce no release.
56
+
57
+ ## Releasing
58
+
59
+ ### 1. Merge feature/fix PRs to main as usual
60
+
61
+ Each PR should use a Conventional Commit title. When such commits land on `main`,
62
+ the `Release Please` workflow opens or updates a single Release PR titled
63
+ `chore(main): release x.y.z`.
64
+
65
+ ### 2. The Release PR is merged
66
+
67
+ - Patch-only Release PRs are merged automatically on the weekly schedule (see
68
+ "How the Release PR is merged" above). No action is needed.
69
+ - Minor/major Release PRs wait for a human. Open the PR, check the proposed
70
+ version and the generated `CHANGELOG.md` entry, then merge when ready.
71
+
72
+ ### 3. Merging the Release PR triggers the release
73
+
74
+ Whichever way it is merged, it:
75
+
76
+ 1. Updates `CHANGELOG.md`, `lib/cronv_rb/version.rb`, and
77
+ `.release-please-manifest.json` on `main`
78
+ 2. Creates the git tag (e.g. `v0.1.1`)
79
+ 3. Creates the GitHub Release with the changelog as its body
80
+
81
+ ### 4. The gem is published automatically
82
+
83
+ The `publish` job in the same workflow runs when a release was created
84
+ (`releases_created == 'true'`). It builds the gem and pushes it to
85
+ [RubyGems](https://rubygems.org/gems/cronv-rb) using Trusted Publishing (OIDC),
86
+ so no API key is stored in the repository.
87
+
88
+ If a publish needs to be re-run or a version needs to be backfilled, trigger the
89
+ workflow manually from the Actions tab (Run workflow) with `force_publish`
90
+ checked. This builds and pushes whatever version is currently on `main`.
91
+
92
+ ### 5. Verify the release
93
+
94
+ - Check the [Releases page](https://github.com/M-Yamashita01/cronv-rb/releases)
95
+ - Confirm the new version is live on [RubyGems](https://rubygems.org/gems/cronv-rb)
96
+ - Confirm the changelog reads correctly
97
+
98
+ ## Release Infrastructure
99
+
100
+ | File | Purpose |
101
+ |------|---------|
102
+ | `.github/workflows/release-please.yml` | Maintains the Release PR on push to `main`; auto-merges patch-only Release PRs weekly; publishes the gem when a release is created |
103
+ | `release-please-config.json` | release-please settings (release type, version bump rules, changelog sections) |
104
+ | `.release-please-manifest.json` | Tracks the current released version |
105
+ | `lib/cronv_rb/version.rb` | The `CronvRb::VERSION` constant; the source of truth for the version, updated by release-please |
106
+
107
+ ## RubyGems Trusted Publishing
108
+
109
+ Publishing uses RubyGems Trusted Publishing (OIDC) instead of a stored API key.
110
+ A one-time setup is required on RubyGems.org (Profile > Trusted Publishers):
111
+
112
+ - Repository: `M-Yamashita01/cronv-rb`
113
+ - Workflow filename: `release-please.yml`
114
+ - Gem name: `cronv-rb`
115
+
116
+ Because the gem is not yet registered, create it as a pending trusted publisher
117
+ so the first OIDC push both creates the gem and registers ownership.
118
+
119
+ ## Renovate
120
+
121
+ Renovate is configured to use `fix(deps):` commit messages (see `renovate.json`),
122
+ so dependency updates become PATCH-level entries in the Release PR. This lets a
123
+ weekly dependency refresh ship as a single patch release when the Release PR is
124
+ merged.
125
+
126
+ ## Fixing a Bad Release
127
+
128
+ Prefer rolling forward: fix the issue on `main` and let the next Release PR ship a
129
+ new patch version (e.g. `v0.1.2`).
130
+
131
+ If a release must be pulled:
132
+
133
+ 1. Delete the release from the GitHub Releases page
134
+ 2. Delete the tag: `git push origin :refs/tags/v0.1.1`
135
+ 3. Yank the gem from RubyGems if it was published: `gem yank cronv-rb -v 0.1.1`
136
+ 4. Fix the issue on `main` and ship a new patch via the normal flow
data/exe/cronv ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.join(__dir__, '..', 'lib'))
5
+
6
+ require 'cronv_rb'
7
+
8
+ CronvRb.main
@@ -0,0 +1,80 @@
1
+ # Sample crontab for comparing output between cronv-rb and the OSS cronv
2
+ # https://github.com/takumakanari/cronv
3
+
4
+ # --- Basic examples ---
5
+
6
+ # Every minute
7
+ * * * * * /usr/local/bin/every_minute.sh
8
+
9
+ # Every 5 minutes
10
+ */5 * * * * /usr/local/bin/every_5_minutes.sh
11
+
12
+ # Every 10 minutes
13
+ */10 * * * * /usr/local/bin/every_10_minutes.sh
14
+
15
+ # Every 15 minutes
16
+ */15 * * * * /usr/local/bin/every_15_minutes.sh
17
+
18
+ # Every 30 minutes
19
+ */30 * * * * /usr/local/bin/every_30_minutes.sh
20
+
21
+ # --- Hourly / Daily ---
22
+
23
+ # Every hour at minute 0
24
+ 0 * * * * /usr/local/bin/hourly.sh
25
+
26
+ # Every day at midnight
27
+ 0 0 * * * /usr/local/bin/daily_midnight.sh
28
+
29
+ # Every day at noon
30
+ 0 12 * * * /usr/local/bin/daily_noon.sh
31
+
32
+ # Every day at 6 AM and 6 PM
33
+ 0 6,18 * * * /usr/local/bin/twice_daily.sh
34
+
35
+ # --- Weekly ---
36
+
37
+ # Every Monday at 9 AM
38
+ 0 9 * * 1 /usr/local/bin/weekly_monday.sh
39
+
40
+ # Every weekday (Mon-Fri) at 8 AM
41
+ 0 8 * * 1-5 /usr/local/bin/weekday_morning.sh
42
+
43
+ # Every weekend (Sat-Sun) at 10 AM
44
+ 0 10 * * 6,0 /usr/local/bin/weekend_morning.sh
45
+
46
+ # --- Monthly ---
47
+
48
+ # First day of every month at midnight
49
+ 0 0 1 * * /usr/local/bin/monthly_first.sh
50
+
51
+ # Last possible day check: 28th of every month at 2 AM
52
+ 0 2 28 * * /usr/local/bin/monthly_28th.sh
53
+
54
+ # --- Specific months ---
55
+
56
+ # Every day in January at midnight
57
+ 0 0 * 1 * /usr/local/bin/january_daily.sh
58
+
59
+ # Every day in July and December at noon
60
+ 0 12 * 7,12 * /usr/local/bin/july_december_noon.sh
61
+
62
+ # --- Step and range combinations ---
63
+
64
+ # Every 2 hours
65
+ 0 */2 * * * /usr/local/bin/every_2_hours.sh
66
+
67
+ # Every 3 hours between 6 AM and 21 PM
68
+ 0 6-21/3 * * * /usr/local/bin/daytime_every_3_hours.sh
69
+
70
+ # Minutes 0, 15, 30, 45 of every hour
71
+ 0,15,30,45 * * * * /usr/local/bin/quarter_hour.sh
72
+
73
+ # --- @ aliases ---
74
+
75
+ @hourly /usr/local/bin/at_hourly.sh
76
+ @daily /usr/local/bin/at_daily.sh
77
+ @weekly /usr/local/bin/at_weekly.sh
78
+ @monthly /usr/local/bin/at_monthly.sh
79
+ @yearly /usr/local/bin/at_yearly.sh
80
+ @reboot /usr/local/bin/at_reboot.sh
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CronvRb
4
+ class Extra
5
+ attr_reader :line, :label, :job
6
+
7
+ def initialize(line:, label:, job:)
8
+ @line = line
9
+ @label = label
10
+ @job = job
11
+ end
12
+ end
13
+
14
+ class Crontab
15
+ attr_reader :line, :schedule, :job
16
+
17
+ def initialize(line:, schedule:, job:)
18
+ @line = line
19
+ @schedule = schedule
20
+ @job = job
21
+ end
22
+
23
+ class << self
24
+ # rubocop:disable Metrics/AbcSize
25
+ # rubocop:disable Lint/UnusedMethodArgument
26
+ # rubocop:disable Style/OptionalArguments
27
+ # return [crontab, extra]
28
+ def parse(context = {}, line)
29
+ parts = line.split
30
+ schedule = nil
31
+ job = []
32
+
33
+ if parts[0].start_with?('@')
34
+ raise ArgumentError, "invalid crontab line: '#{line}'" if parts.length < 2
35
+
36
+ if parts[0] == '@reboot'
37
+ return [nil,
38
+ Extra.new(
39
+ line: line,
40
+ label: parts[0],
41
+ job: parts[1..].join(' ')
42
+ )]
43
+ end
44
+
45
+ schedule = Schedule.new(
46
+ minutes: nil,
47
+ hour: nil,
48
+ day_of_month: nil,
49
+ month: nil,
50
+ day_of_week: nil,
51
+ year: nil,
52
+ schedule_alias: parts[0]
53
+ )
54
+
55
+ return [Crontab.new(
56
+ line: line,
57
+ schedule: schedule,
58
+ job: parts[1..].join(' ')
59
+ ), nil]
60
+ else
61
+ raise ArgumentError, "invalid crontab line: '#{line}'" if parts.length < 5
62
+
63
+ schedule = Schedule.new(
64
+ minutes: parts[0],
65
+ hour: parts[1],
66
+ day_of_month: parts[2],
67
+ month: parts[3],
68
+ day_of_week: parts[4],
69
+ year: nil,
70
+ schedule_alias: nil
71
+ )
72
+
73
+ job = parts[5..].join(' ')
74
+ end
75
+
76
+ [Crontab.new(
77
+ line: line,
78
+ schedule: schedule,
79
+ job: job
80
+ ), nil]
81
+ end
82
+ end
83
+ # rubocop:enable Metrics/AbcSize
84
+ # rubocop:enable Lint/UnusedMethodArgument
85
+ # rubocop:enable Style/OptionalArguments
86
+
87
+ def running_every_minutes?(schedule)
88
+ fields = schedule.to_crontab.split
89
+ fields.each_with_index.all? do |field, i|
90
+ if i.zero?
91
+ %w[* */1].include?(field)
92
+ else
93
+ field == '*'
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ class Logger
6
+ class << self
7
+ def info(message)
8
+ info_logger.info(message)
9
+ end
10
+
11
+ def error(message)
12
+ error_logger.error(message)
13
+ end
14
+
15
+ private
16
+
17
+ def info_logger
18
+ @info_logger ||= Logger.new($stdout)
19
+ end
20
+
21
+ def error_logger
22
+ @error_logger ||= Logger.new($stderr)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module CronvRb
6
+ # rubocop:disable Metrics/AbcSize
7
+ def self.main
8
+ option = Option.new_cronv_option(Time.now)
9
+
10
+ OptionParser.new do |opt|
11
+ opt.on('-o', '--output PATH', 'path to .html file to output') { |v| option.output_file_path = v }
12
+ opt.on('-d', '--duration DURATION', 'duration to visualize in N{suffix} style. e.g.) 1d(day)/1h(hour)/1m(minute)') { |v| option.duration = v }
13
+ opt.on('--from-date DATE', "start date in the format '#{Option::OPT_DATE_FORMAT}' to visualize") { |v| option.from_date = v }
14
+ opt.on('--from-time TIME', "start time in the format '#{Option::OPT_TIME_FORMAT}' to visualize") { |v| option.from_time = v }
15
+ opt.on('-t', '--title TITLE', 'title/label of output') { |v| option.title = v }
16
+ opt.on('-w', '--width WIDTH', 'Table width of output') { |v| option.width = v.to_i }
17
+ opt.on('-v', '--version', 'Show version') do
18
+ puts "cronv-rb #{VERSION}"
19
+ exit
20
+ end
21
+ end.parse!
22
+
23
+ visualizer = Visualizer.new_visualizer(option)
24
+
25
+ $stdin.each_line do |line|
26
+ visualizer.add(line)
27
+ end
28
+
29
+ path = visualizer.dump
30
+ puts "[#{option.title}] #{visualizer.records.size} tasks."
31
+ puts "[#{option.title}] '#{path}' generated."
32
+ end
33
+ # rubocop:enable Metrics/AbcSize
34
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CronvRb
4
+ class Option
5
+ OPT_DATE_FORMAT = '%Y/%m/%d'
6
+ OPT_TIME_FORMAT = '%H:%M'
7
+ OPT_DEFAULT_DURATION = '6h'
8
+ OPT_DEFAULT_OUTPUT_PATH = './crontab.html'
9
+ OPT_DEFAULT_TITLE = 'Cron Tasks'
10
+ OPT_DEFAULT_WIDTH = 100
11
+
12
+ attr_accessor :output_file_path, :duration, :from_date, :from_time, :title, :width
13
+
14
+ def to_from_time
15
+ parsed = Time.strptime("#{@from_date} #{@from_time}", "#{OPT_DATE_FORMAT} #{OPT_TIME_FORMAT}")
16
+ Time.utc(parsed.year, parsed.month, parsed.day, parsed.hour, parsed.min, 0)
17
+ rescue ArgumentError => e
18
+ raise ArgumentError, "invalid date/time format: '#{@from_date} #{@from_time}', #{e.message}"
19
+ end
20
+
21
+ def to_duration_minutes
22
+ raise ArgumentError, "invalid duration format: '#{@duration}'" if @duration.length < 2
23
+
24
+ duration_str = @duration[0..-2]
25
+ unit = @duration[-1]
26
+
27
+ begin
28
+ duration_i = Integer(duration_str)
29
+ rescue ArgumentError => e
30
+ raise ArgumentError, "invalid duration format: '#{@duration}', #{e.message}"
31
+ end
32
+
33
+ case unit.downcase
34
+ when 'd'
35
+ Float(duration_i * 24 * 60)
36
+ when 'h'
37
+ Float(duration_i * 60)
38
+ when 'm'
39
+ Float(duration_i)
40
+ else
41
+ raise ArgumentError, "invalid duration format: '#{@duration}', '#{unit}' is not in d/h/m"
42
+ end
43
+ end
44
+
45
+ def self.new_cronv_option(now)
46
+ utc_now = now.utc
47
+ new(utc_now.strftime(OPT_DATE_FORMAT), utc_now.strftime(OPT_TIME_FORMAT))
48
+ end
49
+
50
+ private
51
+
52
+ def initialize(from_date, from_time)
53
+ @output_file_path = OPT_DEFAULT_OUTPUT_PATH
54
+ @duration = OPT_DEFAULT_DURATION
55
+ @from_date = from_date
56
+ @from_time = from_time
57
+ @title = OPT_DEFAULT_TITLE
58
+ @width = OPT_DEFAULT_WIDTH
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fugit'
4
+
5
+ module CronvRb
6
+ class Record
7
+ class << self
8
+ def new_record(line:, start_time:, duration_minutes:, context: {})
9
+ crontab, extra = Crontab.parse(context, line)
10
+
11
+ return nil, extra if crontab.nil?
12
+
13
+ fugit_cron = Fugit.parse_cron(crontab.schedule.to_crontab)
14
+
15
+ new(crontab:, fugit_cron:, start_time:, duration_minutes:)
16
+ end
17
+ end
18
+
19
+ attr_reader :crontab, :fugit_cron, :start_time, :duration_minutes
20
+
21
+ def initialize(crontab:, fugit_cron:, start_time:, duration_minutes:)
22
+ @crontab = crontab
23
+ @fugit_cron = fugit_cron
24
+ # start_time class Time
25
+ @start_time = start_time
26
+ # duration_minutes class Integer
27
+ @duration_minutes = duration_minutes
28
+ end
29
+
30
+ Exec = Struct.new(:start, :end)
31
+
32
+ def iter
33
+ array = []
34
+
35
+ end_time = @start_time + (@duration_minutes * 60)
36
+ return array if @fugit_cron.nil?
37
+
38
+ # next_time is an EtOrbi::EoTime; use .to_t (converts to plain Ruby Time)
39
+ # for comparison to avoid timezone label mismatches ("Z" vs "UTC") that
40
+ # cause string comparison to fail for executions at the exact end boundary.
41
+ @next_time = @fugit_cron.next_time(@start_time.to_s)
42
+
43
+ while @next_time.to_t <= end_time
44
+ array.push({ start: @next_time.to_s, end: (@next_time + 60).to_s })
45
+ @next_time = @fugit_cron.next_time(@next_time.to_s)
46
+ end
47
+
48
+ array
49
+ end
50
+ end
51
+ end