1defmodule Timex.Convert do
2  @moduledoc false
3
4  @doc """
5  Converts a map to a Date, NaiveDateTime or DateTime, depending on the amount
6  of date/time information in the map.
7  """
8  @spec convert_map(map) :: Date.t | DateTime.t | NaiveDateTime.t | {:error, term}
9  def convert_map(%{__struct__: _} = struct) do
10    convert_map(Map.from_struct(struct))
11  end
12  def convert_map(map) when is_map(map) do
13    case convert_keys(map) do
14      {:error, _} = err ->
15        err
16      datetime_map when is_map(datetime_map) ->
17        year  = Map.get(datetime_map, :year)
18        month = Map.get(datetime_map, :month)
19        day   = Map.get(datetime_map, :day)
20        cond do
21          not(is_nil(year)) and not(is_nil(month)) and not(is_nil(day)) ->
22            case Map.get(datetime_map, :hour) do
23              nil ->
24                with {:ok, date} <- Date.new(year, month, day), do: date
25              hour ->
26                minute = Map.get(datetime_map, :minute, 0)
27                second = Map.get(datetime_map, :second, 0)
28                us     = Map.get(datetime_map, :microsecond, {0, 0})
29                tz     = Map.get(datetime_map, :time_zone, nil)
30                case tz do
31                  s when is_binary(s) ->
32                    Timex.DateTime.Helpers.construct({{year,month,day},{hour,minute,second,us}}, tz)
33                  nil ->
34                    {:ok, nd} = NaiveDateTime.new(year, month, day, hour, minute, second, us)
35                    nd
36                end
37            end
38          :else ->
39            {:error, :insufficient_date_information}
40        end
41    end
42  end
43  def try_convert(_), do: {:error, :invalid_date}
44
45  @allowed_keys_atom [
46    :year, :month, :day,
47    :hour, :minute, :min, :mins, :second, :sec, :secs,
48    :milliseconds, :millisecond, :ms,
49    :microsecond
50  ]
51  @allowed_keys Enum.concat(@allowed_keys_atom, Enum.map(@allowed_keys_atom, &Atom.to_string/1))
52  @valid_keys_map %{
53    :min          => :minute,
54    :mins         => :minute,
55    :secs         => :second,
56    :sec          => :second,
57    :milliseconds => :millisecond,
58    :ms           => :millisecond,
59    :microsecond  => :microsecond,
60    :tz           => :time_zone,
61    :timezone     => :time_zone,
62    :time_zone    => :time_zone
63  }
64
65  def convert_keys(map) when is_map(map) do
66    Enum.reduce(map, %{}, fn
67      {_, _}, {:error, _} = err -> err
68      {k, v}, acc when k in [:microsecond, "microsecond"] ->
69        case v do
70          {us, pr} when is_integer(us) and pr >= 0 and pr <= 6 ->
71            Map.put(acc, :microsecond, {us, pr})
72          us when is_integer(us) ->
73            Map.put(acc, :microsecond, {us, 6})
74          _ -> acc
75        end
76      {k, v}, acc when k in [:milliseconds, "milliseconds", :ms, "ms", :millisecond, "millisecond"] ->
77        case v do
78          n when is_integer(n) ->
79            us = Timex.DateTime.Helpers.construct_microseconds(n*1_000)
80            Map.put(acc, :microsecond, us)
81          :error ->
82            {:error, {:expected_integer, for: k, got: v}}
83        end
84      {k, v}, acc when k in [:tz, "tz", :timezone, "timezone", :time_zone, "time_zone"] ->
85        case v do
86          s when is_binary(s) -> Map.put(acc, :time_zone, s)
87          %{"full_name" => s} -> Map.put(acc, :time_zone, s)
88          _ -> acc
89        end
90      {k, v}, acc when k in @allowed_keys and is_integer(v) ->
91        Map.put(acc, get_valid_key(k), v)
92      {k, v}, acc when k in @allowed_keys and is_binary(v) ->
93        case Integer.parse(v) do
94          {n, _} ->
95            Map.put(acc, get_valid_key(k), n)
96          :error ->
97            {:error, {:expected_integer, for: k, got: v}}
98        end
99      {_, _}, acc -> acc
100    end)
101  end
102
103  defp get_valid_key(key) when is_atom(key),
104    do: Map.get(@valid_keys_map, key, key)
105
106  defp get_valid_key(key),
107    do: key |> String.to_atom() |> get_valid_key()
108end
109