1221 lines
43 KiB
Erlang
1221 lines
43 KiB
Erlang
% vim: ts=4 sw=4 et
|
|
% Copyright (c) 2013-2016 Jesse Gumm
|
|
% See LICENSE for licensing information.
|
|
%
|
|
-module(qdate).
|
|
|
|
-export([
|
|
start/0,
|
|
stop/0
|
|
]).
|
|
|
|
-export([
|
|
to_string/1,
|
|
to_string/2,
|
|
to_string/3,
|
|
to_string/4,
|
|
to_date/1,
|
|
to_date/2,
|
|
to_date/3,
|
|
to_now/1,
|
|
to_now/2,
|
|
to_unixtime/1,
|
|
to_unixtime/2,
|
|
unixtime/0
|
|
]).
|
|
|
|
-export([
|
|
beginning_minute/1,
|
|
beginning_minute/0,
|
|
beginning_hour/1,
|
|
beginning_hour/0,
|
|
beginning_day/1,
|
|
beginning_day/0,
|
|
%beginning_week/2, %% needs to be /2 because we also need to define what day is considered "beginning of week", since some calendars do sunday and some do monday. We'll hold off on implementation here
|
|
beginning_month/1,
|
|
beginning_month/0,
|
|
beginning_year/1,
|
|
beginning_year/0
|
|
]).
|
|
|
|
-export([
|
|
end_minute/1,
|
|
end_minute/0,
|
|
end_hour/1,
|
|
end_hour/0,
|
|
end_day/1,
|
|
end_day/0,
|
|
end_month/1,
|
|
end_month/0,
|
|
end_year/1,
|
|
end_year/0
|
|
]).
|
|
|
|
-export([
|
|
compare/2,
|
|
compare/3,
|
|
between/2,
|
|
between/3,
|
|
between/5
|
|
]).
|
|
|
|
-export([
|
|
add_seconds/2,
|
|
add_seconds/1,
|
|
add_minutes/2,
|
|
add_minutes/1,
|
|
add_hours/2,
|
|
add_hours/1,
|
|
add_days/2,
|
|
add_days/1,
|
|
add_weeks/2,
|
|
add_weeks/1,
|
|
add_months/2,
|
|
add_months/1,
|
|
add_years/2,
|
|
add_years/1,
|
|
add_unit/2,
|
|
add_unit/3,
|
|
add_date/2
|
|
]).
|
|
|
|
-export([
|
|
range/4,
|
|
range_seconds/3,
|
|
range_minutes/3,
|
|
range_hours/3,
|
|
range_days/3,
|
|
range_weeks/3,
|
|
range_months/3,
|
|
range_years/3
|
|
]).
|
|
|
|
-export([
|
|
register_parser/2,
|
|
register_parser/1,
|
|
deregister_parser/1,
|
|
deregister_parsers/0,
|
|
get_parsers/0,
|
|
|
|
register_format/2,
|
|
deregister_format/1,
|
|
get_formats/0,
|
|
|
|
set_timezone/1,
|
|
set_timezone/2,
|
|
get_timezone/0,
|
|
get_timezone/1,
|
|
clear_timezone/0,
|
|
clear_timezone/1
|
|
]).
|
|
|
|
-export([
|
|
parse_relative/1
|
|
]).
|
|
|
|
%% Exported for API compatibility with ec_date
|
|
-export([
|
|
format/1,format/2,
|
|
nparse/1,
|
|
parse/1
|
|
]).
|
|
|
|
%% This the value in gregorian seconds for jan 1st 1970, 12am
|
|
%% It's used to convert to and from unixtime, since unixtime starts
|
|
%% 1970-01-01 12:00am
|
|
-define(UNIXTIME_BASE,62167219200).
|
|
|
|
|
|
-define(DETERMINE_TZ, determine_timezone()).
|
|
-define(DEFAULT_DISAMBIG, prefer_standard).
|
|
-define(else, true).
|
|
|
|
start() ->
|
|
application:load(qdate).
|
|
|
|
stop() ->
|
|
ok.
|
|
|
|
to_string(Format) ->
|
|
to_string(Format, os:timestamp()).
|
|
|
|
to_string(Format, Date) ->
|
|
to_string(Format, ?DETERMINE_TZ, Date).
|
|
|
|
to_string(Format, ToTZ, Date) ->
|
|
to_string(Format, ToTZ, ?DEFAULT_DISAMBIG, Date).
|
|
|
|
to_string(FormatKey, ToTZ, Disambiguate, Date) when is_atom(FormatKey) orelse is_tuple(FormatKey) ->
|
|
Format = case qdate_srv:get_format(FormatKey) of
|
|
undefined -> throw({undefined_format_key,FormatKey});
|
|
F -> F
|
|
end,
|
|
to_string(Format, ToTZ, Disambiguate, Date);
|
|
to_string(Format, ToTZ, Disambiguate, Date) when is_binary(Format) ->
|
|
list_to_binary(to_string(binary_to_list(Format), ToTZ, Disambiguate, Date));
|
|
to_string(Format, ToTZ, Disambiguate, Date) when is_list(Format) ->
|
|
%% it may seem odd that we're ensuring it here, and then again
|
|
%% as one of the last steps of the to_date process, but we need
|
|
%% the actual name for the strings for the PHP "T" and "e", so
|
|
%% we extract the Timezone in case ToTZ is actually a timezone key
|
|
%% Then we can pass it on to to_date as well. That way we don't have
|
|
%% to do it twice, since it's already ensured.
|
|
ActualToTZ = ensure_timezone(ToTZ),
|
|
case to_date(ActualToTZ, Disambiguate, Date) of
|
|
{ambiguous, Standard, Daylight} ->
|
|
{ambiguous,
|
|
to_string_worker(Format, ActualToTZ, prefer_standard, Standard),
|
|
to_string_worker(Format, ActualToTZ, prefer_daylight, Daylight)};
|
|
ActualDate ->
|
|
case tz_name(ActualDate,Disambiguate, ActualToTZ) of
|
|
{ambiguous,_,_} ->
|
|
{ambiguous,
|
|
to_string_worker(Format, ActualToTZ, prefer_standard, ActualDate),
|
|
to_string_worker(Format, ActualToTZ, prefer_daylight, ActualDate)};
|
|
_ ->
|
|
to_string_worker(Format, ActualToTZ, Disambiguate, ActualDate)
|
|
end
|
|
end.
|
|
|
|
|
|
|
|
to_string_worker([], _, _, _) ->
|
|
"";
|
|
to_string_worker([$\\,H|RestFormat], ToTZ, Disamb, Date) ->
|
|
[H|to_string_worker(RestFormat, ToTZ, Disamb, Date)];
|
|
to_string_worker([$e|RestFormat], ToTZ, Disamb, Date) ->
|
|
ToTZ ++ to_string_worker(RestFormat, ToTZ, Disamb, Date);
|
|
to_string_worker([$I|RestFormat], ToTZ, Disamb, Date) ->
|
|
I = case localtime_dst:check(Date, ToTZ) of
|
|
is_in_dst -> "1";
|
|
is_not_in_dst -> "0";
|
|
ambiguous_time -> "?"
|
|
end,
|
|
I ++ to_string_worker(RestFormat, ToTZ, Disamb, Date);
|
|
to_string_worker([H | RestFormat], ToTZ, Disamb, Date) when H==$O orelse H==$P ->
|
|
Shift = get_timezone_shift(ToTZ, Disamb, Date),
|
|
Separator = case H of
|
|
$O -> "";
|
|
$P -> ":"
|
|
end,
|
|
format_shift(Shift,Separator) ++ to_string_worker(RestFormat, ToTZ, Disamb, Date);
|
|
to_string_worker([$T | RestFormat], ToTZ, Disamb, Date) ->
|
|
ShortName = tz_name(Date, Disamb, ToTZ),
|
|
ShortName ++ to_string_worker(RestFormat, ToTZ, Disamb, Date);
|
|
to_string_worker([$Z | RestFormat], ToTZ, Disamb, Date) ->
|
|
{Sign, Hours, Mins} = get_timezone_shift(ToTZ, Disamb, Date),
|
|
Seconds = (Hours * 3600) + (Mins * 60),
|
|
atom_to_list(Sign) ++ integer_to_list(Seconds) ++ to_string_worker(RestFormat, ToTZ, Disamb, Date);
|
|
to_string_worker([$r | RestFormat], ToTZ, Disamb, Date) ->
|
|
NewFormat = "D, d M Y H:i:s O",
|
|
to_string_worker(NewFormat, ToTZ, Disamb, Date) ++ to_string_worker(RestFormat, ToTZ, Disamb, Date);
|
|
to_string_worker([$c | RestFormat], ToTZ, Disamb, Date) ->
|
|
Format1 = "Y-m-d",
|
|
Format2 = "H:i:sP",
|
|
to_string_worker(Format1, ToTZ, Disamb, Date)
|
|
++ "T"
|
|
++ to_string_worker(Format2, ToTZ, Disamb, Date)
|
|
++ to_string_worker(RestFormat, ToTZ, Disamb, Date);
|
|
to_string_worker([H | RestFormat], ToTZ, Disamb, Date) ->
|
|
ec_date:format([H], Date) ++ to_string_worker(RestFormat, ToTZ, Disamb, Date).
|
|
|
|
tz_name(Date, Disambiguate, ToTZ) ->
|
|
case localtime:tz_name(Date, ToTZ) of
|
|
{ShortName, _} when is_list(ShortName) ->
|
|
ShortName;
|
|
{{ShortStandard,_},{ShortDST,_}} ->
|
|
case Disambiguate of
|
|
prefer_standard -> ShortStandard;
|
|
prefer_daylight -> ShortDST;
|
|
both -> {ambiguous, ShortStandard, ShortDST}
|
|
end
|
|
end.
|
|
|
|
format_shift({Sign,Hours,Mins},Separator) ->
|
|
SignStr = atom_to_list(Sign),
|
|
MinStr = leading_zero(Mins),
|
|
HourStr = leading_zero(Hours),
|
|
SignStr ++ HourStr ++ Separator ++ MinStr.
|
|
|
|
leading_zero(I) when I < 10 ->
|
|
"0" ++ integer_to_list(I);
|
|
leading_zero(I) ->
|
|
integer_to_list(I).
|
|
|
|
|
|
format(Format) ->
|
|
to_string(Format).
|
|
|
|
format(Format, Date) ->
|
|
to_string(Format, Date).
|
|
|
|
parse(String) ->
|
|
to_date(String).
|
|
|
|
nparse(String) ->
|
|
to_now(String).
|
|
|
|
|
|
to_date(RawDate) ->
|
|
to_date(?DETERMINE_TZ, RawDate).
|
|
|
|
to_date(ToTZ, RawDate) ->
|
|
to_date(ToTZ, ?DEFAULT_DISAMBIG, RawDate).
|
|
|
|
to_date(ToTZ, Disambiguate, RawDate) when is_binary(RawDate) ->
|
|
to_date(ToTZ, Disambiguate, binary_to_list(RawDate));
|
|
to_date(ToTZ, Disambiguate, RawDate) when is_binary(ToTZ) ->
|
|
to_date(binary_to_list(ToTZ), Disambiguate, RawDate);
|
|
to_date(ToTZ, Disambiguate, RawDate) ->
|
|
{ExtractedDate, ExtractedTZ} = extract_timezone(RawDate),
|
|
{RawDate3, FromTZ} = case try_registered_parsers(RawDate) of
|
|
undefined ->
|
|
{ExtractedDate, ExtractedTZ};
|
|
{ParsedDate,undefined} ->
|
|
{ParsedDate,ExtractedTZ};
|
|
{ParsedDate,ParsedTZ} ->
|
|
{ParsedDate,ParsedTZ}
|
|
end,
|
|
try raw_to_date(RawDate3) of
|
|
D={{_,_,_},{_,_,_}} ->
|
|
date_tz_to_tz(D, Disambiguate, FromTZ, ToTZ);
|
|
{{Year, Month, Date},{Hour,Minute,Second,_Millis}} ->
|
|
date_tz_to_tz({{Year, Month, Date},{Hour,Minute,Second}}, Disambiguate, FromTZ, ToTZ)
|
|
catch
|
|
_:_ ->
|
|
case raw_to_date(RawDate) of
|
|
D2={{_,_,_},{_,_,_}} ->
|
|
date_tz_to_tz(D2, Disambiguate, ?DETERMINE_TZ, ToTZ)
|
|
end
|
|
end.
|
|
|
|
%% This converts dates without regard to timezone.
|
|
%% Unixtime just goes to UTC
|
|
raw_to_date(Unixtime) when is_integer(Unixtime) ->
|
|
unixtime_to_date(Unixtime);
|
|
raw_to_date(DateString) when is_list(DateString) ->
|
|
ec_date:parse(DateString, get_deterministic_datetime());
|
|
raw_to_date(Now = {_,_,_}) ->
|
|
calendar:now_to_datetime(Now);
|
|
raw_to_date(Date = {{_,_,_},{_,_,_}}) ->
|
|
Date.
|
|
|
|
get_deterministic_datetime() ->
|
|
DateZero = {1970,1,1},
|
|
TimeZero = {0,0,0},
|
|
case application:get_env(qdate, deterministic_parsing) of
|
|
{ok, {zero, zero}} -> {DateZero, TimeZero};
|
|
{ok, {zero, now}} -> {DateZero, time()};
|
|
{ok, {now, zero}} -> {date(), TimeZero};
|
|
{ok, {now, now}} -> {date(), time()};
|
|
undefined -> {DateZero, TimeZero};
|
|
{ok, Val} -> throw({invalid_env_var, {qdate, deterministic_parsing, Val}})
|
|
end.
|
|
|
|
to_unixtime(Date) ->
|
|
to_unixtime(?DEFAULT_DISAMBIG, Date).
|
|
|
|
to_unixtime(_, Unixtime) when is_integer(Unixtime) ->
|
|
Unixtime;
|
|
|
|
to_unixtime(_, {MegaSecs,Secs,_}) when is_integer(MegaSecs), is_integer(Secs) ->
|
|
MegaSecs*1000000 + Secs;
|
|
to_unixtime(Disamb, ToParse) ->
|
|
%% We want to treat all unixtimes as GMT
|
|
case to_date("GMT", Disamb, ToParse) of
|
|
{ambiguous, Standard, Daylight} ->
|
|
{ambiguous,
|
|
calendar:datetime_to_gregorian_seconds(Standard) - ?UNIXTIME_BASE,
|
|
calendar:datetime_to_gregorian_seconds(Daylight) - ?UNIXTIME_BASE};
|
|
Date ->
|
|
calendar:datetime_to_gregorian_seconds(Date) - ?UNIXTIME_BASE
|
|
end.
|
|
|
|
unixtime() ->
|
|
to_unixtime(os:timestamp()).
|
|
|
|
to_now(Date) ->
|
|
to_now(?DEFAULT_DISAMBIG, Date).
|
|
|
|
to_now(_, Now = {_,_,_}) ->
|
|
Now;
|
|
to_now(Disamb, ToParse) ->
|
|
case to_unixtime(Disamb, ToParse) of
|
|
{ambiguous, Standard, Daylight} ->
|
|
{ambiguous,
|
|
unixtime_to_now(Standard),
|
|
unixtime_to_now(Daylight)};
|
|
Unixtime ->
|
|
unixtime_to_now(Unixtime)
|
|
end.
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Beginning/Truncation %%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
beginning_minute() ->
|
|
beginning_minute({date(),time()}).
|
|
|
|
beginning_minute(Date) ->
|
|
{{Y,M,D},{H,I,_}} = to_date(Date),
|
|
{{Y,M,D},{H,I,0}}.
|
|
|
|
beginning_hour() ->
|
|
beginning_hour({date(),time()}).
|
|
|
|
beginning_hour(Date) ->
|
|
{{Y,M,D},{H,_,_}} = to_date(Date),
|
|
{{Y,M,D},{H,0,0}}.
|
|
|
|
beginning_day() ->
|
|
beginning_day(unixtime()).
|
|
|
|
beginning_day(Date) ->
|
|
{{Y,M,D},{_,_,_}} = to_date(Date),
|
|
{{Y,M,D},{0,0,0}}.
|
|
|
|
beginning_month() ->
|
|
beginning_month({date(),time()}).
|
|
|
|
beginning_month(Date) ->
|
|
{{Y,M,_},{_,_,_}} = to_date(Date),
|
|
{{Y,M,1},{0,0,0}}.
|
|
|
|
beginning_year() ->
|
|
beginning_year({date(),time()}).
|
|
|
|
beginning_year(Date) ->
|
|
{{Y,_,_},{_,_,_}} = to_date(Date),
|
|
{{Y,1,1},{0,0,0}}.
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%% End of Period (day/hour, etc) %%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
|
|
end_minute() ->
|
|
end_minute({date(),time()}).
|
|
|
|
end_minute(Date) ->
|
|
{{Y,M,D},{H,I,_}} = to_date(Date),
|
|
{{Y,M,D},{H,I,59}}.
|
|
|
|
end_hour() ->
|
|
end_hour({date(),time()}).
|
|
|
|
end_hour(Date) ->
|
|
{{Y,M,D},{H,_,_}} = to_date(Date),
|
|
{{Y,M,D},{H,59,59}}.
|
|
|
|
end_day() ->
|
|
end_day({date(),time()}).
|
|
|
|
end_day(Date) ->
|
|
{{Y,M,D},_} = to_date(Date),
|
|
{{Y,M,D},{23,59,59}}.
|
|
|
|
end_month() ->
|
|
end_month({date(), time()}).
|
|
|
|
end_month(Date) ->
|
|
Beginning = beginning_month(Date),
|
|
add_seconds(-1, add_months(1, Beginning)).
|
|
|
|
end_year() ->
|
|
end_year({date(),time()}).
|
|
|
|
end_year(Date) ->
|
|
{{Y,_,_},_} = to_date(Date),
|
|
{{Y,12,31},{23,59,59}}.
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Comparisons %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
compare(A, B) ->
|
|
NowA = to_now(A),
|
|
NowB = to_now(B),
|
|
if
|
|
NowA == NowB -> 0;
|
|
NowA < NowB -> -1;
|
|
NowA > NowB -> 1
|
|
end.
|
|
|
|
compare(A, Op, B) ->
|
|
Comp = compare(A, B),
|
|
case Op of
|
|
'==' -> Comp =:= 0;
|
|
'=' -> Comp =:= 0;
|
|
|
|
'!=' -> Comp =/= 0;
|
|
'=/=' -> Comp =/= 0;
|
|
'/=' -> Comp =/= 0;
|
|
|
|
'before'-> Comp =:= -1;
|
|
'<' -> Comp =:= -1;
|
|
'<=' -> Comp =:= -1 orelse Comp =:= 0;
|
|
'=<' -> Comp =:= -1 orelse Comp =:= 0;
|
|
|
|
'after' -> Comp =:= 1;
|
|
'>' -> Comp =:= 1;
|
|
'>=' -> Comp =:= 1 orelse Comp =:= 0;
|
|
'=>' -> Comp =:= 1 orelse Comp =:= 0
|
|
end.
|
|
|
|
between(A, B) ->
|
|
between(A, unixtime(), B).
|
|
|
|
between(A, Date, B) ->
|
|
between(A, '=<', Date, '=<', B).
|
|
|
|
between(A, Op1, Date, Op2, B) ->
|
|
compare(A, Op1, Date) andalso compare(Date, Op2, B).
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Date Math %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
add_seconds(Seconds, Date) ->
|
|
to_unixtime(Date) + Seconds.
|
|
|
|
add_seconds(Seconds) ->
|
|
add_seconds(Seconds, os:timestamp()).
|
|
|
|
add_minutes(Minutes, Date) ->
|
|
add_seconds(Minutes * 60, Date).
|
|
|
|
add_minutes(Minutes) ->
|
|
add_minutes(Minutes, os:timestamp()).
|
|
|
|
add_hours(Hours, Date) ->
|
|
add_seconds(Hours * 3600, Date).
|
|
|
|
add_hours(Hours) ->
|
|
add_hours(Hours, os:timestamp()).
|
|
|
|
add_days(Days, Date0) ->
|
|
{{Y,M,D},Time} = to_date(Date0),
|
|
to_unixtime(fix_maybe_improper_date({{Y, M, D+Days}, Time})).
|
|
|
|
add_days(Days) ->
|
|
add_days(Days, os:timestamp()).
|
|
|
|
add_weeks(Weeks, Date) ->
|
|
add_days(Weeks * 7, Date).
|
|
|
|
add_weeks(Weeks) ->
|
|
add_weeks(Weeks, os:timestamp()).
|
|
|
|
add_months(Months, Date) ->
|
|
{{Y,M,D}, Time} = to_date(Date),
|
|
{TargetYear, TargetMonth} = fix_year_month({Y,M+Months}),
|
|
DaysInMonth = calendar:last_day_of_the_month(TargetYear, TargetMonth),
|
|
NewD = lists:min([DaysInMonth, D]),
|
|
to_unixtime(fix_maybe_improper_date({{Y, M+Months, NewD}, Time})).
|
|
|
|
add_months(Months) ->
|
|
add_months(Months, os:timestamp()).
|
|
|
|
add_years(Years, Date) ->
|
|
{{Y,M,D}, Time} = to_date(Date),
|
|
TargetYear = Y+Years,
|
|
NewD = case M of
|
|
2 ->
|
|
DaysInMonth = calendar:last_day_of_the_month(TargetYear, M),
|
|
lists:min([DaysInMonth, D]);
|
|
_ ->
|
|
D
|
|
end,
|
|
to_unixtime({{Y+Years, M, NewD}, Time}).
|
|
|
|
add_years(Years) ->
|
|
add_years(Years, os:timestamp()).
|
|
|
|
add_unit(second, Value, Date) ->
|
|
add_unit(seconds, Value, Date);
|
|
add_unit(seconds, Value, Date) ->
|
|
add_seconds(Value, Date);
|
|
add_unit(minute, Value, Date) ->
|
|
add_unit(minutes, Value, Date);
|
|
add_unit(minutes, Value, Date) ->
|
|
add_minutes(Value, Date);
|
|
add_unit(hour, Value, Date) ->
|
|
add_unit(hours, Value, Date);
|
|
add_unit(hours, Value, Date) ->
|
|
add_hours(Value, Date);
|
|
add_unit(day, Value, Date) ->
|
|
add_unit(days, Value, Date);
|
|
add_unit(days, Value, Date) ->
|
|
add_days(Value, Date);
|
|
add_unit(week, Value, Date) ->
|
|
add_unit(weeks, Value, Date);
|
|
add_unit(weeks, Value, Date) ->
|
|
add_weeks(Value, Date);
|
|
add_unit(month, Value, Date) ->
|
|
add_unit(months, Value, Date);
|
|
add_unit(months, Value, Date) ->
|
|
add_months(Value, Date);
|
|
add_unit(year, Value, Date) ->
|
|
add_unit(years, Value, Date);
|
|
add_unit(years, Value, Date) ->
|
|
add_years(Value, Date).
|
|
|
|
add_unit(Unit, Value) ->
|
|
add_unit(Unit, Value, os:timestamp()).
|
|
|
|
|
|
add_date({{AddY, AddM, AddD}, {AddH, AddI, AddS}}, Date) ->
|
|
{{Y, M, D}, {H, I, S}} = to_date(Date),
|
|
Date1 = fix_maybe_improper_date({{Y+AddY, M+AddM, D+AddD}, {H, I, S}}),
|
|
Date2 = to_unixtime(Date1),
|
|
Date2 + AddS + (AddI*60) + (AddH*3600).
|
|
|
|
|
|
-define(IS_LEAP_YEAR(Y), (Y rem 4 =:= 0 andalso
|
|
(Y rem 100 =/= 0
|
|
orelse Y rem 400 =:= 0))).
|
|
|
|
fix_maybe_improper_date({Date0, Time}) ->
|
|
Date = fmid(Date0),
|
|
{Date, Time}.
|
|
|
|
fix_year_month({Y, M}) when M > 12 ->
|
|
YearsOver = M div 12,
|
|
{Y + YearsOver, M-(YearsOver*12)};
|
|
fix_year_month({Y, M}) when M < 1 ->
|
|
YearsUnder = (abs(M-1) div 12) + 1,
|
|
{Y - YearsUnder, M+(YearsUnder*12)};
|
|
fix_year_month({Y, M}) ->
|
|
{Y, M}.
|
|
|
|
|
|
fmid({Y, M, D}) when M > 12;
|
|
M < 1 ->
|
|
{NewY, NewM} = fix_year_month({Y, M}),
|
|
fmid({NewY, NewM, D});
|
|
|
|
fmid({Y, M, D}) when (D > 30 andalso (
|
|
M=:=4 orelse
|
|
M=:=6 orelse
|
|
M=:=9 orelse
|
|
M=:=11)) ->
|
|
fmid({Y, M+1, D-30});
|
|
fmid({Y, M, D}) when M=:=2 andalso D > 29 andalso ?IS_LEAP_YEAR(Y) ->
|
|
fmid({Y, M+1, D-29});
|
|
fmid({Y, M, D}) when M =:= 2 andalso D > 28 andalso not(?IS_LEAP_YEAR(Y)) ->
|
|
fmid({Y, M+1, D-28});
|
|
fmid({Y, M, D}) when D > 31 ->
|
|
fmid({Y, M+1, D-31});
|
|
|
|
fmid({Y, M, D}) when D < 1 ->
|
|
TargetMonth = case M-1 of
|
|
0 -> 12;
|
|
X -> X
|
|
end,
|
|
DaysInTargetMonth = calendar:last_day_of_the_month(Y, TargetMonth),
|
|
fmid({Y, M-1, D+DaysInTargetMonth});
|
|
|
|
|
|
fmid(Date) ->
|
|
Date.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Ranges %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
range(seconds, Interval, Start, Finish) ->
|
|
range_inner(fun add_seconds/2, Interval, Start, Finish);
|
|
range(minutes, Interval, Start, Finish) ->
|
|
range_inner(fun add_minutes/2, Interval, Start, Finish);
|
|
range(hours, Interval, Start, Finish) ->
|
|
range_inner(fun add_hours/2, Interval, Start, Finish);
|
|
range(days, Interval, Start, Finish) ->
|
|
range_inner(fun add_days/2, Interval, Start, Finish);
|
|
range(weeks, Interval, Start, Finish) ->
|
|
range_inner(fun add_weeks/2, Interval, Start, Finish);
|
|
range(months, Interval, Start, Finish) ->
|
|
range_inner(fun add_months/2, Interval, Start, Finish);
|
|
range(years, Interval, Start, Finish) ->
|
|
range_inner(fun add_years/2, Interval, Start, Finish).
|
|
|
|
range_inner(IntervalFun, Interval, Start, Finish) when Interval > 0 ->
|
|
%% If Interval>0, then we're ascending, and we want to compare start/end
|
|
%% dates normally
|
|
CompareFun = fun(S, F) -> compare(S, F) end,
|
|
range_normalizer(IntervalFun, Interval, CompareFun, Start, Finish);
|
|
range_inner(IntervalFun, Interval, Start, Finish) when Interval < 0 ->
|
|
%% If Interval<0, then we're descending, and we want to compare start/end
|
|
%% dates backwards (we want to end when the Start Date is Lower than
|
|
%% Finish)
|
|
CompareFun = fun(S, F) -> compare(F, S) end,
|
|
range_normalizer(IntervalFun, Interval, CompareFun, Start, Finish);
|
|
range_inner(_, Interval, _, _) when Interval==0 ->
|
|
throw(interval_cannot_be_zero).
|
|
|
|
range_normalizer(IntervalFun, Interval, CompareFun, Start0, Finish0) ->
|
|
%% Convert dates to unixtime for speed's sake
|
|
Start = to_unixtime(Start0),
|
|
Finish = to_unixtime(Finish0),
|
|
%% Prepare the incrementer, so we just need to pass the date to the incrementer.
|
|
Incrementer = fun(D) -> IntervalFun(Interval, D) end,
|
|
range_worker(Incrementer, CompareFun, Start, Finish).
|
|
|
|
range_worker(Incrementer, CompareFun, Start, Finish) ->
|
|
case CompareFun(Start, Finish) of
|
|
0 -> [Finish]; %% Equal, so we add our Finish value
|
|
1 -> []; %% Start is after Finish, so we add nothing
|
|
-1 -> %% Start is before Finish, so we include it, and recurse
|
|
NextDay = Incrementer(Start),
|
|
[Start | range_worker(Incrementer, CompareFun, NextDay, Finish)]
|
|
end.
|
|
|
|
range_seconds(Interval, Start, Finish) ->
|
|
range(seconds, Interval, Start, Finish).
|
|
|
|
range_minutes(Interval, Start, Finish) ->
|
|
range(minutes, Interval, Start, Finish).
|
|
|
|
range_hours(Interval, Start, Finish) ->
|
|
range(hours, Interval, Start, Finish).
|
|
|
|
range_days(Interval, Start, Finish) ->
|
|
range(days, Interval, Start, Finish).
|
|
|
|
range_weeks(Interval, Start, Finish) ->
|
|
range(weeks, Interval, Start, Finish).
|
|
|
|
range_months(Interval, Start, Finish) ->
|
|
range(months, Interval, Start, Finish).
|
|
|
|
range_years(Interval, Start, Finish) ->
|
|
range(years, Interval, Start, Finish).
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%% Relative Date Parsing %%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
parse_relative({relative, Date, Relation}) when is_atom(Relation) ->
|
|
parse_relative({relative, Date, atom_to_list(Relation)});
|
|
parse_relative({relative, Date, Relation}) when is_list(Relation); is_binary(Relation) ->
|
|
case parse_actual_relation(Relation) of
|
|
undefined -> undefined;
|
|
{OpStr, NumStr, UnitStr} ->
|
|
{Num, Unit} = normalize_relative_matches(OpStr, NumStr, UnitStr),
|
|
add_unit(Unit, Num, Date)
|
|
end;
|
|
parse_relative(now) ->
|
|
unixtime();
|
|
parse_relative("now") ->
|
|
unixtime();
|
|
parse_relative(<<"now">>) ->
|
|
unixtime();
|
|
parse_relative(Relation) when is_list(Relation); is_binary(Relation) ->
|
|
parse_relative({relative, unixtime(), Relation});
|
|
parse_relative(_) ->
|
|
undefined.
|
|
|
|
|
|
%% I would do this function recursively, but the return order of arguments
|
|
%% inconsistent, so I just leave it like this. It's a little nasty to have the
|
|
%% nested case expressions, but I can deal with it.
|
|
parse_actual_relation(Relation) ->
|
|
PrefixRE = "^(\\-|\\+|in)\\s?(\\d+) (second|minute|hour|day|week|month|year)s?$",
|
|
SuffixRE = "^(\\d+) (second|minute|hour|day|week|month|year)s?\\s?(ago|from now)?$",
|
|
case re:run(Relation, PrefixRE, [{capture, all_but_first, list}]) of
|
|
nomatch ->
|
|
case re:run(Relation, SuffixRE, [{capture, all_but_first, list}]) of
|
|
nomatch -> undefined;
|
|
{match, [NumStr, UnitStr, OpStr]} ->
|
|
{OpStr, NumStr, UnitStr}
|
|
end;
|
|
{match, [OpStr, NumStr, UnitStr]} ->
|
|
{OpStr, NumStr, UnitStr}
|
|
end.
|
|
|
|
normalize_relative_matches(OpStr, NumStr, UnitStr) ->
|
|
Op = normalize_relative_op(OpStr),
|
|
Num = list_to_integer(Op ++ NumStr),
|
|
Unit = list_to_existing_atom(UnitStr),
|
|
{Num, Unit}.
|
|
|
|
normalize_relative_op(Op) ->
|
|
case Op of
|
|
"+" -> "+";
|
|
"-" -> "-";
|
|
"ago" -> "-";
|
|
"from now" -> "+";
|
|
"in" -> "+"
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Timezone Stuff %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
get_timezone_shift(TZ, Disambiguate, Date) ->
|
|
case localtime:tz_shift(Date, TZ) of
|
|
unable_to_detect -> {error,unable_to_detect};
|
|
{error,T} -> {error,T};
|
|
{Sh, _} when Disambiguate==prefer_standard -> Sh;
|
|
{_, Sh} when Disambiguate==prefer_daylight -> Sh;
|
|
Sh -> Sh
|
|
end.
|
|
|
|
|
|
extract_timezone(Unixtime) when is_integer(Unixtime) ->
|
|
{Unixtime, "GMT"};
|
|
extract_timezone(DateString) when is_list(DateString) ->
|
|
case extract_gmt_relative_timezone(DateString) of
|
|
undefined ->
|
|
AllTimezones = localtime:list_timezones(),
|
|
RevDate = lists:reverse(DateString),
|
|
extract_timezone_helper(RevDate, AllTimezones);
|
|
{Date, GMTRel} ->
|
|
{Date, GMTRel}
|
|
end;
|
|
extract_timezone(Date={{_,_,_},{_,_,_}}) ->
|
|
{Date, ?DETERMINE_TZ};
|
|
extract_timezone(Rel={relative, _, _}) ->
|
|
{Rel, "GMT"};
|
|
extract_timezone(Now={_,_,_}) ->
|
|
{Now, "GMT"};
|
|
extract_timezone({MiscDate,TZ}) ->
|
|
{MiscDate,TZ}.
|
|
|
|
extract_gmt_relative_timezone(DateString) ->
|
|
RE = "^(.*?)(?:GMT|UTC)?([+-])(\\d{1,2}):?(\\d{2})?$",
|
|
case re:run(DateString,RE,[{capture,all_but_first,list},caseless]) of
|
|
{match, [NewDateStr, Sign, HourStr, MinStr]} ->
|
|
{NewDateStr, minutes_from_gmt_relative_timezone(Sign, HourStr, MinStr)};
|
|
{match, [NewDateStr, Sign, HourStr]} ->
|
|
{NewDateStr, minutes_from_gmt_relative_timezone(Sign, HourStr, "0")};
|
|
nomatch ->
|
|
undefined
|
|
end.
|
|
|
|
%% The number of minutes a the timezone is behind gmt
|
|
minutes_from_gmt_relative_timezone("+", HourStr, MinStr) ->
|
|
-minutes_from_gmt_relative_timezone("-", HourStr, MinStr);
|
|
minutes_from_gmt_relative_timezone("-", HourStr, MinStr) ->
|
|
list_to_integer(HourStr)*60 + list_to_integer(MinStr).
|
|
|
|
extract_timezone_helper(RevDate, []) ->
|
|
{lists:reverse(RevDate), ?DETERMINE_TZ};
|
|
extract_timezone_helper(RevDate, [TZ | TZs]) when length(RevDate) >= length(TZ) ->
|
|
RevTZ = lists:reverse(TZ),
|
|
case lists:split(length(TZ),RevDate) of
|
|
{RevTZ," " ++ Remainder} ->
|
|
{lists:reverse(Remainder), TZ};
|
|
_ ->
|
|
extract_timezone_helper(RevDate, TZs)
|
|
end;
|
|
extract_timezone_helper(RevDate, [_TZ | TZs]) ->
|
|
extract_timezone_helper(RevDate, TZs).
|
|
|
|
|
|
%% This is the timezone only if the qdate application variable
|
|
%% "default_timezone" isn't set or is set to undefined.
|
|
%% It's recommended that your app sets the var in a config, or at least using
|
|
%%
|
|
%% application:set_env(qdate, default_timezone, "GMT").
|
|
%%
|
|
default_timezone() ->
|
|
case application:get_env(qdate, default_timezone) of
|
|
undefined -> "GMT";
|
|
{ok, {Mod, Fun}} -> Mod:Fun();
|
|
{ok, TZ} -> TZ
|
|
end.
|
|
|
|
determine_timezone() ->
|
|
case qdate_srv:get_timezone() of
|
|
undefined -> default_timezone();
|
|
TZ -> TZ
|
|
end.
|
|
|
|
%% If FromTZ is an integer, then it's an integer that represents the number of minutes
|
|
%% relative to GMT. So we convert the date to GMT based on that number, then we can
|
|
%% do the other timezone conversion.
|
|
date_tz_to_tz(Date, Disambiguate, FromTZ, ToTZ) when is_integer(FromTZ) ->
|
|
NewDate = localtime:adjust_datetime(Date, FromTZ),
|
|
date_tz_to_tz(NewDate, Disambiguate, "GMT", ToTZ);
|
|
date_tz_to_tz(Date, Disambiguate, FromTZ, ToTZ) ->
|
|
ActualToTZ = ensure_timezone(ToTZ),
|
|
case Disambiguate of
|
|
prefer_standard ->
|
|
localtime:local_to_local(Date, FromTZ, ActualToTZ);
|
|
prefer_daylight ->
|
|
localtime:local_to_local_dst(Date, FromTZ, ActualToTZ);
|
|
both ->
|
|
date_tz_to_tz_both(Date, FromTZ, ToTZ)
|
|
end.
|
|
|
|
date_tz_to_tz_both(Date, FromTZ, ToTZ) ->
|
|
Standard = localtime:local_to_local(Date, FromTZ, ToTZ),
|
|
Daylight = localtime:local_to_local_dst(Date, FromTZ, ToTZ),
|
|
if
|
|
Standard=:=Daylight ->
|
|
Standard;
|
|
?else ->
|
|
{ambiguous, Standard, Daylight}
|
|
end.
|
|
|
|
set_timezone(TZ) when is_binary(TZ) ->
|
|
set_timezone(binary_to_list(TZ));
|
|
set_timezone(TZ) ->
|
|
qdate_srv:set_timezone(TZ).
|
|
|
|
|
|
set_timezone(Key,TZ) when is_binary(TZ) ->
|
|
set_timezone(Key, binary_to_list(TZ));
|
|
set_timezone(Key,TZ) ->
|
|
qdate_srv:set_timezone(Key, TZ).
|
|
|
|
get_timezone() ->
|
|
?DETERMINE_TZ.
|
|
|
|
get_timezone(Key) ->
|
|
qdate_srv:get_timezone(Key).
|
|
|
|
ensure_timezone(auto) ->
|
|
?DETERMINE_TZ;
|
|
ensure_timezone(Key) when is_atom(Key) orelse is_tuple(Key) ->
|
|
case get_timezone(Key) of
|
|
undefined -> throw({timezone_key_not_found,Key});
|
|
ToTZ -> ToTZ
|
|
end;
|
|
ensure_timezone(TZ) when is_binary(TZ) ->
|
|
binary_to_list(TZ);
|
|
ensure_timezone(TZ) when is_list(TZ) ->
|
|
TZ.
|
|
|
|
clear_timezone() ->
|
|
qdate_srv:clear_timezone().
|
|
|
|
clear_timezone(Key) ->
|
|
qdate_srv:clear_timezone(Key).
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Register Parsers %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
get_parsers() ->
|
|
qdate_srv:get_parsers().
|
|
|
|
register_parser(Key, Parser) when is_function(Parser,1) ->
|
|
qdate_srv:register_parser(Key,Parser).
|
|
|
|
register_parser(Parser) when is_function(Parser,1) ->
|
|
qdate_srv:register_parser(Parser).
|
|
|
|
deregister_parser(Key) ->
|
|
qdate_srv:deregister_parser(Key).
|
|
|
|
deregister_parsers() ->
|
|
qdate_srv:deregister_parsers().
|
|
|
|
try_registered_parsers(RawDate) ->
|
|
Parsers = qdate_srv:get_parsers(),
|
|
try_parsers(RawDate,Parsers).
|
|
|
|
try_parsers(_RawDate,[]) ->
|
|
undefined;
|
|
try_parsers(RawDate,[{ParserKey,Parser}|Parsers]) ->
|
|
try Parser(RawDate) of
|
|
Timestamp when is_integer(Timestamp) ->
|
|
{Timestamp, "GMT"};
|
|
{{_,_,_},{_,_,_}} = DateTime ->
|
|
{DateTime,undefined};
|
|
{DateTime={{_,_,_},{_,_,_}},Timezone} ->
|
|
{DateTime,Timezone};
|
|
undefined ->
|
|
try_parsers(RawDate, Parsers);
|
|
Other ->
|
|
throw({invalid_parser_return_value,[{parser_key,ParserKey},{return,Other}]})
|
|
catch
|
|
Error:Reason ->
|
|
Stacktrace = erlang:get_stacktrace(),
|
|
throw({error_in_parser,[{error,{Error,Reason}},{parser_key,ParserKey}, {stacktrace, Stacktrace}]})
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Register Formats %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
get_formats() ->
|
|
qdate_srv:get_formats().
|
|
|
|
register_format(Key, Format) ->
|
|
qdate_srv:register_format(Key, Format).
|
|
|
|
deregister_format(Key) ->
|
|
qdate_srv:deregister_format(Key).
|
|
|
|
|
|
|
|
unixtime_to_now(T) when is_integer(T) ->
|
|
MegaSec = floor(T/1000000),
|
|
Secs = T - MegaSec*1000000,
|
|
{MegaSec,Secs,0}.
|
|
|
|
unixtime_to_date(T) ->
|
|
Now = unixtime_to_now(T),
|
|
calendar:now_to_datetime(Now).
|
|
|
|
floor(N) when N >= 0 ->
|
|
trunc(N);
|
|
floor(N) when N < 0 ->
|
|
Int = trunc(N),
|
|
if
|
|
Int==N -> Int;
|
|
true -> Int-1
|
|
end.
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% TESTS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
%% emulates as if a forum-type website has a Site tz, and a user-specified tz
|
|
-define(SITE_TZ,"PST").
|
|
-define(USER_TZ,<<"CST">>).
|
|
-define(SELF_TZ,"EST"). %% Self will be the pid of the current running process
|
|
-define(SITE_KEY,test_site_key).
|
|
-define(USER_KEY,test_user_key).
|
|
|
|
tz_test_() ->
|
|
{
|
|
setup,
|
|
fun start_test/0,
|
|
fun stop_test/1,
|
|
fun(SetupData) ->
|
|
{inorder,[
|
|
simple_test(SetupData),
|
|
compare_test(SetupData),
|
|
tz_tests(SetupData),
|
|
parser_format_test(SetupData),
|
|
test_deterministic_parser(SetupData),
|
|
test_disambiguation(SetupData),
|
|
arith_tests(SetupData)
|
|
]}
|
|
end
|
|
}.
|
|
|
|
test_deterministic_parser(_) ->
|
|
{inorder, [
|
|
?_assertEqual(ok, application:set_env(qdate, deterministic_parsing, {now, now})),
|
|
?_assertEqual({date(), {7,0,0}}, qdate:to_date("7am")),
|
|
?_assertEqual({{2012,5,10}, time()}, qdate:to_date("2012-5-10")),
|
|
|
|
?_assertEqual(ok, application:set_env(qdate, deterministic_parsing, {zero, zero})),
|
|
?_assertEqual({{1970,1,1}, {7,0,0}}, qdate:to_date("7am")),
|
|
?_assertEqual({{2012,5,10}, {0,0,0}}, qdate:to_date("2012-5-10")),
|
|
|
|
?_assertEqual(ok, application:unset_env(qdate, deterministic_parsing)),
|
|
?_assertEqual({{1970,1,1}, {7,0,0}}, qdate:to_date("7am")),
|
|
?_assertEqual({{2012,5,10}, {0,0,0}}, qdate:to_date("2012-5-10"))
|
|
]}.
|
|
|
|
test_disambiguation(_) ->
|
|
{inorder, [
|
|
?_assertEqual(ok, set_timezone("America/New York")),
|
|
?_assertEqual({ambiguous, {{2013,11,3},{6,0,0}}, {{2013,11,3},{5,0,0}}}, qdate:to_date("GMT",both,{{2013,11,3},{1,0,0}})),
|
|
?_assertEqual({{2013,11,3},{6,0,0}}, qdate:to_date("GMT",prefer_standard,{{2013,11,3},{1,0,0}})),
|
|
?_assertEqual({{2013,11,3},{5,0,0}}, qdate:to_date("GMT",prefer_daylight,{{2013,11,3},{1,0,0}})),
|
|
?_assertEqual({ambiguous, "GMT","GMT"}, qdate:to_string("T", "GMT", both, {{2013,11,3},{1,0,0}})),
|
|
?_assertEqual({ambiguous, "EST","EDT"}, qdate:to_string("T", auto, both, {{2013,11,3},{1,0,0}})),
|
|
?_assertEqual(ok, set_timezone("GMT")),
|
|
?_assertEqual({ambiguous, {{2013,11,3},{1,0,0}}, {{2013,11,3},{2,0,0}}}, qdate:to_date("America/New York", both, {{2013,11,3},{6,0,0}})),
|
|
?_assertEqual({{2013,11,3},{2,0,0}}, qdate:to_date("America/New York", prefer_daylight, {{2013,11,3},{6,0,0}})),
|
|
?_assertEqual({{2013,11,3},{1,0,0}}, qdate:to_date("America/New York", prefer_standard, {{2013,11,3},{6,0,0}}))
|
|
]}.
|
|
|
|
|
|
tz_tests(_) ->
|
|
{inorder,[
|
|
?_assertEqual(ok,set_timezone(<<"Europe/Moscow">>)),
|
|
?_assertEqual("Europe/Moscow", get_timezone()),
|
|
?_assertEqual(ok,set_timezone(?SELF_TZ)),
|
|
?_assertEqual(?SELF_TZ,get_timezone()),
|
|
?_assertEqual("CST",get_timezone(?USER_KEY)),
|
|
?_assertEqual(?SITE_TZ,get_timezone(?SITE_KEY)),
|
|
?_assertEqual({{2013,3,7},{0,0,0}}, to_date(?USER_KEY,"3/7/2013 1:00am EST")),
|
|
?_assertEqual({{2013,3,7},{0,0,0}}, to_date(?SITE_KEY,"3/7/2013 3:00am EST")),
|
|
?_assertEqual({{2013,3,7},{2,0,0}}, to_date("3/7/2013 1:00am CST")), %% will use the current pid's setting
|
|
?_assertEqual("America/Chicago",to_string("e","America/Chicago","3/7/2013 1:00am")),
|
|
?_assertEqual("-0500",to_string("O","EST","3/7/2013 1:00am CST")),
|
|
?_assertEqual("-05:00",to_string("P","EST","3/7/2013 1:00am CST")),
|
|
?_assertEqual("EST",to_string("T","America/New York","3/7/2013 1:00am CST")),
|
|
?_assertEqual(?SITE_TZ,to_string("T",?SITE_KEY,"3/7/2013 1:00am CST")),
|
|
?_assertEqual(integer_to_list(-5 * 3600), to_string("Z","EST","3/7/2013 1:00am CST")),
|
|
?_assertEqual("Thu, 07 Mar 2013 13:15:00 -0500", to_string("r","EST", "3/7/2013 1:15:00pm")),
|
|
?_assertEqual("2013-03-07T13:15:00-05:00", to_string("c", "EST", "3/7/2013 1:15:00pm")),
|
|
|
|
?_assertEqual({{2013,3,7},{6,0,0}}, to_date("GMT","3/7/2013 12:00am -0600")),
|
|
?_assertEqual({{2013,3,7},{6,0,0}}, to_date("GMT","3/7/2013 12:00am -600")),
|
|
?_assertEqual({{2013,3,7},{6,0,0}}, to_date("GMT","3/7/2013 12:00am GMT-0600")),
|
|
?_assertEqual({{2013,3,7},{6,0,0}}, to_date("GMT","3/7/2013 12:00am utc-0600")),
|
|
?_assertEqual({{2013,3,7},{1,0,0}}, to_date("EST","3/7/2013 12:00am utc-0600")),
|
|
?_assertEqual({{2013,3,6},{18,0,0}}, to_date("GMT","3/7/2013 12:00am +0600")),
|
|
?_assertEqual({{2013,3,6},{12,0,0}}, to_date("CST","3/7/2013 12:00am +0600")),
|
|
|
|
%% These next two test check to make sure that the tz database properly
|
|
%% interprets GMT+/-X timezones (an earlier issue with
|
|
%% erlang_localtime's tz database had it incrementing/decrementing the
|
|
%% minute field rather than hours.
|
|
%%
|
|
%% It also ensures that GMT+/-X handling is interpreted the way you'd
|
|
%% intuitively expect, rather than the POSIX way, which is, quite
|
|
%% frankly, broken.
|
|
?_assertEqual({{2013,3,7},{10,0,0}}, to_date("GMT-0","3/7/2013 10:00am GMT")),
|
|
?_assertEqual({{2013,3,7},{10,0,0}}, to_date("GMT+0","3/7/2013 10:00am GMT")),
|
|
?_assertEqual({{2013,3,7},{9,0,0}}, to_date("GMT-1","3/7/2013 10:00am GMT")),
|
|
?_assertEqual({{2013,3,7},{11,0,0}}, to_date("GMT+1","3/7/2013 10:00am GMT")),
|
|
|
|
|
|
%% parsing, then reformatting the same time with a different timezone using the php "r" (rfc2822)
|
|
?_assertEqual("Thu, 07 Mar 2013 12:15:00 -0600",
|
|
to_string("r","CST",to_string("r","EST",{{2013,3,7},{13,15,0}}))),
|
|
|
|
%% A bunch of unixtime and now tests with timezones
|
|
?_assertEqual("1987-08-10 00:59:15 GMT",to_string("Y-m-d H:i:s T","GMT",555555555)),
|
|
?_assertEqual("1987-08-09 19:59:15 CDT",to_string("Y-m-d H:i:s T","CDT",555555555)),
|
|
?_assertEqual("1987-08-09 20:59:15 EDT",to_string("Y-m-d H:i:s T","America/New York",555555555)),
|
|
?_assertEqual(ok, set_timezone("GMT")),
|
|
?_assertEqual(555555555,to_unixtime("1987-08-10 00:59:15 GMT")),
|
|
?_assertEqual({555,555555,0},to_now("1987-08-10 00:59:15 GMT")),
|
|
?_assertEqual(ok, set_timezone("EST")),
|
|
?_assertEqual(555555555,to_unixtime("1987-08-10 00:59:15 GMT")),
|
|
?_assertEqual({555,555555,0},to_now("1987-08-10 00:59:15 GMT")),
|
|
?_assertEqual(ok, set_timezone("GMT")),
|
|
?_assertEqual({{1970, 1, 1}, {1, 0, 0}}, to_date("CET", "1970-01-01T00:00:00Z")),
|
|
?_assertEqual(ok, set_timezone("UTC")),
|
|
?_assertEqual(1521945120, to_unixtime("2018-3-25T2:32:00")),
|
|
?_assertEqual(true, between("-1 seconds", os:timestamp(), "+1 seconds")),
|
|
?_assertEqual(true, between("60 hours ago", unixtime(), "in 15 days")),
|
|
?_assertEqual(false, between("+1 seconds", qdate:to_string("n/j/Y g:ia"), "+2 seconds")),
|
|
?_assertEqual(false, between("5 seconds ago","1 second ago"))
|
|
]}.
|
|
|
|
|
|
simple_test(_) ->
|
|
{inorder,[
|
|
?_assertEqual(ok,clear_timezone()),
|
|
?_assertEqual(0,to_unixtime({0,0,0})),
|
|
?_assertEqual({0,0,0},to_now(0)),
|
|
?_assertEqual(0,to_unixtime("1970-01-01 12:00am GMT")),
|
|
?_assertEqual(21600,to_unixtime("1970-01-01 12:00am CST")),
|
|
?_assertEqual(0,to_unixtime({{1970,1,1},{0,0,0}})),
|
|
?_assertEqual({{1970,1,1},{0,0,0}},to_date(0)),
|
|
?_assertEqual({{2013,3,7},{0,0,0}},to_date(to_unixtime("2013-03-07 12am"))),
|
|
?_assertEqual("2013-12-21 12:24pm",to_string("Y-m-d g:ia",{{2013,12,21},{12,24,21}})),
|
|
?_assertEqual("2012-12-01 1:00pm", to_string("Y-m-d g:ia","EST","2012-12-01 12:00pm CST")),
|
|
?_assertEqual(<<"2012-12-01 1:00pm">>, to_string(<<"Y-m-d g:ia">>,"EST","2012-12-01 12:00pm CST")),
|
|
?_assertEqual(<<"2012-12-01 1:00pm">>, to_string(<<"Y-m-d g:ia">>,"EST",<<"2012-12-01 12:00pm CST">>)),
|
|
?_assertEqual("2012-12-01 1:00pm", to_string("Y-m-d g:ia","EST",<<"2012-12-01 12:00pm CST">>)),
|
|
?_assertEqual("2012-12-01 1:00pm", to_string("Y-m-d g:ia",<<"EST">>,<<"2012-12-01 12:00pm CST">>)),
|
|
?_assertEqual(to_unixtime("2012-01-01 12:00pm CST"), to_unixtime("2012-01-01 10:00am PST")),
|
|
?_assertEqual({{2012,12,31},{18,15,15}},to_date("Dec 31, 2012 6:15:15pm")),
|
|
?_assertEqual({{2013,1,1},{0,15,15}},to_date("GMT", "December 31, 2012 6:15:15pm CST"))
|
|
]}.
|
|
|
|
|
|
compare_test(_) ->
|
|
{inorder,[
|
|
?_assertEqual(true, compare({{2013,9,10},{0,0,0}},'=',"Sep 10th, 2013 12:00am")),
|
|
?_assertEqual(true, compare("9/10/2013 1am EDT",'==',"Sep 10th, 2013 12:00:00am CDT")),
|
|
?_assertEqual(true, compare({{2013,9,10},{0,0,0}},'=<',"Sep 10th, 2013 12:00am")),
|
|
?_assertEqual(false, compare({{2013,9,10},{0,0,1}},'=',"Sep 10th, 2013 12:00am")),
|
|
?_assertEqual(true, compare({{2013,9,10},{0,0,1}},'=/=',"Sep 10th, 2013 12:00am")),
|
|
?_assertEqual(true, compare({{2013,9,10},{0,0,1}},'>',"Sep 10th, 2013 12:00am")),
|
|
?_assertEqual(false, compare({{2013,9,10},{0,0,1}},'<',"Sep 10th, 2013 12:00am")),
|
|
?_assertEqual(true, compare({{2013,9,10},{0,0,1}},'<',"Sep 10th, 2013 12:00:02am")),
|
|
?_assertEqual(true, compare({{2013,9,10},{0,0,1}},'<',"Sep 10th, 2013 12:02am")),
|
|
?_assertEqual(true, compare({{2013,9,10},{0,0,1}},'<',"Sep 10th, 2013 1am")),
|
|
?_assertEqual(true, compare({{2013,9,9},{23,59,59}},'<',"Sep 10th, 2013 12am")),
|
|
?_assertEqual(false, compare({{2013,9,9},{23,59,59}},'>',"Sep 10th, 2013 12am")),
|
|
?_assertEqual(true, compare("11am EST",'==',"10am CST"))
|
|
]}.
|
|
|
|
parser_format_test(_) ->
|
|
{inorder,[
|
|
?_assertEqual({{2008,2,8},{0,0,0}},to_date("20080208")),
|
|
?_assertThrow({ec_date,{bad_date,_}},to_date("20111232")), %% invalid_date with custom format
|
|
?_assertEqual("2/8/2008",to_string(shortdate,{{2008,2,8},{0,0,0}})),
|
|
?_assertEqual("2/8/2008",to_string(shortdate,"20080208")), %% both regged format and parser
|
|
?_assertEqual("2/8/2008 12:00am",to_string(longdate,"2008-02-08 12:00am")),
|
|
?_assertEqual("2/8/2008 12:00am",to_string(longdate,"20080208"))
|
|
]}.
|
|
|
|
arith_tests(_) ->
|
|
{inorder,[
|
|
?_assertEqual({{2012,2,29},{23,59,59}}, to_date(add_seconds(-1, {{2012,3,1},{0,0,0}}))),
|
|
?_assertEqual({{2013,2,28},{23,59,59}}, to_date(add_seconds(-1, {{2013,3,1},{0,0,0}}))),
|
|
?_assertEqual({{2015,1,1},{0,0,0}}, to_date(add_years(1, {{2014,1,1},{0,0,0}}))),
|
|
?_assertEqual({{2015,1,1},{0,0,0}}, to_date(add_seconds(1, {{2014,12,31},{23,59,59}}))),
|
|
?_assertEqual({{2015,1,1},{0,0,59}}, to_date(add_minutes(1, {{2014,12,31},{23,59,59}}))),
|
|
?_assertEqual({{2015,1,1},{0,59,59}}, to_date(add_hours(1, {{2014,12,31},{23,59,59}}))),
|
|
?_assertEqual({{2015,1,1},{23,59,59}}, to_date(add_days(1, {{2014,12,31},{23,59,59}}))),
|
|
?_assertEqual({{2015,1,7},{23,59,59}}, to_date(add_weeks(1, {{2014,12,31},{23,59,59}}))),
|
|
?_assertEqual({{2015,1,31},{23,59,59}}, to_date(add_months(1, {{2014,12,31},{23,59,59}}))),
|
|
?_assertEqual({{2015,2,28},{0,0,0}}, to_date(add_months(2, {{2014,12,31},{0,0,0}}))),
|
|
?_assertEqual({{2016,2,28},{0,0,0}}, to_date(add_years(1, {{2015,2,28},{0,0,0}}))),
|
|
?_assertEqual({{2014,2,28},{0,0,0}}, to_date(add_months(-24, {{2016,2,29},{0,0,0}}))),
|
|
?_assertEqual({{2012,2,29},{0,0,0}}, to_date(add_months(-48, {{2016,2,29},{0,0,0}}))),
|
|
?_assertEqual({{2016,2,29},{0,0,0}}, to_date(add_months(-1, {{2016,3,31},{0,0,0}}))),
|
|
?_assertEqual({{2017,2,28},{0,0,0}}, to_date(add_years(1, {{2016,2,29},{0,0,0}}))),
|
|
?_assertEqual({{2015,3,1},{0,0,0}}, to_date(add_days(1, {{2015,2,28},{0,0,0}}))),
|
|
?_assertEqual({{2015,3,3},{0,0,0}}, to_date(add_days(3, {{2015,2,28},{0,0,0}})))
|
|
]}.
|
|
|
|
|
|
start_test() ->
|
|
application:start(qdate),
|
|
set_timezone(?SELF_TZ),
|
|
set_timezone(?SITE_KEY,?SITE_TZ),
|
|
set_timezone(?USER_KEY,?USER_TZ),
|
|
register_parser(compressed,fun compressed_parser/1),
|
|
register_parser(microsoft_date,fun microsoft_parser/1),
|
|
register_parser(parse_relative, fun parse_relative/1),
|
|
register_format(shortdate,"n/j/Y"),
|
|
register_format(longdate,"n/j/Y g:ia").
|
|
|
|
compressed_parser(List) when length(List)==8 ->
|
|
try re:run(List,"^(\\d{4})(\\d{2})(\\d{2})$",[{capture,all_but_first,list}]) of
|
|
nomatch -> undefined;
|
|
{match, [Y,M,D]} ->
|
|
Date = {list_to_integer(Y),list_to_integer(M),list_to_integer(D)},
|
|
case calendar:valid_date(Date) of
|
|
true ->
|
|
{Date,{0,0,0}};
|
|
false -> undefined
|
|
end
|
|
catch
|
|
_:_ -> undefined
|
|
end;
|
|
compressed_parser(_) ->
|
|
undefined.
|
|
|
|
microsoft_parser(FloatDate) when is_float(FloatDate) ->
|
|
try
|
|
DaysSince1900 = floor(FloatDate),
|
|
Days0to1900 = calendar:date_to_gregorian_days(1900,1,1),
|
|
GregorianDays = Days0to1900 + DaysSince1900,
|
|
Date = calendar:gregorian_days_to_date(GregorianDays),
|
|
Seconds = round(86400 * (FloatDate - DaysSince1900)),
|
|
Time = calendar:seconds_to_time(Seconds),
|
|
{Date,Time}
|
|
catch
|
|
_:_ -> undefined
|
|
end;
|
|
microsoft_parser(_) ->
|
|
undefined.
|
|
|
|
|
|
|
|
stop_test(_) ->
|
|
ok.
|