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 * @param s input. @param c the character. @return true when present.
205 * @complexity O(n). @alloc none.
206 * @test CheatahString.ContainsChar
207 * @crtest StringCompileRun.Contains
208 * @systest StdlibE2E.String
209 */
210inline bool contains(std::string_view s, char c) { return s.find(c) != std::string_view::npos; }
211/**
212 * First index of @p sub.
213 *
214 * Returns the 0-based byte index of the first (leftmost) case-sensitive match,
215 * or -1 if not found; an empty @p sub returns 0.
216 * @param s input.
217 * @param sub needle.
218 * @return index, or -1.
219 * @complexity O(n·m).
220 * @alloc none.
221 * @test CheatahString.SearchAndTest
222 * @crtest StringCompileRun.Find
223 * @systest StdlibE2E.String
224 */
225long find(std::string_view s, std::string_view sub);
226/**
227 * First index of @p sub at or after @p start.
228 *
229 * Like @ref find but begins the search at byte offset @p start (matching Python's
230 * `str.find(sub, start)`): a negative @p start is treated as 0, and a @p start past the
231 * end returns -1. Lets a caller scan a large buffer for successive matches WITHOUT slicing
232 * the tail each step — turning an otherwise O(n²) repeated-search loop into O(n).
233 * @param s input.
234 * @param sub needle.
235 * @param start byte offset to begin searching from.
236 * @return index (absolute, into @p s), or -1.
237 * @complexity O(n·m).
238 * @alloc none.
239 * @test CheatahString.SearchAndTest
240 * @crtest StringCompileRun.Find
241 * @systest StdlibE2E.String
242 */
243long find(std::string_view s, std::string_view sub, long start);
244/**
245 * Last index of @p sub.
246 *
247 * Returns the 0-based byte index of the last (rightmost) case-sensitive match,
248 * or -1 if not found; an empty @p sub returns the length of @p s.
249 * @param s input.
250 * @param sub needle.
251 * @return index, or -1.
252 * @complexity O(n·m).
253 * @alloc none.
254 * @test CheatahString.SearchAndTest
255 * @crtest StringCompileRun.Rfind
256 * @systest StdlibE2E.String
257 */
258long rfind(std::string_view s, std::string_view sub);
259/**
260 * Count non-overlapping @p sub.
261 *
262 * Counts left-to-right, non-overlapping case-sensitive matches; matching Python,
263 * an empty @p sub returns `len(s) + 1`.
264 * @param s input.
265 * @param sub needle.
266 * @return occurrence count.
267 * @complexity O(n·m).
268 * @alloc none.
269 * @test CheatahString.SearchAndTest
270 * @crtest StringCompileRun.Count
271 * @systest StdlibE2E.String
272 */
273long count(std::string_view s, std::string_view sub);
275// ---- transform ----
276/**
277 * Replace all @p from with @p to.
278 *
279 * Replaces every non-overlapping, case-sensitive occurrence of @p from with @p to;
280 * an empty @p from leaves @p s unchanged (unlike Python).
281 * @param s input.
282 * @param from,to needle/replacement.
283 * @return new string.
284 * @complexity O(n·m + result length).
285 * @alloc allocates.
286 * @test CheatahString.Transform
287 * @crtest StringCompileRun.Replace
288 * @systest StdlibE2E.String
289 */
290std::string replace(std::string_view s, std::string_view from, std::string_view to);
291/**
292 * Split on @p sep.
293 *
294 * Splits at each non-overlapping occurrence of @p sep, keeping empty fields
295 * (e.g. "a,,b" yields three parts, leading/trailing separators yield empty
296 * strings); the result always has at least one element.
297 * @param s input.
298 * @param sep separator (empty → the whole string as one part).
299 * @return the parts.
300 * @complexity O(n·m).
301 * @alloc allocates a vector of strings.
302 * @test CheatahString.Transform, CheatahString.SplitEmptySeparator
303 * @crtest StringCompileRun.Split
304 * @systest StdlibE2E.String
305 */
306std::vector<std::string> split(std::string_view s, std::string_view sep);
307/**
308 * Split on runs of whitespace.
309 *
310 * Splits on maximal runs of ASCII whitespace and discards empty fields, so leading,
311 * trailing, and repeated whitespace produce no empty parts; a blank/empty input
312 * yields an empty vector.
313 * @param s input.
314 * @return the non-empty parts.
315 * @complexity O(n).
316 * @alloc allocates a vector of strings.
317 * @test CheatahString.Transform
318 * @crtest StringCompileRun.SplitWhitespace
319 * @systest StdlibE2E.String
320 */
321std::vector<std::string> split(std::string_view s);
322/**
323 * Split into lines.
324 *
325 * Breaks on `\n`, `\r`, and `\r\n` (treated as a single break) with the line
326 * terminators removed; a trailing newline does not produce a final empty line,
327 * and an empty input yields an empty vector.
328 * @param s input.
329 * @return the lines (newlines removed).
330 * @complexity O(n).
331 * @alloc allocates a vector of strings.
332 * @test CheatahString.Transform
333 * @crtest StringCompileRun.Splitlines
334 * @systest StdlibE2E.String
335 */
336std::vector<std::string> splitlines(std::string_view s);
337/**
338 * Python `string.capwords`: split on whitespace, capitalize, re-join with spaces.
339 *
340 * Capitalizes each whitespace-delimited word (first letter upper, rest lower) and
341 * re-joins with single spaces, so all runs of original whitespace collapse and
342 * leading/trailing whitespace is dropped.
343 * @param s input.
344 * @return the result.
345 * @complexity O(n).
346 * @alloc allocates a vector of words plus the result.
347 * @test CheatahString.Transform
348 * @crtest StringCompileRun.Capwords
349 * @systest StdlibE2E.String
350 */
351std::string capwords(std::string_view s);
353/// StringViewable<T>: a `std::string_view` can be built from T — what join() needs.
354template <typename T>
355concept StringViewable = requires(const T& v) { std::string_view(v); };
357/**
358 * Join @p parts with @p sep.
359 *
360 * Concatenates each element of @p parts with @p sep inserted only between elements
361 * (no leading or trailing separator); an empty range yields an empty string.
362 * @param sep separator.
363 * @param parts any range of string-like values.
364 * @return the joined string.
365 * @complexity O(total length).
366 * @alloc allocates the result.
367 * @test CheatahString.Transform
368 * @crtest StringCompileRun.Join
369 * @systest StdlibE2E.String
370 */
371template <std::ranges::input_range Range>
372 requires StringViewable<std::ranges::range_value_t<Range>>
373std::string join(std::string_view sep, const Range& parts) {
374 std::string out;
375 bool first = true;
376 for (const auto& part : parts) {
377 if (!first) {
378 out += sep;
379 }
380 out += std::string_view(part);
381 first = false;
382 }
383 return out;
386// ---- padding (fill defaults to a space; first character of `fill` is used) ----
387/**
388 * Left-justify to @p width.
389 *
390 * Pads @p s on the right with the fill character up to @p width; if @p s is already
391 * at least @p width long it is returned unchanged. Only the first character of
392 * @p fill is used (an empty @p fill defaults to a space).
393 * @param s input.
394 * @param width target.
395 * @param fill pad char.
396 * @return padded @p s (or @p s if already ≥ width).
397 * @complexity O(n + width).
398 * @alloc allocates.
399 * @test CheatahString.Padding
400 * @crtest StringCompileRun.Ljust
401 * @systest StdlibE2E.String
402 */
403std::string ljust(std::string_view s, std::size_t width, std::string_view fill = " ");
404/**
405 * Right-justify to @p width.
406 *
407 * Pads @p s on the left with the fill character up to @p width; if @p s is already
408 * at least @p width long it is returned unchanged. Only the first character of
409 * @p fill is used (an empty @p fill defaults to a space).
410 * @param s input.
411 * @param width target.
412 * @param fill pad char.
413 * @return padded @p s.
414 * @complexity O(n + width).
415 * @alloc allocates the result plus concatenation temporaries.
416 * @test CheatahString.Padding
417 * @crtest StringCompileRun.Rjust
418 * @systest StdlibE2E.String
419 */
420std::string rjust(std::string_view s, std::size_t width, std::string_view fill = " ");
421/**
422 * Center within @p width.
423 *
424 * Pads both sides with the fill character; when the padding is odd the extra
425 * character goes on the right. Returns @p s unchanged if it is already at least
426 * @p width long, and only the first character of @p fill is used (empty → space).
427 * @param s input.
428 * @param width target.
429 * @param fill pad char.
430 * @return padded @p s.
431 * @complexity O(n + width).
432 * @alloc allocates the result plus concatenation temporaries.
433 * @test CheatahString.Padding
434 * @crtest StringCompileRun.Center
435 * @systest StdlibE2E.String
436 */
437std::string center(std::string_view s, std::size_t width, std::string_view fill = " ");
438/**
439 * Zero-fill on the left to @p width.
440 *
441 * Left-pads with `'0'` to @p width; if @p s begins with a `'+'` or `'-'` sign the
442 * zeros are inserted after the sign. Returns @p s unchanged if already at least
443 * @p width long.
444 * @param s input.
445 * @param width target.
446 * @return `'0'`-padded @p s.
447 * @complexity O(n + width).
448 * @alloc allocates the result plus concatenation temporaries.
449 * @test CheatahString.Padding
450 * @crtest StringCompileRun.Zfill
451 * @systest StdlibE2E.String
452 */
453std::string zfill(std::string_view s, std::size_t width);
455// ---- whole-string classification (False for the empty string, like Python) ----
456/**
457 * All digits?
458 *
459 * True only if @p s is non-empty and every character is an ASCII decimal digit;
460 * the empty string returns false (matching Python).
461 * @param s input.
462 * @return true iff non-empty and all `09`.
463 * @complexity O(n).
464 * @alloc none.
465 * @test CheatahString.Classification
466 * @crtest StringCompileRun.Isdigit
467 * @systest StdlibE2E.String
468 */
469bool isdigit(std::string_view s);
470/**
471 * All letters?
472 *
473 * True only if @p s is non-empty and every character is an ASCII letter (`std::isalpha`);
474 * the empty string returns false.
475 * @param s input.
476 * @return true iff non-empty and all alphabetic.
477 * @complexity O(n).
478 * @alloc none.
479 * @test CheatahString.Classification
480 * @crtest StringCompileRun.Isalpha
481 * @systest StdlibE2E.String
482 */
483bool isalpha(std::string_view s);
484/**
485 * All alphanumeric?
486 *
487 * True only if @p s is non-empty and every character is an ASCII letter or digit
488 * (`std::isalnum`); the empty string returns false.
489 * @param s input.
490 * @return true iff non-empty and all letters/digits.
491 * @complexity O(n).
492 * @alloc none.
493 * @test CheatahString.ClassificationAlnumAndSpace
494 * @crtest StringCompileRun.Isalnum
495 * @systest StdlibE2E.String
496 */
497bool isalnum(std::string_view s);
498/**
499 * All whitespace?
500 *
501 * True only if @p s is non-empty and every character is ASCII whitespace
502 * (`std::isspace`: space, tab, newline, CR, form-feed, vertical tab); the empty
503 * string returns false.
504 * @param s input.
505 * @return true iff non-empty and all whitespace.
506 * @complexity O(n).
507 * @alloc none.
508 * @test CheatahString.ClassificationAlnumAndSpace
509 * @crtest StringCompileRun.Isspace
510 * @systest StdlibE2E.String
511 */
512bool isspace(std::string_view s);
513/**
514 * All uppercase?
515 *
516 * True iff @p s contains at least one ASCII uppercase letter and no lowercase letters;
517 * non-letter characters are ignored, so e.g. "ABC123" is uppercase but "123" and the
518 * empty string are not.
519 * @param s input.
520 * @return true iff @p s has ≥ 1 uppercase letter and no lowercase.
521 * @complexity O(n).
522 * @alloc none.
523 * @test CheatahString.Classification
524 * @crtest StringCompileRun.Isupper
525 * @systest StdlibE2E.String
526 */
527bool isupper(std::string_view s);
528/**
529 * All lowercase?
530 *
531 * True iff @p s contains at least one ASCII lowercase letter and no uppercase letters;
532 * non-letter characters are ignored, so e.g. "abc123" is lowercase but "123" and the
533 * empty string are not.
534 * @param s input.
535 * @return true iff @p s has ≥ 1 lowercase letter and no uppercase.
536 * @complexity O(n).
537 * @alloc none.
538 * @test CheatahString.Classification
539 * @crtest StringCompileRun.Islower
540 * @systest StdlibE2E.String
541 */
542bool islower(std::string_view s);
544} // namespace cheatah::string