I have a record that contains :naive_datetime field
I want to save he value in GMT timezone but display it in Asia/Jerusalem timezone
so, I created the type like this
defmodule Jot.DatetimeType do
@moduledoc false
use Ecto.Type
def type, do: :naive_datetime
def load(tim) do
# Convert GMT time to local time
tim = DateTime.from_naive!(tim, "GMT")
|> Timex.Timezone.convert("Asia/Jerusalem")
{:ok, tim}
end
def dump(:naive_datetime = datetime) do
{:ok, Timex.parse!(datetime, "{YYYY}-{0M}-{0M} {h24}:{0m}")}
end
def dump(_), do: :error
def cast(datetime) when is_binary(datetime) do
{:ok, Timex.parse!(datetime, "{YYYY}-{0M}-{0M} {h24}:{0m}")}
end
# This is the problematic function
#=============================
def cast(datetime) do
datetime = DateTime.from_naive!(datetime, "Asia/Jerusalem")
|> Timex.Timezone.convert("GMT")
{:ok, DateTime.to_naive(datetime)}
end
end
When I want to update the record like this:
Presence.update_entry_tran(t, %{punch_date: d})
I am getting an error
(Ecto.ChangeError) value `~N[2021-10-19 15:02:00]` for `Jot.Presence.EntryTran.punch_date` in `update` does not match type Jot.DatetimeType
I’d probably only operate on UTC datetimes in the model layer and move timezone logic to the view layer.
As for the timezone logic itself, you can transform a naive datetime to a datetime with timezone with DateTime.from_naive and optional DateTime.shift_zone.
Also you probably have a typo in the dump function, def dump(:naive_datetime = datetime) do would never match a naive date time struct, and if you have a datetime struct coming into a dump/1 function, you don’t need to modify it in any way since postgrex knows how to encode it.
defmodule Jot.DatetimeType do
@moduledoc false
use Ecto.Type
@impl true
def type, do: :naive_datetime
@impl true
def cast(datetime) when is_binary(datetime) do
# if Timex.parse! fails to parse a raw datetime, it would raise an exception and crash the process
# instead, a non-raising version should be used for the failed cast to be turned into an error in a changeset
with {:error, _reason} <- Timex.parse(datetime, "{YYYY}-{0M}-{0M} {h24}:{0m}") do
:error
end
end
def cast(%NaiveDateTime{} = naive) do
# you can do timezone shifting here as well
{:ok, naive}
end
def cast(%DateTime{} = datetime) do
{:ok, datetime}
end
@impl true
def load(%NaiveDateTime{} = naive) do
jerusalem =
naive
|> DateTime.from_naive!("Etc/UTC")
|> DateTime.shift_zone!("Asia/Jerusalem")
{:ok, jerusalem}
end
@impl true
def dump(%NaiveDateTime{} = naive) do
{:ok, naive}
end
def dump(%DateTime{} = jerusalem) do
naive =
jerusalem
|> DateTime.shift_zone!("Etc/UTC")
|> DateTime.to_naive()
{:ok, naive}
end
end