cheatah
Source

stdlib/string/string.hpp

1// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).
2// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.
3#pragma once
5/**
6 * @file string.hpp
7 * @brief cheatah `string` — text operations + Python's `string` constants,
8 * surfaced as free functions (a .purr program writes `string.upper("x")`).
9 *
10 * `import string` includes this header and links `libcheatah_string`. Unit tests:
11 * `stdlib/tests/string_test.cpp`; the suite runs under AddressSanitizer (the `asan`
12 * preset) and Valgrind (`security/run-valgrind.sh`) on every QA-gate run.
13 *
14 * @note Functions returning `std::string` / `std::vector<std::string>` allocate
15 * their result on the heap; the predicate/index functions (returning `bool`/
16 * `long`) do not allocate. `n` below is the input length.
17 */
18#include <ranges>
19#include <string>
20#include <string_view>
21#include <vector>
23namespace cheatah::string {
25// ---- constants (Python `string` module) ----
26inline constexpr std::string_view ascii_lowercase = "abcdefghijklmnopqrstuvwxyz"; ///< `a–z`.
27inline constexpr std::string_view ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; ///< `A–Z`.
28inline constexpr std::string_view ascii_letters =
29 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; ///< `a–zA–Z`.
30inline constexpr std::string_view digits = "0123456789"; ///< `0–9`.
31inline constexpr std::string_view hexdigits = "0123456789abcdefABCDEF"; ///< hex digits.
32inline constexpr std::string_view octdigits = "01234567"; ///< octal digits.
33/// ASCII punctuation.
34inline constexpr std::string_view punctuation = R"(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)";
35inline constexpr std::string_view whitespace = " \t\n\r\f\v"; ///< ASCII whitespace.
37// ---- case ----
38/**
39 * Uppercase.
40 *
41 * Returns a new string with every ASCII lowercase letter mapped to uppercase
42 * via `std::toupper`; non-letters and bytes ≥ 0x80 are copied unchanged (ASCII-only).
43 * @param s input.
44 * @return @p s uppercased.
45 * @complexity O(n).
46 * @alloc allocates the result.
47 * @test CheatahString.Case
48 * @crtest StringCompileRun.Upper
49 * @systest StdlibE2E.String
50 */
51std::string upper(std::string_view s);
52/**
53 * Lowercase.
54 *
55 * Returns a new string with every ASCII uppercase letter mapped to lowercase
56 * via `std::tolower`; non-letters and bytes ≥ 0x80 are copied unchanged (ASCII-only).
57 * @param s input.
58 * @return @p s lowercased.
59 * @complexity O(n).
60 * @alloc allocates the result.
61 * @test CheatahString.Case
62 * @crtest StringCompileRun.Lower
63 * @systest StdlibE2E.String
64 */
65std::string lower(std::string_view s);
66/**
67 * Capitalize: first char upper, rest lower.
68 *
69 * Uppercases the first character and lowercases all remaining characters (ASCII-only);
70 * an empty input is returned unchanged.
71 * @param s input.
72 * @return capitalized @p s.
73 * @complexity O(n).
74 * @alloc allocates.
75 * @test CheatahString.Case
76 * @crtest StringCompileRun.Capitalize
77 * @systest StdlibE2E.String
78 */
79std::string capitalize(std::string_view s);
80/**
81 * Title-case each word.
82 *
83 * Uppercases the first letter of every run of letters and lowercases the rest;
84 * any non-letter (digits, punctuation, whitespace) acts as a word boundary (ASCII-only).
85 * @param s input.
86 * @return title-cased @p s.
87 * @complexity O(n).
88 * @alloc allocates.
89 * @test CheatahString.Case
90 * @crtest StringCompileRun.Title
91 * @systest StdlibE2E.String
92 */
93std::string title(std::string_view s);
94/**
95 * Swap the case of each letter.
96 *
97 * Returns a new string with each ASCII letter's case inverted; non-letters are
98 * left unchanged (ASCII-only).
99 * @param s input.
100 * @return case-swapped @p s.
101 * @complexity O(n).
102 * @alloc allocates.
103 * @test CheatahString.Case
104 * @crtest StringCompileRun.Swapcase
105 * @systest StdlibE2E.String
106 */
107std::string swapcase(std::string_view s);
109// ---- trimming (default: ASCII whitespace) ----
110/**
111 * Strip leading+trailing @p chars.
112 *
113 * Removes characters from both ends as long as each is present in the @p chars
114 * set (the set is a bag of characters, not a substring); defaults to ASCII
115 * whitespace. An empty @p chars set strips nothing.
116 * @param s input.
117 * @param chars cut set.
118 * @return trimmed @p s.
119 * @complexity O(n·m) (m = size of the @p chars set; a constant for the default).
120 * @alloc allocates the result plus lstrip's intermediate string.
121 * @test CheatahString.Trimming
122 * @crtest StringCompileRun.Strip
123 * @systest StdlibE2E.String
124 */
125std::string strip(std::string_view s, std::string_view chars = whitespace);
126/**
127 * Strip leading @p chars.
128 *
129 * Removes characters from the front only, as long as each is in the @p chars set
130 * (a bag of characters, not a substring); defaults to ASCII whitespace.
131 * @param s input.
132 * @param chars cut set.
133 * @return left-trimmed @p s.
134 * @complexity O(n·m) (m = size of the @p chars set; a constant for the default).
135 * @alloc allocates.
136 * @test CheatahString.Trimming
137 * @crtest StringCompileRun.Lstrip
138 * @systest StdlibE2E.String
139 */
140std::string lstrip(std::string_view s, std::string_view chars = whitespace);
141/**
142 * Strip trailing @p chars.
143 *
144 * Removes characters from the end only, as long as each is in the @p chars set
145 * (a bag of characters, not a substring); defaults to ASCII whitespace.
146 * @param s input.
147 * @param chars cut set.
148 * @return right-trimmed @p s.
149 * @complexity O(n·m) (m = size of the @p chars set; a constant for the default).
150 * @alloc allocates.
151 * @test CheatahString.Trimming
152 * @crtest StringCompileRun.Rstrip
153 * @systest StdlibE2E.String
154 */
155std::string rstrip(std::string_view s, std::string_view chars = whitespace);
157// ---- search / test ----
158/**
159 * Prefix test.
160 *
161 * Case-sensitive, byte-exact comparison; an empty @p prefix always matches.
162 * @param s input.
163 * @param prefix sought prefix.
164 * @return true iff @p s starts with @p prefix.
165 * @complexity O(n).
166 * @alloc none.
167 * @test CheatahString.SearchAndTest
168 * @crtest StringCompileRun.Startswith
169 * @systest StdlibE2E.String
170 */
171bool startswith(std::string_view s, std::string_view prefix);
172/**
173 * Suffix test.
174 *
175 * Case-sensitive, byte-exact comparison; an empty @p suffix always matches.
176 * @param s input.
177 * @param suffix sought suffix.
178 * @return true iff @p s ends with @p suffix.
179 * @complexity O(n).
180 * @alloc none.
181 * @test CheatahString.SearchAndTest
182 * @crtest StringCompileRun.Endswith
183 * @systest StdlibE2E.String
184 */
185bool endswith(std::string_view s, std::string_view suffix);
186/**
187 * Substring test.
188 *
189 * Case-sensitive search for @p sub anywhere in @p s; an empty @p sub is always
190 * considered present.
191 * @param s input.
192 * @param sub needle.
193 * @return true iff @p sub occurs in @p s.
194 * @complexity O(n·m).
195 * @alloc none.
196 * @test CheatahString.SearchAndTest
197 * @crtest StringCompileRun.Contains
198 * @systest StdlibE2E.String
199 */
200bool contains(std::string_view s, std::string_view sub);
202/**
203 * contains() with a single-char needle — what iterating a string yields (`for ch in s`).
204 *
205 * Case-sensitive byte search for @p c anywhere in @p s; an empty @p s returns false.
206 * @param s input.
207 * @param c the character.
208 * @return true when present.
209 * @complexity O(n).
210 * @alloc none.
211 * @test CheatahString.ContainsChar
212 * @systest RequestsSys.QueryParams
213 */
214inline bool contains(std::string_view s, char c) { return s.find(c) != std::string_view::npos; }
215/**
216 * First index of @p sub.
217 *
218 * Returns the 0-based byte index of the first (leftmost) case-sensitive match,
219 * or -1 if not found; an empty @p sub returns 0.
220 * @param s input.
221 * @param sub needle.
222 * @return index, or -1.
223 * @complexity O(n·m).
224 * @alloc none.
225 * @test CheatahString.SearchAndTest
226 * @crtest StringCompileRun.Find
227 * @systest StdlibE2E.String
228 */
229long find(std::string_view s, std::string_view sub);
230/**
231 * First index of @p sub at or after @p start.
232 *
233 * Like @ref find but begins the search at byte offset @p start (matching Python's
234 * `str.find(sub, start)`): a negative @p start is treated as 0, and a @p start past the
235 * end returns -1. Lets a caller scan a large buffer for successive matches WITHOUT slicing
236 * the tail each step — turning an otherwise O(n²) repeated-search loop into O(n).
237 * @param s input.
238 * @param sub needle.
239 * @param start byte offset to begin searching from.
240 * @return index (absolute, into @p s), or -1.
241 * @complexity O(n·m).
242 * @alloc none.
243 * @test CheatahString.SearchAndTest
244 * @systest RequestsSys.HeaderLookup
245 */
246long find(std::string_view s, std::string_view sub, long start);
247/**
248 * Last index of @p sub.
249 *
250 * Returns the 0-based byte index of the last (rightmost) case-sensitive match,
251 * or -1 if not found; an empty @p sub returns the length of @p s.
252 * @param s input.
253 * @param sub needle.
254 * @return index, or -1.
255 * @complexity O(n·m).
256 * @alloc none.
257 * @test CheatahString.SearchAndTest
258 * @crtest StringCompileRun.Rfind
259 * @systest StdlibE2E.String
260 */
261long rfind(std::string_view s, std::string_view sub);
262/**
263 * Count non-overlapping @p sub.
264 *
265 * Counts left-to-right, non-overlapping case-sensitive matches; matching Python,
266 * an empty @p sub returns `len(s) + 1`.
267 * @param s input.
268 * @param sub needle.
269 * @return occurrence count.
270 * @complexity O(n·m).
271 * @alloc none.
272 * @test CheatahString.SearchAndTest
273 * @crtest StringCompileRun.Count
274 * @systest StdlibE2E.String
275 */
276long count(std::string_view s, std::string_view sub);
278// ---- transform ----
279/**
280 * Replace all @p from with @p to.
281 *
282 * Replaces every non-overlapping, case-sensitive occurrence of @p from with @p to;
283 * an empty @p from leaves @p s unchanged (unlike Python).
284 * @param s input.
285 * @param from,to needle/replacement.
286 * @return new string.
287 * @complexity O(n·m + result length).
288 * @alloc allocates.
289 * @test CheatahString.Transform
290 * @crtest StringCompileRun.Replace
291 * @systest StdlibE2E.String
292 */
293std::string replace(std::string_view s, std::string_view from, std::string_view to);
294/**
295 * Split on @p sep.
296 *
297 * Splits at each non-overlapping occurrence of @p sep, keeping empty fields
298 * (e.g. "a,,b" yields three parts, leading/trailing separators yield empty
299 * strings); the result always has at least one element.
300 * @param s input.
301 * @param sep separator (empty → the whole string as one part).
302 * @return the parts.
303 * @complexity O(n·m).
304 * @alloc allocates a vector of strings.
305 * @test CheatahString.Transform, CheatahString.SplitEmptySeparator
306 * @crtest StringCompileRun.Split
307 * @systest StdlibE2E.String
308 */
309std::vector<std::string> split(std::string_view s, std::string_view sep);
310/**
311 * Split on runs of whitespace.
312 *
313 * Splits on maximal runs of ASCII whitespace and discards empty fields, so leading,
314 * trailing, and repeated whitespace produce no empty parts; a blank/empty input
315 * yields an empty vector.
316 * @param s input.
317 * @return the non-empty parts.
318 * @complexity O(n).
319 * @alloc allocates a vector of strings.
320 * @test CheatahString.Transform
321 * @crtest StringCompileRun.SplitWhitespace
322 * @systest StdlibE2E.String
323 */
324std::vector<std::string> split(std::string_view s);
325/**
326 * Split into lines.
327 *
328 * Breaks on `\n`, `\r`, and `\r\n` (treated as a single break) with the line
329 * terminators removed; a trailing newline does not produce a final empty line,
330 * and an empty input yields an empty vector.
331 * @param s input.
332 * @return the lines (newlines removed).
333 * @complexity O(n).
334 * @alloc allocates a vector of strings.
335 * @test CheatahString.Transform
336 * @crtest StringCompileRun.Splitlines
337 * @systest StdlibE2E.String
338 */
339std::vector<std::string> splitlines(std::string_view s);
340/**
341 * Python `string.capwords`: split on whitespace, capitalize, re-join with spaces.
342 *
343 * Capitalizes each whitespace-delimited word (first letter upper, rest lower) and
344 * re-joins with single spaces, so all runs of original whitespace collapse and
345 * leading/trailing whitespace is dropped.
346 * @param s input.
347 * @return the result.
348 * @complexity O(n).
349 * @alloc allocates a vector of words plus the result.
350 * @test CheatahString.Transform
351 * @crtest StringCompileRun.Capwords
352 * @systest StdlibE2E.String
353 */
354std::string capwords(std::string_view s);
356/// StringViewable<T>: a `std::string_view` can be built from T — what join() needs.
357template <typename T>
358concept StringViewable = requires(const T& v) { std::string_view(v); };
360/**
361 * Join @p parts with @p sep.
362 *
363 * Concatenates each element of @p parts with @p sep inserted only between elements
364 * (no leading or trailing separator); an empty range yields an empty string.
365 * @param sep separator.
366 * @param parts any range of string-like values.
367 * @return the joined string.
368 * @complexity O(total length).
369 * @alloc allocates the result.
370 * @test CheatahString.Transform
371 * @crtest StringCompileRun.Join
372 * @systest StdlibE2E.String
373 */
374template <std::ranges::input_range Range>
375 requires StringViewable<std::ranges::range_value_t<Range>>
376std::string join(std::string_view sep, const Range& parts) {
377 std::string out;
378 bool first = true;
379 for (const auto& part : parts) {
380 if (!first) {
381 out += sep;
382 }
383 out += std::string_view(part);
384 first = false;
385 }
386 return out;
389// ---- padding (fill defaults to a space; first character of `fill` is used) ----
390/**
391 * Left-justify to @p width.
392 *
393 * Pads @p s on the right with the fill character up to @p width; if @p s is already
394 * at least @p width long it is returned unchanged. Only the first character of
395 * @p fill is used (an empty @p fill defaults to a space).
396 * @param s input.
397 * @param width target.
398 * @param fill pad char.
399 * @return padded @p s (or @p s if already ≥ width).
400 * @complexity O(n + width).
401 * @alloc allocates.
402 * @test CheatahString.Padding
403 * @crtest StringCompileRun.Ljust
404 * @systest StdlibE2E.String
405 */
406std::string ljust(std::string_view s, std::size_t width, std::string_view fill = " ");
407/**
408 * Right-justify to @p width.
409 *
410 * Pads @p s on the left with the fill character up to @p width; if @p s is already
411 * at least @p width long it is returned unchanged. Only the first character of
412 * @p fill is used (an empty @p fill defaults to a space).
413 * @param s input.
414 * @param width target.
415 * @param fill pad char.
416 * @return padded @p s.
417 * @complexity O(n + width).
418 * @alloc allocates the result plus concatenation temporaries.
419 * @test CheatahString.Padding
420 * @crtest StringCompileRun.Rjust
421 * @systest StdlibE2E.String
422 */
423std::string rjust(std::string_view s, std::size_t width, std::string_view fill = " ");
424/**
425 * Center within @p width.
426 *
427 * Pads both sides with the fill character; when the padding is odd the extra
428 * character goes on the right. Returns @p s unchanged if it is already at least
429 * @p width long, and only the first character of @p fill is used (empty → space).
430 * @param s input.
431 * @param width target.
432 * @param fill pad char.
433 * @return padded @p s.
434 * @complexity O(n + width).
435 * @alloc allocates the result plus concatenation temporaries.
436 * @test CheatahString.Padding
437 * @crtest StringCompileRun.Center
438 * @systest StdlibE2E.String
439 */
440std::string center(std::string_view s, std::size_t width, std::string_view fill = " ");
441/**
442 * Zero-fill on the left to @p width.
443 *
444 * Left-pads with `'0'` to @p width; if @p s begins with a `'+'` or `'-'` sign the
445 * zeros are inserted after the sign. Returns @p s unchanged if already at least
446 * @p width long.
447 * @param s input.
448 * @param width target.
449 * @return `'0'`-padded @p s.
450 * @complexity O(n + width).
451 * @alloc allocates the result plus concatenation temporaries.
452 * @test CheatahString.Padding
453 * @crtest StringCompileRun.Zfill
454 * @systest StdlibE2E.String
455 */
456std::string zfill(std::string_view s, std::size_t width);
458// ---- whole-string classification (False for the empty string, like Python) ----
459/**
460 * All digits?
461 *
462 * True only if @p s is non-empty and every character is an ASCII decimal digit;
463 * the empty string returns false (matching Python).
464 * @param s input.
465 * @return true iff non-empty and all `0–9`.
466 * @complexity O(n).
467 * @alloc none.
468 * @test CheatahString.Classification
469 * @crtest StringCompileRun.Isdigit
470 * @systest StdlibE2E.String
471 */
472bool isdigit(std::string_view s);
473/**
474 * All letters?
475 *
476 * True only if @p s is non-empty and every character is an ASCII letter (`std::isalpha`);
477 * the empty string returns false.
478 * @param s input.
479 * @return true iff non-empty and all alphabetic.
480 * @complexity O(n).
481 * @alloc none.
482 * @test CheatahString.Classification
483 * @crtest StringCompileRun.Isalpha
484 * @systest StdlibE2E.String
485 */
486bool isalpha(std::string_view s);
487/**
488 * All alphanumeric?
489 *
490 * True only if @p s is non-empty and every character is an ASCII letter or digit
491 * (`std::isalnum`); the empty string returns false.
492 * @param s input.
493 * @return true iff non-empty and all letters/digits.
494 * @complexity O(n).
495 * @alloc none.
496 * @test CheatahString.ClassificationAlnumAndSpace
497 * @crtest StringCompileRun.Isalnum
498 * @systest StdlibE2E.String
499 */
500bool isalnum(std::string_view s);
501/**
502 * All whitespace?
503 *
504 * True only if @p s is non-empty and every character is ASCII whitespace
505 * (`std::isspace`: space, tab, newline, CR, form-feed, vertical tab); the empty
506 * string returns false.
507 * @param s input.
508 * @return true iff non-empty and all whitespace.
509 * @complexity O(n).
510 * @alloc none.
511 * @test CheatahString.ClassificationAlnumAndSpace
512 * @crtest StringCompileRun.Isspace
513 * @systest StdlibE2E.String
514 */
515bool isspace(std::string_view s);
516/**
517 * All uppercase?
518 *
519 * True iff @p s contains at least one ASCII uppercase letter and no lowercase letters;
520 * non-letter characters are ignored, so e.g. "ABC123" is uppercase but "123" and the
521 * empty string are not.
522 * @param s input.
523 * @return true iff @p s has ≥ 1 uppercase letter and no lowercase.
524 * @complexity O(n).
525 * @alloc none.
526 * @test CheatahString.Classification
527 * @crtest StringCompileRun.Isupper
528 * @systest StdlibE2E.String
529 */
530bool isupper(std::string_view s);
531/**
532 * All lowercase?
533 *
534 * True iff @p s contains at least one ASCII lowercase letter and no uppercase letters;
535 * non-letter characters are ignored, so e.g. "abc123" is lowercase but "123" and the
536 * empty string are not.
537 * @param s input.
538 * @return true iff @p s has ≥ 1 lowercase letter and no uppercase.
539 * @complexity O(n).
540 * @alloc none.
541 * @test CheatahString.Classification
542 * @crtest StringCompileRun.Islower
543 * @systest StdlibE2E.String
544 */
545bool islower(std::string_view s);
547} // namespace cheatah::string