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
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,25 @@ dstrrange(s, 2, -1); // keep from index 2 to the last character

Keeps only the `[start, end]` slice of `s`, in place (`end` is inclusive). 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` past `end` or past the end of the string yields an empty `dstr`. No allocation happens: the existing buffer is shifted with `memmove` and truncated in place. No-op if `s` is `NULL` or empty.

### Trim

```c
dstrtrim(s); // strip leading/trailing ASCII whitespace
dstrtrim(s, "xy"); // strip leading/trailing 'x'/'y' bytes instead
```

Strips leading and trailing bytes from `s`, in place. The one-argument form strips ASCII whitespace (`' '`, `'\t'`, `'\n'`, `'\v'`, `'\f'`, `'\r'`); the two-argument form strips any byte found in the null-terminated `cutset` instead. No allocation happens: the kept slice is shifted with `memmove` and truncated in place. No-op if `s` is `NULL` or empty, or (two-argument form) if `cutset` is `NULL` or empty.

### Insert

```c
s = dstrinsert(s, 5, ", "); // insert at index 5
s = dstrinsert(s, 0, ">> "); // prepend
s = dstrinsert(s, -1, "!"); // insert before the last character
```

Inserts the null-terminated string `s2` into `s1` at `idx`, in place. Negative indices count from the end of the string, as in `dstrrange`. Out-of-range indices are clamped instead of erroring. May reallocate `s1`; always use the returned pointer.

### Split

```c
Expand Down Expand Up @@ -230,11 +249,6 @@ dstr s = $("hello", 64);
- All functions that return `dstr` may return a reallocated pointer; always reassign.
- Capacity arguments are hints and are silently increased if insufficient.

## Roadmap

- **dstrinsert** — insert substring at a given index
- **dstrtrim** — strip leading/trailing whitespace or a given charset

## License

BSD-2-Clause
131 changes: 131 additions & 0 deletions dstr.c
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,80 @@ void dstrrange(dstr s, ssize_t start, ssize_t end)
_dstr_set_len(hd, newlen, t);
}

static inline bool _dstr_is_space(unsigned char c)
{
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' ||
c == '\r';
}

// dstrtrim
void dstrtrim_base(dstr s)
{
enum dstrhd_type t;
void* hd = _dstr_get_hdr_and_type(s, &t);
if (!hd)
return;

size_t len = dstrlen(s);
if (len == 0)
return;

size_t start = 0;
while (start < len && _dstr_is_space((unsigned char)s[start]))
start++;

size_t end = len;
while (end > start && _dstr_is_space((unsigned char)s[end - 1]))
end--;

size_t newlen = end - start;
if (start)
memmove(s, s + start, newlen);
s[newlen] = '\0';
_dstr_set_len(hd, newlen, t);
}

static inline void _dstr_cutset_bitmap(const char* cutset, uint64_t bitmap[4])
{
bitmap[0] = bitmap[1] = bitmap[2] = bitmap[3] = 0;
for (const unsigned char* p = (const unsigned char*)cutset; *p; p++)
bitmap[*p >> 6] |= (uint64_t)1 << (*p & 63);
}

static inline bool _dstr_in_bitmap(const uint64_t bitmap[4], unsigned char c)
{
return (bitmap[c >> 6] >> (c & 63)) & 1;
}

void dstrtrim_custom(dstr s, const char* cutset)
{
enum dstrhd_type t;
void* hd = _dstr_get_hdr_and_type(s, &t);
if (!hd || !cutset || cutset[0] == '\0')
return;

size_t len = dstrlen(s);
if (len == 0)
return;

uint64_t bitmap[4];
_dstr_cutset_bitmap(cutset, bitmap);

size_t start = 0;
while (start < len && _dstr_in_bitmap(bitmap, (unsigned char)s[start]))
start++;

size_t end = len;
while (end > start && _dstr_in_bitmap(bitmap, (unsigned char)s[end - 1]))
end--;

size_t newlen = end - start;
if (start)
memmove(s, s + start, newlen);
s[newlen] = '\0';
_dstr_set_len(hd, newlen, t);
}

// dstrsplit
dstr* dstrsplit(dstr s, const char* delim, size_t* out_count)
{
Expand Down Expand Up @@ -561,6 +635,63 @@ dstr dstrappend_custom(dstr s1, const dstr s2, size_t cap)
return _dstr_concat_impl(s1, dstrlen(s1), s2, dstrlen(s2), cap);
}

