%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod:
%%%
%%%------------------------------------------------------------------------
%%% @doc
%%% ==String manipulation functions==
%%% @end
%%%
%%% MIT License
%%%
%%% Copyright (c) 2009-2020 Michael Truog <mjtruog at protonmail dot com>
%%%
%%% Permission is hereby granted, free of charge, to any person obtaining a
%%% copy of this software and associated documentation files (the "Software"),
%%% to deal in the Software without restriction, including without limitation
%%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
%%% and/or sell copies of the Software, and to permit persons to whom the
%%% Software is furnished to do so, subject to the following conditions:
%%%
%%% The above copyright notice and this permission notice shall be included in
%%% all copies or substantial portions of the Software.
%%%
%%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
%%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
%%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
%%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
%%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
%%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
%%% DEALINGS IN THE SOFTWARE.
%%%
%%% @author Michael Truog <mjtruog at protonmail dot com>
%%% @copyright 2009-2020 Michael Truog
%%% @version 2.0.1 {@date} {@time}
%%%------------------------------------------------------------------------

-module(cloudi_string).
-author('mjtruog at protonmail dot com').

%% external interface
-export([afterl/2,
         afterl/3,
         afterr/2,
         afterr/3,
         beforel/2,
         beforel/3,
         beforer/2,
         beforer/3,
         binary_to_term/1,
         compare_constant/2,
         compare_constant_list/2,
         compare_constant_binary/2,
         findl/2,
         findr/2,
         format/2,
         format_to_binary/2,
         format_to_list/2,
         join/2,
         list_to_term/1,
         lowercase/1,
         split/2,
         splitl/2,
         splitl/3,
         splitr/2,
         splitr/3,
         term_to_binary/1,
         term_to_binary_compact/1,
         term_to_list/1,
         term_to_list_compact/1,
         titlecase/1,
         trim/1,
         trim/2,
         triml/1,
         triml/2,
         trimr/1,
         trimr/2,
         uppercase/1]).

% based on unicode_util:whitespace/0
-define(WHITESPACE, [13,9,10,11,12,13,32,133,8206,8207,8232,8233]).
% keep output in a single line when using ~p (printable) formatting
-ifdef(ERLANG_OTP_VERSION_21_FEATURES).
-define(COMPACT_LIMIT, "0").
-else.
-define(COMPACT_LIMIT, "1000000000000").
-endif.

-include("cloudi_core_i_constants.hrl").

%%%------------------------------------------------------------------------
%%% External interface functions
%%%------------------------------------------------------------------------

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string that occurs after a character, otherwise return an empty string, when traversing left to right.===
%% @end
%%-------------------------------------------------------------------------

-spec afterl(Char :: non_neg_integer(),
             Input :: string()) ->
    string().

afterl(_, []) ->
    [];
afterl(Char, [Char | Rest]) ->
    Rest;
afterl(Char, [_ | Rest]) ->
    afterl(Char, Rest).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string that occurs after a character, otherwise return based on the failure atom, when traversing left to right.===
%% @end
%%-------------------------------------------------------------------------

-spec afterl(Char :: non_neg_integer(),
             Input :: string(),
             empty | input) ->
    string().

afterl(Char, Input, empty) ->
    afterl(Char, Input);
afterl(Char, Input, input) ->
    afterl_input(Char, Input, Input).
afterl_input(_, [], Input) ->
    Input;
afterl_input(Char, [Char | Rest], _) ->
    Rest;
afterl_input(Char, [_ | Rest], Input) ->
    afterl_input(Char, Rest, Input).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string that occurs after a character, otherwise return an empty string, when traversing right to left.===
%% @end
%%-------------------------------------------------------------------------

-spec afterr(Char :: non_neg_integer(),
             Input :: string()) ->
    string().

afterr(Char, Input) ->
    afterr_empty([], Char, Input).
afterr_empty(L, _, []) ->
    L;
afterr_empty(_, Char, [Char | Rest]) ->
    afterr_empty(Rest, Char, Rest);
afterr_empty(L, Char, [_ | Rest]) ->
    afterr_empty(L, Char, Rest).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string that occurs after a character, otherwise return based on the failure atom, when traversing right to left.===
