Skip to content

Latest commit

 

History

History
181 lines (147 loc) · 4.73 KB

File metadata and controls

181 lines (147 loc) · 4.73 KB

TZ

Mix.install([
  {:ecto_sql, "~> 3.12"},
  {:ecto_sqlite3, "~> 0.18.1"},
  {:tz, "~> 0.28.1"}
])

Section

defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :my_app, adapter: Ecto.Adapters.SQLite3
end

defmodule MyApp.Repo.Migrations.Initial do
  use Ecto.Migration

  def change do
    create table(:events_v1) do
      add(:name, :string, null: false)
      add(:start_datetime, :utc_datetime, null: false)
    end

     create table(:events_v2) do
      add(:name, :string, null: false)
      add(:start_datetime, :utc_datetime, null: false)
      add(:start_datetime_time_zone, :string, null: false)
    end
    
     create table(:events_v3) do
      add(:name, :string, null: false)
      add(:start_datetime, :naive_datetime, null: false)
      add(:start_datetime_time_zone, :string, null: false)
      add(:start_datetime_utc, :utc_datetime, null: false)       
    end    
  end
end

db_config = [database: ":memory:", pool_size: 1]
MyApp.Repo.start_link(db_config)
Ecto.Migrator.up(MyApp.Repo, 1, MyApp.Repo.Migrations.Initial)
Calendar.put_time_zone_database(Tz.TimeZoneDatabase)

The first Attemp: TIMESTAMP WITH TIME ZONE

defmodule EventV1 do
  use Ecto.Schema
  import Ecto.Changeset

  schema "events_v1" do
    field(:name, :string)
    field(:start_datetime, :utc_datetime)
  end

  def changeset(product, params \\ %{}) do
    product
    |> cast(params, [:name, :start_datetime])
    |> validate_required([:name, :start_datetime])
  end


  @doc """
      iex> expected_time_zone = "Europe/Berlin"
      iex> event_date = DateTime.shift_zone!(~U[2025-02-07 19:00:00.0Z], expected_time_zone)
      iex> event_date.time_zone
      "Europe/Berlin"
      iex> event = EventV1.create!(%{name: "Elixir Berlin Feb 2025", start_datetime: event_date})
      iex> assert event.start_datetime.time_zone == expected_time_zone
  """
  def create!(attrs) do
    %EventV1{}
    |> EventV1.changeset(attrs)
    |> MyApp.Repo.insert!()
  end
end

The Second Attempt: Storing the Time Zone Separately

defmodule EventV2 do
  use Ecto.Schema
  import Ecto.Changeset

  schema "events_v2" do
    field(:name, :string)
    field(:start_datetime, :utc_datetime)
    field(:start_datetime_time_zone, :string)
  end

  def changeset(product, params \\ %{}) do
    product
    |> cast(params, [:name, :start_datetime, :start_datetime_time_zone])
    |> validate_required([:name, :start_datetime, :start_datetime_time_zone])
  end


  @doc """
      iex> expected_time_zone = "Europe/Berlin"
      iex> event_date = DateTime.shift_zone!(~U[2025-02-07 19:00:00.0Z], expected_time_zone)
      iex> event_date.time_zone
      "Europe/Berlin"
      iex> event = EventV2.create!(%{name: "Elixir Berlin Feb 2025", start_datetime: event_date, start_datetime_time_zone: event_date.time_zone})
      iex> assert event.start_datetime.time_zone == expected_time_zone
  """

  def create!(attrs) do
    %EventV2{}
    |> EventV2.changeset(attrs)
    |> MyApp.Repo.insert!()
    |> load_start_datetime()
  end

  def get!(id) do
    id
    |> MyApp.Repo.get!()
    |> load_start_datetime()
  end

  defp load_start_datetime(event) do
    %{event | start_datetime: DateTime.shift_zone!(event.start_datetime, event.start_datetime_time_zone)}
  end
end

The Final Approach: The “Wall Clock” Timestamp

defmodule EventV3 do
  use Ecto.Schema
  import Ecto.Changeset

  schema "events_v3" do
    field(:name, :string)
    field(:start_datetime, :naive_datetime)
    field(:start_datetime_time_zone, :string)
    field(:start_datetime_utc, :utc_datetime)
  end

  def changeset(product, params \\ %{}) do
    product
    |> cast(params, [:name, :start_datetime, :start_datetime_time_zone])
    |> validate_required([:name, :start_datetime, :start_datetime_time_zone])
    |> maybe_cast_utc_datetime()
  end

  defp maybe_cast_utc_datetime(%{valid?: true} = changeset) do
    start_datetime = get_change(changeset, :start_datetime)
    start_datetime_time_zone = get_change(changeset, :start_datetime_time_zone)
    datetime_utc = 
      DateTime.shift_zone!(DateTime.from_naive!(start_datetime, start_datetime_time_zone), "Etc/UTC")
     Ecto.Changeset.change(changeset, %{start_datetime_utc: datetime_utc})
  end

  defp maybe_cast_utc_datetime(changeset) do
    changeset
  end


  @doc """
      iex> expected_time_zone = "Europe/Berlin"
      iex> event_date = ~N[2025-02-07 19:00:00]
      iex> event = EventV3.create!(%{name: "Elixir Berlin Feb 2025", start_datetime: event_date, start_datetime_time_zone: expected_time_zone})
      iex> assert event.start_datetime == event_date
      iex> assert event.start_datetime_utc == ~U[2025-02-07 18:00:00Z]
  """

  def create!(attrs) do
    %EventV3{}
    |> EventV3.changeset(attrs)
    |> MyApp.Repo.insert!()
  end
end