// dstrinsert
dstr dstrinsert(dstr s1, ssize_t idx, const char* s2)
{
if (!s1 || !s2)
return s1;

size_t s1_len = dstrlen(s1);
size_t s2_len = strlen(s2);
if (s2_len == 0)
return s1;
if (s2_len > SIZE_MAX - s1_len)
return NULL;

if (idx < 0) {
idx += (ssize_t)s1_len;
if (idx < 0)
idx = 0;
}
size_t pos = ((size_t)idx > s1_len) ? s1_len : (size_t)idx;
size_t new_len = s1_len + s2_len;

uintptr_t s1_addr = (uintptr_t)s1;
uintptr_t s2_addr = (uintptr_t)s2;
size_t s1_cap = dstrcap(s1);
bool s2_aliases_s1 = s2_addr >= s1_addr && s2_addr - s1_addr < s1_cap;
size_t s2_offset = s2_aliases_s1 ? (size_t)(s2_addr - s1_addr) : 0;

dstr tmp = dstrreserve(s1, _dstr_grow_cap(s1_cap, new_len + 1));
if (!tmp)
return NULL;
s1 = tmp;

if (s2_aliases_s1)
s2 = s1 + s2_offset;

enum dstrhd_type t;
void* hd = _dstr_get_hdr_and_type(s1, &t);

size_t tail_len = s1_len - pos + 1;

if (s2_aliases_s1 && s2_offset + s2_len > pos) {
char* scratch = DSTR_MALLOC(s2_len);
if (!scratch)
return NULL;
memcpy(scratch, s2, s2_len);
memmove(s1 + pos + s2_len, s1 + pos, tail_len);
memcpy(s1 + pos, scratch, s2_len);
DSTR_FREE(scratch);
} else {
memmove(s1 + pos + s2_len, s1 + pos, tail_len);
memcpy(s1 + pos, s2, s2_len);
}

_dstr_set_len(hd, new_len, t);
return s1;
}

// strnew
static dstr _dstrnew_allocator(enum dstrhd_type t,
const char* msg,
Expand Down
31 changes: 31 additions & 0 deletions dstr.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ typedef ptrdiff_t ssize_t;
#define dstrappend(...) \
GET_DSTRAPPEND(__VA_ARGS__, dstrappend_custom, dstrappend_base)(__VA_ARGS__)

#define GET_DSTRTRIM(_1, _2, NAME, ...) NAME
#define dstrtrim(...) \
GET_DSTRTRIM(__VA_ARGS__, dstrtrim_custom, dstrtrim_base)(__VA_ARGS__)

#define W_UNUSED_RESULT __attribute__((warn_unused_result))

typedef char* dstr;
Expand Down Expand Up @@ -138,6 +142,20 @@ ssize_t dstrfind(dstr s, const char* needle) W_UNUSED_RESULT;
*/
void dstrrange(dstr s, ssize_t start, ssize_t end);

/*
* dstrtrim(s)
* dstrtrim(s, cutset)
*
* Strip leading and trailing bytes from s, in place. The one-argument form
* strips ASCII whitespace (' ', '\t', '\n', '\v', '\f', '\r'); the
* two-argument form strips any byte found in the null-terminated cutset
* instead. No allocation, no new pointer: the kept slice is shifted with
* memmove and truncated in place. No-op if s is NULL, empty, or (for the
* two-argument form) if cutset is NULL or empty.
*/
void dstrtrim_base(dstr s);
void dstrtrim_custom(dstr s, const char* cutset);

/*
* dstrsplit(s, delim, out_count)
*
Expand Down Expand Up @@ -282,6 +300,19 @@ dstr dstrcat_custom(dstr s1, const char* s2, size_t cap) W_UNUSED_RESULT;
dstr dstrappend_base(dstr s1, const dstr s2) W_UNUSED_RESULT;
dstr dstrappend_custom(dstr s1, const dstr s2, size_t cap) W_UNUSED_RESULT;

/*
* dstrinsert(s1, idx, s2)
*
* Insert the null-terminated string s2 into s1 at idx, in place. Negative
* indices count from the end of the string, as in s[len + idx]. Out-of-range
* indices are clamped instead of erroring, so an idx at or before the start
* prepends and an idx at or past the end appends.
* May reallocate s1; always use the returned pointer.
* Returns s1 unchanged if s1 or s2 is NULL, or if s2 is empty.
* Returns NULL on allocation failure.
*/
dstr dstrinsert(dstr s1, ssize_t idx, const char* s2) W_UNUSED_RESULT;

/*
* dstrauto
*
Expand Down
Loading
Loading