%% @end
%%-------------------------------------------------------------------------

-spec afterr(Char :: non_neg_integer(),
             Input :: string(),
             empty | input) ->
    string().

afterr(Char, Input, empty) ->
    afterr_empty([], Char, Input);
afterr(Char, Input, input) ->
    afterr_input([], Char, Input, Input).
afterr_input([], _, [], Input) ->
    Input;
afterr_input(L, _, [], _) ->
    L;
afterr_input(_, Char, [Char | Rest], Input) ->
    afterr_input(Rest, Char, Rest, Input);
afterr_input(L, Char, [_ | Rest], Input) ->
    afterr_input(L, Char, Rest, Input).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string that occurs before a character, otherwise return an empty string, when traversing left to right.===
%% @end
%%-------------------------------------------------------------------------

-spec beforel(Char :: non_neg_integer(),
              Input :: string()) ->
    string().

beforel(Char, Input) ->
    beforel_empty([], Char, Input).
beforel_empty(_, _, []) ->
    [];
beforel_empty(Before, Char, [Char | _]) ->
    lists:reverse(Before);
beforel_empty(Before, Char, [H | Input]) ->
    beforel_empty([H | Before], Char, Input).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string that occurs before a character, otherwise return based on the failure atom, when traversing left to right.===
%% @end
%%-------------------------------------------------------------------------

-spec beforel(Char :: non_neg_integer(),
              Input :: string(),
              empty | input) ->
    string().

beforel(Char, Input, empty) ->
    beforel_empty([], Char, Input);
beforel(Char, Input, input) ->
    beforel_input([], Char, Input, Input).
beforel_input(_, _, [], Input) ->
    Input;
beforel_input(Before, Char, [Char | _], _) ->
    lists:reverse(Before);
beforel_input(Before, Char, [H | Rest], Input) ->
    beforel_input([H | Before], Char, Rest, Input).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string that occurs before a character, otherwise return an empty string, when traversing right to left.===
%% @end
%%-------------------------------------------------------------------------

-spec beforer(Char :: non_neg_integer(),
              Input :: string()) ->
    string().

beforer(Char, Input) ->
    beforer_empty([], [], Char, Input).
beforer_empty(Before, _, _, []) ->
    Before;
beforer_empty(Before, L, Char, [Char | Rest]) ->
    beforer_empty(Before ++ lists:reverse(L), [Char], Char, Rest);
beforer_empty(Before, L, Char, [H | Rest]) ->
    beforer_empty(Before, [H | L], Char, Rest).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string that occurs before a character, otherwise return based on the failure atom, when traversing right to left.===
%% @end
%%-------------------------------------------------------------------------

-spec beforer(Char :: non_neg_integer(),
              Input :: string(),
              empty | input) ->
    string().

beforer(Char, Input, empty) ->
    beforer_empty([], [], Char, Input);
beforer(Char, Input, input) ->
    beforer_input([], [], Char, Input, Input).
beforer_input([], _, _, [], Input) ->
    Input;
beforer_input(Before, _, _, [], _) ->
    Before;
beforer_input(Before, L, Char, [Char | Rest], Input) ->
    beforer_input(Before ++ lists:reverse(L), [Char], Char, Rest, Input);
beforer_input(Before, L, Char, [H | Rest], Input) ->
    beforer_input(Before, [H | L], Char, Rest, Input).

%%-------------------------------------------------------------------------
%% @doc
%% ===Convert a binary string to an Erlang term.===
%% @end
%%-------------------------------------------------------------------------

-spec binary_to_term(B :: binary()) ->
    any().

binary_to_term(B) ->
    list_to_term(erlang:binary_to_list(B)).

%%-------------------------------------------------------------------------
%% @doc
%% ===Time insensitive compare to avoid a timing leak.===
%% Use for password or other authentication comparisons.
%% Execution time is based on the length of Test.
%% @end
%%-------------------------------------------------------------------------

-spec compare_constant(Test :: string(),
                       Correct :: nonempty_string()) ->
    boolean().

