Skip to content

Commit f01ee59

Browse files
petermmpguyot
andcommitted
Add OTP-compatible string:to_integer/1
Parse a leading optionally signed ASCII integer from unicode:chardata(), returning {Int, Rest} or {error, no_integer | badarg}. Accept flat lists, binaries, and mixed/deep chardata; keep binary remainders for pure binary input; validate the maximal leading +-/digit run (OTP take semantics); and reject malformed UTF-8 and improper list spines encountered while parsing. Register test_string in the estdlib suite and cover binary/mixed input, grammar boundaries, badarg cases, and AtomVM bigint limits. Co-authored-by: Paul Guyot <pguyot@kallisys.net> Signed-off-by: Peter M <petermm@gmail.com>
1 parent f7594af commit f01ee59

4 files changed

Lines changed: 321 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3232
- Added USB CDC port drivers for ESP32, RP2, and STM32 platforms
3333
- Added a Linux `gpio` driver for the generic_unix port (in `avm_unix`) using sysfs
3434
- Added `console:print_err/1` to write to standard error
35+
- Added `string:to_integer/1`
3536

3637
### Changed
3738
- Updated network type db() to dbm() to reflect the actual representation of the type

libs/estdlib/src/string.erl

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
trim/1, trim/2,
3535
find/2, find/3,
3636
length/1,
37+
to_integer/1,
3738
jaro_similarity/2
3839
]).
3940

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

