fazibear/ex_ical

View on GitHub
lib/ex_ical/recurrence.ex

Summary

Maintainability
Test Coverage
defmodule ExIcal.Recurrence do
  @moduledoc """
  Adds support for recurring events.

  Events can recur by frequency, count, interval, and/or start/end date. To
  see the specific rules and examples, see `add_recurring_events/2` below.
  """

  alias ExIcal.Event

  @doc """
  Add recurring events to events list

  ## Parameters

    - `events`: List of events that each may contain an rrule. See `ExIcal.Event`.

    - `end_date` *(optional)*: A date time that represents the fallback end date
      for a recurring event. This value is only used when the options specified
      in rrule result in an infinite recurrance (ie. when neither `count` nor
      `until` is set). If no end_date is set, it will default to
      `DateTime.utc_now()`.

  ## Event rrule options

    Event recurrance details are specified in the `rrule`. The following options
    are considered:

    - `freq`: Represents how frequently the event recurs. Allowed frequencies
      are `DAILY`, `WEEKLY`, and `MONTHLY`. These can be further modified by
      the `interval` option.

    - `count` *(optional)*: Represents the number of times that an event will
      recur. This takes precedence over the `end_date` parameter and the
      `until` option.

    - `interval` *(optional)*: Represents the interval at which events occur.
      This option works in concert with `freq` above; by using the `interval`
      option, an event could recur every 5 days or every 3 weeks.

    - `until` *(optional)*: Represents the end date for a recurring event.
      This takes precedence over the `end_date` parameter.

    The `freq` option is required for a valid rrule, but the others are
    optional. They may be used either individually (ex. just `freq`) or in
    concert (ex. `freq` + `interval` + `until`).

  ## Examples

      iex> dt = Timex.Date.from({2016,8,13})
      iex> dt_end = Timex.Date.from({2016, 8, 23})
      iex> events = [%ExIcal.Event{rrule:%{freq: "DAILY"}, start: dt, end: dt}]
      iex> ExIcal.Recurrence.add_recurring_event(events, dt_end) |> length
      10
  """

  @spec add_recurring_events([%Event{}])              :: [%Event{}]
  @spec add_recurring_events([%Event{}], %DateTime{}) :: [%Event{}]
  def add_recurring_events(events, end_date \\ DateTime.utc_now()) do
    events ++ (events |> Enum.reduce([], fn(event, revents) ->
      case event.rrule do
        nil ->
          revents
        %{freq: "DAILY", count: count, interval: interval} ->
          revents ++ (event |> add_recurring_events_count(count, [days: interval]))
        %{freq: "DAILY", until: until, interval: interval} ->
          revents ++ (event |> add_recurring_events_until(until, [days: interval]))
        %{freq: "DAILY", count: count} ->
          revents ++ (event |> add_recurring_events_count(count, [days: 1]))
        %{freq: "DAILY", until: until} ->
          revents ++ (event |> add_recurring_events_until(until, [days: 1]))
        %{freq: "DAILY", interval: interval} ->
          revents ++ (event |> add_recurring_events_until(end_date, [days: interval]))
        %{freq: "DAILY"} ->
          revents ++ (event |> add_recurring_events_until(end_date, [days: 1]))

        %{freq: "WEEKLY", count: count, interval: interval} ->
          revents ++ (event |> add_recurring_events_count(count, [days: interval * 7]))
        %{freq: "WEEKLY", until: until, interval: interval} ->
          revents ++ (event |> add_recurring_events_until(until, [days: interval * 7]))
        %{freq: "WEEKLY", count: count} ->
          revents ++ (event |> add_recurring_events_count(count, [days: 7]))
        %{freq: "WEEKLY", until: until} ->
          revents ++ (event |> add_recurring_events_until(until, [days: 7]))
        %{freq: "WEEKLY", interval: interval} ->
          revents ++ (event |> add_recurring_events_until(end_date, [days: interval * 7]))
        %{freq: "WEEKLY"} ->
          revents ++ (event |> add_recurring_events_until(end_date, [days: 7]))


        %{freq: "MONTHLY", count: count, interval: interval} ->
          revents ++ (event |> add_recurring_events_count(count, [months: interval]))
        %{freq: "MONTHLY", until: until, interval: interval} ->
          revents ++ (event |> add_recurring_events_until(until, [months: interval]))
        %{freq: "MONTHLY", count: count} ->
          revents ++ (event |> add_recurring_events_count(count, [months: 1]))
        %{freq: "MONTHLY", until: until} ->
          revents ++ (event |> add_recurring_events_until(until, [months: 1]))
        %{freq: "MONTHLY", interval: interval} ->
          revents ++ (event |> add_recurring_events_until(end_date, [months: interval]))
        %{freq: "MONTHLY"} ->
          revents ++ (event |> add_recurring_events_until(end_date, [months: 1]))

        %{freq: "YEARLY", count: count, interval: interval} ->
          revents ++ (event |> add_recurring_events_count(count, [years: interval]))
        %{freq: "YEARLY", until: until, interval: interval} ->
          revents ++ (event |> add_recurring_events_until(until, [years: interval]))
        %{freq: "YEARLY", count: count} ->
          revents ++ (event |> add_recurring_events_count(count, [years: 1]))
        %{freq: "YEARLY", until: until} ->
          revents ++ (event |> add_recurring_events_until(until, [years: 1]))
        %{freq: "YEARLY", interval: interval} ->
          revents ++ (event |> add_recurring_events_until(end_date, [years: interval]))
        %{freq: "YEARLY"} ->
          revents ++ (event |> add_recurring_events_until(end_date, [years: 1]))
      end
    end))
  end

  defp add_recurring_events_until(event, until, shift_opts) do
    new_event = shift_event(event, shift_opts)

    case Timex.compare(new_event.start, until) do
     -1 -> [new_event] ++ add_recurring_events_until(new_event, until, shift_opts)
      0 -> [new_event]
      1 -> []
    end
  end

  defp add_recurring_events_count(event, count, shift_opts) do
    new_event = shift_event(event, shift_opts)
    if count > 1 do
      [new_event] ++ add_recurring_events_count(new_event, count - 1, shift_opts)
    else
      [new_event]
    end
  end

  defp shift_event(event, shift_opts) do
    new_event = event
    new_event = %{new_event | start: shift_date(event.start, shift_opts)}
    new_event = %{new_event | end: shift_date(event.end, shift_opts)}
    new_event
  end

  defp shift_date(date, shift_opts) do
    case Timex.shift(date, shift_opts) do
      %Timex.AmbiguousDateTime{} = new_date ->
        new_date.after
      new_date ->
        new_date
    end
  end
end