From b77464a87cd8a0e71ed26532ed7e9c9ed1e291f5 Mon Sep 17 00:00:00 2001 From: frapank Date: Fri, 3 Jul 2026 21:09:58 +0200 Subject: [PATCH 1/3] add: dstrupper and dstrlower --- dstr.c | 28 ++++++++++++++++++++++++++++ dstr.h | 18 ++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/dstr.c b/dstr.c index a33926d..a867c00 100644 --- a/dstr.c +++ b/dstr.c @@ -289,6 +289,34 @@ void dstrzero(dstr s) memset(s, 0, cap); } +// dstrtolower +void dstrtolower(dstr s) +{ + if (!s) + return; + + size_t len = dstrlen(s); + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)s[i]; + if (c >= 'A' && c <= 'Z') + s[i] = (char)(c + ('a' - 'A')); + } +} + +// dstrtoupper +void dstrtoupper(dstr s) +{ + if (!s) + return; + + size_t len = dstrlen(s); + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)s[i]; + if (c >= 'a' && c <= 'z') + s[i] = (char)(c - ('a' - 'A')); + } +} + // strreserve dstr dstrreserve(dstr s, size_t new_cap) { diff --git a/dstr.h b/dstr.h index 0e9e822..3d0efc4 100644 --- a/dstr.h +++ b/dstr.h @@ -116,6 +116,24 @@ void dstrzero(dstr s); */ bool dstrequal(dstr s1, dstr s2) W_UNUSED_RESULT; +/* + * dstrtolower(s) + * + * Convert every ASCII uppercase letter ('A'-'Z') in s to lowercase, in + * place. Bytes outside that range, including non-ASCII bytes, are left + * untouched. Length and capacity are unchanged. No-op if s is NULL. + */ +void dstrtolower(dstr s); + +/* + * dstrtoupper(s) + * + * Convert every ASCII lowercase letter ('a'-'z') in s to uppercase, in + * place. Bytes outside that range, including non-ASCII bytes, are left + * untouched. Length and capacity are unchanged. No-op if s is NULL. + */ +void dstrtoupper(dstr s); + /* * dstrreserve(s, new_cap) * From 5be79ea41e3b3003601eac0312da1f30bbbe0108 Mon Sep 17 00:00:00 2001 From: frapank Date: Fri, 3 Jul 2026 21:10:33 +0200 Subject: [PATCH 2/3] docs: dstrtolower and dstrtoupper --- README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6cc0c60..c4a7ba4 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,15 @@ if (dstrequal(s1, s2)) { ... } Returns `true` if the strings are equal, `false` otherwise. Both arguments must be `dstr`. For `dstr` vs `char*` comparisons use `strcmp` directly. +### Case conversion + +```c +dstrtolower(s); +dstrtoupper(s); +``` + +Converts ASCII letters in place. Bytes outside `'A'-'Z'`/`'a'-'z'`, including non-ASCII bytes, are left untouched. Length and capacity are unchanged; no reallocation occurs. No-op if `s` is `NULL`. + ### Find substring ```c @@ -192,7 +201,6 @@ dstr s = $("hello", 64); - **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` -- **dstrtolower / dstrtoupper** — in-place ASCII case conversion ## License From 3d6792884551f0045bb8419b74548c82b7a14f44 Mon Sep 17 00:00:00 2001 From: frapank Date: Fri, 3 Jul 2026 21:11:08 +0200 Subject: [PATCH 3/3] add: test suit for dstrtoupper and dstrtolower --- tester.c | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/tester.c b/tester.c index f8279cc..2290037 100644 --- a/tester.c +++ b/tester.c @@ -125,6 +125,102 @@ static void test_clear_and_zero(void) { dstrfree(s); } +static void test_tolower_basic(void) { + dstr s = dstrnew("Hello World! 123"); + size_t len = dstrlen(s); + size_t cap = dstrcap(s); + + dstrtolower(s); + + ASSERT_STR_EQUAL("hello world! 123", s); + ASSERT_TRUE(dstrlen(s) == len); + ASSERT_TRUE(dstrcap(s) == cap); + + dstrfree(s); +} + +static void test_toupper_basic(void) { + dstr s = dstrnew("Hello World! 123"); + size_t len = dstrlen(s); + size_t cap = dstrcap(s); + + dstrtoupper(s); + + ASSERT_STR_EQUAL("HELLO WORLD! 123", s); + ASSERT_TRUE(dstrlen(s) == len); + ASSERT_TRUE(dstrcap(s) == cap); + + dstrfree(s); +} + +static void test_case_conversion_already_converted(void) { + dstr lower = dstrnew("already lower"); + dstrtolower(lower); + ASSERT_STR_EQUAL("already lower", lower); + dstrfree(lower); + + dstr upper = dstrnew("ALREADY UPPER"); + dstrtoupper(upper); + ASSERT_STR_EQUAL("ALREADY UPPER", upper); + dstrfree(upper); +} + +static void test_case_conversion_empty(void) { + dstr s = dstrnew(""); + + dstrtolower(s); + ASSERT_STR_EQUAL("", s); + ASSERT_TRUE(dstrlen(s) == 0); + + dstrtoupper(s); + ASSERT_STR_EQUAL("", s); + ASSERT_TRUE(dstrlen(s) == 0); + + dstrfree(s); +} + +static void test_case_conversion_null(void) { + // Must not crash on NULL input. + dstrtolower(NULL); + dstrtoupper(NULL); + ASSERT_TRUE(true); +} + +static void test_case_conversion_non_ascii_untouched(void) { + // Bytes >= 0x80 (e.g. UTF-8 continuation/lead bytes) must be left + // untouched, and must not trigger undefined behavior on signed char. + dstr s = dstrnew("\xC3\x89LLO \xC3\xA9llo"); + size_t len = dstrlen(s); + + dstrtolower(s); + ASSERT_STR_EQUAL("\xC3\x89llo \xC3\xA9llo", s); + ASSERT_TRUE(dstrlen(s) == len); + + dstr s2 = dstrnew("\xC3\x89LLO \xC3\xA9llo"); + dstrtoupper(s2); + ASSERT_STR_EQUAL("\xC3\x89LLO \xC3\xA9LLO", s2); + + dstrfree(s); + dstrfree(s2); +} + +static void test_case_conversion_roundtrip(void) { + dstr s = dstrnew("MiXeD CaSe 42!"); + dstr original = dstrdup(s); + + dstrtoupper(s); + dstrtolower(s); + + // Original had lowercase letters already lowercase and uppercase + // letters that become lowercase too, so compare against a fully + // lowercased reference instead of the original. + dstrtolower(original); + ASSERT_STR_EQUAL(original, s); + + dstrfree(s); + dstrfree(original); +} + static void test_auto_cleanup(void) { // Isolated block to test cleanup attribute { @@ -147,6 +243,13 @@ int main(void) { RUN_TEST(test_concatenation); RUN_TEST(test_push_char); RUN_TEST(test_clear_and_zero); + RUN_TEST(test_tolower_basic); + RUN_TEST(test_toupper_basic); + RUN_TEST(test_case_conversion_already_converted); + RUN_TEST(test_case_conversion_empty); + RUN_TEST(test_case_conversion_null); + RUN_TEST(test_case_conversion_non_ascii_untouched); + RUN_TEST(test_case_conversion_roundtrip); RUN_TEST(test_auto_cleanup); printf("\n\033[1;34m=== FINAL REPORT ===\033[0m\n");