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 ->
5758upper_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
0 commit comments