Slide 1

Slide 1 text

Recurrences & Intervals Wojtek Mach @wojtekmach @wojtekmach

Slide 2

Slide 2 text

Parts 1. Recurrences 2. Intervals 3. „Exploring Time” 4. Libraries

Slide 3

Slide 3 text

Part I: Recurrences

Slide 4

Slide 4 text

Google Calendar

Slide 5

Slide 5 text

Apple iCal

Slide 6

Slide 6 text

iCalendar

Slide 7

Slide 7 text

iCalendar Example DTSTART;TZID=Europe/Warsaw:20180101T090000 RRULE:FREQ=WEEKLY;BYDAY=MO,TU;COUNT=4 2018-01-01 09:00 CET (Monday) 2018-01-02 09:00 CET (Tuesday) 2018-01-08 09:00 CET (Monday) 2018-01-09 09:00 CET (Tuesday)

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

Beginning of Recurrence

Slide 10

Slide 10 text

Beginning of Recurrence

Slide 11

Slide 11 text

Aside: Time zones

Slide 12

Slide 12 text

iCalendar Example DTSTART;TZID=Europe/Warsaw:20180101T090000 RRULE:FREQ=DAILY;BYDAY=MO,TU;COUNT=4 2018-01-01 09:00 CET 2018-01-02 09:00 CET 2018-01-08 09:00 CET 2018-01-09 09:00 CET

Slide 13

Slide 13 text

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)

Slide 14

Slide 14 text

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)

Slide 15

Slide 15 text

Code

Slide 16

Slide 16 text

defmodule Recurrence do def stream(start) do Stream.iterate(start, &Date.add(&1, 1)) end end

Slide 17

Slide 17 text

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…>

Slide 18

Slide 18 text

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]]

Slide 19

Slide 19 text

Part II: Intervals

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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 


Slide 24

Slide 24 text

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 


Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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: ~

Slide 28

Slide 28 text

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: ~

Slide 29

Slide 29 text

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")

Slide 30

Slide 30 text

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 ???

Slide 31

Slide 31 text

Part III:
 “Exploring Time”

Slide 32

Slide 32 text

“Exploring Time” Eric Evans
 Explore DDD 2017 - Denver, Sept. 21-22

Slide 33

Slide 33 text

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>}

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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]}

Slide 36

Slide 36 text

Arithmetic ~D[2017-01-20] + "1 month" => ~D[2017-02-20]

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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]

Slide 39

Slide 39 text

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]

Slide 40

Slide 40 text

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]

Slide 41

Slide 41 text

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]

Slide 42

Slide 42 text

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] ^^

Slide 43

Slide 43 text

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)

Slide 44

Slide 44 text

Time Count ..., 2018-01-01, 2018-01-02, 2018-01-03, ...

Slide 45

Slide 45 text

Time Count ..., 2018-01-01, 2018-01-02, 2018-01-03, ... ..., 2018-12-31, 2019-01-01, 2019-01-02, ...

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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, ...

Slide 48

Slide 48 text

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, ...

Slide 49

Slide 49 text

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, ...

Slide 50

Slide 50 text

Time Count: Nesting iex> nest(~I"2018") {~I"2018-01", “2018-12"}

Slide 51

Slide 51 text

Time Count: Nesting iex> nest(~I"2018") {~I"2018-01", "2018-12"} iex> nest(~I"2018-01") {~I"2018-01-01", “2018-01-31”}

Slide 52

Slide 52 text

Time Count: Nesting iex> nest(~I"2018") ~I"2018-01/12" iex> nest(~I"2018-01") ~I”2018-01-01/31"

Slide 53

Slide 53 text

Time Count: Nesting iex> nest(~I"2018") ~I"2018-01/12" iex> nest(~I"2018-01") ~I”2018-01-01/31" iex> nest(~I"2018", :minute) ~I"2018-01-01 00:00/12-31 23:59"

Slide 54

Slide 54 text

Time Count: Enclosing iex> enclose(~I"2018-06") ~I"2018"

Slide 55

Slide 55 text

Time Count: Enclosing iex> enclose(~I"2018-06") ~I”2018" iex> enclose(~I"2018-01-01 00:00", :month) ~I"2018-01"

Slide 56

Slide 56 text

Representation ~I"2018-01/12" ~I"2018-01"

Slide 57

Slide 57 text

Representation ~I"2018-01/12" ~I"2018-01/01"

Slide 58

Slide 58 text

Time Count: Composition ~I"2018-05-19 09:00"

Slide 59

Slide 59 text

Time Count: Composition ~I"2018-05-19 09:00" |> enclose(:month) # => ~I"2018-05"

Slide 60

Slide 60 text

Time Count: Composition ~I"2018-05-19 09:00" |> enclose(:month) # => ~I"2018-05" |> nest(:day) # => ~I"2018-05-01/31"

Slide 61

Slide 61 text

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"

Slide 62

Slide 62 text

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"

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

Time Count: Comparisons iex> compare(~I"2018-01/03", ~I"2018-02/04") ???

Slide 67

Slide 67 text

Allen’s Interval Algebra

Slide 68

Slide 68 text

Allen’s Interval Algebra

Slide 69

Slide 69 text

Allen’s Interval Algebra: Examples relation(~I"2018", ~I"2018") # => :equal

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

iex> relation(~I"2018-01/03", ~I"2018-02/04") :overlaps

Slide 77

Slide 77 text

Part IV: Libraries

Slide 78

Slide 78 text

No content

Slide 79

Slide 79 text

defmodule CalendarInterval do defstruct [:first, :last, :precision] @type t() :: %CalendarInterval{ first: NaiveDateTime.t(), last: NaiveDateTime.t(), precision: precision() } @type precision() :: :year |:month | ... end

Slide 80

Slide 80 text

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"

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

No content

Slide 84

Slide 84 text

defmodule CalendarRecurrence do defimpl Enumerable do def reduce(recurrence, acc, fun) do ... end # ... end end

Slide 85

Slide 85 text

NimbleParsec

Slide 86

Slide 86 text


 # 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)

Slide 87

Slide 87 text

defp deps() do [ {:nimble_parsec, "~> 0.3", only: [:dev, :test]} ] end

Slide 88

Slide 88 text


 # 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)

Slide 89

Slide 89 text


 # 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)

Slide 90

Slide 90 text

$ mix nimble_parsec.compile lib/calendar_recurrence/rrule_parser.ex.exs Generating lib/calendar_recurrence/rrule_parser.ex mix nimble_parsec.compile

Slide 91

Slide 91 text

# 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

Slide 92

Slide 92 text

Extensibility "FREQ=DAILY" |> CalendarRecurrence.RRULE.to_recurrence(~D"2018-01-01") # Date |> Enum.take(3) #=> [~D[2018-01-01], ~D[2018-01-02], ~D[2018-01-03]]

Slide 93

Slide 93 text

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"]

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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"] Fast |> Enum.take(3)

Slide 96

Slide 96 text

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"]

Slide 97

Slide 97 text

Further work • Implement remaining RRULE options • Support timezones in intervals • Support interval features from the new ISO 8601 draft

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

Thank you!