compare_constant(Test, Correct) ->
    compare_constant_list(Test, Correct).

%%-------------------------------------------------------------------------
%% @doc
%% ===Time insensitive compare to avoid a timing leak with strings as lists.===
%% Use for password or other authentication comparisons.
%% Execution time is based on the length of Test.
%% @end
%%-------------------------------------------------------------------------

-spec compare_constant_list(Test :: string(),
                            Correct :: nonempty_string()) ->
    boolean().

compare_constant_list(_, []) ->
    erlang:exit(badarg);
compare_constant_list(Test, [_ | _] = Correct) ->
    compare_constant_list(Test, Correct,
                          [-1 || _ <- Test], [], 0) =:= 0.

compare_constant_list([], [], _, _, Bits) ->
    Bits;
compare_constant_list([], [_ | _], _, _, _) ->
    -1;
compare_constant_list([C1 | Test], [] = Correct,
                      [C2 | Busy], BusyEmpty, Bits) ->
    compare_constant_list(Test, Correct,
                          Busy, BusyEmpty, Bits bor (C1 bxor C2));
compare_constant_list([C1 | Test], [C2 | Correct],
                      Busy, [] = BusyEmpty, Bits) ->
    compare_constant_list(Test, Correct,
                          Busy, BusyEmpty, Bits bor (C1 bxor C2)).

%%-------------------------------------------------------------------------
%% @doc
%% ===Time insensitive compare to avoid a timing leak with strings as binaries.===
%% Use for password or other authentication comparisons.
%% Execution time is based on the length of Test.
%% @end
%%-------------------------------------------------------------------------

-spec compare_constant_binary(Test :: <<_:_*8>>,
                              Correct :: <<_:8, _:_*8>>) ->
    boolean().

compare_constant_binary(_, <<>>) ->
    erlang:exit(badarg);
compare_constant_binary(Test, Correct) ->
    compare_constant_binary(Test, Correct,
                            <<<<255>> || <<_>> <= Test>>, <<>>, 0) =:= 0.

compare_constant_binary(<<>>, <<>>, _, _, Bits) ->
    Bits;
compare_constant_binary(<<>>, <<_:8, _/binary>>, _, _, _) ->
    -1;
compare_constant_binary(<<C1:8, Test/binary>>, <<>> = Correct,
                        <<C2:8/signed, Busy/binary>>, BusyEmpty, Bits) ->
    compare_constant_binary(Test, Correct,
                            Busy, BusyEmpty, Bits bor (C1 bxor C2));
compare_constant_binary(<<C1:8, Test/binary>>, <<C2:8, Correct/binary>>,
                        Busy, <<>> = BusyEmpty, Bits) ->
    compare_constant_binary(Test, Correct,
                            Busy, BusyEmpty, Bits bor (C1 bxor C2)).

%%-------------------------------------------------------------------------
%% @doc
%% ===Find the beginning of a substring in a string from the left.===
%% @end
%%-------------------------------------------------------------------------

-spec findl(SearchPattern :: string() | binary(),
            String :: string() | binary()) ->
    string() | binary() | false.

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
findl(SearchPattern, String) ->
    case string:find(String, SearchPattern, leading) of
        nomatch ->
            false;
        Result ->
            Result
    end.
-else.
findl(SearchPattern, String)
    when is_list(SearchPattern) ->
    StringList = if
        is_binary(String) ->
            erlang:binary_to_list(String);
        is_list(String) ->
            lists:flatten(String)
    end,
    SearchPatternList = if
        is_binary(SearchPattern) ->
            erlang:binary_to_list(SearchPattern);
        is_list(SearchPattern) ->
            lists:flatten(SearchPattern)
    end,
    case string:str(StringList, SearchPatternList) of
        0 ->
            false;
        Index ->
            Result = lists:nthtail(Index - 1, StringList),
            if
                is_binary(String) ->
                    erlang:list_to_binary(Result);
                is_list(String) ->
                    Result
            end
    end.
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Find the beginning of a substring in a string from the right.===
%% @end
%%-------------------------------------------------------------------------

