defmodule TimexInterval.DateTimeInterval do use Timex @default_from Date.now() @default_until Date.shift(@default_from, days: 7) @default_left_open false @default_right_open true @default_step [days: 1] @derive Access defstruct from: @default_from, until: @default_until, left_open: @default_left_open, right_open: @default_right_open, step: @default_step @doc """ Make a new interval. Note that by default intervals are right open. Valid keywords: - from: the date the interval starts at (defaults to `Date.now()`) - until: either the date the interval ends at, or a time shift that will be applied to the "from" date (defaults to `[days: 7]`) - left_open: whether the interval is left open, defaults to #{@default_left_open} - right_open: whether the interval is right open, defaults to #{@default_right_open} - step: the iteration step for enumerations, defaults to `[days: 1]` Shifts should be keyword lists valid for use with `Timex.Date.shift`. ## Examples > use Timex > alias TimexInterval.DateTimeInterval, as: Interval > Interval.new(from: Date.from({2014, 9, 22}), until: Date.from({2014, 9, 29})) |> Interval.format!("%Y-%m-%d") #=> "[2014-09-22, 2014-09-29)" > Interval.new(from: Date.from({2014, 9, 22}), until: [months: 5]) |> Interval.format!("%Y-%m-%d") #=> "[2014-09-22, 2015-02-22)" > Interval.new(from: Date.from({{2014, 9, 22}, {15, 30, 0}}), until: [mins: 20], right_open: false) |> Interval.format!("%H:%M") #=> "[15:30, 15:50]" """ def new(keywords \\ []) do from = Dict.get(keywords, :from, Date.now()) until = Dict.get(keywords, :until, [days: 7]) left_open = Dict.get(keywords, :left_open, @default_left_open) right_open = Dict.get(keywords, :right_open, @default_right_open) step = Dict.get(keywords, :step, @default_step) %TimexInterval.DateTimeInterval{from: from, until: (if is_list(until), do: Date.shift(from, until), else: until), left_open: left_open, right_open: right_open, step: step} end @doc """ Return the interval duration, given a unit. When the unit is one of `:secs`, `:mins`, `:hours`, `:days`, `:weeks`, `:months`, `:years`, the result is an `integer`. When the unit is `:timestamp`, the result is a tuple representing a valid `Timex.Time`. ## Example > use Timex > alias TimexInterval.DateTimeInterval, as: Interval > Interval.new(from: Date.from({2014, 9, 22}), until: [months: 5]) |> Interval.duration(:months) #=> 5 > Interval.new(from: Date.from({{2014, 9, 22}, {15, 30, 0}}), until: [mins: 20]) |> Interval.duration(:timestamp) #=> {0, 0, 1200} """ def duration(interval, unit) do Date.diff(interval.from, interval.until, unit) end @doc """ Update the step for this interval. The step should be a keyword list valid for use with `Timex.Date.shift`. ## Examples > use Timex > alias TimexInterval.DateTimeInterval, as: Interval > i = Interval.new(from: Date.from({2014, 9, 22}), until: [days: 3], right_open: false) > i |> Interval.with_step([days: 1]) |> Enum.map(fn(dt) -> DateFormat.format!(dt, "%Y-%m-%d", :strftime) end) #=> ["2014-09-22", "2014-09-23", "2014-09-24", "2014-09-25"] > i |> Interval.with_step([days: 2]) |> Enum.map(fn(dt) -> DateFormat.format!(dt, "%Y-%m-%d", :strftime) end) #=> ["2014-09-22", "2014-09-24"] > i |> Interval.with_step([days: 3]) |> Enum.map(fn(dt) -> DateFormat.format!(dt, "%Y-%m-%d", :strftime) end) #=> ["2014-09-22", "2014-09-25"] """ def with_step(interval, step) do %TimexInterval.DateTimeInterval{interval | step: step} end @doc """ Return a human readable version of the interval. ## Examples > use Timex > alias TimexInterval.DateTimeInterval, as: Interval > Interval.new(from: Date.from({2014, 9, 22}), until: [days: 3], right_open: false) |> Interval.format!() #=> "[2014-09-22 00:00, 2014-09-25 00:00]" > Interval.new(from: Date.from({2014, 9, 22}), until: [days: 3]) |> Interval.format!("%Y-%m-%d") #=> "[2014-09-22, 2014-09-25)" """ def format!(interval, format_string \\ "%Y-%m-%d %H:%M", formatter \\ :strftime) do s1 = if interval.left_open, do: "(", else: "[" s2 = DateFormat.format!(interval.from, format_string, formatter) s3 = ", " s4 = DateFormat.format!(interval.until, format_string, formatter) s5 = if interval.right_open, do: ")", else: "]" s1 <> s2 <> s3 <> s4 <> s5 end defimpl Enumerable do def reduce(interval, acc, fun) do do_reduce({get_starting_date(interval), interval.until, interval.right_open, interval.step}, acc, fun) end def member?(_interval, _value), do: {:error, __MODULE__} def count(_interval), do: {:error, __MODULE__} ## Private defp do_reduce(_state, {:halt, acc}, _fun), do: {:halted, acc} defp do_reduce( state, {:suspend, acc}, fun), do: {:suspended, acc, &do_reduce(state, &1, fun)} defp do_reduce({current_date, end_date, right_open, keywords}, {:cont, acc}, fun) do if has_recursion_ended?(current_date, end_date, right_open) do {:done, acc} else next_date = Date.shift(current_date, keywords) do_reduce({next_date, end_date, right_open, keywords}, fun.(current_date, acc), fun) end end defp get_starting_date(interval) do if interval.left_open do Date.shift(interval.from, interval.step) else interval.from end end defp has_recursion_ended?(current_date, end_date, true), do: Date.compare(end_date, current_date) <= 0 defp has_recursion_ended?(current_date, end_date, false), do: Date.compare(end_date, current_date) < 0 end end