%% @author Rusty Klophaus
%% @copyright Copyright (c) 2008-2009 Rusty Klophaus, Copyright (c) 2009-2023 Marc Worrell
%% @doc Conversion functions for all kinds of data types. Changes to
%% Rusty's version: added date conversion, undefined handling and more
%% to_bool cases.
%% @end
%% Copyright 2008-2009 Rusty Klophaus
%% Copyright 2009-2023 Marc Worrell
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
-module(z_convert).
-author("Rusty Klophaus").
-author("Marc Worrell <marc@worrell.nl>").
-author("Arjan Scherpenisse <arjan@scherpenisse.net>").
-define(ST_JUTTEMIS, {{9999,8,17}, {12,0,0}}).
-export ([
clean_lower/1,
to_list/1,
to_flatlist/1,
to_atom/1,
to_binary/1,
to_binary/2,
to_integer/1,
to_float/1,
to_bool_strict/1,
to_bool/1,
to_utc/1,
to_localtime/1,
to_datetime/1,
to_date/1,
to_time/1,
to_isotime/1,
to_json/1,
unicode_to_utf8/1,
convert_json/1,
ip_to_list/1,
ip_to_long/1,
long_to_ip/1
]).
%%% CONVERSION %%%
%% @doc Convert to lower case, strip surrounding whitespace.
-spec clean_lower(binary()|list()|atom()) -> binary().
clean_lower(L) -> z_string:trim(z_string:to_lower(L)).
%% @doc Convert (almost) any value to a list.
-spec to_list(term()) -> string().
to_list(undefined) -> "";
to_list(<<>>) -> "";
to_list({rsc_list, L}) -> L;
to_list(L) when is_list(L) -> L;
to_list(A) when is_atom(A) -> atom_to_list(A);
to_list(B) when is_binary(B) -> binary_to_list(B);
to_list(I) when is_integer(I) -> integer_to_list(I);
to_list(F) when is_float(F) -> z_mochinum:digits(F).
%% @doc Flatten list and convert to string.
-spec to_flatlist(term()) -> string().
to_flatlist(L) when is_list(L) ->
case z_string:is_string(L) of
true -> L;
false -> lists:flatten(to_list(iolist_to_binary(L)))
end;
to_flatlist(L) ->
lists:flatten(to_list(L)).
%% @doc Convert (almost) any value to an atom.
-spec to_atom(term()) -> atom() | undefined.
to_atom(<<>>) -> undefined;
to_atom("") -> undefined;
to_atom(A) when is_atom(A) -> A;
to_atom(B) when is_binary(B) -> binary_to_atom(B, utf8);
to_atom(I) when is_integer(I) -> list_to_atom(integer_to_list(I));
to_atom(L) when is_list(L) -> list_to_atom(binary_to_list(iolist_to_binary(L))).
%% @doc Convert (almost) any value to an atom.
-spec to_binary(term()) -> binary().
to_binary(undefined) -> <<>>;
to_binary(A) when is_atom(A) -> atom_to_binary(A, utf8);
to_binary(B) when is_binary(B) -> B;
to_binary(I) when is_integer(I) -> integer_to_binary(I);
to_binary(F) when is_float(F) -> to_binary(to_list(F));
to_binary(L) when is_list(L) -> iolist_to_binary(L);
to_binary({trans, [ {_, B} | _ ] = Tr}) when is_binary(B) ->
case proplists:get_value(en, Tr) of
undefined -> B;
V -> to_binary(V)
end;
to_binary({{_, _, _}, {_, _, _}} = DT) ->
to_binary(z_dateformat:format(DT, "Y-m-d H:i:s", []));
to_binary({trans, []}) ->
<<>>.
%% Specific Zotonic callback, please keep here.
to_binary({trans, _} = Tr, Context) -> to_binary(trans_lookup_fallback(Tr, Context));
to_binary(A, _Context) -> to_binary(A).
% Add nowarn because the z_trans module is optional (and from Zotonic core)
-dialyzer({[ nowarn_function ], trans_lookup_fallback/2}).
trans_lookup_fallback(Tr, Context) ->
z_trans:lookup_fallback(Tr, Context).
%% @doc Convert (almost) any value to an integer.
-spec to_integer(term()) -> integer() | undefined.
to_integer(undefined) -> undefined;
to_integer("") -> undefined;
to_integer(<<>>) -> undefined;
to_integer(A) when is_atom(A) -> to_integer(atom_to_list(A));
to_integer(B) when is_binary(B) -> binary_to_integer(B);
to_integer(I) when is_integer(I) -> I;
to_integer(F) when is_float(F) -> erlang:round(F);
to_integer([C]) when is_integer(C) andalso (C > $9 orelse C < $0) -> C;
to_integer(L) when is_list(L) -> list_to_integer(L).
%% @doc Convert (almost) any value to a float.
-spec to_float(term()) -> float() | undefined.
to_float(undefined) -> undefined;
to_float([]) -> undefined;
to_float(A) when is_atom(A) -> to_float(atom_to_list(A));
to_float(B) when is_binary(B) -> to_float(binary_to_list(B));
to_float(I) when is_integer(I) -> I + 0.0;
to_float(F) when is_float(F) -> F;
to_float(L) when is_list(L) ->
case lists:member($., L) of
true -> list_to_float(L);
false -> list_to_float(L++".0") %% list_to_float("1") gives a badarg
end.
%% @doc Quite loose conversion of values to boolean
-spec to_bool(term()) -> boolean().
to_bool("false") -> false;
to_bool("FALSE") -> false;
to_bool("n") -> false;
to_bool("N") -> false;
to_bool("no") -> false;
to_bool("NO") -> false;
to_bool(<<"false">>) -> false;
to_bool(<<"FALSE">>) -> false;
to_bool(<<"n">>) -> false;
to_bool(<<"N">>) -> false;
to_bool(<<"no">>) -> false;
to_bool(<<"NO">>) -> false;
to_bool("disabled") -> false;
to_bool(<<"disabled">>) -> false;
to_bool("DISABLED") -> false;
to_bool(<<"DISABLED">>) -> false;
to_bool([0]) -> false;
to_bool(V) -> to_bool_strict(V).
% @doc Convert values to boolean values according to the Django rules
-spec to_bool_strict(term()) -> boolean().
to_bool_strict(undefined) -> false;
to_bool_strict(false) -> false;
to_bool_strict(0) -> false;
to_bool_strict(0.0) -> false;
to_bool_strict(<<>>) -> false;
to_bool_strict(<<0>>) -> false;
to_bool_strict([]) -> false;
to_bool_strict({rsc_list, []}) -> false;
to_bool_strict({trans, []}) -> false;
to_bool_strict({trans, Tr}) ->
lists:any(
fun({_, V}) -> V =/= <<>> end,
Tr);
to_bool_strict("0") -> false;
to_bool_strict(<<"0">>) -> false;
to_bool_strict(M) when is_map(M) ->
maps:size(M) =/= 0;
to_bool_strict({{9999,M,D},{H,I,S}}) % ?ST_JUTTEMIS
when is_integer(M), is_integer(D),
is_integer(H), is_integer(I), is_integer(S),
M >= 1, M =< 12, D >= 1, D =< 31, H >= 0, H =< 23,
I >= 0, I =< 59, S >= 0, S =< 60 -> false;
to_bool_strict(_) -> true.
%% @doc Convert a local date time to utc
-spec to_utc( z_dateformat:datetime() | undefined ) -> z_dateformat:datetime() | undefined.
to_utc(undefined) ->
undefined;
to_utc({{9999,_,_}, _}) ->
?ST_JUTTEMIS;
to_utc(D) ->
case catch calendar:local_time_to_universal_time_dst(D) of
[] -> D; % This time never existed in the local time, just take it as-is
[UTC] -> UTC;
[DstUTC, _UTC] -> DstUTC;
{'EXIT', _} -> D
end.
%% @doc Convert a utc date time to local
-spec to_localtime( z_dateformat:datetime() | undefined ) -> z_dateformat:datetime() | undefined.
to_localtime(undefined) ->
undefined;
to_localtime({{9999,_,_},_}) ->
?ST_JUTTEMIS;
to_localtime(D) ->
case catch calendar:universal_time_to_local_time(D) of
{'EXIT', _} -> D;
LocalD -> LocalD
end.
%% @doc Convert an input to a (universal) datetime, using to_date/1 and
%% to_time/1. If the input is a string, it is expected to be in iso
%% 8601 format, although it can also handle timestamps without time
%% zones. The time component of the datatime is optional.
-spec to_datetime( z_dateformat:datetime() | calendar:date() | binary() | string() | undefined ) -> z_dateformat:datetime() | undefined.
to_datetime(undefined) -> undefined;
to_datetime({{_,_,_},{_,_,_}} = DT) -> DT;
to_datetime({_,_,_} = D) -> {D, {0,0,0}};
to_datetime(B) when is_binary(B) ->
to_datetime(binary_to_list(B));
to_datetime("") ->
undefined;
to_datetime(L) when is_list(L) ->
try
case string:tokens(L, " T") of
[Date,Time] ->
WithTZ = fun(Tm, Tz, Mul) ->
TZTime = to_time(Tz),
Add = calendar:datetime_to_gregorian_seconds({{0,1,1},TZTime}),
Secs = calendar:datetime_to_gregorian_seconds({to_date(Date), to_time(Tm)}),
calendar:gregorian_seconds_to_datetime(Secs+(Mul*Add))
end,
case string:tokens(Time, "+") of
[Time1, TZ] ->
%% Timestamp with positive time zone
WithTZ(Time1, TZ, -1);
_ ->
case string:tokens(Time, "-") of
[Time1, TZ] ->
%% Timestamp with negative time zone
WithTZ(Time1, TZ, 1);
_ ->
case lists:reverse(Time) of
[$Z|Rest] ->
%% Timestamp ending on Z (= UTC)
{to_date(Date), to_time(lists:reverse(Rest))};
_ ->
%% Timestamp without time zone
{to_date(Date), to_time(Time)}
end
end
end;
[Date] ->
{to_date(Date), {0,0,0}}
end
catch
_:_ -> undefined
end.
%% @doc Convert an input to a date. Input is expected to be YYYY-MM-DD
%% or YYYY/MM/DD.
-spec to_date( z_dateformat:date() | binary() | string() | undefined ) -> z_dateformat:date() | undefined.
to_date(undefined) -> undefined;
to_date({_,_,_} = D) -> D;
to_date(B) when is_binary(B) ->
to_date(binary_to_list(B));
to_date("") -> undefined;
to_date(L) when is_list(L) ->
case string:tokens(L, "-/") of
[D,M,Y] when length(Y) =:= 4 ->
{to_integer(Y),to_integer(M),to_integer(D)};
[Y,M,D] ->
{to_integer(Y),to_integer(M),to_integer(D)};
_ ->
undefined
end.
%% @doc Convert an input to a time. Input is expected to be HH:MM:SS
%% or HH.MM.SS.
-spec to_time( calendar:time() | binary() | string() | undefined ) -> calendar:time() | undefined.
to_time(undefined) -> undefined;
to_time({_,_,_} = D) -> D;
to_time(B) when is_binary(B) ->
to_time(binary_to_list(B));
to_time("") -> undefined;
to_time([H1,H2,M1,M2]) ->
to_time([H1,H2,$:,M1,M2]);
to_time(L) when is_list(L) ->
[H,I,S|_] = lists:flatten([[to_integer(X) ||X <- string:tokens(L, ":.")], 0, 0]),
{H,I,S}.
%% @doc Convert a datetime (in universal time) to an ISO time string.
-spec to_isotime( z_dateformat:datetime() ) -> binary().
to_isotime(DateTime) ->
z_dateformat:format(DateTime, "x-m-d\\TH:i:s\\Z", []).
%%
%% @doc Convert an Erlang structure to a format that can be serialized by mochijson.
%%
%% Simple values
to_json(undefined) ->
null;
to_json(X) when is_atom(X) ->
X;
to_json(X) when is_integer(X) ->
X;
to_json(X) when is_float(X) ->
X;
to_json(X) when is_binary(X) ->
X;
%% Tuple values
to_json({{Y,M,D},{H,I,S}} = DateTime)
when is_integer(Y), is_integer(M), is_integer(D),
is_integer(H), is_integer(I), is_integer(S) ->
z_dateformat:format(DateTime, "c", [{utc,DateTime}]);
to_json({array, X}) ->
%% Explicit request for array (to prevent string conversion for some lists)
{array, [to_json(V) || V <- X]};
to_json({struct, X}) ->
{struct, X};
to_json({X, Y}) ->
{struct, to_json_struct([{X, Y}])};
to_json(X) when is_tuple(X) ->
{array, [to_json(V) || V <- tuple_to_list(X)]};
%% List values
to_json([{X, Y}]) when is_atom(X) ->
{struct, to_json_struct([{X, Y}])};
to_json([{X, Y} | Z]) when is_atom(X) ->
{struct, to_json_struct([{X, Y} | Z])};
to_json(X) when is_list(X) ->
case z_string:is_string(X) of
true ->
X;
false ->
{array, [to_json(V) || V <- X]}
end.
%% Handle structs specially
to_json_struct([]) ->
[];
to_json_struct([{X,Y}|T]) ->
[{to_json_struct_key(X), to_json(Y)} | to_json_struct(T)].
to_json_struct_key(X) when is_atom(X) orelse is_integer(X) orelse is_binary(X) ->
X;
to_json_struct_key(X) when is_list(X) ->
case z_string:is_string(X) of
true ->
X;
false ->
invalid_key
end;
to_json_struct_key(_) ->
invalid_key.
ip_to_list({IP,Port}) when is_tuple(IP), is_integer(Port) ->
ip_to_list(IP);
ip_to_list({N1,N2,N3,N4} ) ->
lists:flatten([integer_to_list(N1), $., integer_to_list(N2), $., integer_to_list(N3), $., integer_to_list(N4)]);
ip_to_list({_K1,_K2,_K3,_K4,_K5,_K6,_K7,_K8} = IPv6) ->
L = lists:map(fun(0) -> "";
(N) -> io_lib:format("~.16b", [N])
end,
tuple_to_list(IPv6)),
lists:flatten(string:join(L, ":")).
%% Taken from egeoip (http://code.google.com/p/egeoip/source/browse/trunk/egeoip/src/egeoip.erl?r=19)
%% @spec ip_to_long(Address) -> {ok, integer()} | {error, badmatch}
%% @doc Convert an IP address from a string, IPv4 tuple or IPv6 tuple to the
%% big endian integer representation.
ip_to_long({B3, B2, B1, B0}) ->
{ok, (B3 bsl 24) bor (B2 bsl 16) bor (B1 bsl 8) bor B0};
ip_to_long({W7, W6, W5, W4, W3, W2, W1, W0}) ->
{ok, (W7 bsl 112) bor (W6 bsl 96) bor (W5 bsl 80) bor (W4 bsl 64) bor
(W3 bsl 48) bor (W2 bsl 32) bor (W1 bsl 16) bor W0};
ip_to_long(_) ->
{error, badmatch}.
%% @doc Convert long int to IP address tuple. FIXME: ipv6
long_to_ip(L) ->
{ok, {(L band (255 bsl 24)) bsr 24,
(L band (255 bsl 16)) bsr 16,
(L band (255 bsl 8)) bsr 8,
L band 255}}.
%% @doc Convert json from facebook favour to an easy to use format for zotonic templates.
convert_json({K, V}) when is_binary(K) ->
{z_convert:to_atom(K), convert_json(V)};
convert_json({struct, PropList}) when is_list(PropList) ->
convert_json(PropList);
convert_json(L) when is_list(L) ->
[convert_json(V) || V <- L];
convert_json(V) ->
V.
unicode_to_utf8(List) when is_list(List) -> lists:flatmap(fun unicode_to_utf8/1, List);
unicode_to_utf8(Ch) -> char_to_utf8(Ch).
char_to_utf8(Ch) when is_integer(Ch), Ch >= 0 ->
if Ch < 128 ->
%% 0yyyyyyy
[Ch];
Ch < 16#800 ->
%% 110xxxxy 10yyyyyy
[16#C0 + (Ch bsr 6),
128+(Ch band 16#3F)];
Ch < 16#10000 ->
%% 1110xxxx 10xyyyyy 10yyyyyy
if Ch < 16#D800; Ch > 16#DFFF, Ch < 16#FFFE ->
[16#E0 + (Ch bsr 12),
128+((Ch bsr 6) band 16#3F),
128+(Ch band 16#3F)];
true -> [$?]
end;
Ch < 16#200000 ->
%% 11110xxx 10xxyyyy 10yyyyyy 10yyyyyy
[16#F0+(Ch bsr 18),
128+((Ch bsr 12) band 16#3F),
128+((Ch bsr 6) band 16#3F),
128+(Ch band 16#3F)];
Ch < 16#4000000 ->
%% 111110xx 10xxxyyy 10yyyyyy 10yyyyyy 10yyyyyy
[16#F8+(Ch bsr 24),
128+((Ch bsr 18) band 16#3F),
128+((Ch bsr 12) band 16#3F),
128+((Ch bsr 6) band 16#3F),
128+(Ch band 16#3F)];
Ch < 16#80000000 ->
%% 1111110x 10xxxxyy 10yyyyyy 10yyyyyy 10yyyyyy 10yyyyyy
[16#FC+(Ch bsr 30),
128+((Ch bsr 24) band 16#3F),
128+((Ch bsr 18) band 16#3F),
128+((Ch bsr 12) band 16#3F),
128+((Ch bsr 6) band 16#3F),
128+(Ch band 16#3F)];
true -> [$?]
end.