-spec findr(SearchPattern :: string() | binary(),
            String :: string() | binary()) ->
    string() | binary() | false.

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
findr(SearchPattern, String) ->
    case string:find(String, SearchPattern, trailing) of
        nomatch ->
            false;
        Result ->
            Result
    end.
-else.
findr(SearchPattern, String) ->
    StringList = if
        is_binary(String) ->
            erlang:binary_to_list(String);
        is_list(String) ->
            lists:flatten(String)
    end,
    SearchPatternList = if
        is_binary(SearchPattern) ->
            erlang:binary_to_list(SearchPattern);
        is_list(SearchPattern) ->
            lists:flatten(SearchPattern)
    end,
    case string:rstr(StringList, SearchPatternList) of
        0 ->
            false;
        Index ->
            Result = lists:nthtail(Index - 1, StringList),
            if
                is_binary(String) ->
                    erlang:list_to_binary(Result);
                is_list(String) ->
                    Result
            end
    end.
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Format a string based on the arguments.===
%% @end
%%-------------------------------------------------------------------------

-spec format(L :: string(),
             A :: list()) ->
    string().

-compile({inline, [{format, 2}]}).

format(L, A) ->
    format_to_list(L, A).

%%-------------------------------------------------------------------------
%% @doc
%% ===Format a string based on the arguments, stored as a binary.===
%% Output is a utf8 encoded binary.
%% @end
%%-------------------------------------------------------------------------

-spec format_to_binary(L :: string(),
                       A :: list()) ->
    binary().

format_to_binary(L, A) when is_list(L), is_list(A) ->
    unicode:characters_to_binary(io_lib:format(L, A)).

%%-------------------------------------------------------------------------
%% @doc
%% ===Format a string based on the arguments, stored as a list.===
%% Output may include unicode characters with a numerical value greater
%% than 255 (preventing the output from being used directly with
%% erlang:iolist_to_binary/1)..
%% @end
%%-------------------------------------------------------------------------

-spec format_to_list(L :: string(),
                     A :: list()) ->
    string().

-compile({inline, [{format_to_list, 2}]}).

format_to_list(L, A) when is_list(L), is_list(A) ->
    lists:flatten(io_lib:format(L, A)).

%%-------------------------------------------------------------------------
%% @doc
%% ===Join a list of strings with a string.===
%% @end
%%-------------------------------------------------------------------------

-spec join(Delimiters :: string() | binary(),
           L:: list(string() | binary())) ->
    string() | binary().

join(Delimiters, L) when is_binary(Delimiters) ->
    [[_ | H] | T] = [[Delimiters, S] || S <- L],
    unicode:characters_to_binary([H | T]);
join(Delimiters, L) when is_list(Delimiters) ->
    join_list(L, [], Delimiters).

join_list([], [], _) ->
    [];
join_list([H], Output, _) ->
    lists:reverse(Output, H);
join_list([H | T], Output, Delimiters) ->
    join_list(T, join_list(Delimiters, join_list(H, Output)), Delimiters).

join_list([], Output) ->
    Output;
join_list([H | T], Output) ->
    join_list(T, [H | Output]).

%%-------------------------------------------------------------------------
%% @doc
%% ===Convert a string to an Erlang term.===
%% @end
%%-------------------------------------------------------------------------

-spec list_to_term(L :: string()) ->
    any().

list_to_term(L) ->
    {ok, S, _} = erl_scan:string(L ++ "."),
    case erl_parse:parse_term(S) of
        {ok, Term} ->
            Term;
        {error, Reason} ->
            throw(Reason)
    end.

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string in lowercase.===
%% @end
%%-------------------------------------------------------------------------

-spec lowercase(String :: string() | binary()) ->
    string() | binary().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
lowercase(String) ->
    string:lowercase(String).
-else.
lowercase(String)
    when is_list(String) ->
    string:to_lower(String);
lowercase(String)
    when is_binary(String) ->
    erlang:list_to_binary(string:to_lower(erlang:binary_to_list(String))).
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Split the string at all occurrences of the search pattern.===
%% @end
%%-------------------------------------------------------------------------

