1defmodule Timex.Comparable.Diff do
2  @moduledoc false
3
4  alias Timex.Types
5  alias Timex.Duration
6  alias Timex.Comparable
7
8  @units [:years, :months, :weeks, :calendar_weeks, :days,
9          :hours, :minutes, :seconds, :milliseconds, :microseconds,
10          :duration]
11
12  @spec diff(Types.microseconds, Types.microseconds, Comparable.granularity) :: integer
13  @spec diff(Types.valid_datetime, Types.valid_datetime, Comparable.granularity) :: integer
14  def diff(a, b, granularity) when is_integer(a) and is_integer(b) and is_atom(granularity) do
15    do_diff(a, b, granularity)
16  end
17  def diff(a, b, granularity) do
18    case {Timex.to_gregorian_microseconds(a), Timex.to_gregorian_microseconds(b)} do
19      {{:error, _} = err, _} -> err
20      {_, {:error, _} = err} -> err
21      {au, bu} when is_integer(au) and is_integer(bu) -> diff(au, bu, granularity)
22    end
23  end
24
25  defp do_diff(a, b, :duration), do: Duration.from_seconds(do_diff(a,b,:seconds))
26  defp do_diff(a, b, :microseconds), do: a - b
27  defp do_diff(a, b, :milliseconds), do: div(a - b, 1_000)
28  defp do_diff(a, b, :seconds),      do: div(a - b, 1_000*1_000)
29  defp do_diff(a, b, :minutes),      do: div(a - b, 1_000*1_000*60)
30  defp do_diff(a, b, :hours),        do: div(a - b, 1_000*1_000*60*60)
31  defp do_diff(a, b, :days),         do: div(a - b, 1_000*1_000*60*60*24)
32  defp do_diff(a, b, :weeks),        do: div(a - b, 1_000*1_000*60*60*24*7)
33  defp do_diff(a, b, :calendar_weeks) do
34    adate      = :calendar.gregorian_seconds_to_datetime(div(a, 1_000*1_000))
35    bdate      = :calendar.gregorian_seconds_to_datetime(div(b, 1_000*1_000))
36    days = cond do
37      a > b ->
38        ending     = Timex.end_of_week(adate)
39        start      = Timex.beginning_of_week(bdate)
40        endu       = Timex.to_gregorian_microseconds(ending)
41        startu     = Timex.to_gregorian_microseconds(start)
42        do_diff(endu, startu, :days)
43      :else ->
44        ending     = Timex.end_of_week(bdate)
45        start      = Timex.beginning_of_week(adate)
46        endu       = Timex.to_gregorian_microseconds(ending)
47        startu     = Timex.to_gregorian_microseconds(start)
48        do_diff(startu, endu, :days)
49    end
50    cond do
51      days >= 0 && rem(days, 7) != 0 -> div(days, 7) + 1
52      days <= 0 && rem(days, 7) != 0 -> div(days, 7) - 1
53      :else -> div(days, 7)
54    end
55  end
56  defp do_diff(a, b, :months) do
57    diff_months(a, b)
58  end
59  defp do_diff(a, b, :years) do
60    diff_years(a, b)
61  end
62  defp do_diff(_, _, granularity) when not granularity in @units,
63    do: {:error, {:invalid_granularity, granularity}}
64
65  defp diff_years(a, b) do
66    {start_date, _} = :calendar.gregorian_seconds_to_datetime(div(a, 1_000*1_000))
67    {end_date, _} = :calendar.gregorian_seconds_to_datetime(div(b, 1_000*1_000))
68    if a > b do
69      diff_years(end_date, start_date, 0)
70    else
71      diff_years(start_date, end_date, 0) * -1
72    end
73  end
74  defp diff_years({y, _, _}, {y, _, _}, acc) do
75    acc
76  end
77  defp diff_years({y1, m, d}, {y2, _, _} = ed, acc) when y1 < y2 do
78    sd2 = {y1+1, m, d}
79    if :calendar.valid_date(sd2) do
80      sd2_secs = :calendar.datetime_to_gregorian_seconds({sd2,{0,0,0}})
81      ed_secs = :calendar.datetime_to_gregorian_seconds({ed,{0,0,0}})
82      if sd2_secs <= ed_secs do
83        diff_years(sd2, ed, acc+1)
84      else
85        acc
86      end
87    else
88      # This date is a leap day, so subtract a day and try again
89      diff_years({y1, m, d-1}, ed, acc)
90    end
91  end
92
93  defp diff_months(a, a), do: 0
94  defp diff_months(a, b) do
95    {start_date, _} = :calendar.gregorian_seconds_to_datetime(div(a, 1_000*1_000))
96    {end_date, _} = :calendar.gregorian_seconds_to_datetime(div(b, 1_000*1_000))
97    do_diff_months(start_date, end_date)
98  end
99
100  defp do_diff_months({y1, m1, d1}, {y2, m2, d2}) do
101    months = (y1 - y2) * 12 + m1 - m2
102    days_in_month2 = Timex.days_in_month(y2, m2)
103
104    cond do
105      months < 0 && d2 < d1 && (days_in_month2 >= d1 || days_in_month2 != d2) ->
106        months + 1
107      months > 0 && d2 > d1 ->
108        months - 1
109      true ->
110        months
111    end
112  end
113end
114