Skip to content

Commit

Permalink
bugfix/cron-date-normalization (#194)
Browse files Browse the repository at this point in the history
* Fixed date normalization bug by reordering the datetime components check so we start smaller and move towards higher granularity
* Added additional test cases
* Removed redundant date operations from next() function
  • Loading branch information
ndortega authored May 3, 2024
1 parent 780d3fc commit 5b756c2
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 64 deletions.
95 changes: 44 additions & 51 deletions src/cron.jl
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ monthnames = Dict(
"DEC" => 12
)

function translate(input::SubString)
function translate(input::AbstractString)
if haskey(weeknames, input)
return weeknames[input]
elseif haskey(monthnames, input)
Expand Down Expand Up @@ -202,9 +202,7 @@ function nthweekdayofmonth(time::DateTime, n::Int64)
elseif isweekday(after) && month(after) == current_month
return after
end
if day(before) > 1
before -= Day(1)
elseif day(after) < Dates.daysinmonth(time)
if day(after) < Dates.daysinmonth(time)
after += Day(1)
else
break
Expand Down Expand Up @@ -248,7 +246,7 @@ function getoccurance(time::DateTime, daynumber::Int64, occurance::Int64)
end


function matchexpression(input::Nullable{SubString}, time::DateTime, converter, maxvalue, adjustedmax=nothing) :: Bool
function matchexpression(input::Nullable{AbstractString}, time::DateTime, converter, maxvalue, adjustedmax=nothing) :: Bool
try

# base case: return true if
Expand Down Expand Up @@ -325,7 +323,7 @@ function matchexpression(input::Nullable{SubString}, time::DateTime, converter,
end


function match_special(input::Nullable{SubString}, time::DateTime, converter, maxvalue, adjustedmax=nothing) :: Bool
function match_special(input::Nullable{AbstractString}, time::DateTime, converter, maxvalue, adjustedmax=nothing) :: Bool

# base case: return true if
if isnothing(input)
Expand Down Expand Up @@ -411,7 +409,7 @@ end


function iscronmatch(expression::String, time::DateTime) :: Bool
parsed_expression::Vector{Nullable{SubString{String}}} = split(strip(expression), " ")
parsed_expression::Vector{Nullable{AbstractString}} = split(strip(expression), " ")

# fill in any missing arguments with nothing, so the array is always
fill_length = 6 - length(parsed_expression)
Expand Down Expand Up @@ -482,7 +480,7 @@ expression
"""
function next(cron_expr::String, start_time::DateTime)::DateTime

parsed_expression::Vector{Nullable{SubString{String}}} = split(strip(cron_expr), " ")
parsed_expression::Vector{Nullable{AbstractString}} = split(strip(cron_expr), " ")

# fill in any missing arguments with nothing, so the array is always
fill_length = 6 - length(parsed_expression)
Expand All @@ -500,68 +498,63 @@ function next(cron_expr::String, start_time::DateTime)::DateTime
# loop until candidate time matches all fields of cron expression
while true

# check if candidate time matches month field
if !match_month(month_expression, candidate_time)
# increment candidate time by one month and reset day, hour,
# minute and second to minimum values
candidate_time += Month(1) - Day(day(candidate_time)) + Day(1) -
Hour(hour(candidate_time)) + Hour(0) -
Minute(minute(candidate_time)) + Minute(0) -
Second(second(candidate_time)) + Second(0)
continue
# check if candidate time matches second field
if !match_seconds(seconds_expression, candidate_time)
# increment candidate time by one second
candidate_time += Second(1)
continue
end

# check if candidate time matches day of month field
if !match_dayofmonth(dayofmonth_expression, candidate_time)
# increment candidate time by one day and reset hour,
# minute and second to minimum values
candidate_time += Day(1) - Hour(hour(candidate_time)) +
Hour(0) - Minute(minute(candidate_time)) +
Minute(0) - Second(second(candidate_time)) +
Second(0)
# check if candidate time matches minute field
if !match_minutes(minute_expression, candidate_time)
# increment candidate time by one minute and reset second
# to minimum value
candidate_time += Minute(1) - Second(second(candidate_time))
continue
end

# check if candidate time matches hour field
if !match_hour(hour_expression, candidate_time)
# increment candidate time by one hour and reset minute
# and second to minimum values
candidate_time += Hour(1) - Minute(minute(candidate_time)) -
Second(second(candidate_time))
continue
end

# check if candidate time matches day of week field
if !match_dayofweek(dayofweek_expression, candidate_time)
# increment candidate time by one day and reset hour,
# minute and second to minimum values
candidate_time += Day(1) - Hour(hour(candidate_time)) +
Hour(0) - Minute(minute(candidate_time)) +
Minute(0) - Second(second(candidate_time)) +
Second(0)
candidate_time += Day(1) - Hour(hour(candidate_time)) -
Minute(minute(candidate_time)) -
Second(second(candidate_time))
continue
end

# check if candidate time matches hour field
if !match_hour(hour_expression, candidate_time)
# increment candidate time by one hour and reset minute
# and second to minimum values
candidate_time += Hour(1) - Minute(minute(candidate_time))
+ Minute(0) - Second(second(candidate_time))
+ Second(0)
# check if candidate time matches day of month field
if !match_dayofmonth(dayofmonth_expression, candidate_time)
# increment candidate time by one day and reset hour,
# minute and second to minimum values
candidate_time += Day(1) - Hour(hour(candidate_time)) -
Minute(minute(candidate_time)) -
Second(second(candidate_time))
continue
end

# check if candidate time matches minute field
if !match_minutes(minute_expression, candidate_time)
# increment candidate time by one minute and reset second
# to minimum value
candidate_time += Minute(1) - Second(second(candidate_time))
+ Second(0)
continue
end

# check if candidatet ime matches second field
if !match_seconds(seconds_expression, candidate_time)
# increment candidatet ime by one second
candidate_time += Second(1)
continue
# check if candidate time matches month field
if !match_month(month_expression, candidate_time)
# increment candidate time by one month and reset day, hour,
# minute and second to minimum values
candidate_time += Month(1) - Day(day(candidate_time)) + Day(1) -
Hour(hour(candidate_time)) -
Minute(minute(candidate_time)) -
Second(second(candidate_time))
continue
end

break # exit the loop as all fields match
end

return remove_milliseconds(candidate_time) # return the next matching tme
end

Expand Down
135 changes: 124 additions & 11 deletions test/crontests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,44 @@ using JSON3
using StructTypes
using Sockets
using Dates

using Oxygen
using Oxygen.Cron: iscronmatch, isweekday, lastweekdayofmonth,
next, sleep_until, lastweekday, nthweekdayofmonth,
matchexpression
matchexpression, translate

using ..Constants

@testset "Cron Module Tests" begin
@testset "translate function tests" begin
# Day translations
@test translate("SUN") == 7
@test translate("MON") == 1
@test translate("TUE") == 2
@test translate("WED") == 3
@test translate("THU") == 4
@test translate("FRI") == 5
@test translate("SAT") == 6

# Month translations
@test translate("JAN") == 1
@test translate("FEB") == 2
@test translate("MAR") == 3
@test translate("APR") == 4
@test translate("MAY") == 5
@test translate("JUN") == 6
@test translate("JUL") == 7
@test translate("AUG") == 8
@test translate("SEP") == 9
@test translate("OCT") == 10
@test translate("NOV") == 11
@test translate("DEC") == 12

# Unmatched Translations
@test translate("XYZ") == "XYZ"
@test translate("WOW") == "WOW"
@test translate("") == ""
end


@testset "methods" begin
@test lastweekday(DateTime(2022,1,1,0,0,0)) == DateTime(2021,12,31,0,0,0)
Expand All @@ -32,6 +61,28 @@ using ..Constants
@testset "nthweekdayofmonth" begin
@test_throws ErrorException nthweekdayofmonth(DateTime(2022,1,1,0,0,0), -1)
@test_throws ErrorException nthweekdayofmonth(DateTime(2022,1,1,0,0,0), 50)
@test nthweekdayofmonth(DateTime(2024, 4, 27), 27) == DateTime(2024, 4, 26)
@test nthweekdayofmonth(DateTime(2024, 5, 5), 5) == DateTime(2024, 5, 6)
@test nthweekdayofmonth(DateTime(2022, 5, 1), 1) == DateTime(2022, 5, 2)
@test nthweekdayofmonth(DateTime(2024, 6, 1), 1) == DateTime(2024, 6, 3)

# Test with n as the first day of the month and the first day is a weekday
@test nthweekdayofmonth(DateTime(2022, 3, 1), 1) == DateTime(2022, 3, 1)

# Test with n as the last day of the month and the last day is a weekday
@test nthweekdayofmonth(DateTime(2022, 4, 30), 30) == DateTime(2022, 4, 29)

# Test with n as a day in the middle of the month and the day is a weekend
@test nthweekdayofmonth(DateTime(2022, 5, 15), 15) == DateTime(2022, 5, 16)

# Test with n as a day in the middle of the month and the day is a weekend
@test nthweekdayofmonth(DateTime(2022, 6, 18), 18) == DateTime(2022, 6, 17)

# Test with n as a day in the middle of the month and the day and the day before are weekends
@test nthweekdayofmonth(DateTime(2022, 7, 17), 17) == DateTime(2022, 7, 18)

# Test with n as a day in the middle of the month and the day, the day before and the day after are weekends
@test nthweekdayofmonth(DateTime(2022, 12, 25), 25) == DateTime(2022, 12, 26)
end

@testset "sleep_until" begin
Expand Down Expand Up @@ -122,7 +173,64 @@ using ..Constants
@test next("30 * * * * *", start_time) == DateTime(2023, 4, 3, 12, 30, 30)
end

using Test
@testset "next() whole hour normalization tests" begin
# these would work fine
@test next("1 0 13", DateTime("2024-05-01T10:00:00")) == DateTime("2024-05-01T13:00:01")
@test next("0 1 13", DateTime("2024-05-01T10:00:00")) == DateTime("2024-05-01T13:01:00")

# these wouldn't previously be normalized with zero values
@test next("0 0 13", DateTime("2024-05-01T10:00:00")) == DateTime("2024-05-01T13:00:00")
@test next("0 0 13", DateTime("2024-05-01T14:00:00")) == DateTime("2024-05-02T13:00:00")

# Test cases where the current time is exactly on the hour
@test next("0 0 14", DateTime("2024-05-01T14:00:00")) == DateTime("2024-05-02T14:00:00")
@test next("0 0 0", DateTime("2024-05-01T00:00:00")) == DateTime("2024-05-02T00:00:00")

# Test cases where the current time is exactly one second before the hour
@test next("0 0 15", DateTime("2024-05-01T14:59:59")) == DateTime("2024-05-01T15:00:00")
@test next("0 0 1", DateTime("2024-05-01T00:59:59")) == DateTime("2024-05-01T01:00:00")

# Test cases where the current time is exactly one second after the hour
@test next("0 0 16", DateTime("2024-05-01T16:00:01")) == DateTime("2024-05-02T16:00:00")
@test next("0 0 2", DateTime("2024-05-01T02:00:01")) == DateTime("2024-05-02T02:00:00")

# Test cases where the current time is exactly on the half hour
@test next("0 30 17", DateTime("2024-05-01T17:30:00")) == DateTime("2024-05-02T17:30:00")
@test next("0 30 3", DateTime("2024-05-01T03:30:00")) == DateTime("2024-05-02T03:30:00")

# Test cases where the current time is exactly one second before the half hour
@test next("0 30 18", DateTime("2024-05-01T18:29:59")) == DateTime("2024-05-01T18:30:00")
@test next("0 30 4", DateTime("2024-05-01T04:29:59")) == DateTime("2024-05-01T04:30:00")


# Test cases where the current time is exactly one second after the half hour
@test next("0 30 19", DateTime("2024-05-01T19:30:01")) == DateTime("2024-05-02T19:30:00")
@test next("0 30 5", DateTime("2024-05-01T05:30:01")) == DateTime("2024-05-02T05:30:00")

# Test case for second
@test next("* * * * *", DateTime(2022, 5, 15, 14, 30, 0)) == DateTime(2022, 5, 15, 14, 30, 1)

# Test case for minute
@test next("0 * * * *", DateTime(2022, 5, 15, 14, 30)) == DateTime(2022, 5, 15, 14, 31)

# Test case for hour
@test next("0 0 * *", DateTime(2022, 5, 15, 14, 30)) == DateTime(2022, 5, 15, 15, 0)

# Test case for day of month
@test next("0 0 0 * *", DateTime(2022, 5, 15)) == DateTime(2022, 5, 16)

# # Test case for month
@test next("0 0 0 1 *", DateTime(2022, 5, 15)) == DateTime(2022, 6, 1)

# Test case for day of week
@test next("0 0 * * * MON", DateTime(2022, 5, 15)) == DateTime(2022, 5, 16)

# invalid valude for day of month
# @test next("0 0 0 0 *", DateTime(2022, 5, 15)) == DateTime(2022, 5, 16)


end

@testset "matchexpression function tests" begin
time = DateTime(2023, 4, 3, 23, 59, 59)
minute = Dates.minute
Expand Down Expand Up @@ -432,16 +540,21 @@ using ..Constants
server = serve(async=true, port=PORT, show_banner=false)
sleep(3)

internalrequest(HTTP.Request("GET", "/cron-increment"))
internalrequest(HTTP.Request("GET", "/cron-increment"))

@testset "Testing CRON API access" begin
r = internalrequest(HTTP.Request("GET", "/get-cron-increment"))
@test r.status == 200
@test parse(Int64, text(r)) > 0
try
internalrequest(HTTP.Request("GET", "/cron-increment"))
internalrequest(HTTP.Request("GET", "/cron-increment"))
@testset "Testing CRON API access" begin
r = internalrequest(HTTP.Request("GET", "/get-cron-increment"))
@test r.status == 200
@test parse(Int64, text(r)) > 0
end
catch e
println(e)
finally
close(server)
end

close(server)


end

Expand Down
2 changes: 0 additions & 2 deletions test/oxidise.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ using HTTP
using ..Constants
using Oxygen; @oxidise

PORT = 6060

@get "/test" function(req)
return "Hello World"
end
Expand Down

0 comments on commit 5b756c2

Please sign in to comment.