-spec split(SearchPattern :: string() | binary() | list(string() | binary()),
            String :: string() | binary()) ->
    list(string() | binary()).

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
split(SearchPattern, String) ->
    string:split(String, SearchPattern, all).
-else.
split(SearchPattern, String)
    when is_list(String) ->
    [erlang:binary_to_list(S)
     || S <- split(SearchPattern, erlang:list_to_binary(String))];
split(SearchPattern, String)
    when is_binary(String) ->
    Pattern = if
        is_binary(SearchPattern) ->
            [SearchPattern];
        is_integer(hd(SearchPattern)) ->
            [erlang:list_to_binary(SearchPattern)];
        is_list(SearchPattern) ->
            [unicode:characters_to_binary(S) || S <- SearchPattern]
    end,
    binary:split(String, Pattern, [global]).
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the two strings split at the first occurrence of the character, otherwise return an empty string, when traversing left to right.===
%% @end
%%-------------------------------------------------------------------------

-spec splitl(Char :: non_neg_integer(),
             Input :: string()) ->
    {string(), string()}.

splitl(Char, Input) ->
    splitl_empty([], Char, Input).
splitl_empty(_, _, []) ->
    {[], []};
splitl_empty(Before, Char, [Char | Rest]) ->
    {lists:reverse(Before), Rest};
splitl_empty(Before, Char, [H | Rest]) ->
    splitl_empty([H | Before], Char, Rest).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the two strings split at the first occurrence of the character, otherwise return based on the failure atom, when traversing left to right.===
%% @end
%%-------------------------------------------------------------------------

-spec splitl(Char :: non_neg_integer(),
             Input :: string(),
             empty | input) ->
    {string(), string()}.

splitl(Char, Input, empty) ->
    splitl_empty([], Char, Input);
splitl(Char, Input, input) ->
    splitl_input([], Char, Input, Input).
splitl_input(_, _, [], Input) ->
    {Input, []};
splitl_input(Before, Char, [Char | Rest], _) ->
    {lists:reverse(Before), Rest};
splitl_input(Before, Char, [H | Rest], Input) ->
    splitl_input([H | Before], Char, Rest, Input).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the two strings split at the first occurrence of the character, otherwise return an empty string, when traversing right to left.===
%% @end
%%-------------------------------------------------------------------------

-spec splitr(Char :: non_neg_integer(),
             Input :: string()) ->
    {string(), string()}.

splitr(Char, Input) ->
    splitr_empty([], [], Char, Input).
splitr_empty([Char | L1], [], Char, []) ->
    {lists:reverse(L1), []};
splitr_empty(_, [], _, []) ->
    {[], []};
splitr_empty(L1, L2, Char, []) ->
    {splitr_prefix(L1, L2, Char), L2};
splitr_empty(L1, _, Char, [Char | Rest]) ->
    splitr_empty([Char | L1], Rest, Char, Rest);
splitr_empty(L1, L2, Char, [C | Rest]) ->
    splitr_empty([C | L1], L2, Char, Rest).
splitr_prefix([Char | L1], [], Char) ->
    lists:reverse(L1);
splitr_prefix([_ | L1], [_ | L2], Char) ->
    splitr_prefix(L1, L2, Char).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the two strings split at the first occurrence of the character, otherwise return based on the failure atom, when traversing right to left.===
%% @end
%%-------------------------------------------------------------------------

-spec splitr(Char :: non_neg_integer(),
             Input :: string(),
             empty | input) ->
    {string(), string()}.

splitr(Char, Input, empty) ->
    splitr_empty([], [], Char, Input);
splitr(Char, Input, input) ->
    splitr_input([], [], Char, Input, Input).
splitr_input([Char | L1], [], Char, [], _) ->
    {lists:reverse(L1), []};
splitr_input(_, [], _, [], Input) ->
    {[], Input};
splitr_input(L1, L2, Char, [], _) ->
    {splitr_prefix(L1, L2, Char), L2};
splitr_input(L1, _, Char, [Char | Rest], Input) ->
    splitr_input([Char | L1], Rest, Char, Rest, Input);
splitr_input(L1, L2, Char, [C | Rest], Input) ->
    splitr_input([C | L1], L2, Char, Rest, Input).

