Packages
timex
3.1.15
3.7.13
3.7.12
3.7.11
3.7.9
3.7.8
3.7.7
3.7.6
3.7.5
3.7.3
3.7.2
3.7.1
3.7.0
3.6.4
3.6.3
3.6.2
retired
3.6.1
3.6.0
3.5.0
3.4.2
3.4.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.25
retired
3.1.24
3.1.23
3.1.22
3.1.21
3.1.20
3.1.19
3.1.18
3.1.17
3.1.16
3.1.15
3.1.13
3.1.12
3.1.11
3.1.10
3.1.9
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.2.1
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.0
1.0.2
1.0.1
1.0.0
1.0.0-rc4
1.0.0-rc3
1.0.0-rc2
1.0.0-rc1
1.0.0-pre
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.2
0.18.1
0.18.0
0.17.0
0.16.2
0.16.1
0.16.0
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.9
0.12.8
0.12.7
0.12.6
0.12.5
0.12.4
0.12.3
0.12.2
0.12.1
0.12.0
0.11.0
0.10.2
0.10.1
0.10.0
0.9.0
0.8.0
0.7.1
0.6.0
0.5.0
0.4.8
0.4.7
0.4.6
Timex is a rich, comprehensive Date/Time library for Elixir projects, with full timezone support via the :tzdata package. If you need to manipulate dates, times, datetimes, timestamps, etc., then Timex is for you!
Current section
Files
Jump to
Current section
Files
lib/timezone/timezone_local.ex
defmodule Timex.Timezone.Local do
@moduledoc """
This module is responsible for determining the timezone configuration of the
local machine. It determines this from a number of sources, depending on platform,
but the order of precedence is as follows:
ALL:
- TZ environment variable. Ignored if nil/empty
OSX:
- /etc/localtime
- systemsetup -gettimezone (if admin rights are present)
UNIX:
- /etc/timezone
- /etc/sysconfig/clock
- /etc/conf.d/clock
- /etc/localtime
- /usr/local/etc/localtime
Windows:
- SYSTEM registry for the currently configured TimeZoneInformation
Each location is tried, and if an error is encountered, the next is attempted,
until either a successful lookup is performed, or we run out of locations to check.
"""
alias Timex.Timezone.Utils
alias Timex.Parse.ZoneInfo.Parser
alias Timex.Parse.ZoneInfo.Parser.{TransitionInfo, Zone}
@_ETC_TIMEZONE "/etc/timezone"
@_ETC_SYS_CLOCK "/etc/sysconfig/clock"
@_ETC_CONF_CLOCK "/etc/conf.d/clock"
@_ETC_LOCALTIME "/etc/localtime"
@_USR_ETC_LOCALTIME "/usr/local/etc/localtime"
@type gregorian_seconds :: non_neg_integer
@epoch_seconds :calendar.datetime_to_gregorian_seconds({{1970,1,1},{0,0,0}})
@doc """
Looks up the local timezone configuration. Returns the name of a timezone
in the Olson database.
If no reference time is provided (in gregorian seconds), the current time in UTC will be used.
If one is provided, the reference time will be used to find the local timezone for that reference time,
if it exists.
"""
@spec lookup() :: String.t | {:error, term}
@spec lookup(gregorian_seconds) :: String.t | {:error, term}
def lookup(), do: lookup(:calendar.datetime_to_gregorian_seconds(:calendar.universal_time()))
def lookup(secs) when is_integer(secs) and secs > 0 do
case Application.get_env(:timex, :local_timezone) do
nil ->
tz = case :os.type() do
{:unix, :darwin} -> localtz(:osx, secs)
{:unix, _} -> localtz(:unix, secs)
{:win32, :nt} -> localtz(:win, secs)
_ -> {:error, {:localtz, :unsupported_operating_system}}
end
Application.put_env(:timex, :local_timezone, tz)
tz
tz -> tz
end
end
# Get the locally configured timezone on OSX systems
@spec localtz(:osx | :unix | :win, gregorian_seconds) :: String.t | no_return
defp localtz(:osx, date) do
# Allow TZ environment variable to override lookup
case System.get_env("TZ") do
nil ->
# Most accurate local timezone will come from /etc/localtime,
# since we can lookup proper timezones for arbitrary dates
case read_timezone_data(nil, @_ETC_LOCALTIME, date) do
{:ok, tz} -> tz
_ ->
# Fallback and ask systemsetup
{tz, 0} = System.cmd("systemsetup", ["-gettimezone"])
tz = tz
|> String.strip(?\n)
|> String.replace("Time Zone: ", "")
if String.length(tz) > 0 do
tz
else
raise("Unable to find local timezone.")
end
end
tz -> tz
end
end
# Get the locally configured timezone on *NIX systems
defp localtz(:unix, date) do
case System.get_env("TZ") do
# Not found
nil ->
# Since that failed, check distro specific config files
# containing the timezone name. To clean up the code here
# we're using pipes, even though we may find the value we
# are looking for on the first try. The way the function
# defs are set up, if we find a value, it's just passed
# along through the pipe until we're done. If we don't,
# this will try each fallback location in order.
{:ok, tz} = read_timezone_data(@_ETC_TIMEZONE, date)
|> read_timezone_data(@_ETC_SYS_CLOCK, date)
|> read_timezone_data(@_ETC_CONF_CLOCK, date)
|> read_timezone_data(@_ETC_LOCALTIME, date)
|> read_timezone_data(@_USR_ETC_LOCALTIME, date)
tz
tz -> tz
end
end
# Get the locally configured timezone on Windows systems
@local_tz_key 'SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation'
@sys_tz_key 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones'
@tz_key_name 'TimeZoneKeyName'
# We ignore the reference date here, since there is no way to lookup
# transition times for historical/future dates
defp localtz(:win, _date) do
# Windows has many of its own unique time zone names, which can
# also be translated to the OS's language.
{:ok, handle} = :win32reg.open([:read])
:ok = :win32reg.change_key(handle, '\\local_machine\\#{@local_tz_key}')
{:ok, values} = :win32reg.values(handle)
if List.keymember?(values, @tz_key_name, 0) do
#Extract the time zone name that windows has recorded
{@tz_key_name,time_zone_name} = List.keyfind(values, @tz_key_name, 0)
# Windows 7/Vista
# On some systems the string value might be padded with excessive \0 bytes, trim them
time_zone_name
|> Enum.take_while(fn ?\0 -> false; _ -> true end)
|> IO.iodata_to_binary
|> Utils.to_olson
else
# Windows 2000 or XP
# This is the localized name:
localized = List.keyfind(values, 'StandardName', 0)
# Open the list of timezones to look up the real name:
:ok = :win32reg.change_key(handle, @sys_tz_key)
{:ok, subkeys} = :win32reg.sub_keys(handle)
# Iterate over each subkey (timezone), and match against the localized name
tzone = Enum.find subkeys, fn subkey ->
:ok = :win32reg.change_key(handle, subkey)
{:ok, values} = :win32reg.values(handle)
case List.keyfind(values, 'Std', 0) do
{_, zone} when zone == localized -> zone
_ -> nil
end
end
# If we don't have a timezone yet, we've failed,
# Otherwise, we need to lookup the final timezone name
# in the dictionary of unique Windows timezone names
cond do
tzone == nil -> raise "Could not find Windows time zone configuration!"
tzone ->
timezone = tzone |> IO.iodata_to_binary
case Utils.to_olson(timezone) do
nil ->
# Try appending "Standard Time"
case Utils.to_olson("#{timezone} Standard Time") do
nil -> raise "Could not find Windows time zone configuration!"
final -> final
end
final -> final
end
end
end
end
# Attempt to read timezone data from /etc/timezone
@spec read_timezone_data({:ok, String.t} | nil, String.t, gregorian_seconds) ::
{:ok, String.t} | nil | no_return
defp read_timezone_data(result \\ nil, file, date)
# If we've found a timezone, just keep on piping it through
defp read_timezone_data({:ok, _} = result, _, _),
do: result
# Otherwise, read the next fallback location
defp read_timezone_data(_, @_ETC_TIMEZONE, date) do
case File.read(@_ETC_TIMEZONE) do
{:ok, etctz} ->
cond do
String.starts_with?(etctz, "TZif2") ->
case parse_tzfile(etctz, date) do
{:error, m} -> raise m
{:ok, _} = res -> res
end
true ->
[no_hostdefs | _] = String.split(etctz, " ", [global: false, trim: true])
[no_comments | _] = String.split(no_hostdefs, "#", [global: false, trim: true])
{:ok, no_comments |> String.replace(" ", "_") |> String.strip(?\n)}
end
{:error, _} ->
nil
end
end
defp read_timezone_data(_, file, _date) when file == @_ETC_SYS_CLOCK or file == @_ETC_CONF_CLOCK do
case File.exists?(file) do
true ->
match = file
|> File.stream!
|> Stream.filter(fn line -> Regex.match?(~r/(^ZONE=)|(^TIMEZONE=)/, line) end)
|> Enum.to_list
|> List.first
case match do
nil -> nil
m ->
[_, tz, _] = String.split(m, "\"")
{:ok, String.replace(tz, " ", "_")}
end
_ ->
nil
end
end
defp read_timezone_data(_, file, date) when file == @_ETC_LOCALTIME or file == @_USR_ETC_LOCALTIME do
case File.read(file) do
{:ok, contents} ->
case parse_tzfile(contents, date) do
{:ok, tz} ->
# We have a valid timezone, so get symlinked zone name, since `tz` here is an abbreviation
zone_file = file |> get_real_path |> String.replace(~r(^.*/zoneinfo/), "")
cond do
zone_file == "" -> {:ok, tz}
true -> {:ok, zone_file}
end
{:error, err} ->
raise err
end
{:error, _} ->
nil
end
end
@spec get_real_path(String.t) :: String.t
defp get_real_path(path) do
case path |> String.to_charlist |> :file.read_link_info do
{:ok, {:file_info, _, :regular, _, _, _, _, _, _, _, _, _, _, _}} ->
path
{:ok, {:file_info, _, :symlink, _, _, _, _, _, _, _, _, _, _, _}} ->
{:ok, sym} = path |> String.to_charlist |> :file.read_link
case sym |> :filename.pathtype do
:absolute ->
sym |> IO.iodata_to_binary
:relative ->
symlink = sym |> IO.iodata_to_binary
path |> Path.dirname |> Path.join(symlink) |> Path.expand
end
end
end
@doc """
Given a binary representing the data from a tzfile (not the source version),
parses out the timezone for the curent date/time in UTC.
"""
@spec parse_tzfile(binary) :: {:ok, String.t} | {:error, term}
def parse_tzfile(tzdata),
do: parse_tzfile(tzdata, :calendar.datetime_to_gregorian_seconds(:calendar.universal_time()) - @epoch_seconds)
@doc """
Same as `parse_tzfile/1`, but takes a reference date (in gregorian seconds). The reference
date will be used to locate the timezone period for the local timezone which applies to that date.
"""
@spec parse_tzfile(binary, gregorian_seconds) :: {:ok, String.t} | {:error, term}
def parse_tzfile(tzdata, reference_date) when tzdata != nil and is_integer(reference_date) do
reference_date = reference_date - @epoch_seconds
# Parse file to Zone{}
{:ok, %Zone{transitions: transitions}} = Parser.parse(tzdata)
# Get the zone for the current time
transition = transitions
|> Enum.sort(fn %TransitionInfo{starts_at: utime1}, %TransitionInfo{starts_at: utime2} -> utime1 > utime2 end)
|> Enum.reject(fn %TransitionInfo{starts_at: unix_time} -> unix_time > reference_date end)
|> List.first
# We'll need these handy
# Attempt to get the proper timezone for the current transition we're in
cond do
# Success
transition != nil -> {:ok, transition.abbreviation}
# Fallback to the first standard-time zone available
true ->
fallback = transitions
|> Enum.filter(fn zone -> zone.is_std? end)
|> List.last
case fallback do
# Well, there are no standard-time zones then, just take the first zone available
nil ->
case transitions |> List.last do
nil -> {:error, "Unable to locate the current timezone!"}
last_transition -> {:ok, last_transition.abbreviation}
end
# Found a reasonable fallback zone, success?
%TransitionInfo{abbreviation: abbreviation} ->
{:ok, abbreviation}
end
end
end
end