Skip to content

Commit

Permalink
Merge pull request #20 from peek-travel/doc-updates
Browse files Browse the repository at this point in the history
Update documentation
  • Loading branch information
doughsay authored Feb 3, 2019
2 parents fc7bb75 + 0243c8f commit e0dc6a9
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 35 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,31 @@

NIF bindings to [libical](https://libical.github.io/libical/) for Elixir.

This library is a **WIP**!
This library is still a **WIP**, but works well for basic calls to libical's recurrence iterators.

## Requirements

Excal requires that libical be present on your system, and that it's at least version 3.0.0.
Excal requires that libical (and its development headers) be present on your system, and that it's at least version 3.0.0.

### macOS

You can easily install `libical` using [Homebrew](https://brew.sh/) on macOS:

```
brew install libical
```

Homebrew provides the latest version, as of this writing, which is `3.0.4`.

### linux

Use favorite package manager to install `libical` (may be named slightly differently depending on distro), or maybe `libical-dev` if you're using a Debian based distro like Ubuntu.

NOTE: Make sure you're getting at least version `3.0.0`. Anything below will prevent Excal from compiling.

### Windows

I'm not currently aware of how to get this working on Windows, but if someone wants to try and let me know how, I will add instructions to this readme.

## Installation

Expand Down
19 changes: 19 additions & 0 deletions lib/excal.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Excal do
@moduledoc """
Excal provides basic Elixir bindings to libical, the reference implementation of the iCalendar spec written in C.
There are currently two possible ways to use Excal:
* `Excal.Recurrence.Iterator`
* A simple Elixir wrapper around a libical recurrence iterator.
* `Excal.Recurrence.Stream`
* An Elixir `Stream` wrapper for the above iterator.
Refer to either of the two modules above for documentation on how to use them.
"""

@typedoc """
Recurrence iterators and streams can operate using either Date or NaiveDateTime structs.
"""
@type date_or_datetime :: Date.t() | NaiveDateTime.t()
end
16 changes: 13 additions & 3 deletions lib/excal/interface/recurrence/iterator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@ defmodule Excal.Interface.Recurrence.Iterator do

@on_load :load_nifs

@type initialization_error :: :invalid_dtstart | :invalid_rrule | :bad_iterator
@type iterator_start_error :: :invalid_start | :start_invalid_for_rule

@doc false
def load_nifs, do: :erlang.load_nif('./priv/recurrence/iterator', 0)

def new(_rrule, _dtstart), do: raise("NIF new/2 not implemented")
def set_start(_iterator, _start), do: raise("NIF set_start/2 not implemented")
def next(_iterator), do: raise("NIF next/1 not implemented")
@spec new(String.t(), String.t()) ::
{:ok, reference()} | {:error, initialization_error()}
def new(_rrule, _dtstart), do: :erlang.nif_error("NIF new/2 not implemented")

@spec set_start(reference(), String.t()) :: :ok | {:error, iterator_start_error()}
def set_start(_iterator, _start), do: :erlang.nif_error("NIF set_start/2 not implemented")

@spec next(reference()) :: nil | :calendar.date() | :calendar.datetime()
def next(_iterator), do: :erlang.nif_error("NIF next/1 not implemented")
end
148 changes: 138 additions & 10 deletions lib/excal/recurrence/iterator.ex
Original file line number Diff line number Diff line change
@@ -1,38 +1,152 @@
defmodule Excal.Recurrence.Iterator do
@moduledoc """
Elixir wrapper around an icalendar recurrence iterator.
Elixir wrapper around a libical recurrence iterator.
The iterator is fundamentally a mutable resource, so it acts more like a stateful reference, rather than an immutable
data structure. To create one, you will need a iCalendar recurrence rule string and a start date or datetime.
"""

alias __MODULE__
alias Excal.Interface.Recurrence.Iterator, as: Interface

@enforce_keys [:iterator, :type, :rrule, :dtstart]
defstruct iterator: nil, type: nil, rrule: nil, dtstart: nil, from: nil, until: nil, finished: false

@typedoc """
A struct that represents a recurrence iterator. Consider all the fields to be internal implementation detail at this
time, as they may change without notice.
"""
@type t :: %Iterator{
iterator: reference(),
type: Date | NaiveDateTime,
rrule: String.t(),
dtstart: Excal.date_or_datetime(),
from: nil | Excal.date_or_datetime(),
until: nil | Excal.date_or_datetime(),
finished: boolean()
}

@typedoc """
Possible errors returned from iterator initialization.
"""
@type initialization_error :: :unsupported_datetime_type | Interface.initialization_error()

@typedoc """
Possible errors returned from setting the start date or datetime of an iterator.
"""
@type iterator_start_error :: :unsupported_datetime_type | :datetime_type_mismatch | Interface.iterator_start_error()

@doc """
Creates a new recurrence iterator from an iCalendar recurrence rule (RRULE) string and a start date or datetime.
## Examples
A daily schedule starting on January 1st 2019:
iex> {:ok, iter} = Iterator.new("FREQ=DAILY", ~D[2019-01-01])
...> {_occurrence, iter} = Iterator.next(iter)
...> {_occurrence, iter} = Iterator.next(iter)
...> {occurrence, _iter} = Iterator.next(iter)
...> occurrence
~D[2019-01-03]
A bi-weekly schedule every Monday, Wednesday and Friday:
iex> {:ok, iter} = Iterator.new("FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR", ~D[2019-01-01])
...> {occurrence, _iter} = Iterator.next(iter)
...> occurrence
~D[2019-01-07]
"""
@spec new(String.t(), Excal.date_or_datetime()) :: {:ok, t()} | {:error, initialization_error()}
def new(rrule, date_or_datetime) do
with {:ok, type, dtstart} <- to_ical_time_string(date_or_datetime),
{:ok, iterator} <- Interface.new(rrule, dtstart) do
{:ok, %__MODULE__{iterator: iterator, type: type, rrule: rrule, dtstart: date_or_datetime}}
{:ok, %Iterator{iterator: iterator, type: type, rrule: rrule, dtstart: date_or_datetime}}
end
end

def set_start(%__MODULE__{iterator: iterator_ref, type: type} = iterator, %type{} = date_or_datetime) do
@doc """
Sets the start date or datetime for an existing iterator.
The iterator's start time is not the same thing as the schedule's start time. At creation time, an iterator is given a
recurrence rule string and a schedule start date or datetime, but the iterator's start can be some time farther in the
future than the schedules start time.
This can also be used to reset an existing iterator to a new starting time.
NOTE: You cannot call `set_start/2` on an iterator whose RRULE contains a COUNT clause.
## Example
Consider: an RRULE for Friday on every 3rd week starting January 1st 2016 might look like this:
iex> {:ok, iter} = Iterator.new("FREQ=WEEKLY;INTERVAL=3", ~D[2016-01-01])
...> {next_occurrence, _iter} = Iterator.next(iter)
...> next_occurrence
~D[2016-01-01]
...but if you only cared about the instances starting in 2019, you can't change the start date because that would
affect the cadence of the "every 3rd week" part of the schedule. Instead, just tell the iterator to skip ahead until
2019:
iex> {:ok, iter} = Iterator.new("FREQ=WEEKLY;INTERVAL=3", ~D[2016-01-01])
...> {:ok, iter} = Iterator.set_start(iter, ~D[2019-01-01])
...> {next_occurrence, _iter} = Iterator.next(iter)
...> next_occurrence
~D[2019-01-18]
"""
@spec set_start(t(), Excal.date_or_datetime()) :: {:ok, t()} | {:error, iterator_start_error()}
def set_start(%Iterator{iterator: iterator_ref, type: type} = iterator, %type{} = date_or_datetime) do
with {:ok, _, time_string} <- to_ical_time_string(date_or_datetime),
:ok <- Interface.set_start(iterator_ref, time_string) do
{:ok, %{iterator | from: date_or_datetime}}
end
end

def set_start(_, _), do: {:error, :datetime_type_mismatch}
def set_start(%Iterator{}, _), do: {:error, :datetime_type_mismatch}
def set_start(iterator, _), do: raise(ArgumentError, "invalid iterator: #{inspect(iterator)}")

@doc """
Sets the end date or datetime for an existing iterator.
Once an end time is set for an iterator, the iterator will return `nil` once it has reached the specified end.
## Example
def set_end(%__MODULE__{type: type} = iterator, %type{} = date_or_datetime) do
iex> {:ok, iter} = Iterator.new("FREQ=DAILY", ~D[2019-01-01])
...> {:ok, iter} = Iterator.set_end(iter, ~D[2019-01-03])
...> {_occurrence, iter} = Iterator.next(iter)
...> {_occurrence, iter} = Iterator.next(iter)
...> {occurrence, _iter} = Iterator.next(iter)
...> occurrence
nil
"""
@spec set_end(t(), Excal.date_or_datetime()) :: {:ok, t()} | {:error, :datetime_type_mismatch}
def set_end(%Iterator{type: type} = iterator, %type{} = date_or_datetime) do
{:ok, %{iterator | until: date_or_datetime}}
end

def set_end(_, _), do: {:error, :datetime_type_mismatch}
def set_end(%Iterator{}, _), do: {:error, :datetime_type_mismatch}
def set_end(iterator, _), do: raise(ArgumentError, "invalid iterator: #{inspect(iterator)}")

@doc """
Returns the next date or datetime occurrence of an existing iterator.
If the iterator has reached the end of the set described by the RRULE, or has reached the end time specified by
`set_end/2`, it will return `nil`.
def next(%__MODULE__{finished: true} = iterator), do: {nil, iterator}
## Example
def next(%__MODULE__{iterator: iterator_ref, type: type, until: until} = iterator) do
iex> {:ok, iter} = Iterator.new("FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR", ~D[2019-01-01])
...> {occurrence, _iter} = Iterator.next(iter)
...> occurrence
~D[2019-01-07]
"""
@spec next(t()) :: {Excal.date_or_datetime(), t()} | {nil, t()}
def next(%Iterator{finished: true} = iterator), do: {nil, iterator}

def next(%Iterator{iterator: iterator_ref, type: type, until: until} = iterator) do
occurrence = iterator_ref |> Interface.next() |> from_tuple(type)

cond do
Expand All @@ -57,9 +171,23 @@ defmodule Excal.Recurrence.Iterator do

defp to_ical_time_string(_), do: {:error, :unsupported_datetime_type}

# NOTE:
# Native Elixir Date and NaiveDateTime are heavy to initialize with `new` or `from_erl!` because it checks validity.
# We're bypassing the validity check here, assuming that libical is giving us valid dates and times.

defp from_tuple(nil, _), do: nil
defp from_tuple({year, month, day}, Date), do: %Date{year: year, month: month, day: day}

defp from_tuple({year, month, day}, Date),
do: %Date{year: year, month: month, day: day, calendar: Calendar.ISO}

defp from_tuple({{year, month, day}, {hour, minute, second}}, NaiveDateTime),
do: %NaiveDateTime{year: year, month: month, day: day, hour: hour, minute: minute, second: second}
do: %NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
calendar: Calendar.ISO
}
end
52 changes: 51 additions & 1 deletion lib/excal/recurrence/stream.ex
Original file line number Diff line number Diff line change
@@ -1,11 +1,61 @@
defmodule Excal.Recurrence.Stream do
@moduledoc """
Generates Elixir streams from icalendar rrules.
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()}
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
Expand Down
5 changes: 3 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ defmodule Excal.MixProject do
[
{:benchee, "~> 0.13", only: :dev, runtime: false},
{:credo, "~> 1.0", only: [:dev, :test], runtime: false},
{:excoveralls, "~> 0.9", only: :test, runtime: false},
{:ex_doc, "~> 0.18", only: :dev, runtime: false}
{:dialyxir, "~> 1.0.0-rc.4", only: [:dev], runtime: false},
{:ex_doc, "~> 0.18", only: :dev, runtime: false},
{:excoveralls, "~> 0.9", only: :test, runtime: false}
]
end
end
3 changes: 2 additions & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"credo": {:hex, :credo, "1.0.1", "5a5bc382cf0a12cc7db64cc018526aee05db572c60e867f6bc4b647d7ef9fc61", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"deep_merge": {:hex, :deep_merge, "0.2.0", "c1050fa2edf4848b9f556fba1b75afc66608a4219659e3311d9c9427b5b680b3", [:mix], [], "hexpm"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm"},
"erlex": {:hex, :erlex, "0.1.6", "c01c889363168d3fdd23f4211647d8a34c0f9a21ec726762312e08e083f3d47e", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.10.4", "b86230f0978bbc630c139af5066af7cd74fd16536f71bc047d1037091f9f63a9", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"hackney": {:hex, :hackney, "1.15.0", "287a5d2304d516f63e56c469511c42b016423bcb167e61b611f6bad47e3ca60e", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
Expand All @@ -16,7 +18,6 @@
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},
}
8 changes: 1 addition & 7 deletions src/recurrence/iterator.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#include <stdio.h>
#include <string.h>
#include "libical/ical.h"
#include "erl_nif.h"
Expand All @@ -24,11 +23,6 @@ int load(ErlNifEnv *env, void **priv_data, ERL_NIF_TERM load_info)
return 0;
}

int upgrade(ErlNifEnv *env, void **priv_data, void **old_priv_data, ERL_NIF_TERM load_info)
{
return 0;
}

static ERL_NIF_TERM
icaltime_to_erl_datetime(ErlNifEnv *env, struct icaltimetype datetime)
{
Expand Down Expand Up @@ -232,4 +226,4 @@ static ErlNifFunc nif_funcs[] = {
{"set_start", 2, recurrence_iterator_set_start},
{"next", 1, recurrence_iterator_next}};

ERL_NIF_INIT(Elixir.Excal.Interface.Recurrence.Iterator, nif_funcs, &load, NULL, &upgrade, NULL)
ERL_NIF_INIT(Elixir.Excal.Interface.Recurrence.Iterator, nif_funcs, &load, NULL, NULL, NULL)
Loading

0 comments on commit e0dc6a9

Please sign in to comment.