diff --git a/README.md b/README.md index c4a7ba4..56c08c9 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,16 @@ cc -std=c11 your_file.c libdstr.a -o your_program If not defined, these fall back to the standard allocators. +**Growth strategy** - controls how `dstrcat`, `dstrappend`, and `dstrpush` (the two-argument, non-custom forms) grow the buffer when the current capacity is not enough: + +```c +// dstr_options.h +#define DSTR_GROWTH_ENABLED 1 /* 1 = amortized growth, 0 = allocate exactly what is needed */ +#define DSTR_GROWTH_PERCENT 50 /* extra capacity added on growth, as a percent of the current capacity */ +``` + +With `DSTR_GROWTH_ENABLED` set to 1 (the default), a growth only happens when the requested size exceeds the current capacity, and it adds `DSTR_GROWTH_PERCENT` percent of headroom on top (50 means the new capacity is roughly 1.5x the old one), so repeated small appends do not reallocate on every call. Setting it to 0 disables the headroom: every growth allocates exactly what is needed, trading more frequent reallocations for a tighter memory footprint. Either way, capacity is left untouched whenever it is already sufficient. The `_custom` variants of `dstrcat`, `dstrappend`, and `dstrpush`, as well as the `cap` argument of `dstrnew`, always honor the caller-supplied capacity and are not affected by these options. + ## Types `dstr` is a `char*`. The internal headers (`dstrhd8`, `dstrhd16`, `dstrhd32`, `dstrhd64`) are implementation details and not part of the public API. The header type used for a given string is selected automatically based on the required capacity. @@ -157,6 +167,28 @@ ssize_t index = dstrfind(s, "needle"); Searches for the first occurrence of a null-terminated C string (`needle`) inside the dynamic string `s`. Returns the zero-based index of the first match, or `-1` if the substring is not found. It safely returns `-1` if either argument is `NULL`, if the needle is an empty string, or if the needle is longer than the string itself. +### Range (substring) + +```c +dstr sub = dstrrange(s, 0, 3); // first 3 characters +dstr sub = dstrrange(s, -3, -1); // second-to-last and third-to-last characters +dstr sub = dstrrange(s, 2, -1); // from index 2 up to (not including) the last character +``` + +Returns a new `dstr` holding the `[start, end)` slice of `s` (`end` is exclusive). Negative indices count from the end of the string, i.e. `-1` is the last character. Out-of-range indices are clamped instead of erroring, and a `start` at or past `end` yields an empty `dstr`. Returns `NULL` only if `s` is `NULL` or allocation fails. Free the result with `dstrfree`. + +### Split + +```c +size_t count; +dstr* parts = dstrsplit(s, ",", &count); +for (size_t i = 0; i < count; i++) + puts(parts[i]); +dstrsplitfree(parts, count); +``` + +Splits `s` on every occurrence of `delim`, returning a newly allocated array of `dstr`. Adjacent or leading/trailing delimiters produce empty (`""`) elements. Returns `NULL` if `s`/`delim` is `NULL`, `delim` is empty, or allocation fails. The array and every element must be released together with `dstrsplitfree`. + ### Free ```c @@ -197,10 +229,7 @@ dstr s = $("hello", 64); ## Roadmap - **dstrinsert** — insert substring at a given index -- **dstrfind** — find first or last occurrence of a char or substring - **dstrtrim** — strip leading/trailing whitespace or a given charset -- **dstrrange** — return a substring by start/end index -- **dstrsplit** — split by delimiter into an array of `dstr` ## License diff --git a/dstr.c b/dstr.c index a867c00..4c6fa0b 100644 --- a/dstr.c +++ b/dstr.c @@ -154,33 +154,42 @@ static inline void* _dstr_get_hdr_and_type(dstr s, enum dstrhd_type* t_out) static inline size_t _dstr_grow_cap(size_t old_cap, size_t needed) { + if (needed <= old_cap) + return old_cap; + +#if DSTR_GROWTH_ENABLED size_t grown = old_cap < 16 ? 16 : old_cap; - if (grown <= SIZE_MAX - grown / 2) - grown += grown / 2; + + if (grown <= SIZE_MAX / DSTR_GROWTH_PERCENT) { + size_t increment = grown * DSTR_GROWTH_PERCENT / 100; + if (grown <= SIZE_MAX - increment) + grown += increment; + } + return grown > needed ? grown : needed; +#else + return needed; +#endif } -// strfind -ssize_t dstrfind(dstr s, const char* needle) +static inline ssize_t _dstr_find_from(const char* buf, + size_t len, + const char* needle, + size_t nlen, + size_t from) { - if (!s || !needle || needle[0] == '\0') + if (from > len || nlen > len - from) return -1; - size_t len = dstrlen(s); - size_t nlen = strlen(needle); - - if (nlen > len) - return -1; - - size_t remaining = len - nlen + 1; - const char* cursor = s; + size_t remaining = len - from - nlen + 1; + const char* cursor = buf + from; while (remaining > 0) { const char* hit = memchr(cursor, needle[0], remaining); if (!hit) return -1; if (memcmp(hit, needle, nlen) == 0) - return hit - s; + return hit - buf; remaining -= (size_t)(hit - cursor) + 1; cursor = hit + 1; } @@ -188,6 +197,130 @@ ssize_t dstrfind(dstr s, const char* needle) return -1; } +static dstr _dstr_slice_alloc(const char* buf, size_t len) +{ + enum dstrhd_type t = _dstr_type_by_size(len + 1); + size_t hd_size = _dstr_size_by_type(t); + + void* hd = DSTR_MALLOC(hd_size + len + 1); + if (!hd) + return NULL; + + _dstr_set_len(hd, len, t); + _dstr_set_cap(hd, len + 1, t); + *((uint8_t*)hd + hd_size - 1) = (uint8_t)t; + + char* buf_out = (char*)hd + hd_size; + memcpy(buf_out, buf, len); + buf_out[len] = '\0'; + + return buf_out; +} + +// strfind +ssize_t dstrfind(dstr s, const char* needle) +{ + if (!s || !needle || needle[0] == '\0') + return -1; + + return _dstr_find_from(s, dstrlen(s), needle, strlen(needle), 0); +} + +// dstrrange +dstr dstrrange(dstr s, ssize_t start, ssize_t end) +{ + if (!s) + return NULL; + + size_t len_u = dstrlen(s); + ssize_t len = (len_u > (size_t)PTRDIFF_MAX) ? PTRDIFF_MAX : (ssize_t)len_u; + + if (start < 0) + start += len; + if (start < 0) + start = 0; + if (start > len) + start = len; + + if (end < 0) + end += len; + if (end < 0) + end = 0; + if (end > len) + end = len; + + size_t sub_len = (start >= end) ? 0 : (size_t)(end - start); + + return _dstr_slice_alloc(s + start, sub_len); +} + +// dstrsplit +dstr* dstrsplit(dstr s, const char* delim, size_t* out_count) +{ + if (out_count) + *out_count = 0; + + if (!s || !delim || delim[0] == '\0') + return NULL; + + size_t len = dstrlen(s); + size_t dlen = strlen(delim); + + size_t count = 1; + size_t pos = 0; + for (;;) { + ssize_t idx = _dstr_find_from(s, len, delim, dlen, pos); + if (idx < 0) + break; + count++; + pos = (size_t)idx + dlen; + } + + if (count > SIZE_MAX / sizeof(dstr)) + return NULL; + + dstr* parts = DSTR_MALLOC(count * sizeof(dstr)); + if (!parts) + return NULL; + + size_t n = 0; + pos = 0; + for (;;) { + ssize_t idx = _dstr_find_from(s, len, delim, dlen, pos); + size_t piece_end = (idx < 0) ? len : (size_t)idx; + + parts[n] = _dstr_slice_alloc(s + pos, piece_end - pos); + if (!parts[n]) { + for (size_t i = 0; i < n; i++) + dstrfree(parts[i]); + DSTR_FREE(parts); + return NULL; + } + n++; + + if (idx < 0) + break; + pos = piece_end + dlen; + } + + if (out_count) + *out_count = n; + + return parts; +} + +// dstrsplitfree +void dstrsplitfree(dstr* parts, size_t count) +{ + if (!parts) + return; + + for (size_t i = 0; i < count; i++) + dstrfree(parts[i]); + + DSTR_FREE(parts); +} + // strlen size_t dstrlen(dstr s) { diff --git a/dstr.h b/dstr.h index 3d0efc4..41f612d 100644 --- a/dstr.h +++ b/dstr.h @@ -86,6 +86,38 @@ void dstrfree(dstr s); */ ssize_t dstrfind(dstr s, const char* needle) W_UNUSED_RESULT; +/* + * dstrrange(s, start, end) + * + * Return a new dstr with a copy of the [start, end) slice of s (end is + * exclusive). Negative indices count from the end of the string, as in + * s[len + start]. Out-of-range indices are clamped to [0, len], and a + * start at or past end yields an empty (but non-NULL) dstr. + * Returns NULL if s is NULL or on allocation failure. The result is an + * independent allocation; free it with dstrfree. + */ +dstr dstrrange(dstr s, ssize_t start, ssize_t end) W_UNUSED_RESULT; + +/* + * dstrsplit(s, delim, out_count) + * + * Split s on every occurrence of the null-terminated delim, returning a + * newly allocated array of dstr. Adjacent or leading/trailing delimiters + * produce empty ("") elements. If out_count is not NULL, the number of + * elements is written to it. + * Returns NULL (and *out_count = 0) if s or delim is NULL, delim is + * empty, or on allocation failure. Free the result with dstrsplitfree. + */ +dstr* dstrsplit(dstr s, const char* delim, size_t* out_count) W_UNUSED_RESULT; + +/* + * dstrsplitfree(parts, count) + * + * Free an array returned by dstrsplit, including each element. Passing + * NULL is a no-op. + */ +void dstrsplitfree(dstr* parts, size_t count); + /* * dstrlen(s) - number of characters in s, excluding the null terminator. * dstrcap(s) - total allocated capacity in bytes. diff --git a/dstr_options.h b/dstr_options.h index 05a9361..b2bda12 100644 --- a/dstr_options.h +++ b/dstr_options.h @@ -9,4 +9,20 @@ #define DSTR_REALLOC(p, s) realloc(p, s) #define DSTR_FREE(s) free(s) +/* + * Amortized growth for the dstrcat/dstrappend/dstrpush base variants. + * 1 = grow ahead of need to reduce future reallocations + * 0 = allocate exactly what is needed, nothing more + * The custom variants (and dstrnew's cap argument) always honor the + * caller-supplied capacity regardless of this setting. + */ +#define DSTR_GROWTH_ENABLED 1 + +/* + * Extra capacity requested on growth, as a percentage of the current + * capacity. Only used when DSTR_GROWTH_ENABLED is 1. Must be > 0. + * 50 means +50% (new capacity is roughly 1.5x the old one). + */ +#define DSTR_GROWTH_PERCENT 50 + #endif diff --git a/tester.c b/tester.c index 2290037..7447fd8 100644 --- a/tester.c +++ b/tester.c @@ -3,11 +3,9 @@ #include #include "dstr.h" -// Global counters for test reporting static int tests_run = 0; static int tests_failed = 0; -// Assertion macros with formatted output #define ASSERT_TRUE(expr) do { \ tests_run++; \ if (!(expr)) { \ @@ -31,8 +29,6 @@ static int tests_failed = 0; test_func(); \ } while (0) -// --- TEST SUITE --- - static void test_find_basic(void) { dstr s = dstrnew("Hello World"); @@ -221,6 +217,231 @@ static void test_case_conversion_roundtrip(void) { dstrfree(original); } +static void test_range_basic(void) { + dstr s = dstrnew("Hello World"); + + dstr sub = dstrrange(s, 0, 5); + ASSERT_STR_EQUAL("Hello", sub); + ASSERT_TRUE(dstrlen(sub) == 5); + dstrfree(sub); + + sub = dstrrange(s, 6, 11); + ASSERT_STR_EQUAL("World", sub); + dstrfree(sub); + + sub = dstrrange(s, 0, 11); + ASSERT_STR_EQUAL("Hello World", sub); + dstrfree(sub); + + dstrfree(s); +} + +static void test_range_negative_index(void) { + dstr s = dstrnew("Hello World"); + + dstr sub = dstrrange(s, -5, -1); + ASSERT_STR_EQUAL("Worl", sub); + dstrfree(sub); + + sub = dstrrange(s, -11, -6); + ASSERT_STR_EQUAL("Hello", sub); + dstrfree(sub); + + sub = dstrrange(s, 0, -1); + ASSERT_STR_EQUAL("Hello Worl", sub); + dstrfree(sub); + + sub = dstrrange(s, -5, 11); + ASSERT_STR_EQUAL("World", sub); + dstrfree(sub); + + dstrfree(s); +} + +static void test_range_out_of_bounds(void) { + dstr s = dstrnew("abc"); + + // Indices past either end are clamped, not an error. + dstr sub = dstrrange(s, -100, 100); + ASSERT_STR_EQUAL("abc", sub); + dstrfree(sub); + + sub = dstrrange(s, 5, 10); + ASSERT_STR_EQUAL("", sub); + ASSERT_TRUE(dstrlen(sub) == 0); + dstrfree(sub); + + sub = dstrrange(s, 2, 1); + ASSERT_STR_EQUAL("", sub); + dstrfree(sub); + + sub = dstrrange(s, -100, -50); + ASSERT_STR_EQUAL("", sub); + dstrfree(sub); + + dstrfree(s); +} + +static void test_range_null(void) { + dstr sub = dstrrange(NULL, 0, 5); + ASSERT_TRUE(sub == NULL); +} + +static void test_range_empty_string(void) { + dstr s = dstrnew(""); + dstr sub = dstrrange(s, -5, 5); + ASSERT_STR_EQUAL("", sub); + dstrfree(sub); + dstrfree(s); +} + +static void test_range_independent_allocation(void) { + // Mutating the slice must not affect the source string. + dstr s = dstrnew("Hello"); + dstr sub = dstrrange(s, 0, 5); + + dstrtoupper(sub); + + ASSERT_STR_EQUAL("Hello", s); + ASSERT_STR_EQUAL("HELLO", sub); + + dstrfree(s); + dstrfree(sub); +} + +static void test_split_basic(void) { + dstr s = dstrnew("a,b,c"); + size_t count = 0; + + dstr* parts = dstrsplit(s, ",", &count); + ASSERT_TRUE(parts != NULL); + ASSERT_TRUE(count == 3); + ASSERT_STR_EQUAL("a", parts[0]); + ASSERT_STR_EQUAL("b", parts[1]); + ASSERT_STR_EQUAL("c", parts[2]); + + dstrsplitfree(parts, count); + dstrfree(s); +} + +static void test_split_adjacent_and_edge_delimiters(void) { + dstr s = dstrnew(",a,,b,"); + size_t count = 0; + + dstr* parts = dstrsplit(s, ",", &count); + ASSERT_TRUE(count == 5); + ASSERT_STR_EQUAL("", parts[0]); + ASSERT_STR_EQUAL("a", parts[1]); + ASSERT_STR_EQUAL("", parts[2]); + ASSERT_STR_EQUAL("b", parts[3]); + ASSERT_STR_EQUAL("", parts[4]); + + dstrsplitfree(parts, count); + dstrfree(s); +} + +static void test_split_multichar_delim(void) { + dstr s = dstrnew("one::two::three"); + size_t count = 0; + + dstr* parts = dstrsplit(s, "::", &count); + ASSERT_TRUE(count == 3); + ASSERT_STR_EQUAL("one", parts[0]); + ASSERT_STR_EQUAL("two", parts[1]); + ASSERT_STR_EQUAL("three", parts[2]); + + dstrsplitfree(parts, count); + dstrfree(s); +} + +static void test_split_no_match(void) { + dstr s = dstrnew("no-delimiter-here"); + size_t count = 0; + + dstr* parts = dstrsplit(s, ";", &count); + ASSERT_TRUE(count == 1); + ASSERT_STR_EQUAL("no-delimiter-here", parts[0]); + + dstrsplitfree(parts, count); + dstrfree(s); +} + +static void test_split_edge_cases(void) { + dstr s = dstrnew("data"); + size_t count = 123; + + ASSERT_TRUE(dstrsplit(NULL, ",", &count) == NULL); + ASSERT_TRUE(count == 0); + + count = 123; + ASSERT_TRUE(dstrsplit(s, NULL, &count) == NULL); + ASSERT_TRUE(count == 0); + + count = 123; + ASSERT_TRUE(dstrsplit(s, "", &count) == NULL); + ASSERT_TRUE(count == 0); + + // out_count is optional. + dstr* parts = dstrsplit(s, "z", NULL); + ASSERT_TRUE(parts != NULL); + dstrsplitfree(parts, 1); + + dstrsplitfree(NULL, 0); + + dstrfree(s); +} + +static void test_split_independent_allocations(void) { + // Mutating one part must not affect the source or the other parts. + dstr s = dstrnew("aa,bb"); + size_t count = 0; + dstr* parts = dstrsplit(s, ",", &count); + + dstrtoupper(parts[0]); + + ASSERT_STR_EQUAL("aa,bb", s); + ASSERT_STR_EQUAL("AA", parts[0]); + ASSERT_STR_EQUAL("bb", parts[1]); + + dstrsplitfree(parts, count); + dstrfree(s); +} + +static void test_growth_no_unnecessary_realloc(void) { + dstr s = dstrnew(""); + s = dstrreserve(s, 128); + size_t cap_before = dstrcap(s); + + for (int i = 0; i < 20; i++) + s = dstrcat(s, "x"); + + ASSERT_TRUE(dstrlen(s) == 20); + ASSERT_TRUE(dstrcap(s) == cap_before); + + dstrfree(s); +} + +static void test_growth_correctness_across_reallocs(void) { + dstr s = dstrnew(""); + + for (int i = 0; i < 1000; i++) + s = dstrpush(s, 'a'); + + ASSERT_TRUE(dstrlen(s) == 1000); + ASSERT_TRUE(dstrcap(s) >= 1001); + + bool all_a = true; + for (size_t i = 0; i < dstrlen(s); i++) { + if (s[i] != 'a') { + all_a = false; + break; + } + } + ASSERT_TRUE(all_a); + + dstrfree(s); +} + static void test_auto_cleanup(void) { // Isolated block to test cleanup attribute { @@ -250,6 +471,20 @@ int main(void) { RUN_TEST(test_case_conversion_null); RUN_TEST(test_case_conversion_non_ascii_untouched); RUN_TEST(test_case_conversion_roundtrip); + RUN_TEST(test_range_basic); + RUN_TEST(test_range_negative_index); + RUN_TEST(test_range_out_of_bounds); + RUN_TEST(test_range_null); + RUN_TEST(test_range_empty_string); + RUN_TEST(test_range_independent_allocation); + RUN_TEST(test_split_basic); + RUN_TEST(test_split_adjacent_and_edge_delimiters); + RUN_TEST(test_split_multichar_delim); + RUN_TEST(test_split_no_match); + RUN_TEST(test_split_edge_cases); + RUN_TEST(test_split_independent_allocations); + RUN_TEST(test_growth_no_unnecessary_realloc); + RUN_TEST(test_growth_correctness_across_reallocs); RUN_TEST(test_auto_cleanup); printf("\n\033[1;34m=== FINAL REPORT ===\033[0m\n");