diff --git a/lib/plausible/stats/interval.ex b/lib/plausible/stats/interval.ex index ae80ba8eab32a..116f711c99e4e 100644 --- a/lib/plausible/stats/interval.ex +++ b/lib/plausible/stats/interval.ex @@ -7,8 +7,6 @@ defmodule Plausible.Stats.Interval do `week`, and `month`. """ - alias Plausible.Stats.Query - @type t() :: String.t() @type(opt() :: {:site, Plausible.Site.t()} | {:from, Date.t()}, {:to, Date.t()}) @type opts :: list(opt()) @@ -105,116 +103,4 @@ defmodule Plausible.Stats.Interval do def valid_for_period?(period, interval, opts \\ []) do interval in Map.get(valid_by_period(opts), period, []) end - - def format_datetime(%Date{} = date), do: Date.to_string(date) - - def format_datetime(%DateTime{} = datetime), - do: Timex.format!(datetime, "{YYYY}-{0M}-{0D} {h24}:{m}:{s}") - - # Realtime graphs return numbers - def format_datetime(other), do: other - - @doc """ - Returns list of time bucket labels for the given query. - """ - def time_dimension(query) do - Enum.find(query.dimensions, &String.starts_with?(&1, "time")) - end - - def time_labels(query) do - time_labels_for_dimension(time_dimension(query), query) - end - - defp time_labels_for_dimension("time:month", query) do - n_buckets = - Timex.diff( - query.date_range.last, - Date.beginning_of_month(query.date_range.first), - :months - ) - - Enum.map(n_buckets..0, fn shift -> - query.date_range.last - |> Date.beginning_of_month() - |> Timex.shift(months: -shift) - |> format_datetime() - end) - end - - defp time_labels_for_dimension("time:week", query) do - n_buckets = - Timex.diff( - query.date_range.last, - Date.beginning_of_week(query.date_range.first), - :weeks - ) - - Enum.map(0..n_buckets, fn shift -> - query.date_range.first - |> Timex.shift(weeks: shift) - |> date_or_weekstart(query) - |> format_datetime() - end) - end - - defp time_labels_for_dimension("time:day", query) do - query.date_range - |> Enum.into([]) - |> Enum.map(&format_datetime/1) - end - - @full_day_in_hours 23 - defp time_labels_for_dimension("time:hour", query) do - n_buckets = - if query.date_range.first == query.date_range.last do - @full_day_in_hours - else - end_time = - query.date_range.last - |> Timex.to_datetime() - |> Timex.end_of_day() - - Timex.diff(end_time, query.date_range.first, :hours) - end - - Enum.map(0..n_buckets, fn step -> - query.date_range.first - |> Timex.to_datetime() - |> Timex.shift(hours: step) - |> DateTime.truncate(:second) - |> format_datetime() - end) - end - - # Only supported in dashboards not via API - defp time_labels_for_dimension("time:minute", %Query{period: "30m"}) do - Enum.into(-30..-1, []) - end - - @full_day_in_minutes 24 * 60 - 1 - defp time_labels_for_dimension("time:minute", query) do - n_buckets = - if query.date_range.first == query.date_range.last do - @full_day_in_minutes - else - Timex.diff(query.date_range.last, query.date_range.first, :minutes) - end - - Enum.map(0..n_buckets, fn step -> - query.date_range.first - |> Timex.to_datetime() - |> Timex.shift(minutes: step) - |> format_datetime() - end) - end - - defp date_or_weekstart(date, query) do - weekstart = Timex.beginning_of_week(date) - - if Enum.member?(query.date_range, weekstart) do - weekstart - else - date - end - end end diff --git a/lib/plausible/stats/query_result.ex b/lib/plausible/stats/query_result.ex index e4c8791e81bb0..c59d22ddcf411 100644 --- a/lib/plausible/stats/query_result.ex +++ b/lib/plausible/stats/query_result.ex @@ -1,7 +1,6 @@ defmodule Plausible.Stats.QueryResult do @moduledoc false - alias Plausible.Stats.Interval alias Plausible.Stats.Util alias Plausible.Stats.Filters @@ -49,7 +48,7 @@ defmodule Plausible.Stats.QueryResult do defp dimension_label("time:" <> _ = time_dimension, entry, query) do datetime = Map.get(entry, Util.shortname(query, time_dimension)) - Interval.format_datetime(datetime) + Plausible.Stats.Time.format_datetime(datetime) end defp dimension_label(dimension, entry, query) do @@ -72,7 +71,8 @@ defmodule Plausible.Stats.QueryResult do :unsupported_query -> @imports_unsupported_query_warning _ -> nil end, - time_labels: if(query.include.time_labels, do: Interval.time_labels(query), else: nil) + time_labels: + if(query.include.time_labels, do: Plausible.Stats.Time.time_labels(query), else: nil) } |> Enum.reject(fn {_, value} -> is_nil(value) end) |> Enum.into(%{}) diff --git a/lib/plausible/stats/time.ex b/lib/plausible/stats/time.ex new file mode 100644 index 0000000000000..c8c0d7a71feee --- /dev/null +++ b/lib/plausible/stats/time.ex @@ -0,0 +1,118 @@ +defmodule Plausible.Stats.Time do + @moduledoc """ + Collection of functions to work with time in queries. + """ + + alias Plausible.Stats.Query + def format_datetime(%Date{} = date), do: Date.to_string(date) + + def format_datetime(%DateTime{} = datetime), + do: Timex.format!(datetime, "{YYYY}-{0M}-{0D} {h24}:{m}:{s}") + + # Realtime graphs return numbers + def format_datetime(other), do: other + + @doc """ + Returns list of time bucket labels for the given query. + """ + def time_dimension(query) do + Enum.find(query.dimensions, &String.starts_with?(&1, "time")) + end + + def time_labels(query) do + time_labels_for_dimension(time_dimension(query), query) + end + + defp time_labels_for_dimension("time:month", query) do + n_buckets = + Timex.diff( + query.date_range.last, + Date.beginning_of_month(query.date_range.first), + :months + ) + + Enum.map(n_buckets..0, fn shift -> + query.date_range.last + |> Date.beginning_of_month() + |> Timex.shift(months: -shift) + |> format_datetime() + end) + end + + defp time_labels_for_dimension("time:week", query) do + n_buckets = + Timex.diff( + query.date_range.last, + Date.beginning_of_week(query.date_range.first), + :weeks + ) + + Enum.map(0..n_buckets, fn shift -> + query.date_range.first + |> Timex.shift(weeks: shift) + |> date_or_weekstart(query) + |> format_datetime() + end) + end + + defp time_labels_for_dimension("time:day", query) do + query.date_range + |> Enum.into([]) + |> Enum.map(&format_datetime/1) + end + + @full_day_in_hours 23 + defp time_labels_for_dimension("time:hour", query) do + n_buckets = + if query.date_range.first == query.date_range.last do + @full_day_in_hours + else + end_time = + query.date_range.last + |> Timex.to_datetime() + |> Timex.end_of_day() + + Timex.diff(end_time, query.date_range.first, :hours) + end + + Enum.map(0..n_buckets, fn step -> + query.date_range.first + |> Timex.to_datetime() + |> Timex.shift(hours: step) + |> DateTime.truncate(:second) + |> format_datetime() + end) + end + + # Only supported in dashboards not via API + defp time_labels_for_dimension("time:minute", %Query{period: "30m"}) do + Enum.into(-30..-1, []) + end + + @full_day_in_minutes 24 * 60 - 1 + defp time_labels_for_dimension("time:minute", query) do + n_buckets = + if query.date_range.first == query.date_range.last do + @full_day_in_minutes + else + Timex.diff(query.date_range.last, query.date_range.first, :minutes) + end + + Enum.map(0..n_buckets, fn step -> + query.date_range.first + |> Timex.to_datetime() + |> Timex.shift(minutes: step) + |> format_datetime() + end) + end + + defp date_or_weekstart(date, query) do + weekstart = Timex.beginning_of_week(date) + + if Enum.member?(query.date_range, weekstart) do + weekstart + else + date + end + end +end diff --git a/test/plausible/stats/interval_test.exs b/test/plausible/stats/interval_test.exs index 57d15969ffa81..36e3c68093de2 100644 --- a/test/plausible/stats/interval_test.exs +++ b/test/plausible/stats/interval_test.exs @@ -123,156 +123,4 @@ defmodule Plausible.Stats.IntervalTest do ) end end - - describe "time_labels/1" do - test "with time:month dimension" do - assert time_labels(%{ - dimensions: ["visit:device", "time:month"], - date_range: Date.range(~D[2022-01-17], ~D[2022-02-01]) - }) == [ - "2022-01-01", - "2022-02-01" - ] - - assert time_labels(%{ - dimensions: ["visit:device", "time:month"], - date_range: Date.range(~D[2022-01-01], ~D[2022-03-07]) - }) == [ - "2022-01-01", - "2022-02-01", - "2022-03-01" - ] - end - - test "with time:week dimension" do - assert time_labels(%{ - dimensions: ["time:week"], - date_range: Date.range(~D[2020-12-20], ~D[2021-01-08]) - }) == [ - "2020-12-20", - "2020-12-21", - "2020-12-28", - "2021-01-04" - ] - - assert time_labels(%{ - dimensions: ["time:week"], - date_range: Date.range(~D[2020-12-21], ~D[2021-01-03]) - }) == [ - "2020-12-21", - "2020-12-28" - ] - end - - test "with time:day dimension" do - assert time_labels(%{ - dimensions: ["time:day"], - date_range: Date.range(~D[2022-01-17], ~D[2022-02-02]) - }) == [ - "2022-01-17", - "2022-01-18", - "2022-01-19", - "2022-01-20", - "2022-01-21", - "2022-01-22", - "2022-01-23", - "2022-01-24", - "2022-01-25", - "2022-01-26", - "2022-01-27", - "2022-01-28", - "2022-01-29", - "2022-01-30", - "2022-01-31", - "2022-02-01", - "2022-02-02" - ] - end - - test "with time:hour dimension" do - assert time_labels(%{ - dimensions: ["time:hour"], - date_range: Date.range(~D[2022-01-17], ~D[2022-01-17]) - }) == [ - "2022-01-17 00:00:00", - "2022-01-17 01:00:00", - "2022-01-17 02:00:00", - "2022-01-17 03:00:00", - "2022-01-17 04:00:00", - "2022-01-17 05:00:00", - "2022-01-17 06:00:00", - "2022-01-17 07:00:00", - "2022-01-17 08:00:00", - "2022-01-17 09:00:00", - "2022-01-17 10:00:00", - "2022-01-17 11:00:00", - "2022-01-17 12:00:00", - "2022-01-17 13:00:00", - "2022-01-17 14:00:00", - "2022-01-17 15:00:00", - "2022-01-17 16:00:00", - "2022-01-17 17:00:00", - "2022-01-17 18:00:00", - "2022-01-17 19:00:00", - "2022-01-17 20:00:00", - "2022-01-17 21:00:00", - "2022-01-17 22:00:00", - "2022-01-17 23:00:00" - ] - - assert time_labels(%{ - dimensions: ["time:hour"], - date_range: Date.range(~D[2022-01-17], ~D[2022-01-18]) - }) == [ - "2022-01-17 00:00:00", - "2022-01-17 01:00:00", - "2022-01-17 02:00:00", - "2022-01-17 03:00:00", - "2022-01-17 04:00:00", - "2022-01-17 05:00:00", - "2022-01-17 06:00:00", - "2022-01-17 07:00:00", - "2022-01-17 08:00:00", - "2022-01-17 09:00:00", - "2022-01-17 10:00:00", - "2022-01-17 11:00:00", - "2022-01-17 12:00:00", - "2022-01-17 13:00:00", - "2022-01-17 14:00:00", - "2022-01-17 15:00:00", - "2022-01-17 16:00:00", - "2022-01-17 17:00:00", - "2022-01-17 18:00:00", - "2022-01-17 19:00:00", - "2022-01-17 20:00:00", - "2022-01-17 21:00:00", - "2022-01-17 22:00:00", - "2022-01-17 23:00:00", - "2022-01-18 00:00:00", - "2022-01-18 01:00:00", - "2022-01-18 02:00:00", - "2022-01-18 03:00:00", - "2022-01-18 04:00:00", - "2022-01-18 05:00:00", - "2022-01-18 06:00:00", - "2022-01-18 07:00:00", - "2022-01-18 08:00:00", - "2022-01-18 09:00:00", - "2022-01-18 10:00:00", - "2022-01-18 11:00:00", - "2022-01-18 12:00:00", - "2022-01-18 13:00:00", - "2022-01-18 14:00:00", - "2022-01-18 15:00:00", - "2022-01-18 16:00:00", - "2022-01-18 17:00:00", - "2022-01-18 18:00:00", - "2022-01-18 19:00:00", - "2022-01-18 20:00:00", - "2022-01-18 21:00:00", - "2022-01-18 22:00:00", - "2022-01-18 23:00:00" - ] - end - end end diff --git a/test/plausible/stats/time_test.exs b/test/plausible/stats/time_test.exs new file mode 100644 index 0000000000000..9bf7a0112b47d --- /dev/null +++ b/test/plausible/stats/time_test.exs @@ -0,0 +1,157 @@ +defmodule Plausible.Stats.IntervalTest do + use Plausible.DataCase, async: true + + import Plausible.Stats.Time + + describe "time_labels/1" do + test "with time:month dimension" do + assert time_labels(%{ + dimensions: ["visit:device", "time:month"], + date_range: Date.range(~D[2022-01-17], ~D[2022-02-01]) + }) == [ + "2022-01-01", + "2022-02-01" + ] + + assert time_labels(%{ + dimensions: ["visit:device", "time:month"], + date_range: Date.range(~D[2022-01-01], ~D[2022-03-07]) + }) == [ + "2022-01-01", + "2022-02-01", + "2022-03-01" + ] + end + + test "with time:week dimension" do + assert time_labels(%{ + dimensions: ["time:week"], + date_range: Date.range(~D[2020-12-20], ~D[2021-01-08]) + }) == [ + "2020-12-20", + "2020-12-21", + "2020-12-28", + "2021-01-04" + ] + + assert time_labels(%{ + dimensions: ["time:week"], + date_range: Date.range(~D[2020-12-21], ~D[2021-01-03]) + }) == [ + "2020-12-21", + "2020-12-28" + ] + end + + test "with time:day dimension" do + assert time_labels(%{ + dimensions: ["time:day"], + date_range: Date.range(~D[2022-01-17], ~D[2022-02-02]) + }) == [ + "2022-01-17", + "2022-01-18", + "2022-01-19", + "2022-01-20", + "2022-01-21", + "2022-01-22", + "2022-01-23", + "2022-01-24", + "2022-01-25", + "2022-01-26", + "2022-01-27", + "2022-01-28", + "2022-01-29", + "2022-01-30", + "2022-01-31", + "2022-02-01", + "2022-02-02" + ] + end + + test "with time:hour dimension" do + assert time_labels(%{ + dimensions: ["time:hour"], + date_range: Date.range(~D[2022-01-17], ~D[2022-01-17]) + }) == [ + "2022-01-17 00:00:00", + "2022-01-17 01:00:00", + "2022-01-17 02:00:00", + "2022-01-17 03:00:00", + "2022-01-17 04:00:00", + "2022-01-17 05:00:00", + "2022-01-17 06:00:00", + "2022-01-17 07:00:00", + "2022-01-17 08:00:00", + "2022-01-17 09:00:00", + "2022-01-17 10:00:00", + "2022-01-17 11:00:00", + "2022-01-17 12:00:00", + "2022-01-17 13:00:00", + "2022-01-17 14:00:00", + "2022-01-17 15:00:00", + "2022-01-17 16:00:00", + "2022-01-17 17:00:00", + "2022-01-17 18:00:00", + "2022-01-17 19:00:00", + "2022-01-17 20:00:00", + "2022-01-17 21:00:00", + "2022-01-17 22:00:00", + "2022-01-17 23:00:00" + ] + + assert time_labels(%{ + dimensions: ["time:hour"], + date_range: Date.range(~D[2022-01-17], ~D[2022-01-18]) + }) == [ + "2022-01-17 00:00:00", + "2022-01-17 01:00:00", + "2022-01-17 02:00:00", + "2022-01-17 03:00:00", + "2022-01-17 04:00:00", + "2022-01-17 05:00:00", + "2022-01-17 06:00:00", + "2022-01-17 07:00:00", + "2022-01-17 08:00:00", + "2022-01-17 09:00:00", + "2022-01-17 10:00:00", + "2022-01-17 11:00:00", + "2022-01-17 12:00:00", + "2022-01-17 13:00:00", + "2022-01-17 14:00:00", + "2022-01-17 15:00:00", + "2022-01-17 16:00:00", + "2022-01-17 17:00:00", + "2022-01-17 18:00:00", + "2022-01-17 19:00:00", + "2022-01-17 20:00:00", + "2022-01-17 21:00:00", + "2022-01-17 22:00:00", + "2022-01-17 23:00:00", + "2022-01-18 00:00:00", + "2022-01-18 01:00:00", + "2022-01-18 02:00:00", + "2022-01-18 03:00:00", + "2022-01-18 04:00:00", + "2022-01-18 05:00:00", + "2022-01-18 06:00:00", + "2022-01-18 07:00:00", + "2022-01-18 08:00:00", + "2022-01-18 09:00:00", + "2022-01-18 10:00:00", + "2022-01-18 11:00:00", + "2022-01-18 12:00:00", + "2022-01-18 13:00:00", + "2022-01-18 14:00:00", + "2022-01-18 15:00:00", + "2022-01-18 16:00:00", + "2022-01-18 17:00:00", + "2022-01-18 18:00:00", + "2022-01-18 19:00:00", + "2022-01-18 20:00:00", + "2022-01-18 21:00:00", + "2022-01-18 22:00:00", + "2022-01-18 23:00:00" + ] + end + end +end