From 5a832276053b8371e5766d1a2f0fa7fb34a44e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Dos=C3=A9?= Date: Sun, 27 Jan 2019 18:40:38 -0800 Subject: [PATCH 1/5] Add typespecs + dialyxer and placeholders for all missing documentation --- lib/excal.ex | 7 ++++ lib/excal/interface/recurrence/iterator.ex | 29 +++++++++++-- lib/excal/recurrence/iterator.ex | 49 +++++++++++++++++++++- lib/excal/recurrence/stream.ex | 13 ++++++ mix.exs | 5 ++- mix.lock | 3 +- 6 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 lib/excal.ex diff --git a/lib/excal.ex b/lib/excal.ex new file mode 100644 index 0000000..598474e --- /dev/null +++ b/lib/excal.ex @@ -0,0 +1,7 @@ +defmodule Excal do + @moduledoc """ + TODO: docs + """ + + @type date_or_datetime :: Date.t() | NaiveDateTime.t() +end diff --git a/lib/excal/interface/recurrence/iterator.ex b/lib/excal/interface/recurrence/iterator.ex index 067ec1c..334f005 100644 --- a/lib/excal/interface/recurrence/iterator.ex +++ b/lib/excal/interface/recurrence/iterator.ex @@ -1,11 +1,32 @@ defmodule Excal.Interface.Recurrence.Iterator do - @moduledoc false + @moduledoc """ + TODO: docs + """ @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") + @doc """ + TODO: docs + """ + @spec new(String.t(), String.t()) :: + {:ok, reference()} | {:error, initialization_error()} + def new(_rrule, _dtstart), do: :erlang.nif_error("NIF new/2 not implemented") + + @doc """ + TODO: docs + """ + @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") + + @doc """ + TODO: docs + """ + @spec next(reference()) :: nil | :calendar.date() | :calendar.datetime() + def next(_iterator), do: :erlang.nif_error("NIF next/1 not implemented") end diff --git a/lib/excal/recurrence/iterator.ex b/lib/excal/recurrence/iterator.ex index ee20742..d7e7584 100644 --- a/lib/excal/recurrence/iterator.ex +++ b/lib/excal/recurrence/iterator.ex @@ -1,6 +1,8 @@ defmodule Excal.Recurrence.Iterator do @moduledoc """ Elixir wrapper around an icalendar recurrence iterator. + + TODO: more docs """ alias Excal.Interface.Recurrence.Iterator, as: Interface @@ -8,6 +10,23 @@ defmodule Excal.Recurrence.Iterator do @enforce_keys [:iterator, :type, :rrule, :dtstart] defstruct iterator: nil, type: nil, rrule: nil, dtstart: nil, from: nil, until: nil, finished: false + @type t :: %__MODULE__{ + 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() + } + + @type initialization_error :: :unsupported_datetime_type | Interface.initialization_error() + @type iterator_start_error :: :unsupported_datetime_type | :datetime_type_mismatch | Interface.iterator_start_error() + + @doc """ + TODO: docs + """ + @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 @@ -15,6 +34,10 @@ defmodule Excal.Recurrence.Iterator do end end + @doc """ + TODO: docs + """ + @spec set_start(t(), Excal.date_or_datetime()) :: {:ok, t()} | {:error, iterator_start_error()} def set_start(%__MODULE__{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 @@ -24,12 +47,20 @@ defmodule Excal.Recurrence.Iterator do def set_start(_, _), do: {:error, :datetime_type_mismatch} + @doc """ + TODO: docs + """ + @spec set_end(t(), Excal.date_or_datetime()) :: {:ok, t()} | {:error, :datetime_type_mismatch} def set_end(%__MODULE__{type: type} = iterator, %type{} = date_or_datetime) do {:ok, %{iterator | until: date_or_datetime}} end def set_end(_, _), do: {:error, :datetime_type_mismatch} + @doc """ + TODO: docs + """ + @spec next(t()) :: {Excal.date_or_datetime(), t()} | {nil, t()} def next(%__MODULE__{finished: true} = iterator), do: {nil, iterator} def next(%__MODULE__{iterator: iterator_ref, type: type, until: until} = iterator) do @@ -57,9 +88,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 diff --git a/lib/excal/recurrence/stream.ex b/lib/excal/recurrence/stream.ex index 0ff26a7..55ceb66 100644 --- a/lib/excal/recurrence/stream.ex +++ b/lib/excal/recurrence/stream.ex @@ -1,11 +1,24 @@ defmodule Excal.Recurrence.Stream do @moduledoc """ Generates Elixir streams from icalendar rrules. + + TODO: more docs """ alias Excal.Recurrence.Iterator + @type option :: {:from, Excal.date_or_datetime()} | {:until, Excal.date_or_datetime()} + @type options :: [option()] + + @doc """ + TODO: docs + """ + @spec new(String.t(), Excal.date_or_datetime(), options()) :: + {: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 diff --git a/mix.exs b/mix.exs index 77b77a4..a19aea2 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/mix.lock b/mix.lock index 7dc36da..cbf1422 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, @@ -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"}, } From 922650cca46a54b6dde4d434287c1e48338073de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Dos=C3=A9?= Date: Sun, 27 Jan 2019 19:36:52 -0800 Subject: [PATCH 2/5] remove unused things from iterator.c --- src/recurrence/iterator.c | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/recurrence/iterator.c b/src/recurrence/iterator.c index 1b1021e..cc99126 100644 --- a/src/recurrence/iterator.c +++ b/src/recurrence/iterator.c @@ -1,4 +1,3 @@ -#include #include #include "libical/ical.h" #include "erl_nif.h" @@ -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) { @@ -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) From c4b0357f5f6ff88480312caa7a0d10130f5b1d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Dos=C3=A9?= Date: Sat, 2 Feb 2019 16:36:32 -0800 Subject: [PATCH 3/5] Documentation for Iterator --- lib/excal.ex | 14 ++- lib/excal/interface/recurrence/iterator.ex | 13 +-- lib/excal/recurrence/iterator.ex | 109 ++++++++++++++++++--- lib/excal/recurrence/stream.ex | 2 +- test/excal/recurrence/iterator_test.exs | 2 + 5 files changed, 112 insertions(+), 28 deletions(-) diff --git a/lib/excal.ex b/lib/excal.ex index 598474e..b573609 100644 --- a/lib/excal.ex +++ b/lib/excal.ex @@ -1,7 +1,19 @@ defmodule Excal do @moduledoc """ - TODO: docs + 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 diff --git a/lib/excal/interface/recurrence/iterator.ex b/lib/excal/interface/recurrence/iterator.ex index 334f005..75636df 100644 --- a/lib/excal/interface/recurrence/iterator.ex +++ b/lib/excal/interface/recurrence/iterator.ex @@ -1,7 +1,5 @@ defmodule Excal.Interface.Recurrence.Iterator do - @moduledoc """ - TODO: docs - """ + @moduledoc false @on_load :load_nifs @@ -11,22 +9,13 @@ defmodule Excal.Interface.Recurrence.Iterator do @doc false def load_nifs, do: :erlang.load_nif('./priv/recurrence/iterator', 0) - @doc """ - TODO: docs - """ @spec new(String.t(), String.t()) :: {:ok, reference()} | {:error, initialization_error()} def new(_rrule, _dtstart), do: :erlang.nif_error("NIF new/2 not implemented") - @doc """ - TODO: docs - """ @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") - @doc """ - TODO: docs - """ @spec next(reference()) :: nil | :calendar.date() | :calendar.datetime() def next(_iterator), do: :erlang.nif_error("NIF next/1 not implemented") end diff --git a/lib/excal/recurrence/iterator.ex b/lib/excal/recurrence/iterator.ex index d7e7584..2ed24db 100644 --- a/lib/excal/recurrence/iterator.ex +++ b/lib/excal/recurrence/iterator.ex @@ -1,16 +1,22 @@ defmodule Excal.Recurrence.Iterator do @moduledoc """ - Elixir wrapper around an icalendar recurrence iterator. + Elixir wrapper around a libical recurrence iterator. - TODO: more docs + 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 - @type t :: %__MODULE__{ + @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(), @@ -20,50 +26,125 @@ defmodule Excal.Recurrence.Iterator do 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 """ - TODO: docs + 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 @doc """ - TODO: docs + 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. + + ## 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(%__MODULE__{iterator: iterator_ref, type: type} = iterator, %type{} = date_or_datetime) do + 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 """ - TODO: docs + 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 + + 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(%__MODULE__{type: type} = iterator, %type{} = date_or_datetime) do + 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 """ - TODO: docs + 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`. + + ## Example + + 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(%__MODULE__{finished: true} = iterator), do: {nil, iterator} + def next(%Iterator{finished: true} = iterator), do: {nil, iterator} - def next(%__MODULE__{iterator: iterator_ref, type: type, until: until} = iterator) do + def next(%Iterator{iterator: iterator_ref, type: type, until: until} = iterator) do occurrence = iterator_ref |> Interface.next() |> from_tuple(type) cond do diff --git a/lib/excal/recurrence/stream.ex b/lib/excal/recurrence/stream.ex index 55ceb66..783cefc 100644 --- a/lib/excal/recurrence/stream.ex +++ b/lib/excal/recurrence/stream.ex @@ -1,6 +1,6 @@ defmodule Excal.Recurrence.Stream do @moduledoc """ - Generates Elixir streams from icalendar rrules. + Generates Elixir streams from iCalendar recurrence rules (RRULE). TODO: more docs """ diff --git a/test/excal/recurrence/iterator_test.exs b/test/excal/recurrence/iterator_test.exs index 362403a..7487775 100644 --- a/test/excal/recurrence/iterator_test.exs +++ b/test/excal/recurrence/iterator_test.exs @@ -3,6 +3,8 @@ defmodule Excal.Recurrence.IteratorTest do alias Excal.Recurrence.Iterator + doctest Iterator + describe "Iterator.new/3" do test "returns an iterator struct when valid inputs are given" do assert {:ok, %Iterator{}} = Iterator.new("FREQ=DAILY", ~D[2018-09-09]) From 32c24343164da85f6b0a525e3acdd8fc40c75b83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Dos=C3=A9?= Date: Sat, 2 Feb 2019 19:22:45 -0800 Subject: [PATCH 4/5] Stream documentation and README updates --- README.md | 24 ++++++++++++-- lib/excal/recurrence/iterator.ex | 2 ++ lib/excal/recurrence/stream.ex | 45 ++++++++++++++++++++++++--- test/excal/recurrence/stream_test.exs | 20 ++++++------ 4 files changed, 76 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 3611cf8..ef7a079 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/excal/recurrence/iterator.ex b/lib/excal/recurrence/iterator.ex index 2ed24db..b4573e2 100644 --- a/lib/excal/recurrence/iterator.ex +++ b/lib/excal/recurrence/iterator.ex @@ -74,6 +74,8 @@ defmodule Excal.Recurrence.Iterator do 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: diff --git a/lib/excal/recurrence/stream.ex b/lib/excal/recurrence/stream.ex index 783cefc..ccb42ca 100644 --- a/lib/excal/recurrence/stream.ex +++ b/lib/excal/recurrence/stream.ex @@ -2,18 +2,55 @@ defmodule Excal.Recurrence.Stream do @moduledoc """ Generates Elixir streams from iCalendar recurrence rules (RRULE). - TODO: more docs + 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()} - @type options :: [option()] @doc """ - TODO: docs + 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(), options()) :: + @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, diff --git a/test/excal/recurrence/stream_test.exs b/test/excal/recurrence/stream_test.exs index 8fde592..ef1a9df 100644 --- a/test/excal/recurrence/stream_test.exs +++ b/test/excal/recurrence/stream_test.exs @@ -1,35 +1,37 @@ defmodule Excal.Recurrence.StreamTest do use ExUnit.Case, async: true - alias Excal.Recurrence.Stream, as: RecurrenceStream + alias Excal.Recurrence.Stream + + doctest Stream describe "Stream.new/3" do test "returns a stream when valid inputs are given" do - assert {:ok, stream} = RecurrenceStream.new("FREQ=DAILY", ~D[2018-09-09]) + assert {:ok, stream} = Stream.new("FREQ=DAILY", ~D[2018-09-09]) assert is_function(stream) - assert {:ok, stream} = RecurrenceStream.new("FREQ=DAILY", ~N[2018-09-09 12:30:00]) + assert {:ok, stream} = Stream.new("FREQ=DAILY", ~N[2018-09-09 12:30:00]) assert is_function(stream) end test "raises ArgumentError when not given a string for rrule" do - assert_raise ArgumentError, fn -> RecurrenceStream.new(:invalid, ~D[2018-09-09]) end + assert_raise ArgumentError, fn -> Stream.new(:invalid, ~D[2018-09-09]) end end test "returns an error when an invalid rrule string is given" do - assert {:error, :invalid_rrule} = RecurrenceStream.new("INVALID", ~D[2018-09-09]) + assert {:error, :invalid_rrule} = Stream.new("INVALID", ~D[2018-09-09]) end test "returns an error when an invalid datetime type is given" do - assert {:error, :unsupported_datetime_type} = RecurrenceStream.new("FREQ=DAILY", :invalid) + assert {:error, :unsupported_datetime_type} = Stream.new("FREQ=DAILY", :invalid) end test "accepts an option for start time" do - assert {:ok, stream} = RecurrenceStream.new("FREQ=DAILY", ~D[2018-09-09], from: ~D[2019-09-09]) + assert {:ok, stream} = Stream.new("FREQ=DAILY", ~D[2018-09-09], from: ~D[2019-09-09]) assert is_function(stream) end test "accepts an option for end time" do - assert {:ok, stream} = RecurrenceStream.new("FREQ=DAILY", ~D[2018-09-09], until: ~D[2019-09-09]) + assert {:ok, stream} = Stream.new("FREQ=DAILY", ~D[2018-09-09], until: ~D[2019-09-09]) assert is_function(stream) end end @@ -111,7 +113,7 @@ defmodule Excal.Recurrence.StreamTest do {_, _}, opts -> opts end) - {:ok, stream} = RecurrenceStream.new(rrule, dtstart, opts) + {:ok, stream} = Stream.new(rrule, dtstart, opts) [stream: stream] end From 0243c8fe1d65f9942e5da52e1ed16e079bb4cd00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chris=20Dos=C3=A9?= Date: Sat, 2 Feb 2019 19:28:27 -0800 Subject: [PATCH 5/5] add missing coverage tests --- test/excal/recurrence/iterator_test.exs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/excal/recurrence/iterator_test.exs b/test/excal/recurrence/iterator_test.exs index 7487775..266cb0f 100644 --- a/test/excal/recurrence/iterator_test.exs +++ b/test/excal/recurrence/iterator_test.exs @@ -34,6 +34,10 @@ defmodule Excal.Recurrence.IteratorTest do test "returns an error when the start type doesn't match the iterator's dtstart type", %{iterator: iterator} do assert {:error, :datetime_type_mismatch} = Iterator.set_start(iterator, ~N[2018-09-09 12:30:00]) end + + test "raises if not given an iterator" do + assert_raise ArgumentError, fn -> Iterator.set_start(:foo, ~N[2018-09-09 12:30:00]) end + end end describe "Iterator.set_end/2" do @@ -46,6 +50,10 @@ defmodule Excal.Recurrence.IteratorTest do test "returns an error when the end type doesn't match the iterator's dtstart type", %{iterator: iterator} do assert {:error, :datetime_type_mismatch} = Iterator.set_end(iterator, ~N[2018-09-09 12:30:00]) end + + test "raises if not given an iterator" do + assert_raise ArgumentError, fn -> Iterator.set_end(:foo, ~N[2018-09-09 12:30:00]) end + end end describe "Iterator.next/1" do