-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
215 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,12 @@ | ||
# frozen_string_literal: true | ||
|
||
source "https://rubygems.org" | ||
source 'https://rubygems.org' | ||
|
||
# Specify your gem's dependencies in cron_calc.gemspec | ||
gemspec | ||
|
||
gem "rake", "~> 13.1" | ||
gem 'rake', '~> 13.1' | ||
|
||
gem "rspec", "~> 3.12" | ||
gem 'rspec', '~> 3.12' | ||
|
||
gem "rubocop", "~> 1.57" | ||
gem 'rubocop', '~> 1.57' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,11 @@ | ||
#!/usr/bin/env ruby | ||
# frozen_string_literal: true | ||
|
||
require "bundler/setup" | ||
require "cron_calc" | ||
require 'bundler/setup' | ||
require 'cron_calc' | ||
|
||
# You can add fixtures and/or initialization code here to make experimenting | ||
# with your gem easier. You can also use a different console, if you like. | ||
|
||
require "irb" | ||
require 'irb' | ||
IRB.start(__FILE__) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,22 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative "lib/cron_calc/version" | ||
require_relative 'lib/cron_calc/version' | ||
|
||
Gem::Specification.new do |spec| | ||
spec.name = "cron_calc" | ||
spec.name = 'cron_calc' | ||
spec.version = CronCalc::VERSION | ||
spec.authors = ["Jakub Miziński"] | ||
spec.email = ["[email protected]"] | ||
spec.authors = ['Jakub Miziński'] | ||
spec.email = ['[email protected]'] | ||
|
||
spec.summary = "calculates cron job occurrences" | ||
spec.description = "calculates cron job occurrences within a specified period" | ||
spec.homepage = "TODO: Put your gem's website or public repo URL here." | ||
spec.license = "MIT" | ||
spec.required_ruby_version = ">= 2.6.0" | ||
spec.summary = 'calculates cron job occurrences' | ||
spec.description = 'calculates cron job occurrences within a specified period' | ||
spec.homepage = 'https://github.com/mizinsky/cron_calc' | ||
spec.license = 'MIT' | ||
spec.required_ruby_version = '>= 2.6.0' | ||
|
||
spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'" | ||
|
||
spec.metadata["homepage_uri"] = spec.homepage | ||
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." | ||
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." | ||
spec.metadata['homepage_uri'] = spec.homepage | ||
spec.metadata['source_code_uri'] = 'https://github.com/mizinsky/cron_calc' | ||
spec.metadata['changelog_uri'] = 'https://github.com/mizinsky/cron_calc/blob/main/CHANGELOG.md' | ||
|
||
# Specify which files should be added to the gem when it is released. | ||
# The `git ls-files -z` loads the files in the RubyGem that have been added into git. | ||
|
@@ -28,12 +26,12 @@ Gem::Specification.new do |spec| | |
f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile]) | ||
end | ||
end | ||
spec.bindir = "exe" | ||
spec.bindir = 'exe' | ||
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } | ||
spec.require_paths = ["lib"] | ||
spec.require_paths = ['lib'] | ||
|
||
# Uncomment to register a new dependency of your gem | ||
# spec.add_dependency "example-gem", "~> 1.0" | ||
spec.add_dependency 'rspec', '~> 3.12' | ||
|
||
# For more information and examples about making a new gem, check out our | ||
# guide at: https://bundler.io/guides/creating_gem.html | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,80 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative "cron_calc/version" | ||
require_relative 'cron_calc/version' | ||
require 'time' | ||
|
||
module CronCalc | ||
class Error < StandardError; end | ||
# Your code goes here... | ||
|
||
def self.new(cron_string, period) | ||
Parser.new(cron_string, period) | ||
end | ||
|
||
class Parser | ||
attr_reader :cron_string, :cron_parts, :period | ||
|
||
RANGE = { | ||
minutes: 0..59, | ||
hours: 0..23, | ||
days: 1..31, | ||
months: 1..12 | ||
}.freeze | ||
|
||
def initialize(cron_string, period) | ||
@cron_string = cron_string | ||
@period = period | ||
@cron_parts = split_cron_string | ||
end | ||
|
||
def occurrences | ||
raise 'Cron expression is not supported or invalid' unless cron_string_valid? | ||
|
||
minutes, hours, days, months, years = parse_cron_expression | ||
|
||
years.product(months, days, hours, minutes).each_with_object([]) do |(year, month, day, hour, minute), occ| | ||
time = Time.new(year, month, day, hour, minute) | ||
occ << time if Date.valid_date?(year, month, day) && period.include?(time) | ||
end | ||
end | ||
|
||
private | ||
|
||
def split_cron_string | ||
splitted = cron_string.split | ||
|
||
{ | ||
minutes: splitted[0], | ||
hours: splitted[1], | ||
days: splitted[2], | ||
months: splitted[3] | ||
} | ||
end | ||
|
||
def parse_cron_expression | ||
%i[minutes hours days months].map { |unit| parse_cron_part(unit) } << (period.min.year..period.max.year).to_a | ||
end | ||
|
||
def parse_cron_part(time_unit) | ||
range = RANGE[time_unit] | ||
part = cron_parts[time_unit] | ||
|
||
case part | ||
when '*' | ||
range.to_a | ||
when /,/ | ||
part.split(',').map(&:to_i) | ||
when /-/ | ||
(part.split('-').first.to_i..part.split('-').last.to_i).to_a | ||
when %r{/} | ||
range.step(part.split('/').last.to_i).to_a | ||
else | ||
[part.to_i] | ||
end | ||
end | ||
|
||
def cron_string_valid? | ||
regex = %r{\A(\*|([0-5]?\d)(,([0-5]?\d))*|(\*/\d+)|(\d+-\d+)) (\*|([01]?\d|2[0-3])(,([01]?\d|2[0-3]))*|(\*/\d+)|(\d+-\d+)) (\*|([12]?\d|3[01])(,([12]?\d|3[01]))*|(\*/\d+)|(\d+-\d+)) (\*|([1-9]|1[0-2])(,([1-9]|1[0-2]))*|(\*/\d+)|(\d+-\d+)) \*\z} | ||
cron_string.match?(regex) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
# frozen_string_literal: true | ||
|
||
module CronCalc | ||
VERSION = "0.1.0" | ||
VERSION = '0.1.0' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,121 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe CronCalc do | ||
it "has a version number" do | ||
it 'has a version number' do | ||
expect(CronCalc::VERSION).not_to be nil | ||
end | ||
|
||
it "does something useful" do | ||
expect(false).to eq(true) | ||
describe '#occurrences' do | ||
let(:subject) { described_class.new(cron_string, period).occurrences } | ||
|
||
context 'when "," is used' do | ||
let(:cron_string) { '30 22,23 * * *' } | ||
let(:period) { Time.new(2023, 11, 23)..Time.new(2023, 11, 27) } | ||
|
||
it do | ||
expect(subject).to eq([ | ||
Time.new(2023, 11, 23, 22, 30), | ||
Time.new(2023, 11, 23, 23, 30), | ||
Time.new(2023, 11, 24, 22, 30), | ||
Time.new(2023, 11, 24, 23, 30), | ||
Time.new(2023, 11, 25, 22, 30), | ||
Time.new(2023, 11, 25, 23, 30), | ||
Time.new(2023, 11, 26, 22, 30), | ||
Time.new(2023, 11, 26, 23, 30) | ||
]) | ||
end | ||
end | ||
|
||
context 'when day is used' do | ||
let(:cron_string) { '5 5 12 * *' } | ||
let(:period) { Time.new(2023, 11, 10)..Time.new(2024, 2, 20) } | ||
|
||
it do | ||
expect(subject).to eq([ | ||
Time.new(2023, 11, 12, 5, 5), | ||
Time.new(2023, 12, 12, 5, 5), | ||
Time.new(2024, 1, 12, 5, 5), | ||
Time.new(2024, 2, 12, 5, 5) | ||
]) | ||
end | ||
end | ||
|
||
context 'when month is used' do | ||
let(:cron_string) { '5 5 12 1 *' } | ||
let(:period) { Time.new(2023, 11, 10)..Time.new(2024, 2, 20) } | ||
|
||
it do | ||
expect(subject).to eq([ | ||
Time.new(2024, 1, 12, 5, 5) | ||
]) | ||
end | ||
end | ||
|
||
context 'when range "-" is used' do | ||
let(:cron_string) { '5 5 15-17 * *' } | ||
let(:period) { Time.new(2023, 1, 10)..Time.new(2023, 2, 20) } | ||
|
||
it do | ||
expect(subject).to eq([ | ||
Time.new(2023, 1, 15, 5, 5), | ||
Time.new(2023, 1, 16, 5, 5), | ||
Time.new(2023, 1, 17, 5, 5), | ||
Time.new(2023, 2, 15, 5, 5), | ||
Time.new(2023, 2, 16, 5, 5), | ||
Time.new(2023, 2, 17, 5, 5) | ||
]) | ||
end | ||
end | ||
|
||
context 'when step "/" is used' do | ||
let(:cron_string) { '5 5 28 */3 *' } | ||
let(:period) { Time.new(2023, 1, 1)..Time.new(2024, 1, 31) } | ||
|
||
it do | ||
expect(subject).to eq([ | ||
Time.new(2023, 1, 28, 5, 5), | ||
Time.new(2023, 4, 28, 5, 5), | ||
Time.new(2023, 7, 28, 5, 5), | ||
Time.new(2023, 10, 28, 5, 5), | ||
Time.new(2024, 1, 28, 5, 5) | ||
]) | ||
end | ||
end | ||
|
||
context 'when there is an occurrence with 0 minutes' do | ||
let(:cron_string) { '* 11-12 28 * *' } | ||
let(:period) { Time.new(2023, 1, 28, 11, 58)..Time.new(2023, 1, 28, 12, 2) } | ||
|
||
it do | ||
expect(subject).to eq([ | ||
Time.new(2023, 1, 28, 11, 58), | ||
Time.new(2023, 1, 28, 11, 59), | ||
Time.new(2023, 1, 28, 12), | ||
Time.new(2023, 1, 28, 12, 1), | ||
Time.new(2023, 1, 28, 12, 2) | ||
]) | ||
end | ||
end | ||
|
||
context 'when invalid cron string is used' do | ||
let(:cron_string) { '* 40 * * *' } | ||
let(:period) { Time.new(2023, 1, 1)..Time.new(2023, 1, 2) } | ||
|
||
it do | ||
expect { subject }.to raise_error(RuntimeError, 'Cron expression is not supported or invalid') | ||
end | ||
end | ||
|
||
context 'when there is short month' do | ||
let(:cron_string) { '5 5 31 1,2,3,4 *' } | ||
let(:period) { Time.new(2023, 1, 1, 0, 0)..Time.new(2023, 6, 1, 0, 0) } | ||
|
||
it do | ||
expect(subject).to eq([ | ||
Time.new(2023, 1, 31, 5, 5), | ||
Time.new(2023, 3, 31, 5, 5) | ||
]) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters