Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added USB CDC port drivers for ESP32, RP2, and STM32 platforms
- Added a Linux `gpio` driver for the generic_unix port (in `avm_unix`) using sysfs
- Added `console:print_err/1` to write to standard error
- Added `string:to_integer/1`

### Changed
- Updated network type db() to dbm() to reflect the actual representation of the type
Expand Down
246 changes: 246 additions & 0 deletions libs/estdlib/src/string.erl
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
trim/1, trim/2,
find/2, find/3,
length/1,
to_integer/1,
jaro_similarity/2
]).

Expand All @@ -57,6 +58,251 @@ upper_char(C) when is_integer(C) andalso C >= $a andalso C =< $z ->
upper_char(C) when is_integer(C) ->
C.

%%-----------------------------------------------------------------------------
%% @param String a chardata value possibly beginning with an integer
%% @returns `{Int, Rest}' where `Rest' is the unconsumed chardata suffix, or
%% `{error, no_integer}' if it does not begin with an integer, or
%% `{error, badarg}' if malformed chardata or invalid UTF-8 is
%% encountered while parsing
%% @doc Parse a leading (optionally signed) integer from a `unicode:chardata()'
%% value. Digits and sign are ASCII. The remainder keeps binary form when the
%% original argument is a binary.
%%
%% Matching OTP, the maximal leading run of ASCII `+', `-', and digits is
%% validated before the integer grammar is applied, so leftover signs in that
%% run are examined for chardata errors even when they become part of `Rest'.
%% @end
%%-----------------------------------------------------------------------------
-spec to_integer(String :: unicode:chardata()) ->
{integer(), unicode:chardata()} | {error, no_integer | badarg}.
to_integer(String) ->
try to_integer_cd(String) of
{error, _} = Err ->
Err;
{Int, Rest} ->
{Int, Rest}
catch
error:badarg ->
{error, badarg}
end.

%% @private
to_integer_cd(Bin) when is_binary(Bin) ->
case take_int_bin(Bin) of
no_integer ->
{error, no_integer};
{Int, Rest} ->
{Int, Rest}
end;
to_integer_cd(List) when is_list(List) ->
case take_int_cd(List) of
no_integer ->
{error, no_integer};
{Int, Rest} ->
{Int, Rest}
end;
to_integer_cd(_) ->
{error, badarg}.

%% Take the maximal leading run of ASCII + - digits (OTP string:take/2 set),
%% validate the first codepoint after that run, then apply integer grammar.
%% @private
take_int_bin(Bin) ->
{PrefLen, Tail} = take_set_bin(Bin, 0),
case ensure_utf8_boundary(Tail) of
ok when PrefLen =:= 0 ->
no_integer;
ok ->
Pref = binary:part(Bin, 0, PrefLen),
case parse_int_bin(Pref) of
no_integer ->
no_integer;
{Int, RestPref} ->
{Int, <<RestPref/binary, Tail/binary>>}
end;
error ->
erlang:error(badarg)
end.

%% @private
take_set_bin(<<C, Rest/binary>>, N) when
C =:= $+ orelse C =:= $- orelse (C >= $0 andalso C =< $9)
->
take_set_bin(Rest, N + 1);
take_set_bin(Rest, N) ->
{N, Rest}.

%% @private
parse_int_bin(<<$+, Rest/binary>>) ->
parse_int_digits_bin(Rest, 1);
parse_int_bin(<<$-, Rest/binary>>) ->
parse_int_digits_bin(Rest, -1);
parse_int_bin(Bin) ->
parse_int_digits_bin(Bin, 1).

%% @private
parse_int_digits_bin(Bin, Sign) ->
{N, Rest} = take_digits_bin(Bin, 0),
case N of
0 ->
no_integer;
_ ->
Digits = binary:part(Bin, 0, N),
{Sign * binary_to_integer(Digits), Rest}
end.

%% @private
take_digits_bin(<<C, Rest/binary>>, N) when C >= $0 andalso C =< $9 ->
take_digits_bin(Rest, N + 1);
take_digits_bin(Rest, N) ->
{N, Rest}.

%% @private
ensure_utf8_boundary(<<>>) ->
ok;
ensure_utf8_boundary(<<C, _/binary>>) when C =< 16#7F ->
ok;
ensure_utf8_boundary(<<_/utf8, _/binary>>) ->
ok;
ensure_utf8_boundary(_) ->
error.

%% @private
take_int_cd(CD) ->
{HeadChars, Tail0} = take_set_cd(CD, []),
Tail = normalize_empty_cd(Tail0),
case HeadChars of
[] ->
no_integer;
_ ->
case parse_int_chars(HeadChars) of
no_integer ->
no_integer;
{Int, RestChars} ->
{Int, append_cd(RestChars, Tail)}
end
end.

%% Collect leading + - digit codepoints; stop before first non-member.
%% @private
take_set_cd(CD, Acc) ->
case next_cp(CD) of
empty ->
{lists:reverse(Acc), CD};
{C, Rest} when C =:= $+ orelse C =:= $- orelse (C >= $0 andalso C =< $9) ->
take_set_cd(Rest, [C | Acc]);
{_C, _Rest} ->
{lists:reverse(Acc), CD}
end.

%% @private
parse_int_chars([$+ | Rest]) ->
parse_int_digits_chars(Rest, 1);
parse_int_chars([$- | Rest]) ->
parse_int_digits_chars(Rest, -1);
parse_int_chars(Chars) ->
parse_int_digits_chars(Chars, 1).

%% @private
parse_int_digits_chars([C | Rest], Sign) when C >= $0 andalso C =< $9 ->
parse_int_more_chars(Rest, Sign, [C]);
parse_int_digits_chars(_Rest, _Sign) ->
no_integer.

%% @private
parse_int_more_chars([C | Rest], Sign, Acc) when C >= $0 andalso C =< $9 ->
parse_int_more_chars(Rest, Sign, [C | Acc]);
parse_int_more_chars(Rest, Sign, Acc) ->
{Sign * list_to_integer(lists:reverse(Acc)), Rest}.

%% @private
append_cd([], Tail) ->
Tail;
append_cd(RestChars, Tail) ->
RestChars ++ Tail.

%% @private
normalize_empty_cd(CD) ->
case is_empty_cd(CD) of
true ->
[];
false ->
CD
end.

%% @private
is_empty_cd([]) ->
true;
is_empty_cd(<<>>) ->
true;
is_empty_cd([H | T]) ->
is_empty_cd(H) andalso is_empty_cd(T);
is_empty_cd(_) ->
false.

%% next_cp(Chardata) -> {Codepoint, Rest} | empty
%% Raises error:badarg on malformed UTF-8 / non-chardata, including improper
%% list continuations that are not a list or binary.
%% @private
next_cp([]) ->
empty;
next_cp(<<>>) ->
empty;
next_cp([H | T]) when is_integer(H) ->
if
H >= 0 andalso H =< 16#10FFFF ->
{H, ensure_cd_cont(T)};
true ->
erlang:error(badarg)
end;
next_cp([H | T]) when is_binary(H) ->
case next_cp_bin(H) of
empty ->
next_cp(ensure_cd_cont(T));
{C, RestBin} ->
{C, stack_rest(RestBin, T)}
end;
next_cp([H | T]) when is_list(H) ->
case next_cp(H) of
empty ->
next_cp(ensure_cd_cont(T));
{C, RestH} ->
{C, stack_rest(RestH, T)}
end;
next_cp(Bin) when is_binary(Bin) ->
next_cp_bin(Bin);
next_cp(_) ->
erlang:error(badarg).

%% @private
next_cp_bin(<<>>) ->
empty;
next_cp_bin(<<C/utf8, Rest/binary>>) ->
{C, Rest};
next_cp_bin(_) ->
erlang:error(badarg).

%% @private
stack_rest([], T) ->
ensure_cd_cont(T);
stack_rest(<<>>, T) ->
ensure_cd_cont(T);
stack_rest(H, []) ->
H;
stack_rest(H, T) ->
[H | ensure_cd_cont(T)].

%% A chardata continuation must be [] | binary() | list().
%% @private
ensure_cd_cont([]) ->
[];
ensure_cd_cont(T) when is_binary(T) ->
T;
ensure_cd_cont(T) when is_list(T) ->
T;
ensure_cd_cont(_) ->
erlang:error(badarg).

%%-----------------------------------------------------------------------------
%% @param Input a string or character to convert
%% @returns a Character or string
Expand Down
73 changes: 73 additions & 0 deletions tests/libs/estdlib/test_string.erl
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,79 @@ test() ->
ok = test_trim(),
ok = test_find(),
ok = test_length(),
ok = test_to_integer(),
ok.

test_to_integer() ->
{27, ""} = string:to_integer("27"),
{12, "abc"} = string:to_integer("12abc"),
{-5, "x"} = string:to_integer("-5x"),
{42, ""} = string:to_integer("+42"),
{1, "+2"} = string:to_integer("0001+2"),
{1, <<"+2">>} = string:to_integer(<<"1+2">>),
{123, <<"x">>} = string:to_integer(<<"123x">>),
{-5, <<"x">>} = string:to_integer(<<"-5x">>),
{12, <<"é"/utf8>>} = string:to_integer(<<"12é"/utf8>>),
{12, <<"x">>} = string:to_integer([$1 | <<"2x">>]),
{123, "x"} = string:to_integer([[<<"12">>], "3x"]),
{12, <<"x">>} = string:to_integer([[], <<"12x">>]),
{12, []} = string:to_integer([<<"12">>, [], []]),
{error, no_integer} = string:to_integer("abc"),
{error, no_integer} = string:to_integer(""),
{error, no_integer} = string:to_integer(<<"+">>),
{error, no_integer} = string:to_integer("+"),
{error, no_integer} = string:to_integer("--1"),
{error, no_integer} = string:to_integer("++1"),
{error, no_integer} = string:to_integer([<<"abc">>, 16#FFFFF]),
{error, no_integer} = string:to_integer([[], [<<>>], []]),
{error, no_integer} = string:to_integer([[$-], [], <<>>]),
%% Valid first nonnumeric character: malformed tail is unexamined.
{error, no_integer} = string:to_integer(<<"a", 16#FF>>),
{error, no_integer} = string:to_integer([<<"abc">>, <<16#FF>>]),
%% Boundary char outside take-set: leftover signs are not validated past it.
{1, <<$x, 16#FF>>} = string:to_integer(<<$1, $x, 16#FF>>),
{1, [<<"x">>, <<16#FF>>]} = string:to_integer([<<"1x">>, <<16#FF>>]),
{error, badarg} = string:to_integer(foo),
{error, badarg} = string:to_integer([$3, hello]),
{error, badarg} = string:to_integer([49, 50 | bad]),
{error, badarg} = string:to_integer(<<$3, 16#FF>>),
{error, badarg} = string:to_integer([<<"12">>, <<255>>]),
%% Malformed first codepoint (or after sign) is badarg, not no_integer.
{error, badarg} = string:to_integer(<<16#FF>>),
{error, badarg} = string:to_integer(<<16#C2>>),
{error, badarg} = string:to_integer(<<$+, 16#FF>>),
{error, badarg} = string:to_integer([16#110000]),
{error, badarg} = string:to_integer([$1, 16#110000]),
%% Leftover signs in the take-set prefix are validated (OTP take semantics).
{error, badarg} = string:to_integer(<<$1, $+, 16#FF>>),
{error, badarg} = string:to_integer([$1, $+, <<16#FF>>]),
{error, badarg} = string:to_integer(<<$-, $-, 16#FF>>),
{error, badarg} = string:to_integer([<<"1+">>, <<16#FF>>]),
%% Improper list spines are rejected even when behind a valid boundary.
{error, badarg} = string:to_integer([$a | foo]),
{error, badarg} = string:to_integer([$1, $x | foo]),
{error, badarg} = string:to_integer([<<"1x">> | foo]),
{error, badarg} = string:to_integer([$1 | foo]),
%% Large integer near AtomVM's ~256-bit magnitude limit (supported on both).
BigOk = lists:duplicate(77, $9),
BigOkBin = list_to_binary(BigOk),
{BigOkInt, []} = string:to_integer(BigOk),
true = is_integer(BigOkInt) andalso BigOkInt > 0,
{BigOkInt, <<>>} = string:to_integer(BigOkBin),
%% Oversized for AtomVM (~272 bits); OTP accepts arbitrary size.
BigOver = lists:duplicate(80, $9),
BigOverBin = list_to_binary(BigOver),
case erlang:system_info(machine) of
"BEAM" ->
{BigOverInt, []} = string:to_integer(BigOver),
true = is_integer(BigOverInt) andalso BigOverInt > BigOkInt,
{BigOverInt, <<>>} = string:to_integer(BigOverBin),
ok;
_ ->
{error, badarg} = string:to_integer(BigOver),
{error, badarg} = string:to_integer(BigOverBin),
ok
end,
ok.

test_to_upper() ->
Expand Down
1 change: 1 addition & 0 deletions tests/libs/estdlib/tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ get_non_networking_tests(_OTPVersion) ->
test_queue,
test_timer,
test_spawn,
test_string,
test_supervisor,
test_lists_subtraction,
test_os,
Expand Down
Loading