Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -845,6 +845,9 @@ Generated from doc-comments. Run `.\gen-docs.ps1` to regenerate.
| `l_str_upper` | Uppercase copy in arena (ASCII). | All |
| `l_str_lower` | Lowercase copy in arena (ASCII). | All |
| `l_str_replace` | Replace all occurrences of find with repl in s. Result is arena-allocated. | All |
| `l_str_to_int` | Parse a signed integer from s in the given base (2–36). Mirrors l_strtoll semantics. | All |
| `l_str_to_double` | Parse a floating-point value from s. Mirrors l_strtod semantics. | All |
| `l_str_printf` | Format args into an arena-allocated L_Str. Result length is exact; not NUL-terminated. | All |
| `l_buf_push_str` | Append L_Str to buf. Returns 0 on success, -1 on failure. | All |
| `l_buf_push_cstr` | Append C string to buf. Returns 0 on success, -1 on failure. | All |
| `l_buf_push_int` | Append decimal int to buf. Returns 0 on success, -1 on failure. | All |
Expand Down Expand Up @@ -1309,6 +1312,9 @@ Which `l_os.h` functions work on which platform. Generated from code annotations
| ``l_str_upper`` | ✅ | ✅ | ✅ |
| ``l_str_lower`` | ✅ | ✅ | ✅ |
| ``l_str_replace`` | ✅ | ✅ | ✅ |
| ``l_str_to_int`` | ✅ | ✅ | ✅ |
| ``l_str_to_double`` | ✅ | ✅ | ✅ |
| ``l_str_printf`` | ✅ | ✅ | ✅ |
| ``l_buf_push_str`` | ✅ | ✅ | ✅ |
| ``l_buf_push_cstr`` | ✅ | ✅ | ✅ |
| ``l_buf_push_int`` | ✅ | ✅ | ✅ |
Expand Down Expand Up @@ -1599,6 +1605,9 @@ Which `l_os.h` functions are referenced in the test suite. Generated — run `.\
| `l_str_upper` | ✅ | test_utils.c |
| `l_str_lower` | ✅ | test_utils.c |
| `l_str_replace` | ✅ | test_utils.c |
| `l_str_to_int` | ✅ | test_utils.c |
| `l_str_to_double` | ✅ | test_utils.c |
| `l_str_printf` | ✅ | test_utils.c |
| `l_buf_push_str` | ✅ | test_utils.c |
| `l_buf_push_cstr` | ✅ | test_utils.c |
| `l_buf_push_int` | ✅ | test_utils.c |
Expand Down Expand Up @@ -1684,7 +1693,7 @@ Which `l_os.h` functions are referenced in the test suite. Generated — run `.\
| ``l_socket_recvfrom_addr`` | — | |
| ``l_socket_unix_connect`` | — | |

**Coverage: 244 / 249 functions referenced in tests** (98%)
**Coverage: 247 / 252 functions referenced in tests** (98%)

<!-- END COVERAGE MATRIX -->

Expand Down
39 changes: 39 additions & 0 deletions l_os.h
Original file line number Diff line number Diff line change
Expand Up @@ -925,6 +925,14 @@ static inline L_Str l_str_upper(L_Arena *a, L_Str s);
static inline L_Str l_str_lower(L_Arena *a, L_Str s);
/// Replace all occurrences of find with repl in s. Result is arena-allocated.
static inline L_Str l_str_replace(L_Arena *a, L_Str s, L_Str find, L_Str repl);
/// Parse a signed integer from s in the given base (2–36). Mirrors l_strtoll semantics.
static inline long long l_str_to_int(L_Str s, int base);
/// Parse a floating-point value from s. Mirrors l_strtod semantics.
static inline double l_str_to_double(L_Str s);
#ifdef L_WITHSNPRINTF
/// Format args into an arena-allocated L_Str. Result length is exact; not NUL-terminated.
static inline L_Str l_str_printf(L_Arena *a, const char *fmt, ...);
#endif

/// Append L_Str to buf. Returns 0 on success, -1 on failure.
static inline int l_buf_push_str(L_Buf *b, L_Str s);
Expand Down Expand Up @@ -8963,6 +8971,37 @@ static inline L_Str l_buf_as_str(const L_Buf *b) {
return l_str_from((const char *)b->data, b->len);
}

/* Number parsing from L_Str */
static inline long long l_str_to_int(L_Str s, int base) {
char tmp[70]; /* 64 bits binary + sign + null, plus a little slack */
size_t n = s.len < sizeof(tmp) - 1 ? s.len : sizeof(tmp) - 1;
l_memcpy(tmp, s.data, n);
tmp[n] = '\0';
return l_strtoll(tmp, (char **)0, base);
}
static inline double l_str_to_double(L_Str s) {
char tmp[128];
size_t n = s.len < sizeof(tmp) - 1 ? s.len : sizeof(tmp) - 1;
l_memcpy(tmp, s.data, n);
tmp[n] = '\0';
return l_strtod(tmp, (char **)0);
}
#ifdef L_WITHSNPRINTF
static inline L_Str l_str_printf(L_Arena *a, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int need = l_vsnprintf((char *)0, 0, fmt, ap);
va_end(ap);
if (need < 0) return l_str_null();
char *p = (char *)l_arena_alloc(a, (size_t)need + 1);
if (!p) return l_str_null();
va_start(ap, fmt);
l_vsnprintf(p, (size_t)need + 1, fmt, ap);
va_end(ap);
return l_str_from(p, (size_t)need);
}
#endif /* L_WITHSNPRINTF */