%%-------------------------------------------------------------------------
%% @doc
%% ===Convert an Erlang term to a binary string.===
%% Output is a utf8 encoded binary.
%% @end
%%-------------------------------------------------------------------------

-spec term_to_binary(T :: any()) ->
    binary().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
term_to_binary(T) ->
    unicode:characters_to_binary(io_lib:format("~tw", [T])).
-else.
term_to_binary(T) ->
    unicode:characters_to_binary(io_lib:format("~w", [T])).
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Convert an Erlang term to a compact binary string.===
%% Output is a utf8 encoded binary.
%% @end
%%-------------------------------------------------------------------------

-spec term_to_binary_compact(T :: any()) ->
    binary().

term_to_binary_compact(T) ->
    unicode:characters_to_binary(io_lib:format("~" ?COMPACT_LIMIT "tp", [T])).

%%-------------------------------------------------------------------------
%% @doc
%% ===Convert an Erlang term to a string.===
%% @end
%%-------------------------------------------------------------------------

-spec term_to_list(T :: any()) ->
    string().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
term_to_list(T) ->
    format("~tw", [T]).
-else.
term_to_list(T) ->
    format("~w", [T]).
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Convert an Erlang term to a compact string.===
%% @end
%%-------------------------------------------------------------------------

-spec term_to_list_compact(T :: any()) ->
    string().

term_to_list_compact(T) ->
    format("~" ?COMPACT_LIMIT "tp", [T]).

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string in titlecase.===
%% @end
%%-------------------------------------------------------------------------

-spec titlecase(String :: string() | binary()) ->
    string() | binary().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
titlecase(String) ->
    string:titlecase(String).
-else.
titlecase(String) ->
    % functionality was not present in any form
    String.
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Trim the edges of the string.===
%% @end
%%-------------------------------------------------------------------------

-spec trim(String :: string() | binary()) ->
    string() | binary().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
trim(String) ->
    string:trim(String).
-else.
trim(String) ->
    trim(?WHITESPACE, String).
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Trim the edges of the string.===
%% @end
%%-------------------------------------------------------------------------

-spec trim(Characters :: string() | list(string()),
           String :: string() | binary()) ->
    string() | binary().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
trim(Characters, String) ->
    string:trim(String, both, Characters).
-else.
trim(Characters, String)
    when is_list(String) ->
    trim_list(lists:flatten(Characters), String);
trim(Characters, String)
    when is_binary(String) ->
    erlang:list_to_binary(trim_list(lists:flatten(Characters),
                                    erlang:binary_to_list(String))).

trim_list([], String) ->
    String;
trim_list([H | T], String) ->
    trim_list(T, string:strip(String, both, H)).
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Trim the edges of the string from the left.===
%% @end
%%-------------------------------------------------------------------------

-spec triml(String :: string() | binary()) ->
    string() | binary().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
triml(String) ->
    string:trim(String, leading).
-else.
triml(String) ->
    triml(?WHITESPACE, String).
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Trim the edges of the string from the left.===
%% @end
%%-------------------------------------------------------------------------

-spec triml(Characters :: string() | list(string()),
            String :: string() | binary()) ->
    string() | binary().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
triml(Characters, String) ->
    string:trim(String, leading, Characters).
-else.
triml(Characters, String)
    when is_list(String) ->
    triml_list(lists:flatten(Characters), String);
triml(Characters, String)
    when is_binary(String) ->
    erlang:list_to_binary(triml_list(lists:flatten(Characters),
                                     erlang:binary_to_list(String))).

triml_list([], String) ->
    String;
triml_list([H | T], String) ->
    triml_list(T, string:strip(String, left, H)).
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Trim the edges of the string from the right.===
%% @end
%%-------------------------------------------------------------------------

-spec trimr(String :: string() | binary()) ->
    string() | binary().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
trimr(String) ->
    string:trim(String, trailing).
-else.
trimr(String) ->
    trimr(?WHITESPACE, String).
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Trim the edges of the string from the right.===
%% @end
%%-------------------------------------------------------------------------

-spec trimr(Characters :: string() | list(string()),
            String :: string() | binary()) ->
    string() | binary().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
