Skip to content
Merged
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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
28 changes: 28 additions & 0 deletions dstr.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
18 changes: 18 additions & 0 deletions dstr.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*
Expand Down
103 changes: 103 additions & 0 deletions tester.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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");
Expand Down
Loading