Skip to content

Commit

Permalink
Removes dependency on Timex
Browse files Browse the repository at this point in the history
  • Loading branch information
brianberlin committed Jan 30, 2024
1 parent 80b7c93 commit bfe967a
Show file tree
Hide file tree
Showing 27 changed files with 564 additions and 254 deletions.
4 changes: 2 additions & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
elixir 1.14.5-otp-25
erlang 25.3.2
elixir 1.15.6-otp-26
erlang 26.1
20 changes: 9 additions & 11 deletions lib/cocktail/builder/i_calendar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,18 @@ defmodule Cocktail.Builder.ICalendar do
TODO: write long description
"""

import Cocktail.Util

alias Cocktail.{Rule, Schedule, Validation}
alias Cocktail.Validation.{Day, DayOfMonth, HourOfDay, Interval, MinuteOfHour, SecondOfMinute, TimeOfDay, TimeRange}

@time_format_string "{YYYY}{0M}{0D}T{h24}{m}{s}"
@time_format_string "%Y%m%dT%H%M%S"

@doc ~S"""
Builds an iCalendar format string representation of a `t:Cocktail.Schedule.t/0`.
## Examples
iex> alias Cocktail.Schedule
...> start_time = Timex.to_datetime(~N[2017-01-01 06:00:00], "America/Los_Angeles")
...> start_time = Cocktail.Time.to_datetime(~N[2017-01-01 06:00:00], "America/Los_Angeles")
...> schedule = Schedule.new(start_time)
...> schedule = Schedule.add_recurrence_rule(schedule, :daily, interval: 2, hours: [10, 12])
...> build(schedule)
Expand Down Expand Up @@ -64,7 +62,7 @@ defmodule Cocktail.Builder.ICalendar do
## Examples
iex> alias Cocktail.Schedule
...> start_time = Timex.to_datetime(~N[2017-01-01 06:00:00], "America/Los_Angeles")
...> start_time = Cocktail.Time.to_datetime(~N[2017-01-01 06:00:00], "America/Los_Angeles")
...> schedule = Schedule.new(start_time)
...> schedule = Schedule.add_recurrence_rule(schedule, :daily, interval: 2, hours: [10, 12])
...> build_rule(schedule)
Expand All @@ -91,12 +89,12 @@ defmodule Cocktail.Builder.ICalendar do
@spec build_time(Cocktail.time(), String.t()) :: String.t()
defp build_time(%DateTime{} = time, prefix) do
timezone = time.time_zone
time_string = Timex.format!(time, @time_format_string)
time_string = Calendar.strftime(time, @time_format_string)
"#{prefix};TZID=#{timezone}:#{time_string}"
end

defp build_time(%NaiveDateTime{} = time, prefix) do
time_string = Timex.format!(time, @time_format_string)
time_string = Calendar.strftime(time, @time_format_string)
"#{prefix}:#{time_string}"
end

Expand All @@ -108,17 +106,17 @@ defmodule Cocktail.Builder.ICalendar do

defp build_end_time(%Schedule{start_time: start_time, duration: duration}) do
start_time
|> shift_time(seconds: duration)
|> Cocktail.Time.shift(duration, :second)
|> build_time("DTEND")
end

@spec build_utc_time(Cocktail.time()) :: String.t()
defp build_utc_time(%NaiveDateTime{} = time), do: Timex.format!(time, @time_format_string)
defp build_utc_time(%NaiveDateTime{} = time), do: Calendar.strftime(time, @time_format_string)

defp build_utc_time(%DateTime{} = time) do
time
|> Timex.to_datetime("UTC")
|> Timex.format!(@time_format_string <> "Z")
|> Cocktail.Time.to_datetime("UTC")
|> Calendar.strftime(@time_format_string <> "Z")
end

@spec do_build_rule(Rule.t()) :: String.t()
Expand Down
19 changes: 11 additions & 8 deletions lib/cocktail/parser/i_calendar.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ defmodule Cocktail.Parser.ICalendar do
alias Cocktail.{Rule, Schedule}

