Error when creating Ecto User Type

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

Any idea?

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.

I don’t want to move the logic to the view. I know that I will use it in more then 1 view, and I want it to convert automatically

Try this then:

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
2 Likes

Thanks. Its working now