Current section

Files

Jump to
exosql lib datetime.ex
Raw

lib/datetime.ex

require Logger
defmodule ExoSQL.DateTime do
@moduledoc """
Helpers for datetime
"""
def strftime(dt, format) do
dt = to_datetime(dt)
# Simplifications, %i from sqlite is supported
format =
format
|> String.replace("%i", "%FT%T%z")
Timex.format!(dt, format, :strftime)
|> String.replace("+0000", "Z")
end
def to_datetime(n) when is_number(n) do
{:ok, dt} = DateTime.from_unix(n)
dt
end
def to_datetime(n) when is_binary(n) do
if String.contains?(n, "-") do
case String.length(n) do
10 ->
n = "#{n} 00:00:00Z"
{:ok, td, 0} = DateTime.from_iso8601(n)
td
16 ->
n = "#{n}:00Z"
{:ok, td, 0} = DateTime.from_iso8601(n)
td
19 ->
{:ok, td, 0} = DateTime.from_iso8601(n <> "Z")
td
20 ->
{:ok, td, 0} = DateTime.from_iso8601(n)
td
24 ->
{:ok, td, _} = DateTime.from_iso8601(n)
td
25 ->
{:ok, td, _} = DateTime.from_iso8601(n)
td
end
else
{:ok, unixtime} = ExoSQL.Utils.to_number(n)
# Logger.debug("To datetime #{inspect unixtime}")
to_datetime(unixtime)
end
end
def to_datetime(%DateTime{} = d), do: d
def to_datetime(other) do
raise ArgumentError, message: "cant convert #{inspect(other)} to date"
end
def to_datetime(dt, "-" <> _mod = orig) do
dt = to_datetime(dt)
ExoSQL.DateTime.Duration.datetime_add(dt, orig)
end
def to_datetime(dt, "+" <> _mod = orig) do
dt = to_datetime(dt)
ExoSQL.DateTime.Duration.datetime_add(dt, orig)
end
def to_datetime(dt, tz) do
dt = to_datetime(dt)
Timex.to_datetime(dt, tz)
end
@units %{
"minutes" => 60,
"hours" => 60 * 60,
"days" => 24 * 60 * 60,
"weeks" => 7 * 24 * 60 * 60
}
def datediff({:range, {start, end_}}), do: datediff(start, end_, "days")
def datediff({:range, {start, end_}}, units), do: datediff(start, end_, units)
def datediff(start, end_), do: datediff(start, end_, "days")
def datediff(%DateTime{} = start, %DateTime{} = end_, "years") do
if start > end_ do
-datediff(end_, start, :years)
else
years = end_.year - start.year
if end_.month < start.month do
years - 1
else
years
end
end
end
def datediff(%DateTime{} = start, %DateTime{} = end_, "months") do
if start > end_ do
-datediff(end_, start, :months)
else
years = datediff(start, end_, "years")
months =
if start.month < end_.month do
end_.month - start.month
else
end_.month + 12 - start.month
end
years * 12 + months
end
end
def datediff(%DateTime{} = start, %DateTime{} = end_, "seconds") do
DateTime.diff(end_, start, :second)
end
def datediff(%DateTime{} = start, %DateTime{} = end_, unit) do
div(DateTime.diff(end_, start, :second), Map.get(@units, unit))
end
def datediff(start, end_, unit) do
start = to_datetime(start)
end_ = to_datetime(end_)
datediff(start, end_, unit)
end
end
defmodule ExoSQL.DateTime.Duration do
@moduledoc """
Parses and manipulates 8601 durations
https://en.wikipedia.org/wiki/ISO_8601#Durations
"""
defstruct seconds: 0, days: 0, months: 0, years: 0
def parse(<<?P, rest::binary>>) do
parse(rest)
end
def parse(<<?+, rest::binary>>) do
parse(rest)
end
def parse(<<?-, rest::binary>>) do
case parse(rest) do
{:ok, res} ->
{:ok,
%ExoSQL.DateTime.Duration{
seconds: -res.seconds,
days: -res.days,
months: -res.months,
years: -res.years
}}
other ->
other
end
end
# no need for P
def parse(rest) do
case parse_split(rest) do
{:ok, parsed} ->
{:ok, parse_date(parsed, %ExoSQL.DateTime.Duration{})}
other ->
other
end
end
def parse!(str) do
case parse(str) do
{:ok, ret} -> ret
other -> throw(other)
end
end
defp parse_split(str) do
case Regex.run(~r/^(\d+[YMWDHS]|T)+$/, str) do
nil ->
{:error, :invalid_duration}
_other ->
ret =
Regex.scan(~r/(\d+)([YMWDHS])|(T)/, str)
|> Enum.map(fn
[_, a, b] ->
{n, ""} = Integer.parse(a)
{n, b}
["T" | _rest] ->
:t
end)
{:ok, ret}
end
end
defp parse_date([], acc), do: acc
defp parse_date([{n, "Y"} | rest], acc),
do: parse_date(rest, Map.update(acc, :years, n, &(&1 + n)))
defp parse_date([{n, "M"} | rest], acc),
do: parse_date(rest, Map.update(acc, :months, n, &(&1 + n)))
defp parse_date([{n, "W"} | rest], acc),
do: parse_date(rest, Map.update(acc, :days, n, &(&1 + n * 7)))
defp parse_date([{n, "D"} | rest], acc),
do: parse_date(rest, Map.update(acc, :days, n, &(&1 + n)))
defp parse_date([:t | rest], acc), do: parse_time(rest, acc)
defp parse_time([], acc), do: acc
defp parse_time([{n, "H"} | rest], acc),
do: parse_time(rest, Map.update(acc, :seconds, n * 60 * 60, &(&1 + n * 60 * 60)))
defp parse_time([{n, "M"} | rest], acc),
do: parse_time(rest, Map.update(acc, :seconds, n * 60, &(&1 + n * 60)))
defp parse_time([{n, "S"} | rest], acc),
do: parse_time(rest, Map.update(acc, :seconds, n, &(&1 + n)))
def is_negative(%ExoSQL.DateTime.Duration{
seconds: seconds,
days: days,
months: months,
years: years
}) do
seconds < 0 or days < 0 or months < 0 or years < 0
end
def datetime_add(date, duration) when is_binary(duration) do
datetime_add(date, parse!(duration))
end
def datetime_add(date, %ExoSQL.DateTime.Duration{} = duration) do
date =
case duration.seconds do
0 ->
date
seconds ->
Timex.shift(date, seconds: seconds)
end
date =
case duration.days do
0 ->
date
days ->
Timex.shift(date, days: days)
end
date =
case duration.months do
0 ->
date
months ->
Timex.shift(date, months: months)
end
date =
case duration.years do
0 ->
date
years ->
Timex.shift(date, years: years)
end
date
end
end