Skip to content

Commit ff4becc

Browse files
shaolangmaennchendependabot[bot]smaximov
authored
Support localized cronjob scheduling with ambiguous and gap DateTime instances (#133)
* Allow specifying :earlier and :later for on_ambiguity CronExpression option * Enhance DateHelper.add/4 to handle ambiguities and gaps * Replace unnecessary DateTime creations just to convert to NaiveDateTime * Rename DateHelper.add/4 as shift/4 * Handle nagative shifts from daylight savings back to standard time * Enhance DateHelper.shift/4 to handle shifting backwards correctly * Align shift/4 tests' variable names in DateHelperTest * Replace NaiveDateTime.add/3 calls in Scheduler with DateHelper.shift/4 * Add :on_ambiguity attribute documentation * Remove unused branch in resolve_potential_gap * Additional DST Tests * Install erlef/mix-dependency-submission GH Action * Bump step-security/harden-runner from 2.11.1 to 2.12.0 (#135) Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.11.1 to 2.12.0. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](step-security/harden-runner@c6295a6...0634a26) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-version: 2.12.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump erlef/mix-dependency-submission from 1.0.0.pre.beta.8 to 1.0.1 (#136) Bumps [erlef/mix-dependency-submission](https://github.com/erlef/mix-dependency-submission) from 1.0.0.pre.beta.8 to 1.0.1. - [Release notes](https://github.com/erlef/mix-dependency-submission/releases) - [Commits](erlef/mix-dependency-submission@6b9e140...1e05381) --- updated-dependencies: - dependency-name: erlef/mix-dependency-submission dependency-version: 1.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump erlef/mix-dependency-submission from 1.0.1 to 1.0.2 (#137) * Bump erlef/mix-dependency-submission from 1.0.1 to 1.0.2 Bumps [erlef/mix-dependency-submission](https://github.com/erlef/mix-dependency-submission) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/erlef/mix-dependency-submission/releases) - [Commits](erlef/mix-dependency-submission@1e05381...fa66011) --- updated-dependencies: - dependency-name: erlef/mix-dependency-submission dependency-version: 1.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * Update .github/workflows/mix-dependency-submission.yml --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jonatan Männchen <jonatan@maennchen.ch> * Bump erlef/mix-dependency-submission from 1.0.2 to 1.1.0 (#138) Bumps [erlef/mix-dependency-submission](https://github.com/erlef/mix-dependency-submission) from 1.0.2 to 1.1.0. - [Release notes](https://github.com/erlef/mix-dependency-submission/releases) - [Commits](erlef/mix-dependency-submission@fa66011...a534dac) --- updated-dependencies: - dependency-name: erlef/mix-dependency-submission dependency-version: 1.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump erlef/mix-dependency-submission from 1.1.0 to 1.1.2 (#139) Bumps [erlef/mix-dependency-submission](https://github.com/erlef/mix-dependency-submission) from 1.1.0 to 1.1.2. - [Release notes](https://github.com/erlef/mix-dependency-submission/releases) - [Commits](erlef/mix-dependency-submission@a534dac...caee42b) --- updated-dependencies: - dependency-name: erlef/mix-dependency-submission dependency-version: 1.1.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump erlef/mix-dependency-submission from 1.1.2 to 1.1.3 (#140) Bumps [erlef/mix-dependency-submission](https://github.com/erlef/mix-dependency-submission) from 1.1.2 to 1.1.3. - [Release notes](https://github.com/erlef/mix-dependency-submission/releases) - [Commits](erlef/mix-dependency-submission@caee42b...dd81a2f) --- updated-dependencies: - dependency-name: erlef/mix-dependency-submission dependency-version: 1.1.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Bump step-security/harden-runner from 2.12.0 to 2.12.1 (#141) Bumps [step-security/harden-runner](https://github.com/step-security/harden-runner) from 2.12.0 to 2.12.1. - [Release notes](https://github.com/step-security/harden-runner/releases) - [Commits](step-security/harden-runner@0634a26...002fdce) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-version: 2.12.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Fix a typo in cron_notation.cheatmd (#142) * Bump step-security/harden-runner from 2.12.1 to 2.12.2 (#143) --- updated-dependencies: - dependency-name: step-security/harden-runner dependency-version: 2.12.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Reimplement DateHelper.shift/4 * Reimplement shift/4 to support skipping ambiguous times entirely * Include ambiguity checks in other modules * Rename ambiguous opts from earlier/later to prior/subsequent * Mix format * Change DateHelper.shift/4 ambiguity_opts default to [] * Cosmetic change in typing Co-authored-by: Jonatan Männchen <jonatan@maennchen.ch> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Jonatan Männchen <jonatan@maennchen.ch> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sergei Maximov <s.b.maximov@gmail.com>
1 parent b42e02b commit ff4becc

File tree

10 files changed

+633
-275
lines changed

10 files changed

+633
-275
lines changed

lib/crontab/cron_expression.ex

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ defmodule Crontab.CronExpression do
88
@type t :: %Crontab.CronExpression{
99
extended: boolean,
1010
reboot: boolean,
11+
on_ambiguity: [ambiguity_opt],
1112
second: [value(second)],
1213
minute: [value(minute)],
1314
hour: [value(hour)],
@@ -17,7 +18,9 @@ defmodule Crontab.CronExpression do
1718
year: [value(year)]
1819
}
1920

20-
@type interval :: :second | :minute | :hour | :day | :month | :weekday | :year
21+
@type ambiguity_opt :: :prior | :subsequent
22+
23+
@type interval :: :second | :minute | :hour | :day | :month | :weekday | :year | :ambiguity_opts
2124

2225
@typedoc deprecated: "Use Crontab.CronExpression.min_max/1 instead"
2326
@type min_max :: {:-, time_unit, time_unit}
@@ -63,7 +66,7 @@ defmodule Crontab.CronExpression do
6366
@typedoc deprecated: "Use Calendar.[second|minute|hour|day|month|day_of_week|year]/0 instead"
6467
@type time_unit :: second | minute | hour | day | month | weekday | year
6568

66-
@type condition(name, time_unit) :: {name, [value(time_unit)]}
69+
@type condition(name, time_unit) :: {name, [value(time_unit) | ambiguity_opt]}
6770
@type condition ::
6871
condition(:second, Calendar.second())
6972
| condition(:minute, Calendar.minute())
@@ -72,6 +75,7 @@ defmodule Crontab.CronExpression do
7275
| condition(:month, Calendar.month())
7376
| condition(:weekday, Calendar.day_of_week())
7477
| condition(:year, Calendar.year())
78+
| condition(:ambiguity_opts, [ambiguity_opt()])
7579

7680
@type condition_list :: [condition]
7781

@@ -89,9 +93,15 @@ defmodule Crontab.CronExpression do
8993
+-------------- :second Second (range: 0-59)
9094
9195
The `:extended` attribute defines if the second is taken into account.
96+
When using localized DateTime, the `:on_ambiguity` attribute defines
97+
whether the scheduler should return the prior or subsequent time when
98+
the next run DateTime is ambiguous. `:on_ambiguity` defaults to `[]`
99+
which means run DateTimes that fall within the ambiguous times would
100+
be skipped. To run on both, set it as `[:prior, :subsequent]`.
92101
"""
93102
defstruct extended: false,
94103
reboot: false,
104+
on_ambiguity: [],
95105
second: [:*],
96106
minute: [:*],
97107
hour: [:*],
@@ -108,6 +118,7 @@ defmodule Crontab.CronExpression do
108118
iex> ~e[*]
109119
%Crontab.CronExpression{
110120
extended: false,
121+
on_ambiguity: [],
111122
second: [:*],
112123
minute: [:*],
113124
hour: [:*],
@@ -116,9 +127,10 @@ defmodule Crontab.CronExpression do
116127
weekday: [:*],
117128
year: [:*]}
118129
119-
iex> ~e[*]e
130+
iex> ~e[*]ep
120131
%Crontab.CronExpression{
121132
extended: true,
133+
on_ambiguity: [:prior],
122134
second: [:*],
123135
minute: [:*],
124136
hour: [:*],
@@ -127,9 +139,22 @@ defmodule Crontab.CronExpression do
127139
weekday: [:*],
128140
year: [:*]}
129141
142+
iex> ~e[1 2 3 4 5 6 7]eps
143+
%Crontab.CronExpression{
144+
extended: true,
145+
on_ambiguity: [:prior, :subsequent],
146+
second: [1],
147+
minute: [2],
148+
hour: [3],
149+
day: [4],
150+
month: [5],
151+
weekday: [6],
152+
year: [7]}
153+
130154
iex> ~e[1 2 3 4 5 6 7]e
131155
%Crontab.CronExpression{
132156
extended: true,
157+
on_ambiguity: [],
133158
second: [1],
134159
minute: [2],
135160
hour: [3],
@@ -139,9 +164,18 @@ defmodule Crontab.CronExpression do
139164
year: [7]}
140165
"""
141166
@spec sigil_e(binary, charlist) :: t
142-
def sigil_e(cron_expression, options)
143-
def sigil_e(cron_expression, [?e]), do: Parser.parse!(cron_expression, true)
144-
def sigil_e(cron_expression, _options), do: Parser.parse!(cron_expression, false)
167+
def sigil_e(cron_expression, options \\ [?l]) do
168+
Parser.parse!(
169+
cron_expression,
170+
?e in options,
171+
cond do
172+
?p in options and ?s in options -> [:prior, :subsequent]
173+
?p in options -> [:prior]
174+
?s in options -> [:subsequent]
175+
true -> []
176+
end
177+
)
178+
end
145179

146180
@doc """
147181
Convert `Crontab.CronExpression` struct to tuple List.
@@ -155,7 +189,8 @@ defmodule Crontab.CronExpression do
155189
{:day, [3]},
156190
{:month, [4]},
157191
{:weekday, [5]},
158-
{:year, [6]}]
192+
{:year, [6]},
193+
{:ambiguity_opts, []}]
159194
160195
iex> Crontab.CronExpression.to_condition_list %Crontab.CronExpression{
161196
...> extended: true, second: [0], minute: [1], hour: [2], day: [3], month: [4], weekday: [5], year: [6]}
@@ -165,7 +200,8 @@ defmodule Crontab.CronExpression do
165200
{:day, [3]},
166201
{:month, [4]},
167202
{:weekday, [5]},
168-
{:year, [6]}]
203+
{:year, [6]},
204+
{:ambiguity_opts, []}]
169205
170206
"""
171207
@spec to_condition_list(t) :: condition_list
@@ -176,7 +212,8 @@ defmodule Crontab.CronExpression do
176212
{:day, interval.day},
177213
{:month, interval.month},
178214
{:weekday, interval.weekday},
179-
{:year, interval.year}
215+
{:year, interval.year},
216+
{:ambiguity_opts, interval.on_ambiguity}
180217
]
181218
end
182219