@time_regex ~r/^:?;?(?:TZID=(.+?):)?(.*?)(Z)?$/
@datetime_format "{YYYY}{0M}{0D}T{h24}{m}{s}"
@time_format "{h24}{m}{s}"
@datetime_format "%Y%m%dT%H%M%S"
@time_format "%H%M%S"

@doc ~S"""
Parses a string in iCalendar format into a `t:Cocktail.Schedule.t/0`.
Expand Down Expand Up @@ -38,7 +38,7 @@ defmodule Cocktail.Parser.ICalendar do
|> String.trim()
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> parse_lines(Schedule.new(Timex.now()), 0)
|> parse_lines(Schedule.new(DateTime.utc_now()), 0)
end

@spec parse_lines([String.t()], Schedule.t(), non_neg_integer) :: {:ok, Schedule.t()} | {:error, term}
Expand Down Expand Up @@ -105,15 +105,17 @@ defmodule Cocktail.Parser.ICalendar do
end

@spec parse_naive_datetime(String.t()) :: {:ok, NaiveDateTime.t()} | {:error, term}
defp parse_naive_datetime(time_string), do: Timex.parse(time_string, @datetime_format)
defp parse_naive_datetime(time_string) do
Cocktail.Time.parse(time_string, @datetime_format)
end

@spec parse_utc_datetime(String.t()) :: {:ok, DateTime.t()} | {:error, term}
defp parse_utc_datetime(time_string), do: parse_zoned_datetime(time_string, "UTC")
defp parse_utc_datetime(time_string), do: parse_zoned_datetime(time_string, "Etc/UTC")

@spec parse_zoned_datetime(String.t(), String.t()) :: {:ok, DateTime.t()} | {:error, term}
defp parse_zoned_datetime(time_string, zone) do
with {:ok, naive_datetime} <- Timex.parse(time_string, @datetime_format),
%DateTime{} = datetime <- Timex.to_datetime(naive_datetime, zone) do
with {:ok, naive_datetime} <- Cocktail.Time.parse(time_string, @datetime_format),
%DateTime{} = datetime <- Cocktail.Time.to_datetime(naive_datetime, zone) do
{:ok, datetime}
end
end
Expand Down Expand Up @@ -421,7 +423,8 @@ defmodule Cocktail.Parser.ICalendar do

@spec parse_time(String.t()) :: {:ok, Time.t()} | {:error, :invalid_time_format}
defp parse_time(time_string) do
case Timex.parse(time_string, @time_format) do
case Cocktail.Time.parse(time_string, @time_format) do
{:ok, %Time{} = time} -> {:ok, time}
{:ok, datetime} -> {:ok, NaiveDateTime.to_time(datetime)}
_error -> {:error, :invalid_time_format}
end
Expand Down
2 changes: 1 addition & 1 deletion lib/cocktail/rule_state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ defmodule Cocktail.RuleState do
defp new_state(%__MODULE__{until: nil} = rule_state, time), do: %{rule_state | current_time: time}

defp new_state(%__MODULE__{until: until} = rule_state, time) do
if Timex.compare(until, time) == -1 do
if Cocktail.Time.compare(until, time) == :lt do
%{rule_state | current_time: nil}
else
%{rule_state | current_time: time}
Expand Down
6 changes: 3 additions & 3 deletions lib/cocktail/schedule.ex
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ defmodule Cocktail.Schedule do
@doc false
@spec set_end_time(t, Cocktail.time()) :: t
def set_end_time(%__MODULE__{start_time: start_time} = schedule, end_time) do
duration = Timex.diff(end_time, start_time, :seconds)
duration = Cocktail.Time.diff(end_time, start_time, :second)
%{schedule | duration: duration}
end

Expand Down Expand Up @@ -180,9 +180,9 @@ defmodule Cocktail.Schedule do
~N[2017-10-04 10:00:00]]
# using a DateTime with a time zone
iex> start_time = Timex.to_datetime(~N[2017-01-02 10:00:00], "America/Los_Angeles")
iex> start_time = Cocktail.Time.to_datetime(~N[2017-01-02 10:00:00], "America/Los_Angeles")
...> schedule = start_time |> new() |> add_recurrence_rule(:daily)
...> schedule |> occurrences() |> Enum.take(3) |> Enum.map(&Timex.format!(&1, "{ISO:Extended}"))
...> schedule |> occurrences() |> Enum.take(3) |> Enum.map(&DateTime.to_iso8601/1)
["2017-01-02T10:00:00-08:00",
"2017-01-03T10:00:00-08:00",
"2017-01-04T10:00:00-08:00"]
Expand Down
30 changes: 12 additions & 18 deletions lib/cocktail/schedule_state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ defmodule Cocktail.ScheduleState do
@moduledoc false

alias Cocktail.{RuleState, Schedule, Span}
import Cocktail.Util

@type t :: %__MODULE__{
recurrence_rules: [RuleState.t()],
Expand All @@ -26,19 +25,19 @@ defmodule Cocktail.ScheduleState do

def new(%Schedule{} = schedule, current_time) do
current_time =
if Timex.compare(current_time, schedule.start_time) < 0,
if Cocktail.Time.compare(current_time, schedule.start_time) == :lt,
do: schedule.start_time,
else: current_time

recurrence_times_after_current_time =
schedule.recurrence_times
|> Enum.filter(&(Timex.compare(&1, current_time) >= 0))
|> Enum.sort(&(Timex.compare(&1, &2) <= 0))
|> Enum.filter(&(Cocktail.Time.compare(&1, current_time) in [:gt, :eq]))
|> Enum.sort(&(Cocktail.Time.compare(&1, &2) in [:lt, :eq]))

%__MODULE__{
recurrence_rules: schedule.recurrence_rules |> Enum.map(&RuleState.new/1),
recurrence_times: recurrence_times_after_current_time,
exception_times: schedule.exception_times |> Enum.uniq() |> Enum.sort(&(Timex.compare(&1, &2) <= 0)),
exception_times: schedule.exception_times |> Enum.uniq() |> Enum.sort(&(Cocktail.Time.compare(&1, &2) in [:lt, :eq])),
start_time: schedule.start_time,
current_time: current_time,
duration: schedule.duration
Expand Down Expand Up @@ -92,7 +91,7 @@ defmodule Cocktail.ScheduleState do
defp next_time_from_recurrence_times([next_time | rest], nil), do: {next_time, rest}

defp next_time_from_recurrence_times([next_time | rest] = times, current_time) do
if Timex.compare(next_time, current_time) <= 0 do
if Cocktail.Time.compare(next_time, current_time) in [:lt, :eq] do
{next_time, rest}
else
{current_time, times}
Expand All @@ -105,15 +104,10 @@ defmodule Cocktail.ScheduleState do
defp apply_exception_time(exceptions, nil), do: {false, exceptions}

defp apply_exception_time([next_exception | rest] = exceptions, current_time) do
case Timex.compare(next_exception, current_time) do
0 ->
{true, rest}

-1 ->
apply_exception_time(rest, current_time)

_ ->
{false, exceptions}
case Cocktail.Time.compare(next_exception, current_time) do
:eq -> {true, rest}
:lt -> apply_exception_time(rest, current_time)
:gt -> {false, exceptions}
end
end

Expand All @@ -135,23 +129,23 @@ defmodule Cocktail.ScheduleState do
| recurrence_rules: rules,
recurrence_times: times,
exception_times: exceptions,
current_time: shift_time(time, seconds: 1)
current_time: Cocktail.Time.shift(time, 1, :second)
}

{occurrence, new_state}
end

@spec span_or_time(Cocktail.time() | nil, pos_integer | nil) :: Cocktail.occurrence()
defp span_or_time(time, nil), do: time
defp span_or_time(time, duration), do: Span.new(time, shift_time(time, seconds: duration))
defp span_or_time(time, duration), do: Span.new(time, Cocktail.Time.shift(time, duration, :second))

@spec min_time_for_rules([RuleState.t()]) :: Cocktail.time() | nil
defp min_time_for_rules([]), do: nil
defp min_time_for_rules([rule]), do: rule.current_time

defp min_time_for_rules(rules) do
rules
|> Enum.min_by(&Timex.to_erl(&1.current_time))
|> Enum.min_by(&Cocktail.Time.to_erl(&1.current_time))
|> Map.get(:current_time)
end

Expand Down
30 changes: 15 additions & 15 deletions lib/cocktail/span.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ defmodule Cocktail.Span do
def new(from, until), do: %__MODULE__{from: from, until: until}

@doc """
Uses `Timex.compare/2` to determine which span comes first.
Uses `Cocktail.Time.compare/2` to determine which span comes first.
Compares `from` first, then, if equal, compares `until`.
Expand All @@ -48,21 +48,21 @@ defmodule Cocktail.Span do
iex> span1 = new(~N[2017-01-01 06:00:00], ~N[2017-01-01 10:00:00])
...> span2 = new(~N[2017-01-01 06:00:00], ~N[2017-01-01 10:00:00])
...> compare(span1, span2)
0
:eq
iex> span1 = new(~N[2017-01-01 06:00:00], ~N[2017-01-01 10:00:00])
...> span2 = new(~N[2017-01-01 07:00:00], ~N[2017-01-01 12:00:00])
...> compare(span1, span2)
-1
:lt
iex> span1 = new(~N[2017-01-01 06:00:00], ~N[2017-01-01 10:00:00])
...> span2 = new(~N[2017-01-01 06:00:00], ~N[2017-01-01 07:00:00])
...> compare(span1, span2)
1
:gt
"""
@spec compare(span_compat, span_compat) :: Timex.Comparable.compare_result()
def compare(%{from: t, until: until1}, %{from: t, until: until2}), do: Timex.compare(until1, until2)
def compare(%{from: from1}, %{from: from2}), do: Timex.compare(from1, from2)
@spec compare(span_compat, span_compat) :: :lt | :eq | :gt
def compare(%{from: t, until: until1}, %{from: t, until: until2}), do: Cocktail.Time.compare(until1, until2)
def compare(%{from: from1}, %{from: from2}), do: Cocktail.Time.compare(from1, from2)

@doc """
Returns an `t:overlap_mode/0` to describe how the first span overlaps the second.
Expand Down Expand Up @@ -94,16 +94,16 @@ defmodule Cocktail.Span do

