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
* @param s input. @param c the character. @return true when present.205
* @complexity O(n). @alloc none.206
* @test CheatahString.ContainsChar207
* @crtest StringCompileRun.Contains208
* @systest StdlibE2E.String209
*/210
inline 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.SearchAndTest222
* @crtest StringCompileRun.Find223
* @systest StdlibE2E.String224
*/225
long 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's230
* `str.find(sub, start)`): a negative @p start is treated as 0, and a @p start past the231
* end returns -1. Lets a caller scan a large buffer for successive matches WITHOUT slicing232
* 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.SearchAndTest240
* @crtest StringCompileRun.Find241
* @systest StdlibE2E.String242
*/243
long 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.SearchAndTest255
* @crtest StringCompileRun.Rfind256
* @systest StdlibE2E.String257
*/258
long 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.SearchAndTest270
* @crtest StringCompileRun.Count271
* @systest StdlibE2E.String272
*/273
long 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.Transform287
* @crtest StringCompileRun.Replace288
* @systest StdlibE2E.String289
*/290
std::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 fields295
* (e.g. "a,,b" yields three parts, leading/trailing separators yield empty296
* 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.SplitEmptySeparator303
* @crtest StringCompileRun.Split304
* @systest StdlibE2E.String305
*/306
std::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 input312
* 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.Transform318
* @crtest StringCompileRun.SplitWhitespace319
* @systest StdlibE2E.String320
*/321
std::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 line326
* 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.Transform333
* @crtest StringCompileRun.Splitlines334
* @systest StdlibE2E.String335
*/336
std::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) and341
* re-joins with single spaces, so all runs of original whitespace collapse and342
* 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.Transform348
* @crtest StringCompileRun.Capwords349
* @systest StdlibE2E.String350
*/351
std::string capwords(std::string_view s);353
/// StringViewable<T>: a `std::string_view` can be built from T — what join() needs.354
template <typename T>355
concept 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 elements361
* (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.Transform368
* @crtest StringCompileRun.Join369
* @systest StdlibE2E.String370
*/371
template <std::ranges::input_range Range>372
requires StringViewable<std::ranges::range_value_t<Range>>373
std::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;384
}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 already391
* at least @p width long it is returned unchanged. Only the first character of392
* @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.Padding400
* @crtest StringCompileRun.Ljust401
* @systest StdlibE2E.String402
*/403
std::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 already408
* at least @p width long it is returned unchanged. Only the first character of409
* @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.Padding417
* @crtest StringCompileRun.Rjust418
* @systest StdlibE2E.String419
*/420
std::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 extra425
* character goes on the right. Returns @p s unchanged if it is already at least426
* @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.Padding434
* @crtest StringCompileRun.Center435
* @systest StdlibE2E.String436
*/437
std::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 the442
* zeros are inserted after the sign. Returns @p s unchanged if already at least443
* @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.Padding450
* @crtest StringCompileRun.Zfill451
* @systest StdlibE2E.String452
*/453
std::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 `0–9`.463
* @complexity O(n).464
* @alloc none.465
* @test CheatahString.Classification466
* @crtest StringCompileRun.Isdigit467
* @systest StdlibE2E.String468
*/469
bool 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.Classification480
* @crtest StringCompileRun.Isalpha481
* @systest StdlibE2E.String482
*/483
bool 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 digit488
* (`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.ClassificationAlnumAndSpace494
* @crtest StringCompileRun.Isalnum495
* @systest StdlibE2E.String496
*/497
bool isalnum(std::string_view s);498
/**499
* All whitespace?500
*501
* True only if @p s is non-empty and every character is ASCII whitespace502
* (`std::isspace`: space, tab, newline, CR, form-feed, vertical tab); the empty503
* 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.ClassificationAlnumAndSpace509
* @crtest StringCompileRun.Isspace510
* @systest StdlibE2E.String511
*/512
bool 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 the518
* 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.Classification524
* @crtest StringCompileRun.Isupper525
* @systest StdlibE2E.String526
*/527
bool 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 the533
* 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.Classification539
* @crtest StringCompileRun.Islower540
* @systest StdlibE2E.String541
*/542
bool islower(std::string_view s);544
} // namespace cheatah::string