Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Recurrences & Intervals

Recurrences & Intervals

Empire City Elixir Conference 2018, May 19th - http://empex.co/nyc

This talk is retelling my journey down the rabbit hole of dealing with date & time.

We will start the journey with date/time recurrences. Then, we will look at time intervals and operations around them. Finally, we will look at Eric Evans' talk "Exploring Time" which gives a fresh look at date/time in general and intervals in particular (spoiler alert: there might be some relevant research from the 80s around that too!). We will model both recurrences and intervals as streams and we will try to show that Elixir's extensibility makes it a great language for developing business applications.

Wojtek Mach

May 19, 2018
Tweet

More Decks by Wojtek Mach

Other Decks in Programming

Transcript

  1. iCalendar Example DTSTART=20180101T090000Z RRULE:FREQ=DAILY;BYDAY=MO,TU;COUNT=4 2018-01-01 08:00 UTC (09:00 CET) 2018-01-02

    08:00 UTC (09:00 CET) 2018-01-08 08:00 UTC (09:00 CET) 2018-01-09 08:00 UTC (09:00 CET)
  2. iCalendar Example DTSTART=20180319T090000Z RRULE:FREQ=DAILY;BYDAY=MO,TU;COUNT=4 2018-03-19 08:00 UTC (09:00 CET) 2018-03-20

    08:00 UTC (09:00 CET) 2018-03-26 08:00 UTC (10:00 CET) 2018-03-27 08:00 UTC (10:00 CET)
  3. defmodule Recurrence do def stream(start) do Stream.iterate(start, &Date.add(&1, 1)) end

    end iex> stream = Recurrence.stream(~D[2018-01-01]) #Function<... in Stream…>
  4. defmodule Recurrence do def stream(start) do Stream.iterate(start, &Date.add(&1, 1)) end

    end iex> stream = Recurrence.stream(~D[2018-01-01]) #Function<... in Stream…> iex> Enum.take(stream, 3) [~D[2018-01-01], ~D[2018-01-02], ~D[2018-01-03]]
  5. Examples • the court is open daily between 9am and

    8pm • the court is already booked on January 1st 2018 between 10:00 and 10:30
  6. Examples • the court is open daily between 9am and

    8pm • the court is already booked on January 1st 2018 between 10:00 and 10:30
  7. Problem • court's opening hours, e.g.: 10am - 6pm •

    time window for when we want to search, e.g.:
 2pm - 8pm on January 1st 2018 • already made bookings e.g.: 4pm - 5pm Given: Calculate: available spots to book
  8. 10:00 - 18:00 - opening hours 14:00 - 20:00 -

    search window 16:00 - 17:00 - booking +-----------------------+ | | | +-----------|-----+ | | | | | | ### | | | | ### | | | | | | 10:00 12:00 14:00 16:00 18:00 20:00 

  9. 10:00 - 18:00 - opening hours 14:00 - 20:00 -

    search window 16:00 - 17:00 - booking +-----------------------+ | | | +-----------|-----+ | | | | | | ### | | | | ### | | | | | | 10:00 12:00 14:00 16:00 18:00 20:00 ###### #### ###### #### 10:00 12:00 14:00 16:00 18:00 20:00 

  10. 10:00 - 18:00 - opening hours 14:00 - 20:00 -

    search window 16:00 - 17:00 - booking +-----------------------+ | | | +-----------|-----+ | | | | | | ### | | | | ### | | | | | | 10:00 12:00 14:00 16:00 18:00 20:00 ###### #### ###### #### 10:00 12:00 14:00 16:00 18:00 20:00 
 Results: 14:00 - 16:00 - gap 1
 17:00 - 18:00 - gap 2
  11. defmodule DateTimeRange do defstruct [:first, :last] def new(%NaiveDateTime{}, %NaiveDateTime{}), do:

    ... def intersection(range1, range2), do: ... def subtract(range1, range2), do: ... end
  12. defmodule DateTimeRange do defstruct [:first, :last] def new(%NaiveDateTime{}, %NaiveDateTime{}), do:

    ... def intersection(range1, range2), do: ... def subtract(range1, range2), do: ... end iex> DateTimeRange.new(~N[2018-01-01 09:00:00], ~N[201 %DateTimeRange{first: ~N[2018-01-01 09:00:00], last: ~
  13. defmodule DateTimeRange do defstruct [:first, :last] def new(%NaiveDateTime{}, %NaiveDateTime{}), do:

    ... def intersection(range1, range2), do: ... def subtract(range1, range2), do: ... end iex> r("2018-01-01 09:00..10:00") %DateTimeRange{first: ~N[2018-01-01 09:00:00], last: ~
  14. defmodule DateTimeRange do defstruct [:first, :last] def new(%NaiveDateTime{}, %NaiveDateTime{}), do:

    ... def intersection(range1, range2), do: ... def subtract(range1, range2), do: ... end iex> r("2018-01-01 09:00..10:00") r("2018-01-01 09:00..10:00")
  15. defmodule DateTimeRange do defstruct [:first, :last] def new(%NaiveDateTime{}, %NaiveDateTime{}), do:

    ... def intersection(range1, range2), do: ... def subtract(range1, range2), do: ... end iex> a = r("2018-01-01 09:00..10:00”) iex> b = r("2018-01-01 09:00..10:00") iex> a > b ???
  16. 14:10 - 15:00 Eric Evans - Exploring Time Conference schedule:

    {#DateTime<2017-09-21 14:10:00-07:00 PDT America/ Los_Angeles>, #DateTime<2017-09-21 14:10:00-07:00 PDT America/ Los_Angeles>}
  17. 14:10 - 15:00 Eric Evans - Exploring Time Conference schedule:

    {~N[2017-09-21 14:10:00], ~N[2017-09-21 15:00:00]}
  18. 14:10 - 15:00 Eric Evans - Exploring Time Conference schedule:

    {~N[2017-09-21 14:10:00.000000], ~N[2017-09-21 15:00:00.000000]}
  19. Arithmetic ~D[2017-01-20] + "1 month" => ~D[2017-02-20] ~D[2017-01-20] + "2

    months” => ~D[2017-03-20] ~D[2017-01-31] + "1 month" => ~D[2017-02-28]
  20. Arithmetic ~D[2017-01-20] + "1 month" => ~D[2017-02-20] ~D[2017-01-20] + "2

    months” => ~D[2017-03-20] ~D[2017-01-31] + "1 month" => ~D[2017-02-28] ~D[2017-03-31] + "2 months" => ~D[2017-05-31]
  21. Arithmetic ~D[2017-01-20] + "1 month" => ~D[2017-02-20] ~D[2017-01-20] + "2

    months” => ~D[2017-03-20] ~D[2017-01-31] + "1 month" => ~D[2017-02-28] ~D[2017-03-31] + "2 months" => ~D[2017-05-31] ~D[2017-03-31] + "1 month" => ~D[2017-04-30]
  22. Arithmetic ~D[2017-01-20] + "1 month" => ~D[2017-02-20] ~D[2017-01-20] + "2

    months” => ~D[2017-03-20] ~D[2017-01-31] + "1 month" => ~D[2017-02-28] ~D[2017-03-31] + "2 months" => ~D[2017-05-31] ~D[2017-03-31] + "1 month" => ~D[2017-04-30] ~D[2017-04-30] + "1 month" => ~D[2017-05-30]
  23. Arithmetic ~D[2017-01-20] + "1 month" => ~D[2017-02-20] ~D[2017-01-20] + "2

    months” => ~D[2017-03-20] ~D[2017-01-31] + "1 month" => ~D[2017-02-28] ~D[2017-03-31] + "2 months" => ~D[2017-05-31] ^^ ~D[2017-03-31] + "1 month" => ~D[2017-04-30] ~D[2017-04-30] + "1 month" => ~D[2017-05-30] ^^
  24. Arithmetic ~D[2017-01-20] + "1 month" => ~D[2017-02-20] ~D[2017-01-20] + "2

    months” => ~D[2017-03-20] ~D[2017-01-31] + "1 month" => ~D[2017-02-28] ~D[2017-03-31] + "2 months" => ~D[2017-05-31] ^^ ~D[2017-03-31] + "1 month" => ~D[2017-04-30] ~D[2017-04-30] + "1 month" => ~D[2017-05-30] ^^ (a + b) + c != a + (b + c)
  25. Time Count iex> next("2018-12-31") "2019-01-01" iex> next("2018-12") "2019-01" ..., 2018-01-01,

    2018-01-02, 2018-01-03, ... ..., 2018-12-31, 2019-01-01, 2019-01-02, ...
  26. Time Count iex> next("2018-12-31") "2019-01-01" iex> next("2018-12") "2019-01" iex> next("2018")

    "2019" ..., 2018-01-01, 2018-01-02, 2018-01-03, ... ..., 2018-12-31, 2019-01-01, 2019-01-02, ...
  27. Time Count iex> next(~I"2018-12-31") ~I"2019-01-01" iex> next(~I"2018-12") ~I"2019-01" iex> next(~I"2018")

    ~I"2019" ..., 2018-01-01, 2018-01-02, 2018-01-03, ... ..., 2018-12-31, 2019-01-01, 2019-01-02, ...
  28. Time Count: Composition ~I"2018-05-19 09:00" |> enclose(:month) # => ~I"2018-05"

    |> nest(:day) # => ~I”2018-05-01/31" |> List.last() # => ~I”2018-05-31"
  29. Time Count: Composition def end_of_month(interval) do interval |> enclose(:month) |>

    nest(:day) |> Enum.last() end iex> end_of_month(~I"2018-05-01 09:00") ~I"2018-05-31"
  30. Time Count: Composition def days_in_month(interval) do interval |> enclose(:month) |>

    nest(:day) |> Enum.count() end iex> days_in_month(~I"2018-05-01 09:00") 31
  31. Time Count: Composition def days_in_year(interval) do interval |> enclose(:year) |>

    nest(:day) |> Enum.count() end iex> days_in_year(~I"2018-05-01 09:00") 365
  32. Time Count: Composition def days_in_year(interval) do interval |> enclose(:year) |>

    nest(:day) |> Enum.count() end iex> days_in_year(~I"2018-05-01 09:00") 365 iex> days_in_year(~I"2020-05-01 09:00") 366
  33. Allen’s Interval Algebra: Examples relation(~I"2018", ~I"2018") # => :equal relation(~I"2015",

    ~I"2018") # => :before relation(~I"2017", ~I"2018") # => :meets
  34. Allen’s Interval Algebra: Examples relation(~I"2018", ~I"2018") # => :equal relation(~I"2015",

    ~I"2018") # => :before relation(~I"2017", ~I"2018") # => :meets relation(~I"2018-03", ~I"2018-01/12") # => :during
  35. Allen’s Interval Algebra: Examples relation(~I"2018", ~I"2018") # => :equal relation(~I"2015",

    ~I"2018") # => :before relation(~I"2017", ~I"2018") # => :meets relation(~I"2018-03", ~I"2018-01/12") # => :during relation(~I"2018-01", ~I"2018-01/12") # => :starts
  36. Allen’s Interval Algebra: Examples relation(~I"2018", ~I"2018") # => :equal relation(~I"2015",

    ~I"2018") # => :before relation(~I"2017", ~I"2018") # => :meets relation(~I"2018-03", ~I"2018-01/12") # => :during relation(~I"2018-01", ~I"2018-01/12") # => :starts relation(~I"2018-01/12", ~I"2018-02") # => :contains
  37. Allen’s Interval Algebra: Examples relation(~I"2018", ~I"2018") # => :equal relation(~I"2015",

    ~I"2018") # => :before relation(~I"2017", ~I"2018") # => :meets relation(~I"2018-03", ~I"2018-01/12") # => :during relation(~I"2018-01", ~I"2018-01/12") # => :starts relation(~I"2018-01/12", ~I"2018-02") # => :contains relation(~I"2018-01/04", ~I"2018-02/06") # => :overlaps
  38. defmodule CalendarInterval do defstruct [:first, :last, :precision] @type t() ::

    %CalendarInterval{ first: NaiveDateTime.t(), last: NaiveDateTime.t(), precision: precision() } @type precision() :: :year |:month | ... end
  39. defmodule CalendarInterval do def sigil_I(string, _) do parse!(string) end defimpl

    Inspect do def inspect(interval, _) do ... end end end iex> next(~I"2018") ~I"2018"
  40. defmodule CalendarInterval do defimpl Enumerable do def reduce(interval, acc, fun)

    do ... end def count(interval) do {:error, __MODULE__} end # ... end end iex> Enum.count(~I"2018-01/12") 12
  41. defmodule CalendarInterval do defimpl Enumerable do def reduce(interval, acc, fun)

    do ... end def count(interval) do count = ... # math {:ok, count} end # ... end end iex> Enum.count(~I"2018-01/12") 12
  42. 
 # lib/calendar_recurrence/rrule_parser.ex defmodule CalendarRecurrence.RRULE.Parser do @moduledoc false import NimbleParsec

    freqs = ~w(SECONDLY MINUTELY HOURLY DAILY WEEKLY MONTHLY YEARLY) freq = part("FREQ", any_of(freqs, &string/1)) until = part("UNTIL", wrap(choice([datetime, date]))) count = part("COUNT", integer(min: 1)) # ... part = choice([ freq, until, count, interval, bysecond, byminute, byhour, byday ]) defparsec(:parse, part)
  43. 
 # lib/calendar_recurrence/rrule_parser.ex defmodule CalendarRecurrence.RRULE.Parser do @moduledoc false import NimbleParsec

    freqs = ~w(SECONDLY MINUTELY HOURLY DAILY WEEKLY MONTHLY YEARLY) freq = part("FREQ", any_of(freqs, &string/1)) until = part("UNTIL", wrap(choice([datetime, date]))) count = part("COUNT", integer(min: 1)) # ... part = choice([ freq, until, count, interval, bysecond, byminute, byhour, byday ]) defparsec(:parse, part)
  44. 
 # lib/calendar_recurrence/rrule_parser.ex.eex defmodule CalendarRecurrence.RRULE.Parser do @moduledoc false # parsec:

    CalendarRecurrence.RRULE.Parser import NimbleParsec freqs = ~w(SECONDLY MINUTELY HOURLY DAILY WEEKLY MONTHLY YEARLY) freq = part("FREQ", any_of(freqs, &string/1)) until = part("UNTIL", wrap(choice([datetime, date]))) count = part("COUNT", integer(min: 1)) # ... part = choice([ freq, until, count, interval, bysecond, byminute, byhour, byday ]) defparsec(:parse, part)
  45. # Generated from lib/calendar_recurrence/rrule_parser.ex.exs, do # Generated at 2018-05-16 23:42:45Z.

    defmodule CalendarRecurrence.RRULE.Parser do # ... defp parse__0(rest, acc, stack, context, line, offset) do parse__1(rest, [], [acc | stack], context, line, offset) end defp parse__1(rest, acc, stack, context, line, offset) do parse__82(rest, [], [{rest, context, line, offset}, acc | sta end defp parse__3(<<"BYDAY", "=", rest::binary>>, acc, stack, conte parse__4(rest, ["BYDAY"] ++ acc, stack, context, comb__line, end # ... end
  46. Extensibility "FREQ=DAILY" |> CalendarRecurrence.RRULE.to_recurrence(~I"2018-01-01") # Interval |> Enum.take(3) #=> [~I"2018-01-01",

    ~I"2018-01-02", ~I"2018-01-03"] |> CalendarRecurrence.RRULE.to_recurrence(~I"2018-01-01") # Interval Fast parser, no runtime dependency, extensible
  47. Extensibility "FREQ=DAILY" |> CalendarRecurrence.RRULE.to_recurrence(~I"2018-01-01") # Interval |> Enum.take(3) #=> [~I"2018-01-01",

    ~I"2018-01-02", ~I"2018-01-03"] Extensibility #=> [~I"2018-01-01", ~I"2018-01-02", ~I"2018-01-03"]
  48. Further work • Implement remaining RRULE options • Support timezones

    in intervals • Support interval features from the new ISO 8601 draft
  49. References • iCalendar: Internet Calendaring and Scheduling Core Object Specification

    • How to save datetimes for future events - (when UTC is not the right answer) - Lau Taarnskov • Exploring Time - Eric Evans • Maintaining Knowledge About Temporal Integrals - James F. Allen • https://github.com/wojtekmach/calendar_interval • https://github.com/wojtekmach/calendar_recurrence • https://hex.pm/packages/nimble_parsec • ISO 8601 Draft: Basic rules • ISO 8601 Draft: Extensions • Date.Range