# credo:disable-for-next-line
def overlap_mode(%{from: from1, until: until1}, %{from: from2, until: until2}) do
from_comp = Timex.compare(from1, from2)
until_comp = Timex.compare(until1, until2)
from_comp = Cocktail.Time.compare(from1, from2)
until_comp = Cocktail.Time.compare(until1, until2)

cond do
from_comp <= 0 && until_comp >= 0 -> :contains
from_comp >= 0 && until_comp <= 0 -> :is_inside
Timex.compare(until1, from2) <= 0 -> :is_before
Timex.compare(from1, until2) >= 0 -> :is_after
from_comp < 0 && until_comp < 0 -> :overlaps_the_start_of
from_comp > 0 && until_comp > 0 -> :overlaps_the_end_of
from_comp in [:lt, :eq] && until_comp in [:gt, :eq] -> :contains
from_comp in [:gt, :eq] && until_comp in [:lt, :eq] -> :is_inside
Cocktail.Time.compare(until1, from2) in [:lt, :eq] -> :is_before
Cocktail.Time.compare(from1, until2) in [:gt, :eq] -> :is_after
from_comp == :lt && until_comp == :lt -> :overlaps_the_start_of
from_comp == :gt && until_comp == :gt -> :overlaps_the_end_of
end
end
end
Loading

0 comments on commit bfe967a

Please sign in to comment.