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 once5
/**6
* @file string.hpp7
* @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>` allocate15
* 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>23
namespace cheatah::string {25
// ---- constants (Python `string` module) ----26
inline constexpr std::string_view ascii_lowercase = "abcdefghijklmnopqrstuvwxyz"; ///< `a–z`.27
inline constexpr std::string_view ascii_uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; ///< `A–Z`.28
inline constexpr std::string_view ascii_letters =29
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; ///< `a–zA–Z`.30
inline constexpr std::string_view digits = "0123456789"; ///< `0–9`.31
inline constexpr std::string_view hexdigits = "0123456789abcdefABCDEF"; ///< hex digits.32
inline constexpr std::string_view octdigits = "01234567"; ///< octal digits.33
/// ASCII punctuation.34
inline constexpr std::string_view punctuation = R"(!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~)";35
inline 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 uppercase42
* 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.Case48
* @crtest StringCompileRun.Upper49
* @systest StdlibE2E.String50
*/51
std::string upper(std::string_view s);52
/**53
* Lowercase.54
*55
* Returns a new string with every ASCII uppercase letter mapped to lowercase56
* 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.Case62
* @crtest StringCompileRun.Lower63
* @systest StdlibE2E.String64
*/65
std::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.Case76
* @crtest StringCompileRun.Capitalize77
* @systest StdlibE2E.String78
*/79
std::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.Case90
* @crtest StringCompileRun.Title91
* @systest StdlibE2E.String92
*/93
std::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 are98
* left unchanged (ASCII-only).99
* @param s input.100
* @return case-swapped @p s.101
* @complexity O(n).102
* @alloc allocates.103
* @test CheatahString.Case104
* @crtest StringCompileRun.Swapcase105
* @systest StdlibE2E.String106
*/107
std::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 chars114
* set (the set is a bag of characters, not a substring); defaults to ASCII115
* 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.Trimming122
* @crtest StringCompileRun.Strip123
* @systest StdlibE2E.String124
*/125
std::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 set130
* (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.Trimming137
* @crtest StringCompileRun.Lstrip138
* @systest StdlibE2E.String139
*/140
std::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 set145
* (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.Trimming152
* @crtest StringCompileRun.Rstrip153
* @systest StdlibE2E.String154
*/155
std::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.SearchAndTest168
* @crtest StringCompileRun.Startswith169
* @systest StdlibE2E.String170
*/171
bool 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.SearchAndTest182
* @crtest StringCompileRun.Endswith183
* @systest StdlibE2E.String184
*/185
bool 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 always190
* 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.SearchAndTest197
* @crtest StringCompileRun.Contains198
* @systest StdlibE2E.String199
*/200
bool 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.ContainsChar212
* @systest RequestsSys.QueryParams213
*/214
inline 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.SearchAndTest226
* @crtest StringCompileRun.Find227
* @systest StdlibE2E.String228
*/229
long 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's234
* `str.find(sub, start)`): a negative @p start is treated as 0, and a @p start past the235
* end returns -1. Lets a caller scan a large buffer for successive matches WITHOUT slicing236
* 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.SearchAndTest244
* @systest RequestsSys.HeaderLookup245
*/246
long 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.SearchAndTest258
* @crtest StringCompileRun.Rfind259
* @systest StdlibE2E.String260
*/261
long 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.SearchAndTest273
* @crtest StringCompileRun.Count274
* @systest StdlibE2E.String275
*/276
long 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.Transform290
* @crtest StringCompileRun.Replace291
* @systest StdlibE2E.String292
*/293
std::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 fields298
* (e.g. "a,,b" yields three parts, leading/trailing separators yield empty299
* 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.SplitEmptySeparator306
* @crtest StringCompileRun.Split307
* @systest StdlibE2E.String308
*/309
std::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 input315
* 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.Transform321
* @crtest StringCompileRun.SplitWhitespace322
* @systest StdlibE2E.String323
*/324
std::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 line329
* 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.Transform336
* @crtest StringCompileRun.Splitlines337
* @systest StdlibE2E.String338
*/339
std::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) and344
* re-joins with single spaces, so all runs of original whitespace collapse and345
* 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.Transform351
* @crtest StringCompileRun.Capwords352
* @systest StdlibE2E.String353
*/354
std::string capwords(std::string_view s);356
/// StringViewable<T>: a `std::string_view` can be built from T — what join() needs.357
template <typename T>358
concept 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 elements364
* (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.Transform371
* @crtest StringCompileRun.Join372
* @systest StdlibE2E.String373
*/374
template <std::ranges::input_range Range>375
requires StringViewable<std::ranges::range_value_t<Range>>376
std::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;387
}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 already394
* at least @p width long it is returned unchanged. Only the first character of395
* @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.Padding403
* @crtest StringCompileRun.Ljust404
* @systest StdlibE2E.String405
*/406
std::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 already411
* at least @p width long it is returned unchanged. Only the first character of412
* @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.Padding420
* @crtest StringCompileRun.Rjust421
* @systest StdlibE2E.String422
*/423
std::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 extra428
* character goes on the right. Returns @p s unchanged if it is already at least429
* @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.Padding437
* @crtest StringCompileRun.Center438
* @systest StdlibE2E.String439
*/440
std::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 the445
* zeros are inserted after the sign. Returns @p s unchanged if already at least446
* @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.Padding453
* @crtest StringCompileRun.Zfill454
* @systest StdlibE2E.String455
*/456
std::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.Classification469
* @crtest StringCompileRun.Isdigit470
* @systest StdlibE2E.String471
*/472
bool 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.Classification483
* @crtest StringCompileRun.Isalpha484
* @systest StdlibE2E.String485
*/486
bool 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 digit491
* (`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.ClassificationAlnumAndSpace497
* @crtest StringCompileRun.Isalnum498
* @systest StdlibE2E.String499
*/500
bool isalnum(std::string_view s);501
/**502
* All whitespace?503
*504
* True only if @p s is non-empty and every character is ASCII whitespace505
* (`std::isspace`: space, tab, newline, CR, form-feed, vertical tab); the empty506
* 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.ClassificationAlnumAndSpace512
* @crtest StringCompileRun.Isspace513
* @systest StdlibE2E.String514
*/515
bool 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 the521
* 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.Classification527
* @crtest StringCompileRun.Isupper528
* @systest StdlibE2E.String529
*/530
bool 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 the536
* 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.Classification542
* @crtest StringCompileRun.Islower543
* @systest StdlibE2E.String544
*/545
bool islower(std::string_view s);547
} // namespace cheatah::string