// --- L_Map: arena-backed hash table (FNV-1a, open addressing, linear probing) ---

static inline unsigned int l_map_hash(const char *key, size_t len) {
Expand Down
71 changes: 71 additions & 0 deletions tests/test_utils.c
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,75 @@ void test_str_replace_helper(void) {
TEST_SECTION_PASS("l_str_replace");
}

void test_str_to_num(void) {
TEST_FUNCTION("l_str_to_int / l_str_to_double");

/* l_str_to_int: decimal */
TEST_ASSERT(l_str_to_int(l_str("42"), 10) == 42LL, "str_to_int 42");
TEST_ASSERT(l_str_to_int(l_str("-1"), 10) == -1LL, "str_to_int -1");
TEST_ASSERT(l_str_to_int(l_str("0"), 10) == 0LL, "str_to_int 0");
TEST_ASSERT(l_str_to_int(l_str(" 7"), 10) == 7LL, "str_to_int leading space");
TEST_ASSERT(l_str_to_int(l_str("+123"), 10) == 123LL, "str_to_int +sign");
TEST_ASSERT(l_str_to_int(l_str("99abc"), 10) == 99LL, "str_to_int stops at non-digit");
TEST_ASSERT(l_str_to_int(l_str(""), 10) == 0LL, "str_to_int empty");

/* l_str_to_int: hex */
TEST_ASSERT(l_str_to_int(l_str("ff"), 16) == 255LL, "str_to_int hex ff");
TEST_ASSERT(l_str_to_int(l_str("0xFF"), 16) == 255LL, "str_to_int hex 0xFF");
TEST_ASSERT(l_str_to_int(l_str("10"), 16) == 16LL, "str_to_int hex 10");

/* l_str_to_int: octal */
TEST_ASSERT(l_str_to_int(l_str("10"), 8) == 8LL, "str_to_int octal 10");
TEST_ASSERT(l_str_to_int(l_str("077"), 8) == 63LL, "str_to_int octal 077");

/* l_str_to_double */
{
double d;
d = l_str_to_double(l_str("3.14"));
TEST_ASSERT(d > 3.139 && d < 3.141, "str_to_double 3.14");
d = l_str_to_double(l_str("-2.5"));
TEST_ASSERT(d > -2.501 && d < -2.499, "str_to_double -2.5");
d = l_str_to_double(l_str("0"));
TEST_ASSERT(d == 0.0, "str_to_double 0");
d = l_str_to_double(l_str("1e3"));
TEST_ASSERT(d > 999.9 && d < 1000.1, "str_to_double 1e3");
d = l_str_to_double(l_str(""));
TEST_ASSERT(d == 0.0, "str_to_double empty");
}

TEST_SECTION_PASS("l_str_to_int / l_str_to_double");
}

void test_str_printf_arena(void) {
TEST_FUNCTION("l_str_printf");

L_Arena a = l_arena_init(4096);

/* basic formatting */
L_Str s = l_str_printf(&a, "hello %s", "world");
TEST_ASSERT(l_str_eq(s, l_str("hello world")), "str_printf basic");

/* integer formatting */
L_Str s2 = l_str_printf(&a, "%d + %d = %d", 1, 2, 3);
TEST_ASSERT(l_str_eq(s2, l_str("1 + 2 = 3")), "str_printf ints");

/* empty format */
L_Str s3 = l_str_printf(&a, "no args");
TEST_ASSERT(l_str_eq(s3, l_str("no args")), "str_printf no args");

/* width and padding */
L_Str s4 = l_str_printf(&a, "%5d", 42);
TEST_ASSERT(l_str_eq(s4, l_str(" 42")), "str_printf width pad");

/* length must be exact */
L_Str s5 = l_str_printf(&a, "ab%sc", "XY");
TEST_ASSERT(s5.len == 5, "str_printf length exact");
TEST_ASSERT(l_str_eq(s5, l_str("abXYc")), "str_printf content");

l_arena_free(&a);
TEST_SECTION_PASS("l_str_printf");
}

// Comparator for large structs — compares by the first int field
typedef struct { int key; char pad[512]; } BigElem;
static int big_elem_cmp(const void *a, const void *b) {
Expand Down Expand Up @@ -1258,6 +1327,8 @@ int main(int argc, char *argv[]) {
test_mktime();
test_strtof();
test_str_replace_helper();
test_str_to_num();
test_str_printf_arena();

// Context variant tests
test_rand_ctx();
Expand Down