%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*- % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod: %%% %%%------------------------------------------------------------------------ %%% @doc %%% ==CloudI Cron Expression Functionality== %%% The special character ? and the @reboot macro are not supported %%% because they wouldn't work well with Erlang process restarts. %%% The other Cron expression syntax is implemented, %%% including the optional seconds and year values %%% (info from https://en.wikipedia.org/wiki/Cron#CRON_expression): %%% ``` %%% Field name Required Allowed values Allowed special characters %%% ---------- -------- -------------- -------------------------- %%% Seconds No 0-59 * / , - %%% Minutes Yes 0-59 * / , - %%% Hours Yes 0-23 * / , - %%% Day of month Yes 1-31 * / , - L W %%% Month Yes 1-12 or JAN-DEC * / , - %%% Day of week Yes 0-6 or SUN-SAT * / , - L # %%% Year No 1970–9999 * / , - %%% ''' %%% %%% === Asterisk (*) === %%% An asterisk indicates that the cron expression matches for all values %%% of the field. %%% %%% === Slash (/) === %%% Slashes can be combined with ranges to specify step values. %%% For example, */5 in the minutes field indicates every 5 minutes. %%% %%% === Comma (,) === %%% Commas are used to separate items of a list. For example, %%% using "MON,WED,FRI" in the day-of-week field means %%% Mondays, Wednesdays and Fridays. %%% %%% === Hyphen (-) === %%% Hyphens define ranges. For example, 2000–2010 indicates every year %%% between 2000 and 2010, inclusive. %%% %%% === L === %%% 'L' stands for "last". When used in the day-of-week field, it allows you %%% to specify constructs such as "the last Friday" ("5L") of a given month. %%% In the day-of-month field, it specifies the last day of the month. %%% %%% === W === %%% The 'W' character is allowed for the day-of-month field. %%% This character is used to specify the weekday (Monday-Friday) nearest %%% the given day. As an example, if you were to specify "15W" as the value %%% for the day-of-month field, the meaning is: %%% "the nearest weekday to the 15th of the month". %%% So, if the 15th is a Saturday, the trigger fires on Friday the 14th. %%% If the 15th is a Sunday, the trigger fires on Monday the 16th. %%% If the 15th is a Tuesday, then it fires on Tuesday the 15th. %%% However, if you specify "1W" as the value for day-of-month, and %%% the 1st is a Saturday, the trigger fires on Monday the 3rd, %%% as it does not 'jump' over the boundary of a month's days. %%% %%% This implementation allows the W character to be used in a list. %%% For example, "1W,15W" is valid in the day-of-month field. %%% %%% The W character can be combined with L as LW to mean %%% "the last business day of the month". %%% %%% === Hash (#) === %%% '#' is allowed for the day-of-week field, and must be followed by %%% a number between one and five. It allows you to specify constructs such as %%% "the second Friday" of a given month. For example, entering "5#3" in the %%% day-of-week field corresponds to the third Friday of every month. %%% %%% === Other Details === %%% ``` %%% * If only six fields are present, %%% a 0 second field is prepended %%% * If only five fields are present, %%% a 0 second field is prepended and a wildcard year field is appended %%% * The range for the day-of-week field is 0-7 instead of 0-6, %%% with 7 as Sunday (like 0) (BSD and ATT disagreed about this in the past) %%% * The month names are case-insensitive %%% * The day-of-week names are case-insensitive %%% ''' %%% @end %%% %%% MIT License %%% %%% Copyright (c) 2019-2020 Michael Truog %%% %%% Permission is hereby granted, free of charge, to any person obtaining a %%% copy of this software and associated documentation files (the "Software"), %%% to deal in the Software without restriction, including without limitation %%% the rights to use, copy, modify, merge, publish, distribute, sublicense, %%% and/or sell copies of the Software, and to permit persons to whom the %%% Software is furnished to do so, subject to the following conditions: %%% %%% The above copyright notice and this permission notice shall be included in %%% all copies or substantial portions of the Software. %%% %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING %%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER %%% DEALINGS IN THE SOFTWARE. %%% %%% @author Michael Truog %%% @copyright 2019-2020 Michael Truog %%% @version 2.0.1 {@date} {@time} %%%------------------------------------------------------------------------ -module(cloudi_cron). -author('mjtruog at protonmail dot com'). %% external interface -export([expression/1, new/1, next_datetime/2]). -type field_seconds() :: nonempty_list(0..59) | undefined. -type field_minutes() :: nonempty_list(0..59) | undefined. -type field_hours() :: nonempty_list(0..23) | undefined. -type field_day_of_month() :: nonempty_list(1..31 | last_day_of_month | {weekday, last_day_of_month | 1..31}) | undefined. -type field_month() :: nonempty_list(1..12) | undefined. -type field_day_of_week() :: nonempty_list(0..6 | {last_day_of_week, 0..6} | {every, 1..5, 0..6}) | undefined. -type field_year() :: nonempty_list(1970..9999) | undefined. -type expression_strings() :: {nonempty_string(), nonempty_string(), nonempty_string(), nonempty_string(), nonempty_string(), nonempty_string(), nonempty_string()}. -type expression() :: {field_seconds(), field_minutes(), field_hours(), field_day_of_month(), field_month(), field_day_of_week(), field_year()}. -record(cloudi_cron, { expression_strings :: expression_strings(), expression :: expression() }). -type state() :: #cloudi_cron{}. -export_type([state/0]). %%%------------------------------------------------------------------------ %%% External interface functions %%%------------------------------------------------------------------------ %%------------------------------------------------------------------------- %% @doc %% ===Provide the cron expression string.=== %% @end %%------------------------------------------------------------------------- -spec expression(state()) -> nonempty_string(). expression(#cloudi_cron{expression_strings = ExpressionStrings}) -> cloudi_string:join(" ", erlang:tuple_to_list(ExpressionStrings)). %%------------------------------------------------------------------------- %% @doc %% ===Create a parsed representation of a cron expression.=== %% @end %%------------------------------------------------------------------------- -spec new(Input :: nonempty_string()) -> state(). new([_ | _] = Input) -> SecondsDefault = "0", YearDefault = "*", [Seconds, Minutes, Hours, DayOfMonth, Month, DayOfWeek, Year] = case parse_expression_fields(Input) of [_, _, _, _, _] = Standard -> [SecondsDefault | Standard] ++ [YearDefault]; [_, _, _, _, _, _] = ExpressionWithYear -> [SecondsDefault | ExpressionWithYear]; [_, _, _, _, _, _, _] = Fields -> Fields; [Macro] -> if Macro == "@yearly"; Macro == "@annually" -> [SecondsDefault, "0", "0", "1", "1", "*", YearDefault]; Macro == "@monthly" -> [SecondsDefault, "0", "0", "1", "*", "*", YearDefault]; Macro == "@weekly" -> [SecondsDefault, "0", "0", "*", "*", "0", YearDefault]; Macro == "@daily"; Macro == "@midnight" -> [SecondsDefault, "0", "0", "*", "*", "*", YearDefault]; Macro == "@hourly" -> [SecondsDefault, "0", "*", "*", "*", "*", YearDefault] end end, ExpressionStrings = {Seconds, Minutes, Hours, DayOfMonth, Month, DayOfWeek, Year}, Expression = {parse_seconds(Seconds), parse_minutes(Minutes), parse_hours(Hours), parse_day_of_month(DayOfMonth), parse_month(Month), parse_day_of_week(DayOfWeek), parse_year(Year)}, true = Expression /= {undefined, undefined, undefined, undefined, undefined, undefined, undefined}, #cloudi_cron{expression_strings = ExpressionStrings, expression = Expression}. %%------------------------------------------------------------------------- %% @doc %% ===Determine the next datetime based on the cron expression.=== %% @end %%------------------------------------------------------------------------- -spec next_datetime(DateTime :: calendar:datetime(), state()) -> calendar:datetime() | undefined. next_datetime({Date0, _} = DateTime0, #cloudi_cron{expression = Expression}) -> Equal0 = false, {Equal1, DateTime1} = next_datetime_seconds(Expression, Equal0, DateTime0), {Equal2, DateTime2} = next_datetime_minutes(Expression, Equal1, DateTime1), {Equal3, DateTime3} = next_datetime_hours(Expression, Equal2, DateTime2), {Equal4, DateTime4} = next_datetime_day_of_month(Expression, Equal3, DateTime3), {Equal5, DateTime5} = next_datetime_month(Expression, Equal4, DateTime4), {Equal6, DateTime6} = next_datetime_day_of_week(Expression, Equal5, DateTime5), case next_datetime_year(Expression, Equal6, DateTime6) of undefined -> undefined; {Date0, _} = DateTimeN -> DateTimeN; {DateN, _} -> DateTime7 = {DateN, {0, 0, 0}}, {true, DateTime8} = next_datetime_seconds(Expression, true, DateTime7), {true, DateTime9} = next_datetime_minutes(Expression, true, DateTime8), {true, DateTime10} = next_datetime_hours(Expression, true, DateTime9), {true, DateTime11} = next_datetime_day_of_month(Expression, true, DateTime10), {true, DateTime12} = next_datetime_month(Expression, true, DateTime11), {true, DateTimeN} = next_datetime_day_of_week(Expression, true, DateTime12), DateTimeN end. %%%------------------------------------------------------------------------ %%% Private functions %%%------------------------------------------------------------------------ parse_values("*", _, _, _, _) -> undefined; parse_values(Input, Min, Max, Names, Allow) -> Values = [parse_value(Segment, Min, Max, Names, Allow) || Segment <- cloudi_string:split(",", Input)], lists:usort(lists:flatten(Values)). parse_value(Segment, Min, Max, Names, Allow) -> case cloudi_string:splitr($/, Segment, input) of {"", Segment} -> case cloudi_string:splitl($-, Segment, input) of {Segment, ""} -> parse_value_name(Allow, Segment, Min, Max, Names); {RangeMinStr, RangeMaxStr} -> parse_value_range(RangeMinStr, RangeMaxStr, "1", Min, Max, Names) end; {"*", StepStr} -> Step = erlang:list_to_integer(StepStr), true = (Step > Min) andalso (Step < Max), lists:seq(Min, Max, Step); {RangeStr, StepStr} -> {RangeMinStr, RangeMaxStr} = cloudi_string:splitl($-, RangeStr, empty), parse_value_range(RangeMinStr, RangeMaxStr, StepStr, Min, Max, Names) end. parse_value_name_integer(Value, Min, Max, undefined) -> Integer = erlang:list_to_integer(Value), true = (Min =< Integer) andalso (Integer =< Max), Integer; parse_value_name_integer(Value, Min, Max, Names) -> case cloudi_lists:index(cloudi_string:lowercase(Value), Names) of undefined -> Integer = erlang:list_to_integer(Value), true = (Min =< Integer) andalso (Integer =< Max), if Max =:= 7, Integer =:= 7 -> % day_of_week == 7 becomes 0 for Sunday 0; true -> Integer end; ValuePlus1 -> Integer = ValuePlus1 - 1 + Min, true = (Integer =< Max), Integer end. parse_value_name([], Value, Min, Max, Names) -> parse_value_name_integer(Value, Min, Max, Names); parse_value_name([every | Allow], Value, Min, Max, Names) -> case cloudi_string:splitl($#, Value, input) of {Value, ""} -> parse_value_name(Allow, Value, Min, Max, Names); {ValueAt, MultiplierStr} -> Multiplier = erlang:list_to_integer(MultiplierStr), true = (1 =< Multiplier) andalso (Multiplier =< 5), {every, Multiplier, parse_value_name_integer(ValueAt, Min, Max, Names)} end; parse_value_name([last_day_of_month | Allow], Value, Min, Max, Names) -> if Value == "L" -> last_day_of_month; Value == "LW"; Value == "WL" -> {weekday, last_day_of_month}; true -> parse_value_name(Allow, Value, Min, Max, Names) end; parse_value_name([SuffixName | Allow], Value, Min, Max, Names) when SuffixName =:= last_day_of_week; SuffixName =:= weekday -> SuffixChar = if SuffixName =:= last_day_of_week -> $L; SuffixName =:= weekday -> $W end, case lists:reverse(Value) of [SuffixChar | ValueReversed] -> PrefixStr = lists:reverse(ValueReversed), {SuffixName, parse_value_name_integer(PrefixStr, Min, Max, Names)}; _ -> parse_value_name(Allow, Value, Min, Max, Names) end. parse_value_range(RangeMinStr, RangeMaxStr, StepStr, Min, Max, Names) -> [_ | _] = RangeMinStr, [_ | _] = RangeMaxStr, RangeMin = parse_value_name_integer(RangeMinStr, Min, Max, Names), RangeMax = parse_value_name_integer(RangeMaxStr, Min, Max, Names), Step = erlang:list_to_integer(StepStr), true = is_integer(RangeMin), true = is_integer(RangeMax), true = (RangeMin < RangeMax), true = (Step >= 1) andalso (Step < Max), lists:seq(RangeMin, RangeMax, Step). parse_seconds([_ | _] = Input) -> Min = 0, Max = 59, Names = undefined, Allow = [], parse_values(Input, Min, Max, Names, Allow). parse_minutes([_ | _] = Input) -> Min = 0, Max = 59, Names = undefined, Allow = [], parse_values(Input, Min, Max, Names, Allow). parse_hours([_ | _] = Input) -> Min = 0, Max = 23, Names = undefined, Allow = [], parse_values(Input, Min, Max, Names, Allow). parse_day_of_month([_ | _] = Input) -> Min = 1, Max = 31, Names = undefined, Allow = [last_day_of_month, weekday], parse_values(Input, Min, Max, Names, Allow). parse_month([_ | _] = Input) -> Min = 1, Max = 12, Names = ["jan","feb","mar","apr","may","jun", "jul","aug","sep","oct","nov","dec"], Allow = [], parse_values(Input, Min, Max, Names, Allow). parse_day_of_week([_ | _] = Input) -> Min = 0, Max = 7, Names = ["sun","mon","tue","wed","thu","fri","sat"], Allow = [last_day_of_week, every], parse_values(Input, Min, Max, Names, Allow). parse_year([_ | _] = Input) -> Min = 1970, Max = 9999, Names = undefined, Allow = [], parse_values(Input, Min, Max, Names, Allow). parse_expression_fields(L) -> parse_expression_fields(parse_expression_spaces(L), [], []). parse_expression_fields([], [], Fields) -> lists:reverse(Fields); parse_expression_fields([], Field, Fields) -> lists:reverse([lists:reverse(Field) | Fields]); parse_expression_fields([$ | T], Field, Fields) -> parse_expression_fields(parse_expression_spaces(T), [], [lists:reverse(Field) | Fields]); parse_expression_fields([H | T], Field, Fields) -> parse_expression_fields(T, [H | Field], Fields). parse_expression_spaces([$ | T]) -> parse_expression_spaces(T); parse_expression_spaces(L) -> L. next_datetime_seconds({undefined, _, _, _, _, _, _}, Equal, DateTime) -> {Equal, DateTime}; next_datetime_seconds({FieldSeconds, _, _, _, _, _, _}, Equal, {Date, {TimeHH, TimeMM, TimeSS}} = DateTime) -> DateTimeNew = case get_next_value(FieldSeconds, TimeSS, Equal, Date) of undefined -> [TimeSSNew | _] = FieldSeconds, SecondsIncr = 60 - TimeSS, {{DateYYNew, DateMMNew, DateDDNew}, {TimeHHNew, TimeMMNew, _}} = datetime_add(DateTime, SecondsIncr), {{DateYYNew, DateMMNew, DateDDNew}, {TimeHHNew, TimeMMNew, TimeSSNew}}; TimeSSNew -> {Date, {TimeHH, TimeMM, TimeSSNew}} end, {true, DateTimeNew}. next_datetime_minutes({_, undefined, _, _, _, _, _}, Equal, DateTime) -> {Equal, DateTime}; next_datetime_minutes({_, FieldMinutes, _, _, _, _, _}, Equal, {Date, {TimeHH, TimeMM, TimeSS}} = DateTime) -> DateTimeNew = case get_next_value(FieldMinutes, TimeMM, Equal, Date) of undefined -> [TimeMMNew | _] = FieldMinutes, SecondsIncr = (60 - TimeMM) * 60, {{DateYYNew, DateMMNew, DateDDNew}, {TimeHHNew, _, TimeSSNew}} = datetime_add(DateTime, SecondsIncr), {{DateYYNew, DateMMNew, DateDDNew}, {TimeHHNew, TimeMMNew, TimeSSNew}}; TimeMMNew -> {Date, {TimeHH, TimeMMNew, TimeSS}} end, {true, DateTimeNew}. next_datetime_hours({_, _, undefined, _, _, _, _}, Equal, DateTime) -> {Equal, DateTime}; next_datetime_hours({_, _, FieldHours, _, _, _, _}, Equal, {Date, {TimeHH, TimeMM, TimeSS}}) -> DateTimeNew = case get_next_value(FieldHours, TimeHH, Equal, Date) of undefined -> [TimeHHNew | _] = FieldHours, DateNew = date_add(Date, 1), {DateNew, {TimeHHNew, TimeMM, TimeSS}}; TimeHHNew -> {Date, {TimeHHNew, TimeMM, TimeSS}} end, {true, DateTimeNew}. next_datetime_day_of_month({_, _, _, undefined, _, _, _}, Equal, DateTime) -> {Equal, DateTime}; next_datetime_day_of_month({_, _, _, FieldDayOfMonth, _, _, _} = Expression, Equal, {{_, _, DateDD} = Date, Time} = DateTime) -> case get_next_value(FieldDayOfMonth, DateDD, Equal, Date) of DateDD -> {Equal, DateTime}; _ -> next_datetime_day_of_month(Expression, true, {date_add(Date, 1), Time}) end. next_datetime_month({_, _, _, _, undefined, _, _}, Equal, DateTime) -> {Equal, DateTime}; next_datetime_month({_, _, _, _, FieldMonth, _, _} = Expression, Equal, {{_, DateMM, _} = Date, Time} = DateTime) -> case get_next_value(FieldMonth, DateMM, Equal, Date) of DateMM -> {Equal, DateTime}; _ -> next_datetime_month(Expression, true, {date_add(Date, 1), Time}) end. next_datetime_day_of_week({_, _, _, _, _, undefined, _}, Equal, DateTime) -> {Equal, DateTime}; next_datetime_day_of_week({_, _, _, _, _, FieldDayOfWeek, _} = Expression, Equal, {Date, Time} = DateTime) -> DayOfWeek = day_of_week(Date), case get_next_value(FieldDayOfWeek, DayOfWeek, Equal, Date) of DayOfWeek -> {Equal, DateTime}; _ -> next_datetime_day_of_week(Expression, true, {date_add(Date, 1), Time}) end. next_datetime_year({_, _, _, _, _, _, undefined}, _, DateTime) -> DateTime; next_datetime_year({_, _, _, _, _, _, FieldYear}, Equal, {{DateYY, _, _} = Date, _} = DateTime) -> case get_next_value(FieldYear, DateYY, Equal, Date) of undefined -> undefined; DateYY -> DateTime; DateYYNew -> {{DateYYNew, 1, 1}, {0, 0, 0}} end. get_next_value(L, I, Equal, Date) -> ExpandedL = lists:usort(get_next_value_expand(L, Date)), if Equal =:= true -> get_next_value_compare_greater_or_equal(ExpandedL, I); Equal =:= false -> get_next_value_compare_greater(ExpandedL, I) end. get_next_value_expand([] = L, _) -> L; get_next_value_expand([last_day_of_month | T], {DateYY, DateMM, _} = Date) -> Last = calendar:last_day_of_the_month(DateYY, DateMM), [Last | get_next_value_expand(T, Date)]; get_next_value_expand([{weekday, WeekDay} | T], {DateYY, DateMM, _} = Date) -> DateDD = if WeekDay =:= last_day_of_month -> calendar:last_day_of_the_month(DateYY, DateMM); is_integer(WeekDay) -> WeekDay end, [closest_weekday({DateYY, DateMM, DateDD}) | get_next_value_expand(T, Date)]; get_next_value_expand([{last_day_of_week, DayOfWeek} | T], Date) -> case last_day_of_week(Date, DayOfWeek) of true -> [DayOfWeek | get_next_value_expand(T, Date)]; false -> get_next_value_expand(T, Date) end; get_next_value_expand([{every, Multiplier, DayOfWeek} | T], Date) -> case every_day_of_week(Date, Multiplier, DayOfWeek) of true -> [DayOfWeek | get_next_value_expand(T, Date)]; false -> get_next_value_expand(T, Date) end; get_next_value_expand([H | T], Date) when is_integer(H) -> [H | get_next_value_expand(T, Date)]. get_next_value_compare_greater([], _) -> undefined; get_next_value_compare_greater([H | _], I) when H > I -> H; get_next_value_compare_greater([_ | T], I) -> get_next_value_compare_greater(T, I). get_next_value_compare_greater_or_equal([], _) -> undefined; get_next_value_compare_greater_or_equal([H | _], I) when H >= I -> H; get_next_value_compare_greater_or_equal([_ | T], I) -> get_next_value_compare_greater_or_equal(T, I). closest_weekday({DateYY, DateMM, DateDD0} = Date) -> case day_of_week(Date) of 0 -> % Sunday case date_add(Date, 1) of {DateYY, DateMM, DateDD1} -> DateDD1; _ -> {DateYY, DateMM, DateDD2} = date_add(Date, -2), DateDD2 end; 6 -> % Saturday case date_add(Date, -1) of {DateYY, DateMM, DateDD1} -> DateDD1; _ -> {DateYY, DateMM, DateDD2} = date_add(Date, 2), DateDD2 end; _ -> DateDD0 end. last_day_of_week_find(Date, DayOfWeek) -> case day_of_week(Date) of DayOfWeek -> Date; _ -> last_day_of_week_find(date_add(Date, -1), DayOfWeek) end. last_day_of_week({DateYY, DateMM, _} = Date, DayOfWeek) -> DateDD = calendar:last_day_of_the_month(DateYY, DateMM), Date == last_day_of_week_find({DateYY, DateMM, DateDD}, DayOfWeek). every_day_of_week_find(Date, I, Multiplier, DayOfWeek) -> case day_of_week(Date) of DayOfWeek when I == Multiplier -> Date; DayOfWeek -> every_day_of_week_find(date_add(Date, 7), I + 1, Multiplier, DayOfWeek); _ -> every_day_of_week_find(date_add(Date, 1), I, Multiplier, DayOfWeek) end. every_day_of_week_find(Date, Multiplier, DayOfWeek) -> every_day_of_week_find(Date, 1, Multiplier, DayOfWeek). every_day_of_week({DateYY, DateMM, _} = Date, Multiplier, DayOfWeek) -> Date == every_day_of_week_find({DateYY, DateMM, 1}, Multiplier, DayOfWeek). datetime_add(DateTime, SecondsIncr) -> Seconds = calendar:datetime_to_gregorian_seconds(DateTime), calendar:gregorian_seconds_to_datetime(Seconds + SecondsIncr). date_add(Date, DaysIncr) -> Days = calendar:date_to_gregorian_days(Date), calendar:gregorian_days_to_date(Days + DaysIncr). day_of_week(Date) -> case calendar:day_of_the_week(Date) of 7 -> % Sunday 0; DayOfWeek -> DayOfWeek end. -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). -include("cloudi_service_cron_test.hrl"). module_test_() -> {timeout, ?TEST_TIMEOUT, [ {"parse expression tests", ?_assertOk(t_parse_expression())}, {"next_datetime tests", ?_assertOk(t_next_datetime())} ]}. t_parse_expression() -> ExpressionString0 = " @midnight ", ExpressionStrings0 = {"0", "0", "0", "*", "*", "*", "*"}, Expression0 = {[0], [0], [0], undefined, undefined, undefined, undefined}, #cloudi_cron{expression_strings = ExpressionStrings0, expression = Expression0} = new(ExpressionString0), ExpressionString1 = " 40 16 * * mon-fri ", ExpressionStrings1 = {"0", "40", "16", "*", "*", "mon-fri", "*"}, Expression1 = {[0], [40], [16], undefined, undefined, [1,2,3,4,5], undefined}, #cloudi_cron{expression_strings = ExpressionStrings1, expression = Expression1} = new(ExpressionString1), ExpressionString2 = "18,38,58 17 * * sat", ExpressionStrings2 = {"0", "18,38,58", "17", "*", "*", "sat", "*"}, Expression2 = {[0], [18,38,58], [17], undefined, undefined, [6], undefined}, #cloudi_cron{expression_strings = ExpressionStrings2, expression = Expression2} = new(ExpressionString2), ExpressionString3 = " 0 3 * * sun-mon,wed-sat", ExpressionStrings3 = {"0", "0", "3", "*", "*", "sun-mon,wed-sat", "*"}, Expression3 = {[0], [0], [3], undefined, undefined, [0,1,3,4,5,6], undefined}, #cloudi_cron{expression_strings = ExpressionStrings3, expression = Expression3} = new(ExpressionString3), ExpressionString4 = " 0 3 L * mon#2", ExpressionStrings4 = {"0", "0", "3", "L", "*", "mon#2", "*"}, Expression4 = {[0], [0], [3], [last_day_of_month], undefined, [{every, 2, 1}], undefined}, #cloudi_cron{expression_strings = ExpressionStrings4, expression = Expression4} = new(ExpressionString4), ExpressionString5 = " 0 3 LW * monL", ExpressionStrings5 = {"0", "0", "3", "LW", "*", "monL", "*"}, Expression5 = {[0], [0], [3], [{weekday, last_day_of_month}], undefined, [{last_day_of_week, 1}], undefined}, #cloudi_cron{expression_strings = ExpressionStrings5, expression = Expression5} = new(ExpressionString5), ExpressionString6 = " 0 3 15W * 5L", ExpressionStrings6 = {"0", "0", "3", "15W", "*", "5L", "*"}, Expression6 = {[0], [0], [3], [{weekday, 15}], undefined, [{last_day_of_week, 5}], undefined}, #cloudi_cron{expression_strings = ExpressionStrings6, expression = Expression6} = new(ExpressionString6), ok. t_next_datetime() -> Cron0 = new("18,38,58 17 * * mon-fri"), DateTime0 = {{2019, 10, 25}, {17, 17, 59}}, DateTime1 = {{2019, 10, 25}, {17, 18, 0}}, DateTime1 = next_datetime(DateTime0, Cron0), DateTime2 = {{2019, 10, 25}, {17, 38, 0}}, DateTime2 = next_datetime(DateTime1, Cron0), DateTime3 = {{2019, 10, 25}, {17, 58, 0}}, DateTime3 = next_datetime(DateTime2, Cron0), DateTime4 = {{2019, 10, 28}, {17, 18, 0}}, DateTime4 = next_datetime(DateTime3, Cron0), Cron1 = new("0 17 LW * *"), DateTime5 = {{2019, 10, 30}, {0, 0, 0}}, DateTime6 = {{2019, 10, 31}, {17, 0, 0}}, DateTime6 = next_datetime(DateTime5, Cron1), Cron2 = new("0 17 * * 1#3"), DateTime7 = {{2019, 10, 1}, {0, 0, 0}}, DateTime8 = {{2019, 10, 21}, {17, 0, 0}}, DateTime8 = next_datetime(DateTime7, Cron2), Cron3 = new("0 17 13W * *"), DateTime9 = {{2019, 10, 14}, {17, 0, 0}}, DateTime9 = next_datetime(DateTime7, Cron3), Cron4 = new("0 17 12W * *"), DateTime10 = {{2019, 10, 11}, {17, 0, 0}}, DateTime10 = next_datetime(DateTime7, Cron4), Cron5 = new("0 17 * * 1L"), DateTime11 = {{2019, 10, 28}, {17, 0, 0}}, DateTime11 = next_datetime(DateTime7, Cron5), DateTime12 = {{2019, 11, 3}, {12, 30, 53}}, DateTime13 = {{2019, 11, 4}, {17, 18, 0}}, DateTime13 = next_datetime(DateTime12, Cron0), Cron6 = new("*/1 * * * * 1970"), DateTime14 = {{2020, 8, 17}, {17, 0, 0}}, undefined = next_datetime(DateTime14, Cron6), Cron7 = new("18,38,58 17 * * mon-fri 2020"), DateTime15 = {{2020, 1, 1}, {17, 18, 0}}, DateTime15 = next_datetime(DateTime0, Cron7), DateTime16 = {{2020, 1, 1}, {17, 38, 0}}, DateTime16 = next_datetime(DateTime15, Cron7), ok. -endif.