61+
%%-----------------------------------------------------------------------------
62+
%% @param String a chardata value possibly beginning with an integer
63+
%% @returns `{Int, Rest}' where `Rest' is the unconsumed chardata suffix, or
64+
%% `{error, no_integer}' if it does not begin with an integer, or
65+
%% `{error, badarg}' if malformed chardata or invalid UTF-8 is
66+
%% encountered while parsing
67+
%% @doc Parse a leading (optionally signed) integer from a `unicode:chardata()'
68+
%% value. Digits and sign are ASCII. The remainder keeps binary form when the
69+
%% original argument is a binary.
70+
%%
71+
%% Matching OTP, the maximal leading run of ASCII `+', `-', and digits is
72+
%% validated before the integer grammar is applied, so leftover signs in that
73+
%% run are examined for chardata errors even when they become part of `Rest'.
74+
%% @end
75+
%%-----------------------------------------------------------------------------
76+
-spec to_integer(String :: unicode:chardata()) ->
77+
{integer(), unicode:chardata()} | {error, no_integer | badarg}.
78+
to_integer(String) ->
79+
try to_integer_cd(String) of
80+
{error, _} = Err ->
81+
Err;
82+
{Int, Rest} ->
83+
{Int, Rest}
84+
catch
85+
error:badarg ->
86+
{error, badarg}
87+
end.
88+
89+
%% @private
90+
to_integer_cd(Bin) when is_binary(Bin) ->
91+
case take_int_bin(Bin) of
92+
no_integer ->
93+
{error, no_integer};
94+
{Int, Rest} ->
95+
{Int, Rest}
96+
end;
97+
to_integer_cd(List) when is_list(List) ->
98+
case take_int_cd(List) of
99+
no_integer ->
100+
{error, no_integer};
101+
{Int, Rest} ->
102+
{Int, Rest}
103+
end;
104+
to_integer_cd(_) ->
105+
{error, badarg}.
106+
107+
%% Take the maximal leading run of ASCII + - digits (OTP string:take/2 set),
108+
%% validate the first codepoint after that run, then apply integer grammar.
109+
%% @private
110+
take_int_bin(Bin) ->
111+
{PrefLen, Tail} = take_set_bin(Bin, 0),
112+
case ensure_utf8_boundary(Tail) of
113+
ok when PrefLen =:= 0 ->
114+
no_integer;
115+
ok ->
116+
Pref = binary:part(Bin, 0, PrefLen),
117+
case parse_int_bin(Pref) of
118+
no_integer ->
119+
no_integer;
120+
{Int, RestPref} ->
121+
{Int, <<RestPref/binary, Tail/binary>>}
122+
end;
123+
error ->
124+
erlang:error(badarg)
125+
end.
126+
127+
%% @private
128+
take_set_bin(<<C, Rest/binary>>, N) when
129+
C =:= $+ orelse C =:= $- orelse (C >= $0 andalso C =< $9)
130+
->
131+
take_set_bin(Rest, N + 1);
132+
take_set_bin(Rest, N) ->
133+
{N, Rest}.
134+
135+
%% @private
136+
parse_int_bin(<<$+, Rest/binary>>) ->
137+
parse_int_digits_bin(Rest, 1);
138+
parse_int_bin(<<$-, Rest/binary>>) ->
139+
parse_int_digits_bin(Rest, -1);
140+
parse_int_bin(Bin) ->
141+
parse_int_digits_bin(Bin, 1).
142+
143+
%% @private
144+
parse_int_digits_bin(Bin, Sign) ->
145+
{N, Rest} = take_digits_bin(Bin, 0),
146+
case N of
147+
0 ->
148+
no_integer;
149+
_ ->
150+
Digits = binary:part(Bin, 0, N),
151+
{Sign * binary_to_integer(Digits), Rest}
152+
end.
153+
154+
%% @private
155+
take_digits_bin(<<C, Rest/binary>>, N) when C >= $0 andalso C =< $9 ->
156+
take_digits_bin(Rest, N + 1);
157+
take_digits_bin(Rest, N) ->
158+
{N, Rest}.
159+
160+
%% @private
161+
ensure_utf8_boundary(<<>>) ->
162+
ok;
163+
ensure_utf8_boundary(<<C, _/binary>>) when C =< 16#7F ->
164+
ok;
165+
ensure_utf8_boundary(<<_/utf8, _/binary>>) ->
166+
ok;
167+
ensure_utf8_boundary(_) ->
168+
error.
169+
170+
%% @private
171+
take_int_cd(CD) ->
172+
{HeadChars, Tail0} = take_set_cd(CD, []),
173+
Tail = normalize_empty_cd(Tail0),
174+
case HeadChars of
175+
[] ->
176+
no_integer;
177+
_ ->
178+
case parse_int_chars(HeadChars) of
179+
no_integer ->
180+
no_integer;
181+
{Int, RestChars} ->
182+
{Int, append_cd(RestChars, Tail)}
183+
end
184+
end.
185+
186+
%% Collect leading + - digit codepoints; stop before first non-member.
187+
%% @private
188+
take_set_cd(CD, Acc) ->
189+
case next_cp(CD) of
190+
empty ->
191+
{lists:reverse(Acc), CD};
192+
{C, Rest} when C =:= $+ orelse C =:= $- orelse (C >= $0 andalso C =< $9) ->
193+
take_set_cd(Rest, [C | Acc]);
194+
{_C, _Rest} ->
195+
{lists:reverse(Acc), CD}
196+
end.
197+
198+
%% @private
199+
parse_int_chars([$+ | Rest]) ->
200+
parse_int_digits_chars(Rest, 1);
201+
parse_int_chars([$- | Rest]) ->
202+
parse_int_digits_chars(Rest, -1);
203+
parse_int_chars(Chars) ->
204+
parse_int_digits_chars(Chars, 1).
205+
206+
%% @private
207+
parse_int_digits_chars([C | Rest], Sign) when C >= $0 andalso C =< $9 ->
208+
parse_int_more_chars(Rest, Sign, [C]);
209+
parse_int_digits_chars(_Rest, _Sign) ->
210+
no_integer.
211+
212+
%% @private
213+
parse_int_more_chars([C | Rest], Sign, Acc) when C >= $0 andalso C =< $9 ->
214+
parse_int_more_chars(Rest, Sign, [C | Acc]);
215+
parse_int_more_chars(Rest, Sign, Acc) ->
216+
{Sign * list_to_integer(lists:reverse(Acc)), Rest}.
217+
218+
%% @private
219+
append_cd([], Tail) ->
220+
Tail;
221+
append_cd(RestChars, Tail) ->
222+
RestChars ++ Tail.
223+
224+
%% @private
225+
normalize_empty_cd(CD) ->
226+
case is_empty_cd(CD) of
227+
true ->
228+
[];
229+
false ->
230+
CD
231+
end.
232+
233+
%% @private
234+
is_empty_cd([]) ->
235+
true;
236+
is_empty_cd(<<>>) ->
237+
true;
238+
is_empty_cd([H | T]) ->
239+
is_empty_cd(H) andalso is_empty_cd(T);
240+
is_empty_cd(_) ->
241+
false.
242+
243+
%% next_cp(Chardata) -> {Codepoint, Rest} | empty
244+
%% Raises error:badarg on malformed UTF-8 / non-chardata, including improper
245+
%% list continuations that are not a list or binary.
246+
%% @private
247+
next_cp([]) ->
248+
empty;
249+
next_cp(<<>>) ->
250+
empty;
251+
next_cp([H | T]) when is_integer(H) ->
252+
if
253+
H >= 0 andalso H =< 16#10FFFF ->
254+
{H, ensure_cd_cont(T)};
255+
true ->
256+
erlang:error(badarg)
257+
end;
258+
next_cp([H | T]) when is_binary(H) ->
259+
case next_cp_bin(H) of
260+
empty ->
261+
next_cp(ensure_cd_cont(T));
262+
{C, RestBin} ->
263+
{C, stack_rest(RestBin, T)}
264+
end;
265+
next_cp([H | T]) when is_list(H) ->
266+
case next_cp(H) of
267+
empty ->
268+
next_cp(ensure_cd_cont(T));
269+
{C, RestH} ->
270+
{C, stack_rest(RestH, T)}
271+
end;
272+
next_cp(Bin) when is_binary(Bin) ->
273+
next_cp_bin(Bin);
274+
next_cp(_) ->
275+
erlang:error(badarg).
276+
277+
%% @private
278+
next_cp_bin(<<>>) ->
279+
empty;
280+
next_cp_bin(<<C/utf8, Rest/binary>>) ->
281+
{C, Rest};
282+
next_cp_bin(_) ->
283+
erlang:error(badarg).
284+
285+
%% @private
286+
stack_rest([], T) ->
287+
ensure_cd_cont(T);
288+
stack_rest(<<>>, T) ->
289+
ensure_cd_cont(T);
290+
stack_rest(H, []) ->
291+
H;
292+
stack_rest(H, T) ->
293+
[H | ensure_cd_cont(T)].
294+
295+
%% A chardata continuation must be [] | binary() | list().
296+
%% @private
297+
ensure_cd_cont([]) ->
298+
[];
299+
ensure_cd_cont(T) when is_binary(T) ->
300+
T;
301+
ensure_cd_cont(T) when is_list(T) ->
302+
T;
303+
ensure_cd_cont(_) ->
304+
erlang:error(badarg).
305+
60306
%%-----------------------------------------------------------------------------
61307
%% @param Input a string or character to convert
62308
%% @returns a Character or string

tests/libs/estdlib/test_string.erl

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,79 @@ test() ->
3030
ok = test_trim(),
3131
ok = test_find(),
3232
ok = test_length(),
33+
ok = test_to_integer(),
34+
ok.
35+
36+
test_to_integer() ->
37+
{27, ""} = string:to_integer("27"),
38+
{12, "abc"} = string:to_integer("12abc"),
39+
{-5, "x"} = string:to_integer("-5x"),
40+
{42, ""} = string:to_integer("+42"),
41+
{1, "+2"} = string:to_integer("0001+2"),
42+
{1, <<"+2">>} = string:to_integer(<<"1+2">>),
43+
{123, <<"x">>} = string:to_integer(<<"123x">>),
44+
{-5, <<"x">>} = string:to_integer(<<"-5x">>),
45+
{12, <<"é"/utf8>>} = string:to_integer(<<"12é"/utf8>>),
46+
{12, <<"x">>} = string:to_integer([$1 | <<"2x">>]),
47+
{123, "x"} = string:to_integer([[<<"12">>], "3x"]),
48+
{12, <<"x">>} = string:to_integer([[], <<"12x">>]),
49+
{12, []} = string:to_integer([<<"12">>, [], []]),
50+
{error, no_integer} = string:to_integer("abc"),
51+
{error, no_integer} = string:to_integer(""),
52+
{error, no_integer} = string:to_integer(<<"+">>),
53+
{error, no_integer} = string:to_integer("+"),
54+
{error, no_integer} = string:to_integer("--1"),
55+
{error, no_integer} = string:to_integer("++1"),
56+
{error, no_integer} = string:to_integer([<<"abc">>, 16#FFFFF]),
57+
{error, no_integer} = string:to_integer([[], [<<>>], []]),
58+
{error, no_integer} = string:to_integer([[$-], [], <<>>]),
59+
%% Valid first nonnumeric character: malformed tail is unexamined.
60+
{error, no_integer} = string:to_integer(<<"a", 16#FF>>),
61+
{error, no_integer} = string:to_integer([<<"abc">>, <<16#FF>>]),
62+
%% Boundary char outside take-set: leftover signs are not validated past it.
63+
{1, <<$x, 16#FF>>} = string:to_integer(<<$1, $x, 16#FF>>),
64+
{1, [<<"x">>, <<16#FF>>]} = string:to_integer([<<"1x">>, <<16#FF>>]),
65+
{error, badarg} = string:to_integer(foo),
66+
{error, badarg} = string:to_integer([$3, hello]),
67+
{error, badarg} = string:to_integer([49, 50 | bad]),
68+
{error, badarg} = string:to_integer(<<$3, 16#FF>>),
69+
{error, badarg} = string:to_integer([<<"12">>, <<255>>]),
70+
%% Malformed first codepoint (or after sign) is badarg, not no_integer.
71+
{error, badarg} = string:to_integer(<<16#FF>>),
72+
{error, badarg} = string:to_integer(<<16#C2>>),
73+
{error, badarg} = string:to_integer(<<$+, 16#FF>>),
74+
{error, badarg} = string:to_integer([16#110000]),
75+
{error, badarg} = string:to_integer([$1, 16#110000]),
76+
%% Leftover signs in the take-set prefix are validated (OTP take semantics).
77+
{error, badarg} = string:to_integer(<<$1, $+, 16#FF>>),
78+
{error, badarg} = string:to_integer([$1, $+, <<16#FF>>]),
79+
{error, badarg} = string:to_integer(<<$-, $-, 16#FF>>),
80+
{error, badarg} = string:to_integer([<<"1+">>, <<16#FF>>]),
81+
%% Improper list spines are rejected even when behind a valid boundary.
82+
{error, badarg} = string:to_integer([$a | foo]),
83+
{error, badarg} = string:to_integer([$1, $x | foo]),
84+
{error, badarg} = string:to_integer([<<"1x">> | foo]),
85+
{error, badarg} = string:to_integer([$1 | foo]),
86+
%% Large integer near AtomVM's ~256-bit magnitude limit (supported on both).
87+
BigOk = lists:duplicate(77, $9),
88+
BigOkBin = list_to_binary(BigOk),
89+
{BigOkInt, []} = string:to_integer(BigOk),
90+
true = is_integer(BigOkInt) andalso BigOkInt > 0,
91+
{BigOkInt, <<>>} = string:to_integer(BigOkBin),
92+
%% Oversized for AtomVM (~272 bits); OTP accepts arbitrary size.
93+
BigOver = lists:duplicate(80, $9),
94+
BigOverBin = list_to_binary(BigOver),
95+
case erlang:system_info(machine) of
96+
"BEAM" ->
97+
{BigOverInt, []} = string:to_integer(BigOver),
98+
true = is_integer(BigOverInt) andalso BigOverInt > BigOkInt,
99+
{BigOverInt, <<>>} = string:to_integer(BigOverBin),
100+
ok;
101+
_ ->
102+
{error, badarg} = string:to_integer(BigOver),
103+
{error, badarg} = string:to_integer(BigOverBin),
104+
ok
105+
end,
33106
ok.
34107

35108
test_to_upper() ->

tests/libs/estdlib/tests.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ get_non_networking_tests(_OTPVersion) ->
7575
test_queue,
7676
test_timer,
7777
test_spawn,
78+
test_string,
7879
test_supervisor,
7980
test_lists_subtraction,
8081
test_os,

0 commit comments

Comments
 (0)