defmodule Cocktail.Schedule do
@moduledoc """
Struct used to represent a schedule of recurring events.
Use the `new/2` function to create a new schedule, and the
`add_recurrence_rule/2` function to add rules to describe how to repeat.
Currently, Cocktail supports the following types of repeat rules:
* Weekly - Every week, relative to the schedule's start time
* Daily - Every day at the schedule's start time
* Hourly - Every hour, starting at the schedule's start time
* Minutely - Every minute, starting at the schedule's start time
* Secondly - Every second, starting at the schedule's start time
Once a schedule has been created, you can use `occurrences/2` to generate
a stream of occurrences, which are either `t:Cocktail.time/0`s or
`t:Cocktail.Span.t/0`s if a `duration` option was given to the schedule.
Various options can be given to modify the way the repeat rule and schedule
behave. See `add_recurrence_rule/3` for details on them.
"""
alias Cocktail.{Builder, Parser, Rule, ScheduleState}
@typedoc """
Struct used to represent a schedule of recurring events.
This type should be considered opaque, so its fields shouldn't be modified
directly. Instead, use the functions provided in this module to create and
manipulate schedules.
## Fields:
* `:start_time` - The schedule's start time
* `:duration` - The duration of each occurrence (in seconds)
"""
@type t :: %__MODULE__{
recurrence_rules: [Rule.t()],
recurrence_times: [Cocktail.time()],
exception_times: [Cocktail.time()],
start_time: Cocktail.time(),
duration: pos_integer | nil
}
@enforce_keys [:start_time]
defstruct recurrence_rules: [],
recurrence_times: [],
exception_times: [],
start_time: nil,
duration: nil
@doc """
Creates a new schedule using the given start time and options.
This schedule will be empty and needs recurrence rules added to it before it is useful.
Use `add_recurrence_rule/3` to add rules to a schedule.
## Options
* `:duration` - The duration of each event in the schedule (in seconds).
## Examples
iex> new(~N[2017-01-01 06:00:00], duration: 3_600)
#Cocktail.Schedule<>
"""
@spec new(Cocktail.time(), Cocktail.schedule_options()) :: t
def new(start_time, options \\ []) do
%__MODULE__{
start_time: no_ms(start_time),
duration: options[:duration]
}
end
@doc false
@spec set_start_time(t, Cocktail.time()) :: t
def set_start_time(schedule, start_time), do: %{schedule | start_time: no_ms(start_time)}
@doc false
@spec set_duration(t, pos_integer) :: t
def set_duration(schedule, duration), do: %{schedule | duration: duration}
@doc false
@spec set_end_time(t, Cocktail.time()) :: t
def set_end_time(%__MODULE__{start_time: start_time} = schedule, end_time) do
duration = Timex.diff(end_time, start_time, :seconds)
%{schedule | duration: duration}
end
@doc false
@spec add_recurrence_rule(t, Rule.t()) :: t
def add_recurrence_rule(%__MODULE__{} = schedule, %Rule{} = rule) do
%{schedule | recurrence_rules: [rule | schedule.recurrence_rules]}
end
@doc """
Adds a recurrence rule of the given frequency to a schedule.
The frequency can be one of `:monthly`, `:weekly`, `:daily`, `:hourly`, `:minutely` or `:secondly`
## Options
* `:interval` - How often to repeat, given the frequency. For example a `:daily` rule with interval `2` would be "every other day".
* `:count` - The number of times this rule can produce an occurrence. *(not yet support)*
* `:until` - The end date/time after which the rule will no longer produce occurrences.
* `:days_of_month` - Restrict this rule to specific days of the month. (e.g. `[-1, 10, 31]`)
* `:days` - Restrict this rule to specific days. (e.g. `[:monday, :wednesday, :friday]`)
* `:hours` - Restrict this rule to certain hours of the day. (e.g. `[10, 12, 14]`)
* `:minutes` - Restrict this rule to certain minutes of the hour. (e.g. `[0, 15, 30, 45]`)
* `:seconds` - Restrict this rule to certain seconds of the minute. (e.g. `[0, 30]`)
## Examples
iex> start_time = ~N[2017-01-01 06:00:00]
...> start_time |> new() |> add_recurrence_rule(:daily, interval: 2, hours: [10, 14])
#Cocktail.Schedule<Every 2 days on the 10th and 14th hours of the day>
"""
@spec add_recurrence_rule(t, Cocktail.frequency(), Cocktail.rule_options()) :: t
def add_recurrence_rule(%__MODULE__{} = schedule, frequency, options \\ []) do
rule =
options
|> Keyword.put(:frequency, frequency)
|> Rule.new()
add_recurrence_rule(schedule, rule)
end
@doc """
Adds a one-off recurrence time to the schedule.
This recurrence time can be any time after (or including) the schedule's start
time. When generating occurrences from this schedule, the given time will be
included in the set of occurrences alongside any recurrence rules.
"""
@spec add_recurrence_time(t, Cocktail.time()) :: t
def add_recurrence_time(%__MODULE__{} = schedule, time),
do: %{schedule | recurrence_times: [no_ms(time) | schedule.recurrence_times]}
@doc """
Adds an exception time to the schedule.
This exception time will cancel out any occurrence generated from the
schedule's recurrence rules or recurrence times.
"""
@spec add_exception_time(t, Cocktail.time()) :: t
def add_exception_time(%__MODULE__{} = schedule, time),
do: %{schedule | exception_times: [no_ms(time) | schedule.exception_times]}
@doc """
Creates a stream of occurrences from the given schedule.
An optional `start_time` can be supplied to not start at the schedule's start time.
The occurrences that are produced by the stream can be one of several types:
* If the schedule's start time is a `t:DateTime.t/0`, then it will produce
`t:DateTime.t/0`s
* If the schedule's start time is a `t:NaiveDateTime.t/0`, the it will
produce `t:NaiveDateTime.t/0`s
* If a duration is supplied when creating the schedule, the stream will
produce `t:Cocktail.Span.t/0`s with `:from` and `:until` fields matching
the type of the schedule's start time
## Examples
# using a NaiveDateTime
iex> start_time = ~N[2017-01-01 06:00:00]
...> schedule = start_time |> new() |> add_recurrence_rule(:daily, interval: 2, hours: [10, 14])
...> schedule |> occurrences() |> Enum.take(3)
[~N[2017-01-01 10:00:00],
~N[2017-01-01 14:00:00],
~N[2017-01-03 10:00:00]]
# using an alternate start time
iex> start_time = ~N[2017-01-01 06:00:00]
...> schedule = start_time |> new() |> add_recurrence_rule(:daily, interval: 2, hours: [10, 14])
...> schedule |> occurrences(~N[2017-10-01 06:00:00]) |> Enum.take(3)
[~N[2017-10-02 10:00:00],
~N[2017-10-02 14:00:00],
~N[2017-10-04 10:00:00]]
# using a DateTime with a time zone
iex> start_time = Timex.to_datetime(~N[2017-01-02 10:00:00], "America/Los_Angeles")
...> schedule = start_time |> new() |> add_recurrence_rule(:daily)
...> schedule |> occurrences() |> Enum.take(3) |> Enum.map(&Timex.format!(&1, "{ISO:Extended}"))
["2017-01-02T10:00:00-08:00",
"2017-01-03T10:00:00-08:00",
"2017-01-04T10:00:00-08:00"]
# using a NaiveDateTime with a duration
iex> start_time = ~N[2017-02-01 12:00:00]
...> schedule = start_time |> new(duration: 3_600) |> add_recurrence_rule(:weekly)
...> schedule |> occurrences() |> Enum.take(3)
[%Cocktail.Span{from: ~N[2017-02-01 12:00:00], until: ~N[2017-02-01 13:00:00]},
%Cocktail.Span{from: ~N[2017-02-08 12:00:00], until: ~N[2017-02-08 13:00:00]},
%Cocktail.Span{from: ~N[2017-02-15 12:00:00], until: ~N[2017-02-15 13:00:00]}]
"""
@spec occurrences(t, Cocktail.time() | nil) :: Enumerable.t()
def occurrences(%__MODULE__{} = schedule, start_time \\ nil) do
schedule
|> ScheduleState.new(no_ms(start_time))
|> Stream.unfold(&ScheduleState.next_time/1)
end
@doc """
Add an end time to all recurrence rules in the schedule.
This has the same effect as if you'd passed the `:until` option when adding
all recurrence rules to the schedule.
"""
@spec end_all_recurrence_rules(t, Cocktail.time()) :: t
def end_all_recurrence_rules(%__MODULE__{recurrence_rules: rules} = schedule, end_time),
do: %{schedule | recurrence_rules: Enum.map(rules, &Rule.set_until(&1, end_time))}
@doc """
Parses a string in iCalendar format into a `t:Cocktail.Schedule.t/0`.
see `Cocktail.Parser.ICalendar.parse/1` for details.
"""
@spec from_i_calendar(String.t()) :: {:ok, t} | {:error, term}
def from_i_calendar(i_calendar_string), do: Parser.ICalendar.parse(i_calendar_string)
@doc """
Builds an iCalendar format string representation of a `t:Cocktail.Schedule.t/0`.
see `Cocktail.Builder.ICalendar.build/1` for details.
"""
@spec to_i_calendar(t) :: String.t()
def to_i_calendar(%__MODULE__{} = schedule), do: Builder.ICalendar.build(schedule)
@doc """
Builds an iCalendar RRULE format string representation of a `t:Cocktail.Schedule.t/0`.
see `Cocktail.Builder.ICalendar.build_rule/1` for details.
"""
@spec to_i_calendar_rrule(t) :: String.t()
def to_i_calendar_rrule(%__MODULE__{} = schedule), do: Builder.ICalendar.build_rule(schedule)
@doc """
Builds a human readable string representation of a `t:Cocktail.Schedule.t/0`.
see `Cocktail.Builder.String.build/1` for details.
"""
@spec to_string(t) :: String.t()
def to_string(%__MODULE__{} = schedule), do: Builder.String.build(schedule)
@spec no_ms(Cocktail.time() | nil) :: Cocktail.time() | nil
defp no_ms(nil), do: nil
defp no_ms(time), do: %{time | microsecond: {0, 0}}
defimpl Inspect, for: __MODULE__ do
import Inspect.Algebra
def inspect(schedule, _) do
concat(["#Cocktail.Schedule<", Builder.String.build(schedule), ">"])
end
end
end