Skip to content

Commit

Permalink
Add basic implementation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mizinsky committed Dec 18, 2023
1 parent 62a4935 commit 10244b7
Show file tree
Hide file tree
Showing 8 changed files with 215 additions and 35 deletions.
8 changes: 4 additions & 4 deletions Gemfile
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'
6 changes: 3 additions & 3 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# frozen_string_literal: true

require "bundler/gem_tasks"
require "rspec/core/rake_task"
require 'bundler/gem_tasks'
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec)

require "rubocop/rake_task"
require 'rubocop/rake_task'

RuboCop::RakeTask.new

Expand Down
6 changes: 3 additions & 3 deletions bin/console
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__)
32 changes: 15 additions & 17 deletions cron_calc.gemspec
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.
Expand All @@ -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
Expand Down
76 changes: 74 additions & 2 deletions lib/cron_calc.rb
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
2 changes: 1 addition & 1 deletion lib/cron_calc/version.rb
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
116 changes: 113 additions & 3 deletions spec/cron_calc_spec.rb
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
4 changes: 2 additions & 2 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# frozen_string_literal: true

require "cron_calc"
require 'cron_calc'

RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
config.example_status_persistence_file_path = '.rspec_status'

# Disable RSpec exposing methods globally on `Module` and `main`
config.disable_monkey_patching!
Expand Down

0 comments on commit 10244b7

Please sign in to comment.