| defmodule Plausible.Stats.Time do |
| @moduledoc """ |
| Collection of functions to work with time in queries. |
| """ |
|
|
| alias Plausible.Stats.{Query, DateTimeRange} |
|
|
| def utc_boundaries(%Query{ |
| utc_time_range: time_range, |
| site_native_stats_start_at: native_stats_start_at |
| }) do |
| first = |
| time_range.first |
| |> DateTime.to_naive() |
| |> beginning_of_time(native_stats_start_at) |
|
|
| last = DateTime.to_naive(time_range.last) |
|
|
| {first, last} |
| end |
|
|
| defp beginning_of_time(candidate, native_stats_start_at) do |
| if NaiveDateTime.after?(native_stats_start_at, candidate) do |
| native_stats_start_at |
| else |
| candidate |
| end |
| end |
|
|
| def format_datetime(%Date{} = date), do: Date.to_string(date) |
|
|
| def format_datetime(%mod{} = datetime) when mod in [NaiveDateTime, DateTime], |
| do: Calendar.strftime(datetime, "%Y-%m-%d %H:%M:%S") |
|
|
| |
| def format_datetime(other), do: other |
|
|
| def time_dimension(query) do |
| Enum.find(query.dimensions, &time_dimension?/1) |
| end |
|
|
| def time_dimension?("time" <> _rest), do: true |
| def time_dimension?(_dimension), do: false |
|
|
| @doc """ |
| Returns list of time bucket labels for the given query. |
| """ |
| def time_labels(query) do |
| time_labels_for_dimension(time_dimension(query), query) |
| end |
|
|
| defp time_labels_for_dimension("time:month", query) do |
| date_range = Query.date_range(query) |
|
|
| n_buckets = |
| Plausible.Times.diff( |
| date_range.last, |
| Date.beginning_of_month(date_range.first), |
| :month |
| ) |
|
|
| Enum.map(n_buckets..0//-1, fn shift -> |
| date_range.last |
| |> Date.beginning_of_month() |
| |> Date.shift(month: -shift) |
| |> format_datetime() |
| end) |
| end |
|
|
| defp time_labels_for_dimension("time:week", query) do |
| date_range = Query.date_range(query) |
|
|
| n_buckets = |
| Plausible.Times.diff( |
| date_range.last, |
| Date.beginning_of_week(date_range.first), |
| :week |
| ) |
|
|
| Enum.map(0..n_buckets, fn shift -> |
| date_range.first |
| |> Date.shift(week: shift) |
| |> date_or_weekstart(date_range) |
| |> format_datetime() |
| end) |
| end |
|
|
| defp time_labels_for_dimension("time:day", query) do |
| Query.date_range(query) |
| |> Enum.into([]) |
| |> Enum.map(&format_datetime/1) |
| end |
|
|
| defp time_labels_for_dimension("time:hour", query) do |
| time_range = query.utc_time_range |> DateTimeRange.to_timezone(query.timezone) |
|
|
| from_timestamp = time_range.first |> Map.merge(%{minute: 0, second: 0}) |
| n_buckets = DateTime.diff(time_range.last, from_timestamp, :hour) |
|
|
| Enum.map(0..n_buckets, fn step -> |
| from_timestamp |
| |> DateTime.to_naive() |
| |> NaiveDateTime.shift(hour: step) |
| |> format_datetime() |
| end) |
| end |
|
|
| defp time_labels_for_dimension("time:minute", query) do |
| time_range = query.utc_time_range |> DateTimeRange.to_timezone(query.timezone) |
| first_datetime = Map.put(time_range.first, :second, 0) |
|
|
| first_datetime |
| |> Stream.iterate(fn datetime -> DateTime.shift(datetime, minute: 1) end) |
| |> Enum.take_while(fn datetime -> |
| current_minute = Map.put(query.now, :second, 0) |
|
|
| DateTime.before?(datetime, time_range.last) && |
| DateTime.before?(datetime, current_minute) |
| end) |
| |> Enum.map(&format_datetime/1) |
| end |
|
|
| def partial_time_labels(time_labels, query) do |
| time_dimension = time_dimension(query) |
|
|
| range_start = to_naive_in_tz!(query.utc_time_range.first, query.timezone) |
| range_end = to_naive_in_tz!(query.utc_time_range.last, query.timezone) |
| now = to_naive_in_tz!(query.now, query.timezone) |
|
|
| cutoff = if NaiveDateTime.before?(now, range_end), do: now, else: range_end |
|
|
| first_bucket = List.first(time_labels) |
| last_bucket = List.last(time_labels) |
|
|
| first_partial? = |
| case bucket_start(first_bucket, time_dimension) do |
| nil -> false |
| start -> NaiveDateTime.after?(range_start, start) |
| end |
|
|
| last_partial? = |
| case bucket_end(last_bucket, time_dimension) do |
| nil -> false |
| bucket_end -> NaiveDateTime.after?(bucket_end, cutoff) |
| end |
|
|
| [ |
| if(first_partial?, do: first_bucket), |
| if(last_partial?, do: last_bucket) |
| ] |
| |> Enum.uniq() |
| |> Enum.reject(&is_nil/1) |
| end |
|
|
| defp bucket_start(label, "time:week") do |
| case Date.from_iso8601(label) do |
| {:ok, date} -> NaiveDateTime.new!(Date.beginning_of_week(date), ~T[00:00:00]) |
| _ -> nil |
| end |
| end |
|
|
| defp bucket_start(label, _time_dimension) do |
| case Date.from_iso8601(label) do |
| {:ok, date} -> |
| NaiveDateTime.new!(date, ~T[00:00:00]) |
|
|
| _ -> |
| case NaiveDateTime.from_iso8601(label) do |
| {:ok, naive_datetime} -> naive_datetime |
| _ -> nil |
| end |
| end |
| end |
|
|
| defp bucket_end(label, time_dimension) do |
| shift_unit = |
| case time_dimension do |
| "time:month" -> :month |
| "time:week" -> :week |
| "time:day" -> :day |
| "time:hour" -> :hour |
| "time:minute" -> :minute |
| end |
|
|
| case bucket_start(label, time_dimension) do |
| nil -> nil |
| start -> NaiveDateTime.shift(start, [{shift_unit, 1}, {:second, -1}]) |
| end |
| end |
|
|
| defp to_naive_in_tz!(utc_datetime, timezone) do |
| Plausible.Times.to_naive_datetime!(utc_datetime, timezone) |
| end |
|
|
| def present_index(time_labels, query) do |
| now = DateTime.shift_zone!(query.now, query.timezone) |
|
|
| current_label = |
| case time_dimension(query) do |
| "time:month" -> |
| DateTime.to_date(now) |
| |> Date.beginning_of_month() |
| |> Date.to_string() |
|
|
| "time:week" -> |
| DateTime.to_date(now) |
| |> date_or_weekstart(Query.date_range(query)) |
| |> Date.to_string() |
|
|
| "time:day" -> |
| DateTime.to_date(now) |
| |> Date.to_string() |
|
|
| "time:hour" -> |
| Calendar.strftime(now, "%Y-%m-%d %H:00:00") |
|
|
| "time:minute" -> |
| Calendar.strftime(now, "%Y-%m-%d %H:%M:00") |
| end |
|
|
| Enum.find_index(time_labels, &(&1 == current_label)) |
| end |
|
|
| def date_or_weekstart(date, date_range) do |
| weekstart = Date.beginning_of_week(date) |
|
|
| if Enum.member?(date_range, weekstart) do |
| weekstart |
| else |
| date |
| end |
| end |
| end |
|
|