From 8d2846945acdba9206305d3ec79d763fa912414b Mon Sep 17 00:00:00 2001 From: Frank Hunleth Date: Wed, 10 Mar 2021 15:13:13 -0500 Subject: [PATCH] Add support for returning metadata about time zones If a system's time zone database has limited information, this makes it easier to debug. --- lib/zoneinfo.ex | 13 +++++++++++ lib/zoneinfo/cache.ex | 10 ++++++++ lib/zoneinfo/meta.ex | 46 +++++++++++++++++++++++++++++++++++++ test/zoneinfo/meta_test.exs | 25 ++++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 lib/zoneinfo/meta.ex create mode 100644 test/zoneinfo/meta_test.exs diff --git a/lib/zoneinfo.ex b/lib/zoneinfo.ex index 1a542b5..c0f9733 100644 --- a/lib/zoneinfo.ex +++ b/lib/zoneinfo.ex @@ -66,6 +66,19 @@ defmodule Zoneinfo do end end + @doc """ + Return Zoneinfo metadata on a time zone + + The returned metadata is limited to what's available in the source TZif data + file for the time zone. It's mostly useful for verifying that time zone + information is available for dates used in your application. Note that proper + time zone calculations depend on many things and it's possible that they'll + work outside of the returned ranged. However, it's also possible that a time + zone database was built and then a law changed which invalidates a record. + """ + @spec get_metadata(String.t()) :: {:ok, Zoneinfo.Meta.t()} | {:error, atom()} + defdelegate get_metadata(time_zone), to: Zoneinfo.Cache, as: :meta + defp contains_tzif?(path) do case File.open(path, [:read], &contains_tzif_helper/1) do {:ok, result} -> result diff --git a/lib/zoneinfo/cache.ex b/lib/zoneinfo/cache.ex index 194a83b..3fa3f1f 100644 --- a/lib/zoneinfo/cache.ex +++ b/lib/zoneinfo/cache.ex @@ -29,6 +29,16 @@ defmodule Zoneinfo.Cache do load_time_zone(time_zone) end + @doc """ + Return Zoneinfo metadata on a time zone + """ + @spec meta(String.t()) :: {:ok, Zoneinfo.Meta.t()} | {:error, atom()} + def meta(time_zone) do + with {:ok, tzif} <- get(time_zone) do + {:ok, Zoneinfo.Meta.to_meta(time_zone, tzif)} + end + end + @impl GenServer def init(_args) do @table = :ets.new(@table, [:set, :protected, :named_table]) diff --git a/lib/zoneinfo/meta.ex b/lib/zoneinfo/meta.ex new file mode 100644 index 0000000..6b39d66 --- /dev/null +++ b/lib/zoneinfo/meta.ex @@ -0,0 +1,46 @@ +defmodule Zoneinfo.Meta do + alias Zoneinfo.TZif + + @moduledoc """ + Metadata derived from TZif information + + The metadata here is mostly useful for checking the quality of the TZif files that + were loaded. + """ + defstruct [:time_zone, :tz_string, :earliest_record_utc, :latest_record_utc, :record_count] + + @typedoc """ + Zoneinfo.Meta contains information about one time zone + + * `:time_zone` - the name of the time zone + * `:tz_string` - if a POSIX TZ string is available, this is it + * `:earliest_record_utc` - the UTC time of the earliest time zone record + * `:latest_record_utc` - the UTC time of the latest time zone record + * `:record_count` -- the number of records + """ + @type t() :: %__MODULE__{ + time_zone: String.t(), + tz_string: String.t() | nil, + earliest_record_utc: NaiveDateTime.t(), + latest_record_utc: NaiveDateTime.t(), + record_count: non_neg_integer() + } + + @doc false + @spec to_meta(String.t(), TZif.t()) :: t() + def to_meta(time_zone, tzif) do + %__MODULE__{ + time_zone: time_zone, + tz_string: tzif.tz_string, + earliest_record_utc: ndt(Enum.at(tzif.periods, -2)), + latest_record_utc: ndt(List.first(tzif.periods)), + # The last record is the default for times before the first known one, so + # it doesn't really count + record_count: length(tzif.periods) + } + end + + defp ndt({gregorian_seconds, _utc_offset, _std_offset, _zone_abbr}) do + Zoneinfo.Utils.gregorian_seconds_to_naive_datetime(gregorian_seconds) + end +end diff --git a/test/zoneinfo/meta_test.exs b/test/zoneinfo/meta_test.exs new file mode 100644 index 0000000..110bf0d --- /dev/null +++ b/test/zoneinfo/meta_test.exs @@ -0,0 +1,25 @@ +defmodule Zoneinfo.MetaTest do + use ExUnit.Case + + alias Zoneinfo.Meta + + @fixture_path Path.join(__DIR__, "../fixture") + + defp parse_file(name) do + Path.join(@fixture_path, name) + |> File.read!() + |> Zoneinfo.TZif.parse() + end + + test "to_meta/2" do + {:ok, tzif} = parse_file("Honolulu_v2") + meta = Meta.to_meta("America/Honolulu", tzif) + + assert meta.time_zone == "America/Honolulu" + assert meta.tz_string == "HST10" + assert meta.earliest_record_utc == ~N[1896-01-13 22:31:26] + # Yes, this looks strange. The file was shortened on purpose + assert meta.latest_record_utc == ~N[1947-06-08 12:30:00] + assert meta.record_count == 8 + end +end