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