% vim: ts=4 sw=4 et
% Copyright (c) 2013-2023 Jesse Gumm
% See LICENSE for licensing information.
%
-module(qdate).
%-compile(export_all).
-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/0,
beginning_week/1,
beginning_week/2,
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_week/0,
end_week/1,
end_week/2,
end_month/1,
end_month/0,
end_year/1,
end_year/0
]).
-export([
compare/2,
compare/3,
between/2,
between/3,
between/5
]).
-export([
sort/1,
sort/2,
sort/3
]).
-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([
age/1,
age/2,
age_days/1,
age_days/2
]).
-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
]).
-type qdate() :: any().
-type datetime() :: {{integer(), integer(), integer()}, {integer(), integer(), integer()}} |
{{integer(), integer(), integer()}, {integer(), integer(), integer(), integer()}}.
-type erlnow() :: {integer(), integer(), integer()}.
-type binary_or_string() :: binary() | string().
-type disambiguate() :: prefer_standard | prefer_daylight | both.
%% erlang:get_stacktrace/0 is deprecated in OTP 21
-ifndef(OTP_RELEASE).
-define(WITH_STACKTRACE(T, R, S), T:R -> S = erlang:get_stacktrace(), ).
-else.
-define(WITH_STACKTRACE(T, R, S), T:R:S ->).
-endif.
%% 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).
start() ->
application:ensure_all_started(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).
-spec to_string(Format :: any(), ToTZ :: any(), Disambiguate :: disambiguate(), Date :: qdate()) -> binary_or_string() | {ambiguous, binary_or_string() , binary_or_string()}.
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 = case Date of
{_, {_,_,_,_}} ->
%% Have milliseconds
"H:i:s.fP";
_ ->
"H:i:sP"
end,
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).
-spec to_date(ToTZ :: any(), Disambiguate :: disambiguate(), RawDate :: any()) -> {ambiguous, datetime(), datetime()} | datetime().
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,
PreserveMs = preserve_ms(),
try raw_to_date(RawDate3) of
D={{_,_,_},{_,_,_}} ->
date_tz_to_tz(D, Disambiguate, FromTZ, ToTZ);
{{Year, Month, Date},{Hour,Minute,Second,Millis}} when PreserveMs ->
{ODate, {OHour,OMinute,OSecond}} = date_tz_to_tz({{Year, Month, Date},{Hour,Minute,Second}}, Disambiguate, FromTZ, ToTZ),
{ODate, {OHour,OMinute,OSecond, Millis}};
{{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).
-spec to_unixtime(Disamb :: disambiguate(), qdate()) -> {ambiguous, integer(), integer()} | integer().
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).
-spec to_now(Disamb :: disambiguate(), qdate()) -> erlnow() | {ambiguous, erlnow(), erlnow()}.
to_now(_, Now = {_,_,_}) ->
Now;
to_now(Disamb, ToParse) ->
case to_unixtime(Disamb, ToParse) of
{ambiguous, Standard, Daylight} when is_integer(Standard), is_integer(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}}.
beginning_week() ->
beginning_week({date(), time()}).
%% 1 = Monday, 7 = Sunday
beginning_week(Date) ->
beginning_week(1, Date).
beginning_week(BeginningDayOfWeek, Date) when is_atom(BeginningDayOfWeek) ->
DOW = weekday_map(BeginningDayOfWeek),
beginning_week(DOW, Date);
beginning_week(BeginningDayOfWeek, Date0) when
BeginningDayOfWeek >= 1,
BeginningDayOfWeek =< 7,
is_integer(BeginningDayOfWeek) ->
{DateOnly, _} = Date = to_date(Date0),
CurDOW = calendar:day_of_the_week(DateOnly),
if
CurDOW==BeginningDayOfWeek ->
{DateOnly, {0,0,0}};
CurDOW > BeginningDayOfWeek->
Diff = CurDOW - BeginningDayOfWeek,
beginning_day(add_days(-Diff, Date));
CurDOW < BeginningDayOfWeek ->
Diff = 7 - (BeginningDayOfWeek - CurDOW),
beginning_day(add_days(-Diff, Date))
end.
weekday_map(mon) -> 1;
weekday_map(tue) -> 2;
weekday_map(wed) -> 3;
weekday_map(thu) -> 4;
weekday_map(fri) -> 5;
weekday_map(sat) -> 6;
weekday_map(sun) -> 7;
weekday_map(monday) -> 1;
weekday_map(tuesday) -> 2;
weekday_map(wednesday) -> 3;
weekday_map(thursday) -> 4;
weekday_map(friday) -> 5;
weekday_map(saturday) -> 6;
weekday_map(sunday) -> 7.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%% 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}}.
end_week() ->
end_week({date(), time()}).
end_week(Date) ->
end_week(1, Date).
end_week(BeginningDayOfWeek, Date) ->
Beginning = beginning_week(BeginningDayOfWeek, Date),
PlusWeek = add_weeks(1, Beginning),
add_seconds(-1, PlusWeek).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Comparisons %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
-spec compare(A :: qdate(), B :: qdate()) -> integer().
compare(A, B) ->
NowA = to_now(A),
NowB = to_now(B),
if
NowA == NowB -> 0;
NowA < NowB -> -1;
NowA > NowB -> 1
end.
-spec compare(A :: qdate(), Op :: atom(), B :: qdate()) -> boolean().
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).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Sorting %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
sort(List) ->
sort('=<', List).
sort(Op, List) ->
sort(Op, List, [{non_dates, back}]).
sort(Op, List, Opts) ->
NonDateOpt = proplists:get_value(non_dates, Opts, back),
WithNorm = add_sort_normalization(List, NonDateOpt),
SortFun = make_sort_fun(Op, NonDateOpt),
Sorted = lists:sort(SortFun, WithNorm),
strip_sort_normalization(Sorted).
%% Normalization pre-processes the dates (converting them to unixtimes for easy
%% comparison, and also tags non-dates (dates that crashed during parsing) as such
add_sort_normalization(List, NonDateOpt) ->
lists:map(fun(Date) ->
Sortable = try to_unixtime(Date)
catch _:_ when NonDateOpt=/=crash ->
{non_date, Date}
end,
{Sortable, Date}
end, List).
%% Remove the normalization tag to return the original term
strip_sort_normalization(List) ->
[Date || {_, Date} <- List].
-spec make_sort_fun(Op :: atom(), NonDateOpt :: front | back) -> fun().
make_sort_fun(Op, NonDateOpt) ->
DateComp = sort_op_comp_fun(Op),
fun({{non_date, A}, _}, {{non_date, B},_}) ->
DateComp(A,B);
({{non_date, _}, _}, _) when NonDateOpt == front ->
true;
({{non_date, _}, _}, _) when NonDateOpt == back ->
false;
(_, {{non_date, _}, _}) when NonDateOpt == front ->
false;
(_, {{non_date, _}, _}) when NonDateOpt == back ->
true;
(A, B) ->
DateComp(A, B)
end.
sort_op_comp_fun(Op) ->
fun(A, B) ->
case Op of
'before'-> A < B;
'<' -> A < B;
'<=' -> A =< B;
'=<' -> A =< B;
'after' -> A > B;
'>' -> A > B;
'>=' -> A >= B;
'=>' -> A >= B
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 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()).
-type unit() :: second | seconds |
minute | minutes |
hour | hours |
day | days |
week | weeks |
month | months |
year | years.
-spec add_unit(Unit :: unit(), Value :: integer(), Date :: qdate()) -> qdate().
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}.
%% Originally, this function didn't recurse. Here's the story. Some numbers,
%% like M = 12 (December) or M = -11 (January) would trigger an overflow or
%% underflow, resulting in fix_year_month returning something nonsensical like
%% {2018, 13}. I added some extra clauses to special treat those "overflow but
%% shouldn't" situations, but realized it was just cleaner to recurse, calling
%% fix_year_month on the calculated result, knowing that the numbers will
%% normalize on their own. So for all the clauses of fix_year_month, we recurse
%% as a sanity check, eventually only returning the result of the "Everything
%% Looks good" clause at the bottom.
fix_year_month({Y, M}) when M > 12 ->
YearsOver = M div 12,
fix_year_month({Y + YearsOver, M-(YearsOver*12)});
fix_year_month({Y, M}) when M < 1 ->
YearsUnder = (abs(M-1) div 12) + 1,
fix_year_month({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.
age(Birth) ->
age(Birth, os:timestamp()).
age(Birth, Now) ->
%% B=Birth
{{BY, BM, BD}, _} = to_date(Birth),
%% C=Current
{{CY, CM, CD}, _} = to_date(Now),
if
(CM > BM);
(CM == BM andalso CD >= BD)
-> CY - BY;
true ->
(CY - BY) - 1
end.
age_days(Birth) ->
age_days(Birth, os:timestamp()).
age_days(Birth, Now) ->
case {to_date(Birth), to_date(Now)} of
{{SameDay, _}, {SameDay, _}} ->
0;
{{BirthDate, BirthTime}, {NowDate, NowTime}} ->
BirthDays = calendar:date_to_gregorian_days(BirthDate),
NowDays = calendar:date_to_gregorian_days(NowDate),
DiffDays = NowDays - BirthDays,
if
NowTime >= BirthTime ->
DiffDays;
true ->
DiffDays-1
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 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;
0 -> {'+', 0, 0};
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).
preserve_ms() ->
application:get_env(qdate, preserve_ms, false).
%% 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.
-spec date_tz_to_tz(Date :: datetime(), Disambiguate :: disambiguate(), FromTZ :: any(), ToTZ :: any()) ->
datetime() | {ambiguous, datetime(), datetime()}.
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.
-spec date_tz_to_tz_both(Date :: datetime(), FromTZ :: string(), ToTZ :: string()) -> datetime() | {ambiguous, datetime(), datetime()}.
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;
true ->
{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
?WITH_STACKTRACE(Error, Reason, 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 = flooring(T/1000000),
Secs = T - MegaSec*1000000,
{MegaSec,Secs,0}.
unixtime_to_date(T) ->
Now = unixtime_to_now(T),
calendar:now_to_datetime(Now).
flooring(N) when N >= 0 ->
erlang:trunc(N);
flooring(N) when N < 0 ->
Int = erlang: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
}.
tz_preserve_ms_true_test_() ->
{
setup,
fun start_test/0,
fun stop_test/1,
fun(SetupData) ->
{inorder,[
preserve_ms_true_tests(SetupData)
]}
end
}.
tz_preserve_ms_false_test_() ->
{
setup,
fun start_test/0,
fun stop_test/1,
fun(SetupData) ->
{inorder,[
preserve_ms_false_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({{2017,2,1},{0,0,0}}, to_date(add_months(-11, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2017,1,1},{0,0,0}}, to_date(add_months(-12, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2016,12,1},{0,0,0}}, to_date(add_months(-13, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2018,12,1},{0,0,0}}, to_date(add_months(11, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2019,1,1},{0,0,0}}, to_date(add_months(12, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2019,2,1},{0,0,0}}, to_date(add_months(13, {{2018,1,1},{0,0,0}}))),
?_assertEqual({{2018,1,1},{0,0,0}}, to_date(add_months(-11, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2017,12,1},{0,0,0}}, to_date(add_months(-12, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2017,11,1},{0,0,0}}, to_date(add_months(-13, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2019,11,1},{0,0,0}}, to_date(add_months(11, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2019,12,1},{0,0,0}}, to_date(add_months(12, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2020,1,1},{0,0,0}}, to_date(add_months(13, {{2018,12,1},{0,0,0}}))),
?_assertEqual({{2014,2,28},{0,0,0}}, to_date(add_months(-24, {{2016,2,29},{0,0,0}}))),
?_assertEqual({{2018,12,15},{0,0,0}}, to_date(add_months(24, {{2016,12,15},{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}}))),
?_assertEqual({{2017,1,2},{0,0,0}}, beginning_week({{2017,1,2},{0,0,0}})),
?_assertEqual({{2017,1,2},{0,0,0}}, beginning_week({{2017,1,3},{0,0,0}})),
?_assertEqual({{2017,1,3},{0,0,0}}, beginning_week(2, {{2017,1,4},{0,0,0}})),
?_assertEqual({{2016,12,29},{0,0,0}}, beginning_week(4, {{2017,1,4},{0,0,0}})),
?_assertEqual({{2016,12,31},{0,0,0}}, beginning_week(6, {{2017,1,6},{0,0,0}})),
?_assertEqual({{2017,1,1},{0,0,0}}, beginning_week(7, {{2017,1,6},{0,0,0}})),
?_assertEqual(0, age("1981-01-15", "1981-12-31")),
?_assertEqual(39, age("1981-01-15", "2020-01-15")),
?_assertEqual(39, age("1981-01-15", "2020-01-15 12am")),
?_assertEqual(38, age("1981-01-15", "2020-01-14")),
?_assertEqual(38, age("1981-01-15", "2020-01-14 11:59pm")),
%% checking pre-unix-epoch
?_assertEqual(100, age("1901-01-01","2001-01-01")),
?_assertEqual(20, age("1900-01-01", "1920-01-01")),
?_assertEqual(0, age_days("2020-11-20 12am","2020-11-20 11:59:59pm")),
?_assertEqual(1, age_days("2020-11-19 11:59:59pm","2020-11-20 11:59:59pm")),
?_assertEqual(7, age_days("2020-11-20","2020-11-27")),
?_assertEqual(7, age_days("2020-11-27","2020-12-04"))
]}.
preserve_ms_true_tests(_) ->
application:set_env(qdate, preserve_ms, true),
{inorder, [
?_assertEqual({{2021,5,8},{23,0,16,472000}}, qdate:to_date(<<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16.472000+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:s.fP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:sP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16.472000+00:00">>, qdate:to_string(<<"c">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>))
]}.
preserve_ms_false_tests(_) ->
application:set_env(qdate, preserve_ms, false),
{inorder, [
?_assertEqual({{2021,5,8},{23,0,16}}, qdate:to_date(<<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16.0+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:s.fP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16+00:00">>, qdate:to_string(<<"Y-m-d\\TH:i:sP">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>)),
?_assertEqual(<<"2021-05-08T23:00:16+00:00">>, qdate:to_string(<<"c">>, <<"UTC">>,<<"2021-5-09 0:0:16.472+01:00">>))
]}.
start_test() ->
qdate:start(),
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 = flooring(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.