# Cocktail
[![CI
Status](https://github.com/peek-travel/cocktail/workflows/CI/badge.svg)](https://github.com/peek-travel/cocktail/actions)
[![codecov](https://codecov.io/gh/peek-travel/cocktail/branch/main/graph/badge.svg)](https://codecov.io/gh/peek-travel/cocktail)
[![Hex.pm Version](https://img.shields.io/hexpm/v/cocktail.svg?style=flat)](https://hex.pm/packages/cocktail)
[![License](https://img.shields.io/hexpm/l/cocktail.svg)](LICENSE.md)
Cocktail is an Elixir date recurrence library based on [iCalendar events](https://tools.ietf.org/html/rfc5545#section-3.6.1). Its primary use case currently is to expand schedules with recurrence rules into streams of occurrences. For example: say you wanted to represent a repeating schedule of events that occurred every other week, on Mondays, Wednesdays and Fridays, at 10am and 4pm.
```elixir
iex> schedule = Cocktail.Schedule.new(~N[2017-01-02 10:00:00])
...> schedule = Cocktail.Schedule.add_recurrence_rule(schedule, :weekly, interval: 2, days: [:monday, :wednesday, :friday], hours: [10, 16])
#Cocktail.Schedule<Every 2 weeks on Mondays, Wednesdays and Fridays on the 10th and 16th hours of the day>
```
Then to get a list of the first 10 occurrences of this schedule, you would do:
```elixir
...> stream = Cocktail.Schedule.occurrences(schedule)
...> Enum.take(stream, 10)
[~N[2017-01-02 10:00:00], ~N[2017-01-02 16:00:00], ~N[2017-01-04 10:00:00],
~N[2017-01-04 16:00:00], ~N[2017-01-06 10:00:00], ~N[2017-01-06 16:00:00],
~N[2017-01-16 10:00:00], ~N[2017-01-16 16:00:00], ~N[2017-01-18 10:00:00],
~N[2017-01-18 16:00:00]]
```
## Installation
Cocktail is [available in Hex](https://hex.pm/packages/cocktail) and can be installed
by adding `cocktail` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:cocktail, "~> 0.10"}
]
end
```
## Documentation
Detailed documentation with all available options can be found at <https://hexdocs.pm/cocktail>.
## Quick-start Guide
### Schedules
Everything starts with a [Cocktail.Schedule](https://hexdocs.pm/cocktail/Cocktail.Schedule.html); create one like this:
```elixir
iex> schedule = Cocktail.schedule(start_time, opts)
#Cocktail.Schedule<>
# or
...> schedule = Cocktail.Schedule.new(start_time, opts)
#Cocktail.Schedule<>
```
- `start_time` - Either a `DateTime` or a `NaiveDateTime` representing the beginning of your schedule.
- `opts`:
- `duration` - (optional) How long each occurrence is, in seconds.
### Recurrence Rules
Schedules are pretty useless on their own. To have them do something useful, you add recurrence rules to them. Currently, Cocktail supports:
- Monthly
- Weekly
- Daily
- Hourly
- Minutely
- Secondly
On top of these basic recurrence frequencies, you can add various options. Let's see some examples:
```elixir
iex> every_other_day = Cocktail.Schedule.add_recurrence_rule(schedule, :daily, interval: 2)
#Cocktail.Schedule<Every 2 days>
...> weekly_on_mo_we_fr = Cocktail.Schedule.add_recurrence_rule(schedule, :weekly, days: [:monday, :wednesday, :friday])
#Cocktail.Schedule<Weekly on Mondays, Wednesdays and Fridays>
...> daily_at_9am_and_5pm = Cocktail.Schedule.add_recurrence_rule(schedule, :daily, hours: [9, 17])
#Cocktail.Schedule<Daily on the 9th and 17th hours of the day>
```
For more details about frequencies and options, see [Cocktail.Schedule.add_recurrence_rule/3](https://hexdocs.pm/cocktail/Cocktail.Schedule.html#add_recurrence_rule/3)
### Occurrences
Once you've got a schedule set up the way you want, you can generate a stream of occurrences that match the schedule like so:
```elixir
iex> occurrences = Cocktail.Schedule.occurrences(schedule)
#Function<60.51599720/2 in Stream.unfold/2>
...> Enum.take(occurrences, 3)
[~N[2017-01-01 00:00:00], ~N[2017-01-02 00:00:00], ~N[2017-01-03 00:00:00]]
```
The type of each occurrence depends on what start time type you used, and wether or not you supplied a duration when creating the schedule.
### Duration
If you add the `duration` option when creating a schedule, you'll get `Cocktail.Span` structs as occurrences, with `:from` and `:until` fields of the same type as your start time.
```elixir
iex> schedule = Cocktail.schedule(~N[2017-01-01 00:00:00], duration: 3600) |> Cocktail.Schedule.add_recurrence_rule(:daily)
#Cocktail.Schedule<Daily>
...> occurrences = Cocktail.Schedule.occurrences(schedule)
#Function<60.51599720/2 in Stream.unfold/2>
...> Enum.take(occurrences, 3)
[%Cocktail.Span{from: ~N[2017-01-01 00:00:00], until: ~N[2017-01-01 01:00:00]},
%Cocktail.Span{from: ~N[2017-01-02 00:00:00], until: ~N[2017-01-02 01:00:00]},
%Cocktail.Span{from: ~N[2017-01-03 00:00:00], until: ~N[2017-01-03 01:00:00]}]
```
### Recurrence Times and Exception Times
You can also add one-off recurrence times that don't fit into a normal recurrence pattern, and exception times if you want to exclude a time that would normally be included because of a recurrence rule:
```elixir
iex> schedule = Cocktail.schedule(~N[2017-01-01 08:00:00]) |> Cocktail.Schedule.add_recurrence_rule(:daily)
#Cocktail.Schedule<Daily>
...> schedule = [~N[2017-01-01 09:00:00], ~N[2017-01-02 11:00:00], ~N[2017-01-03 17:00:00]] |> Enum.reduce(schedule, &Cocktail.Schedule.add_recurrence_time(&2, &1))
#Cocktail.Schedule<Daily>
...> schedule = Cocktail.Schedule.add_exception_time(schedule, ~N[2017-01-02 08:00:00])
#Cocktail.Schedule<Daily>
...> Cocktail.Schedule.occurrences(schedule) |> Enum.take(6)
[~N[2017-01-01 08:00:00], ~N[2017-01-01 09:00:00], ~N[2017-01-02 11:00:00],
~N[2017-01-03 08:00:00], ~N[2017-01-03 17:00:00], ~N[2017-01-04 08:00:00]]
```
### iCalendar
You can convert schedules to and from the iCalendar format like this:
```elixir
iex> i_calendar = Cocktail.Schedule.to_i_calendar(schedule)
"DTSTART:20170101T000000\nRRULE:FREQ=DAILY"
...> Cocktail.Schedule.from_i_calendar(i_calendar)
{:ok, #Cocktail.Schedule<Daily>}
```
## Roadmap
- [x] investigate and fix DST bugs when using zoned DateTime
- [ ] support all iCalendar RRULE options
- [ ] support week-start option
- [ ] support iCalendar EXRULE
- [ ] convert to/from JSON representation
## Credits
Cocktail is heavily inspired by and based on a very similar Ruby library, [ice_cube](https://github.com/seejohnrun/ice_cube).
## License
[MIT](LICENSE.md)