@@ -200,14 +237,16 @@ defmodule Crontab.CronExpression do
200237
iex> IO.inspect %Crontab.CronExpression{extended: true}
201238
~e[* * * * * * *]e
202239
240+
iex> import Crontab.CronExpression
241+
iex> IO.inspect %Crontab.CronExpression{extended: true, on_ambiguity: [:prior, :subsequent]}
242+
~e[* * * * * * *]eps
203243
"""
204244
@spec inspect(CronExpression.t(), any) :: String.t()
205-
def inspect(cron_expression = %CronExpression{extended: false}, _options) do
206-
"~e[" <> Composer.compose(cron_expression) <> "]"
207-
end
208-
209-
def inspect(cron_expression = %CronExpression{extended: true}, _options) do
210-
"~e[" <> Composer.compose(cron_expression) <> "]e"
245+
def inspect(cron_expression = %CronExpression{}, _options) do
246+
prior = if(:prior in cron_expression.on_ambiguity, do: "p", else: "")
247+
subsequent = if(:subsequent in cron_expression.on_ambiguity, do: "s", else: "")
248+
extended = if(cron_expression.extended, do: "e", else: "")
249+
"~e[" <> Composer.compose(cron_expression) <> "]#{extended}#{prior}#{subsequent}"
211250
end
212251
end
213252
end

lib/crontab/cron_expression/composer.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ defmodule Crontab.CronExpression.Composer do
5555
compose_interval(tail, opts)
5656
end
5757

58+
defp compose_interval([{:ambiguity_opts, _} | tail], opts), do: compose_interval(tail, opts)
59+
5860
defp compose_interval([{_, conditions} | tail], opts) do
5961
[
6062
Enum.map_join(conditions, ",", fn condition -> compose_condition(condition) end)

lib/crontab/cron_expression/parser.ex

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -72,32 +72,31 @@ defmodule Crontab.CronExpression.Parser do
7272
iex> Crontab.CronExpression.Parser.parse "* * * * *"
7373
{:ok,
7474
%Crontab.CronExpression{day: [:*], hour: [:*], minute: [:*],
75-
month: [:*], weekday: [:*], year: [:*]}}
75+
month: [:*], weekday: [:*], year: [:*], on_ambiguity: []}}
7676
7777
iex> Crontab.CronExpression.Parser.parse "* * * * *", true
7878
{:ok,
7979
%Crontab.CronExpression{extended: true, day: [:*], hour: [:*], minute: [:*],
80-
month: [:*], weekday: [:*], year: [:*], second: [:*]}}
80+
month: [:*], weekday: [:*], year: [:*], second: [:*], on_ambiguity: []}}
8181
8282
iex> Crontab.CronExpression.Parser.parse "fooo"
8383
{:error, "Can't parse fooo as minute."}
8484
8585
"""
86-
@spec parse(binary, boolean) :: result
87-
def parse(cron_expression, extended \\ false)
86+
@spec parse(binary, boolean, [CronExpression.ambiguity_opt()]) :: result
87+
def parse(cron_expression, extended \\ false, ambiguity_opts \\ [])
8888