trimr(Characters, String) ->
    string:trim(String, trailing, Characters).
-else.
trimr(Characters, String)
    when is_list(String) ->
    trimr_list(lists:flatten(Characters), String);
trimr(Characters, String)
    when is_binary(String) ->
    erlang:list_to_binary(trimr_list(lists:flatten(Characters),
                                     erlang:binary_to_list(String))).

trimr_list([], String) ->
    String;
trimr_list([H | T], String) ->
    trimr_list(T, string:strip(String, right, H)).
-endif.

%%-------------------------------------------------------------------------
%% @doc
%% ===Return the string in uppercase.===
%% @end
%%-------------------------------------------------------------------------

-spec uppercase(String :: string() | binary()) ->
    string() | binary().

-ifdef(ERLANG_OTP_VERSION_20_FEATURES).
uppercase(String) ->
    string:uppercase(String).
-else.
uppercase(String)
    when is_list(String) ->
    string:to_upper(String);
uppercase(String)
    when is_binary(String) ->
    erlang:list_to_binary(string:to_upper(erlang:binary_to_list(String))).
-endif.

%%%------------------------------------------------------------------------
%%% Private functions
%%%------------------------------------------------------------------------

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

-include("cloudi_core_i_test.hrl").

module_test_() ->
    {timeout, ?TEST_TIMEOUT, [
        {"afterl tests", ?_assertOk(t_afterl())},
        {"afterr tests", ?_assertOk(t_afterr())},
        {"beforel tests", ?_assertOk(t_beforel())},
        {"beforer tests", ?_assertOk(t_beforer())},
        {"compare_constant_binary", ?_assertOk(t_compare_constant_binary())},
        {"compare_constant_list", ?_assertOk(t_compare_constant_list())},
        {"findl tests", ?_assertOk(t_findl())},
        {"findr tests", ?_assertOk(t_findr())},
        {"join tests", ?_assertOk(t_join())},
        {"split tests", ?_assertOk(t_split())},
        {"splitl tests", ?_assertOk(t_splitl())},
        {"splitr tests", ?_assertOk(t_splitr())},
        {"trim tests", ?_assertOk(t_trim())},
        {"triml tests", ?_assertOk(t_triml())},
        {"trimr tests", ?_assertOk(t_trimr())}
    ]}.

t_afterl() ->
    "this-is-all-input" = afterl($/, "this-is-all-input", input),
    "" = afterl($/, "this-is-all-input", empty),
    "" = afterl($/, "this-is-all-input"),
    "is-all-input" = afterl($-, "this-is-all-input", input),
    "is-all-input" = afterl($-, "this-is-all-input", empty),
    "is-all-input" = afterl($-, "this-is-all-input"),
    ok.

t_afterr() ->
    "this-is-all-input" = afterr($/, "this-is-all-input", input),
    "" = afterr($/, "this-is-all-input", empty),
    "" = afterr($/, "this-is-all-input"),
    "input" = afterr($-, "this-is-all-input", input),
    "input" = afterr($-, "this-is-all-input", empty),
    "input" = afterr($-, "this-is-all-input"),
    ok.

t_beforel() ->
    "this-is-all-input" = beforel($/, "this-is-all-input", input),
    "" = beforel($/, "this-is-all-input", empty),
    "" = beforel($/, "this-is-all-input"),
    "this" = beforel($-, "this-is-all-input", input),
    "this" = beforel($-, "this-is-all-input", empty),
    "this" = beforel($-, "this-is-all-input"),
    ok.

t_beforer() ->
    "this-is-all-input" = beforer($/, "this-is-all-input", input),
    "" = beforer($/, "this-is-all-input", empty),
    "" = beforer($/, "this-is-all-input"),
    "this-is-all" = beforer($-, "this-is-all-input", input),
    "this-is-all" = beforer($-, "this-is-all-input", empty),
    "this-is-all" = beforer($-, "this-is-all-input"),
    ok.

