Current section
Files
Jump to
Current section
Files
lib/excal/recurrence/stream.ex
defmodule Excal.Recurrence.Stream do
@moduledoc """
Generates Elixir streams from iCalendar recurrence rules (RRULE).
This is the most idiomatic way of interacting with iCalendar recurrence rules in Elixir. The streams created here act
like any other Elixir stream would act.
"""
alias Excal.Recurrence.Iterator
@typedoc """
Valid options for `new/3`.
"""
@type option :: {:from, Excal.date_or_datetime()} | {:until, Excal.date_or_datetime()}
@doc """
Creates a stream of date or datetime instances from the given recurrence rule string and schedule start time.
It's also possible to set the start and end time of the stream using the `:from` and `:until` options.
## Options
* `:from` - specifies the start date or datetime of the stream.
* `:until` - specifies the end date or datetime of the stream.
## Examples
An infinite stream of `Date` structs for every Monday, Wednesday and Friday:
iex> {:ok, stream} = Stream.new("FREQ=WEEKLY;BYDAY=MO,WE,FR", ~D[2019-01-01])
...> Enum.take(stream, 5)
[
~D[2019-01-02],
~D[2019-01-04],
~D[2019-01-07],
~D[2019-01-09],
~D[2019-01-11]
]
A finite stream of `NaiveDateTime` using the `:from` and `:until` options:
iex> opts = [from: ~N[2020-01-01 10:00:00], until: ~N[2020-06-01 10:00:00]]
...> {:ok, stream} = Stream.new("FREQ=MONTHLY;BYMONTHDAY=1", ~N[2019-01-01 10:00:00], opts)
...> Enum.to_list(stream)
[
~N[2020-01-01 10:00:00],
~N[2020-02-01 10:00:00],
~N[2020-03-01 10:00:00],
~N[2020-04-01 10:00:00],
~N[2020-05-01 10:00:00]
]
"""
@spec new(String.t(), Excal.date_or_datetime(), [option()]) ::
{:ok, Enumerable.t()} | {:error, Iterator.initialization_error() | Iterator.iterator_start_error()}
def new(rrule, dtstart, opts \\ []) do
# The below call to make_stream will not return any errors until the stream is used,
# so we initialize an iterator first to ensure it can be, to return any possible errors.
# This iterator is not actually used though.
with {:ok, _} <- make_iterator(rrule, dtstart, opts) do
{:ok, make_stream(rrule, dtstart, opts)}
end
end
defp make_iterator(rrule, dtstart, opts) do
with {:ok, iterator} <- Iterator.new(rrule, dtstart) do
process_options(iterator, opts)
end
end
defp process_options(iterator, []), do: {:ok, iterator}
defp process_options(iterator, [{:from, time} | rest]) do
with {:ok, iterator} <- Iterator.set_start(iterator, time) do
process_options(iterator, rest)
end
end
defp process_options(iterator, [{:until, time} | rest]) do
with {:ok, iterator} <- Iterator.set_end(iterator, time) do
process_options(iterator, rest)
end
end
defp make_stream(rrule, dtstart, opts) do
Elixir.Stream.resource(
fn ->
{:ok, iterator} = make_iterator(rrule, dtstart, opts)
iterator
end,
fn iterator ->
case Iterator.next(iterator) do
{nil, iterator} -> {:halt, iterator}
{occurrence, iterator} -> {[occurrence], iterator}
end
end,
fn _ -> nil end
)
end
end