2014-03-07 10:08:12 +04:00
|
|
|
%% @author Dmitry S. Melnikov (dmitryme@gmail.com)
|
|
|
|
%% @copyright 2010 Dmitry S. Melnikov
|
|
|
|
|
2010-08-23 16:17:16 +04:00
|
|
|
-module(localtime).
|
|
|
|
|
|
|
|
-author("Dmitry Melnikov <dmitryme@gmail.com>").
|
|
|
|
|
2010-12-18 21:07:15 +03:00
|
|
|
-include("tz_database.hrl").
|
|
|
|
-include("tz_index.hrl").
|
2010-08-23 16:17:16 +04:00
|
|
|
|
|
|
|
-export(
|
|
|
|
[
|
|
|
|
utc_to_local/2
|
|
|
|
,local_to_utc/2
|
|
|
|
,local_to_local/3
|
2013-10-18 16:08:05 +04:00
|
|
|
,local_to_local_dst/3
|
2010-08-23 16:17:16 +04:00
|
|
|
,tz_name/2
|
2010-12-18 12:39:14 +03:00
|
|
|
,tz_shift/2
|
2010-12-18 22:09:01 +03:00
|
|
|
,tz_shift/3
|
2015-11-05 10:39:57 -06:00
|
|
|
,get_timezone/1
|
|
|
|
,list_timezones/0
|
|
|
|
,adjust_datetime/2
|
2016-01-29 17:33:54 +08:00
|
|
|
,fmt_shift/1
|
2010-08-23 16:17:16 +04:00
|
|
|
]).
|
|
|
|
|
2013-10-18 15:19:36 +04:00
|
|
|
% utc_to_local(UtcDateTime, Timezone) -> LocalDateTime | [LocalDateTime, DstLocalDateTime] | {error, ErrDescr}
|
2010-08-23 16:17:16 +04:00
|
|
|
% UtcDateTime = DateTime()
|
|
|
|
% Timezone = String()
|
|
|
|
% LocalDateTime = DateTime()
|
2013-10-18 15:19:36 +04:00
|
|
|
% DstLocalDateTime = DateTime()
|
2010-12-19 12:02:02 +03:00
|
|
|
% ErrDescr = atom(), unknown_tz
|
2010-08-23 16:17:16 +04:00
|
|
|
utc_to_local(UtcDateTime, Timezone) ->
|
2010-12-18 21:07:15 +03:00
|
|
|
case lists:keyfind(get_timezone(Timezone), 1, ?tz_database) of
|
2010-08-23 16:17:16 +04:00
|
|
|
false ->
|
|
|
|
{error, unknown_tz};
|
|
|
|
{_Tz, _, _, Shift, _DstShift, undef, _DstStartTime, undef, _DstEndTime} ->
|
|
|
|
adjust_datetime(UtcDateTime, Shift);
|
|
|
|
TzRule = {_, _, _, Shift, DstShift, _, _, _, _} ->
|
|
|
|
LocalDateTime = adjust_datetime(UtcDateTime, Shift),
|
|
|
|
case localtime_dst:check(LocalDateTime, TzRule) of
|
|
|
|
Res when (Res == is_in_dst) or (Res == time_not_exists) ->
|
|
|
|
adjust_datetime(LocalDateTime, DstShift);
|
|
|
|
is_not_in_dst ->
|
|
|
|
LocalDateTime;
|
|
|
|
ambiguous_time ->
|
2013-10-18 15:19:36 +04:00
|
|
|
[LocalDateTime, adjust_datetime(LocalDateTime, DstShift)]
|
2010-08-23 16:17:16 +04:00
|
|
|
end
|
|
|
|
end.
|
|
|
|
|
2013-10-18 15:19:36 +04:00
|
|
|
% local_to_utc(LocalDateTime, Timezone) -> UtcDateTime | [UtcDateTime, DstUtcDateTime] | time_not_exists | {error, ErrDescr}
|
2010-08-23 16:17:16 +04:00
|
|
|
% LocalDateTime = DateTime()
|
|
|
|
% Timezone = String()
|
|
|
|
% UtcDateTime = DateTime()
|
2013-10-18 15:19:36 +04:00
|
|
|
% DstUtcDateTime = DateTime()
|
2010-12-19 12:02:02 +03:00
|
|
|
% ErrDescr = atom(), unknown_tz
|
2010-08-23 16:17:16 +04:00
|
|
|
local_to_utc(LocalDateTime, Timezone) ->
|
2010-12-18 21:07:15 +03:00
|
|
|
case lists:keyfind(get_timezone(Timezone), 1, ?tz_database) of
|
2010-08-23 16:17:16 +04:00
|
|
|
false ->
|
|
|
|
{error, unknown_tz};
|
|
|
|
{_Tz, _, _, Shift, _DstShift, undef, _DstStartTime, undef, _DstEndTime} ->
|
|
|
|
adjust_datetime(LocalDateTime, invert_shift(Shift));
|
|
|
|
TzRule = {_, _, _, Shift, DstShift, _, _, _, _} ->
|
|
|
|
UtcDateTime = adjust_datetime(LocalDateTime, invert_shift(Shift)),
|
|
|
|
case localtime_dst:check(LocalDateTime, TzRule) of
|
|
|
|
is_in_dst ->
|
|
|
|
adjust_datetime(UtcDateTime, invert_shift(DstShift));
|
2013-10-18 15:19:36 +04:00
|
|
|
is_not_in_dst ->
|
2010-08-23 16:17:16 +04:00
|
|
|
UtcDateTime;
|
2013-10-18 15:19:36 +04:00
|
|
|
ambiguous_time ->
|
|
|
|
[UtcDateTime, adjust_datetime(UtcDateTime, invert_shift(DstShift))];
|
2010-08-23 16:17:16 +04:00
|
|
|
time_not_exists ->
|
|
|
|
time_not_exists
|
|
|
|
end
|
|
|
|
end.
|
|
|
|
|
2013-10-18 15:19:36 +04:00
|
|
|
% local_to_local(LocalDateTime, TimezoneFrom, TimezoneTo) -> LocalDateTime | ambiguous | time_not_exists | {error, ErrDescr}
|
2010-08-23 16:17:16 +04:00
|
|
|
% LocalDateTime = DateTime()
|
|
|
|
% TimezoneFrom = String()
|
|
|
|
% TimezoneTo = String()
|
2010-12-19 12:02:02 +03:00
|
|
|
% ErrDescr = atom(), unknown_tz
|
2010-08-23 16:17:16 +04:00
|
|
|
local_to_local(LocalDateTime, TimezoneFrom, TimezoneTo) ->
|
|
|
|
case local_to_utc(LocalDateTime, TimezoneFrom) of
|
2013-10-18 16:08:05 +04:00
|
|
|
UtcDateTime = {{_,_,_},{_,_,_}} ->
|
|
|
|
LocalDateTime2 = utc_to_local(UtcDateTime, TimezoneTo);
|
|
|
|
[UtcDateTime, {{_,_,_},{_,_,_}}] ->
|
|
|
|
LocalDateTime2 = utc_to_local(UtcDateTime, TimezoneTo);
|
2010-08-23 16:17:16 +04:00
|
|
|
Res ->
|
2013-10-18 16:08:05 +04:00
|
|
|
LocalDateTime2 = Res
|
|
|
|
end,
|
|
|
|
case LocalDateTime2 of
|
|
|
|
[DateTimeToReturn, {{_,_,_},{_,_,_}}] ->
|
|
|
|
DateTimeToReturn;
|
|
|
|
Other ->
|
|
|
|
Other
|
|
|
|
end.
|
|
|
|
|
|
|
|
% local_to_local_dst(LocalDateTime, TimezoneFrom, TimezoneTo) -> LocalDateTime | ambiguous | time_not_exists | {error, ErrDescr}
|
|
|
|
% LocalDateTime = DateTime()
|
|
|
|
% TimezoneFrom = String()
|
|
|
|
% TimezoneTo = String()
|
|
|
|
% ErrDescr = atom(), unknown_tz
|
|
|
|
local_to_local_dst(LocalDateTime, TimezoneFrom, TimezoneTo) ->
|
|
|
|
case local_to_utc(LocalDateTime, TimezoneFrom) of
|
|
|
|
UtcDateTime = {{_,_,_},{_,_,_}} ->
|
|
|
|
LocalDateTime2 = utc_to_local(UtcDateTime, TimezoneTo);
|
|
|
|
[{{_,_,_},{_,_,_}}, UtcDateTime] ->
|
|
|
|
LocalDateTime2 = utc_to_local(UtcDateTime, TimezoneTo);
|
|
|
|
Res ->
|
|
|
|
LocalDateTime2 = Res
|
|
|
|
end,
|
|
|
|
case LocalDateTime2 of
|
|
|
|
[{{_,_,_},{_,_,_}}, DateTimeToReturn] ->
|
|
|
|
DateTimeToReturn;
|
|
|
|
Other ->
|
|
|
|
Other
|
2010-08-23 16:17:16 +04:00
|
|
|
end.
|
|
|
|
|
2010-12-19 12:02:02 +03:00
|
|
|
% tz_name(DateTime(), Timezone) -> {Abbr, Name} | {{StdAbbr, StdName}, {DstAbbr, DstName}} | unable_to_detect | {error, ErrDesc}
|
2010-08-23 16:17:16 +04:00
|
|
|
% Timezone = String()
|
|
|
|
% Abbr = String()
|
|
|
|
% Name = String()
|
|
|
|
% StdAbbr = String()
|
|
|
|
% StdName = String()
|
|
|
|
% DstAbbr = String()
|
|
|
|
% DstName = String()
|
2010-12-19 12:02:02 +03:00
|
|
|
% ErrDesc = atom(), unknown_tz
|
2010-08-23 16:17:16 +04:00
|
|
|
tz_name(_UtcDateTime, "UTC") ->
|
|
|
|
{"UTC", "UTC"};
|
|
|
|
tz_name(LocalDateTime, Timezone) ->
|
2010-12-18 21:07:15 +03:00
|
|
|
case lists:keyfind(get_timezone(Timezone), 1, ?tz_database) of
|
2010-08-23 16:17:16 +04:00
|
|
|
false ->
|
|
|
|
{error, unknown_tz};
|
|
|
|
{_Tz, StdName, undef, _Shift, _DstShift, undef, _DstStartTime, undef, _DstEndTime} ->
|
|
|
|
StdName;
|
|
|
|
TzRule = {_, StdName, DstName, _Shift, _DstShift, _, _, _, _} ->
|
|
|
|
case localtime_dst:check(LocalDateTime, TzRule) of
|
|
|
|
is_in_dst ->
|
|
|
|
DstName;
|
|
|
|
is_not_in_dst ->
|
|
|
|
StdName;
|
|
|
|
ambiguous_time ->
|
|
|
|
{StdName, DstName};
|
|
|
|
time_not_exists ->
|
|
|
|
unable_to_detect
|
|
|
|
end
|
|
|
|
end.
|
|
|
|
|
2010-12-19 12:02:02 +03:00
|
|
|
% tz_shift(LocalDateTime, Timezone) -> Shift | {Shift, DstSift} | unable_to_detect | {error, ErrDesc}
|
2010-12-18 12:39:14 +03:00
|
|
|
% returns time shift from GMT
|
|
|
|
% LocalDateTime = DateTime()
|
|
|
|
% Timezone = String()
|
|
|
|
% Shift = DstShift = {Sign, Hours, Minutes}
|
|
|
|
% Sign = term(), '+', '-'
|
|
|
|
% Hours = Minutes = Integer(),
|
|
|
|
% {Shift, DstShift} - returns, when shift is ambiguous
|
2010-12-19 12:02:02 +03:00
|
|
|
% ErrDesc = atom(), unknown_tz
|
2010-12-18 12:39:14 +03:00
|
|
|
tz_shift(_UtcDateTime, "UTC") ->
|
|
|
|
0;
|
|
|
|
tz_shift(LocalDateTime, Timezone) ->
|
2010-12-18 21:07:15 +03:00
|
|
|
case lists:keyfind(get_timezone(Timezone), 1, ?tz_database) of
|
2010-08-23 16:17:16 +04:00
|
|
|
false ->
|
2010-12-18 12:39:14 +03:00
|
|
|
{error, unknown_tz};
|
|
|
|
{_Tz, _StdName, undef, Shift, _DstShift, undef, _DstStartTime, undef, _DstEndTime} ->
|
2010-12-18 22:09:01 +03:00
|
|
|
fmt_min(Shift);
|
2010-12-18 12:39:14 +03:00
|
|
|
TzRule = {_, _StdName, _DstName, Shift, DstShift, _, _, _, _} ->
|
|
|
|
case localtime_dst:check(LocalDateTime, TzRule) of
|
|
|
|
is_in_dst ->
|
|
|
|
fmt_min(Shift + DstShift);
|
|
|
|
is_not_in_dst ->
|
|
|
|
fmt_min(Shift);
|
|
|
|
ambiguous_time ->
|
|
|
|
{fmt_min(Shift), fmt_min(Shift + DstShift)};
|
|
|
|
time_not_exists ->
|
|
|
|
unable_to_detect
|
|
|
|
end
|
2010-08-23 16:17:16 +04:00
|
|
|
end.
|
|
|
|
|
2010-12-19 12:02:02 +03:00
|
|
|
% the same as tz_shift/2, but calculates time difference between two local timezones
|
2010-12-18 22:09:01 +03:00
|
|
|
tz_shift(LocalDateTime, TimezoneFrom, TimezoneTo) ->
|
2010-12-19 12:02:02 +03:00
|
|
|
F = fun() ->
|
|
|
|
FromShift = fmt_shift(tz_shift(LocalDateTime, TimezoneFrom)),
|
|
|
|
DateTimeTo = localtime:local_to_local(LocalDateTime, TimezoneFrom, TimezoneTo),
|
|
|
|
ToShift = fmt_shift(tz_shift(DateTimeTo, TimezoneTo)),
|
|
|
|
fmt_min(ToShift-FromShift)
|
|
|
|
end,
|
|
|
|
try F()
|
|
|
|
catch
|
|
|
|
_:Err ->
|
|
|
|
Err
|
|
|
|
end.
|
|
|
|
|
2010-12-18 12:39:14 +03:00
|
|
|
adjust_datetime(DateTime, Minutes) ->
|
|
|
|
Seconds = calendar:datetime_to_gregorian_seconds(DateTime) + Minutes * 60,
|
|
|
|
calendar:gregorian_seconds_to_datetime(Seconds).
|
|
|
|
|
2015-11-05 10:39:57 -06:00
|
|
|
get_timezone(TimeZone) ->
|
|
|
|
get_timezone_inner(TimeZone).
|
|
|
|
|
|
|
|
list_timezones() ->
|
|
|
|
dict:fetch_keys(?tz_index).
|
|
|
|
|
|
|
|
% =======================================================================
|
|
|
|
% privates
|
|
|
|
% =======================================================================
|
|
|
|
|
2010-12-18 12:39:14 +03:00
|
|
|
invert_shift(Minutes) ->
|
|
|
|
-Minutes.
|
|
|
|
|
|
|
|
fmt_min(Shift) when Shift < 0 ->
|
|
|
|
{'-', abs(Shift) div 60, abs(Shift) rem 60};
|
|
|
|
fmt_min(Shift) ->
|
|
|
|
{'+', Shift div 60, Shift rem 60}.
|
2010-12-18 21:07:15 +03:00
|
|
|
|
2010-12-18 22:09:01 +03:00
|
|
|
fmt_shift({'+', H, M}) ->
|
|
|
|
H * 60 + M;
|
|
|
|
fmt_shift({'-', H, M}) ->
|
2010-12-19 12:02:02 +03:00
|
|
|
-(H * 60 + M);
|
2015-11-21 20:30:56 +00:00
|
|
|
fmt_shift(0) ->
|
|
|
|
0;
|
2010-12-19 12:02:02 +03:00
|
|
|
fmt_shift(Any) ->
|
|
|
|
throw(Any).
|
2010-12-18 22:09:01 +03:00
|
|
|
|
2014-01-09 16:46:15 -08:00
|
|
|
tr_char(String, From, To) ->
|
|
|
|
case string:chr(String, From) of
|
|
|
|
0 -> String; % Optimize for String does not contain From.
|
|
|
|
_ -> tr_char(String, From, To, [])
|
|
|
|
end.
|
|
|
|
tr_char([], _From, _To, Acc) ->
|
|
|
|
lists:reverse(Acc);
|
|
|
|
tr_char([H|T], From, To, Acc) ->
|
|
|
|
case H of
|
|
|
|
From -> tr_char(T, From, To, [To|Acc]);
|
|
|
|
_ -> tr_char(T, From, To, [H|Acc])
|
|
|
|
end.
|
|
|
|
|
|
|
|
-define(SPACE_CHAR, 32).
|
2016-01-31 08:54:38 -06:00
|
|
|
get_timezone_inner(TimeZone) when is_binary(TimeZone) ->
|
|
|
|
get_timezone_inner(erlang:binary_to_list(TimeZone));
|
|
|
|
get_timezone_inner(TimeZone) when is_list(TimeZone) ->
|
2014-01-09 16:46:15 -08:00
|
|
|
TimeZoneNoSpaces = tr_char(TimeZone, ?SPACE_CHAR, $_),
|
|
|
|
case dict:find(TimeZoneNoSpaces, ?tz_index) of
|
2010-12-18 21:07:15 +03:00
|
|
|
error ->
|
2014-01-09 16:46:15 -08:00
|
|
|
TimeZoneNoSpaces;
|
2010-12-18 21:07:15 +03:00
|
|
|
{ok, [TZName | _]} ->
|
|
|
|
TZName
|
2016-01-11 10:50:37 +08:00
|
|
|
end;
|
2016-01-31 08:54:38 -06:00
|
|
|
get_timezone_inner(_) ->
|
2016-01-11 10:50:37 +08:00
|
|
|
throw({error, "Timezone should be string/binary"}).
|
2014-01-09 16:46:15 -08:00
|
|
|
|
|
|
|
-ifdef(TEST).
|
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
|
|
|
|
tr_char_test() ->
|
|
|
|
?assertEqual("ABCDE", tr_char("ABCDE", ?SPACE_CHAR, $_)),
|
|
|
|
?assertEqual("AB_DE", tr_char("AB DE", ?SPACE_CHAR, $_)),
|
|
|
|
?assertEqual("A_C_E", tr_char("A C E", ?SPACE_CHAR, $_)).
|
|
|
|
|
|
|
|
get_timezone_test() ->
|
|
|
|
?assertEqual("America/Los_Angeles", get_timezone("America/Los Angeles")).
|
|
|
|
|
|
|
|
tz_shift_test() ->
|
|
|
|
?assertEqual({'+',3,0}, tz_shift({{2014,1,1},{12,0,0}}, "America/Los_Angeles", "America/New_York")).
|
|
|
|
|
|
|
|
-endif. % TEST
|