t_compare_constant_binary() ->
    true = compare_constant_binary(<<"abc">>, <<"abc">>),
    false = compare_constant_binary(<<"abc">>, <<"abd">>),
    false = compare_constant_binary(<<"abc">>, <<"abcd">>),
    false = compare_constant_binary(<<"abcde">>, <<"abcd">>),
    true = compare_constant_binary(<<"abcd">>, <<"abcd">>),
    ok.

t_compare_constant_list() ->
    true = compare_constant_list("abc", "abc"),
    false = compare_constant_list("abc", "abd"),
    false = compare_constant_list("abc", "abcd"),
    false = compare_constant_list("abcde", "abcd"),
    true = compare_constant_list("abcd", "abcd"),
    ok.

t_findl() ->
    "..cd..ef" = findl(".", "ab..cd..ef"),
    <<"..cd..ef">> = findl(".", <<"ab..cd..ef">>),
    <<"..cd..ef">> = findl("..", <<"ab..cd..ef">>),
    "..cd..ef" = findl("..", "ab..cd..ef"),
    false = findl("x", <<"ab..cd..ef">>),
    false = findl("x", "ab..cd..ef"),
    ok.

t_findr() ->
    ".ef" = findr(".", "ab..cd..ef"),
    <<".ef">> = findr(".", <<"ab..cd..ef">>),
    <<"..ef">> = findr("..", <<"ab..cd..ef">>),
    "..ef" = findr("..", "ab..cd..ef"),
    false = findr("x", <<"ab..cd..ef">>),
    false = findr("x", "ab..cd..ef"),
    ok.

t_join() ->
    "ab..bc..cd" = join("..", ["ab","bc","cd"]),
    <<"ab..bc..cd">> = join(<<"..">>, [<<"ab">>,<<"bc">>,<<"cd">>]),
    <<"ab..bc....cd">> = join(<<"..">>, [<<"ab">>,<<"bc">>,<<>>,<<"cd">>]),
    ok.

t_split() ->
    ["ab","bc","cd"] = split("..", "ab..bc..cd"),
    [<<"ab">>,<<"bc">>,<<"cd">>] = split(<<"..">>, <<"ab..bc..cd">>),
    [<<"ab">>,<<"bc">>,<<>>,<<"cd">>] = split(<<"..">>, <<"ab..bc....cd">>),
    ok.

t_splitl() ->
    {"this-is-all-input", ""} = splitl($/, "this-is-all-input", input),
    {"", ""} = splitl($/, "this-is-all-input", empty),
    {"", ""} = splitl($/, "this-is-all-input"),
    {"this", "is-all-input"} = splitl($-, "this-is-all-input", input),
    {"this", "is-all-input"} = splitl($-, "this-is-all-input", empty),
    {"this", "is-all-input"} = splitl($-, "this-is-all-input"),
    ok.

t_splitr() ->
    {"", "this-is-all-input"} = splitr($/, "this-is-all-input", input),
    {"", ""} = splitr($/, "this-is-all-input", empty),
    {"", ""} = splitr($/, "this-is-all-input"),
    {"this-is-all", "input"} = splitr($-, "this-is-all-input", input),
    {"this-is-all", "input"} = splitr($-, "this-is-all-input", empty),
    {"this-is-all", "input"} = splitr($-, "this-is-all-input"),
    ok.

t_trim() ->
    <<"Hello">> = trim(<<"\t  Hello  \n">>),
    "Hello" = trim("\t  Hello  \n"),
    <<".Hello.">> = trim(<<".Hello.\n">>),
    ".Hello." = trim(".Hello.\n"),
    ok.

t_triml() ->
    <<"Hello  \n">> = triml(<<"\t  Hello  \n">>),
    "Hello  \n" = triml("\t  Hello  \n"),
    <<"Hello.\n">> = triml("\n.", <<".Hello.\n">>),
    "Hello.\n" = triml("\n.", ".Hello.\n"),
    ok.

t_trimr() ->
    <<"\t  Hello">> = trimr(<<"\t  Hello  \n">>),
    "\t  Hello" = trimr("\t  Hello  \n"),
    <<".Hello">> = trimr("\n.", <<".Hello.\n">>),
    ".Hello" = trimr("\n.", ".Hello.\n"),
    ok.

-endif.