89-
def parse("@" <> identifier, _) do
89+
def parse("@" <> identifier, _, _) do
9090
special(String.downcase(identifier))
9191
end
9292

93-
def parse(cron_expression, true) do
94-
interpret(String.split(cron_expression, " "), @extended_intervals, %CronExpression{
95-
extended: true
96-
})
97-
end
93+
def parse(cron_expression, is_extended, ambiguity_opts) do
94+
format = if(is_extended, do: @extended_intervals, else: @intervals)
9895

99-
def parse(cron_expression, false) do
100-
interpret(String.split(cron_expression, " "), @intervals, %CronExpression{})
96+
interpret(String.split(cron_expression, " "), format, %CronExpression{
97+
extended: is_extended,
98+
on_ambiguity: ambiguity_opts
99+
})
101100
end
102101

103102
@doc """
@@ -107,19 +106,19 @@ defmodule Crontab.CronExpression.Parser do
107106
108107
iex> Crontab.CronExpression.Parser.parse! "* * * * *"
109108
%Crontab.CronExpression{day: [:*], hour: [:*], minute: [:*],
110-
month: [:*], weekday: [:*], year: [:*]}
109+
month: [:*], weekday: [:*], year: [:*], on_ambiguity: []}
111110
112111
iex> Crontab.CronExpression.Parser.parse! "* * * * *", true
113112
%Crontab.CronExpression{extended: true, day: [:*], hour: [:*], minute: [:*],
114-
month: [:*], weekday: [:*], year: [:*], second: [:*]}
113+
month: [:*], weekday: [:*], year: [:*], second: [:*], on_ambiguity: []}
115114
116115
iex> Crontab.CronExpression.Parser.parse! "fooo"
117116
** (RuntimeError) Can't parse fooo as minute.
118117
119118
"""
120-
@spec parse!(binary, boolean) :: CronExpression.t()
121-
def parse!(cron_expression, extended \\ false) do
122-
case parse(cron_expression, extended) do
119+
@spec parse!(binary, boolean, [CronExpression.ambiguity_opt()]) :: CronExpression.t()
120+
def parse!(cron_expression, extended \\ false, ambiguity_opts \\ []) do
121+
case parse(cron_expression, extended, ambiguity_opts) do
123122
{:ok, result} -> result
124123
{:error, error} -> raise error
125124
end

0 commit comments

Comments
 (0)