17
\$\begingroup\$

I wanted to dust off my C skills, so I decided to write a couple string manipulation functions. I aimed for simplicity and safety. Performance was not a concern.

Please be a thorough as you like. C is not my everyday language, so I might have made some obvious mistakes. I would also appreciate extra attention to the test cases I've provided. Are they easy to understand and expand? Is coverage good enough?

#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <stdbool.h>

// Erases a part of the string's content, filling the end with null chars.
// `startIndex` must be in a valid range inside this string. The char pointed by `startIndex` is included.
// If `count` is less than 0 or greater than the string's length, erases until the end, starting at `startIndex`.
// Does nothing if the string is empty or if `count` is zero.
size_t strErase(char * str, size_t len, size_t startIndex, int count)
{
    assert(str != NULL);
    if (*str == '\0')
    {
        assert(len == 0); // This would indicate a parameter mismatch if not true.
        return len;
    }

    // If the string is not empty, a length must be provided.
    assert(len != 0);
    assert(startIndex < len && "Index out of range!");

    // Do nothing in this case.
    if (count == 0)
    {
        return len;
    }

    // Erase ALL till the end of the string, starting from `startIndex`, if `count` is not in range.
    if (count < 0 || (size_t)count >= len)
    {
        memset(&str[startIndex], 0, len - startIndex);
        return len - startIndex; // New length.
    }

    // Erase `count` chars from an arbitrary position:
    size_t charsToMove = len - (startIndex + count) + 1; // Including the null terminator (+1)
    memmove(&str[startIndex], &str[startIndex + count], charsToMove);
    return len - count; // New length.
}

// Replaces a sequence of characters in a string by a single character.
// `startIndex` must be in a valid range inside this string. The char pointed by `startIndex` is included.
// If `count` is less than 0 or greater than the string's length, fills with `replaceWith` char starting at `startIndex`.
// Does nothing if the string is empty or if `count` is zero.
void strReplace(char * str, size_t len, size_t startIndex, int count, char replaceWith)
{
    assert(str != NULL);
    if (*str == '\0')
    {
        assert(len == 0); // This would indicate a parameter mismatch if not true.
        return;
    }

    // If the string is not empty, a length must be provided.
    assert(len != 0);
    assert(startIndex < len && "Index out of range!");

    // Do nothing in this case.
    if (count == 0)
    {
        return;
    }

    // Fill until end with the given char, starting at pos:
    if (count < 0 || (size_t)count >= len)
    {
        memset(&str[startIndex], replaceWith, len - startIndex);
        return;
    }

    int charsLeft = len - startIndex;
    memset(&str[startIndex], replaceWith, (count <= charsLeft) ? count : charsLeft);
}

// Thin wrapper over strcmp(str1, str2) == 0 for the test cases.
bool strEquals(const char * str1, const char * str2)
{
    return strcmp(str1, str2) == 0;
}

void Test_strReplace(void)
{
    // Replace end of string:
    {
        char s[] = "Hello World";
        strReplace(s, strlen(s), 5, 6, '+');

        assert(strlen(s) == 11);
        assert(strEquals(s, "Hello++++++"));
    }

    // Replace beginning of string:
    {
        char s[] = "Hello World";
        strReplace(s, strlen(s), 0, 6, '+');

        assert(strlen(s) == 11);
        assert(strEquals(s, "++++++World"));
    }

    // Replace middle of string:
    {
        char s[] = "Hello World";
        strReplace(s, strlen(s), 3, 5, '+');

        assert(strlen(s) == 11);
        assert(strEquals(s, "Hel+++++rld"));
    }

    // Replace end with count overflow:
    {
        char s[] = "Hello World";
        strReplace(s, strlen(s), 5, 999, '+');

        assert(strlen(s) == 11);
        assert(strEquals(s, "Hello++++++"));
    }

    // Replace the whole string from the start:
    {
        char s[] = "Hello World";
        strReplace(s, strlen(s), 0, strlen(s), '+');

        assert(strlen(s) == 11);
        assert(strEquals(s, "+++++++++++"));
    }

    // Replace the whole string from the start with count overflow:
    {
        char s[] = "Hello World";
        strReplace(s, strlen(s), 0, 999, '+');

        assert(strlen(s) == 11);
        assert(strEquals(s, "+++++++++++"));
    }

    // Count equal to zero is a no-op:
    {
        char s[] = "Hello World";
        strReplace(s, strlen(s), 2, 0, '+');

        assert(strlen(s) == 11);
        assert(strEquals(s, "Hello World"));
    }
}

void Test_strErase(void)
{
    // Erase end of string:
    {
        char s[] = "Hello World";
        strErase(s, strlen(s), 5, 6);

        assert(strlen(s) == 5);
        assert(strEquals(s, "Hello"));
    }

    // Erase beginning of string:
    {
        char s[] = "Hello World";
        strErase(s, strlen(s), 0, 6);

        assert(strlen(s) == 5);
        assert(strEquals(s, "World"));
    }

    // Erase from the middle of the string:
    {
        char s[] = "Hello World";
        strErase(s, strlen(s), 3, 5);

        assert(strlen(s) == 6);
        assert(strEquals(s, "Helrld"));
    }

    // Erase end with count overflow:
    {
        char s[] = "Hello World";
        strErase(s, strlen(s), 5, 999);

        assert(strlen(s) == 5);
        assert(strEquals(s, "Hello"));
    }

    // Erase the whole string from the start:
    {
        char s[] = "Hello World";
        strErase(s, strlen(s), 0, strlen(s));

        assert(*s == '\0');
    }

    // Erase the whole string from the start with count overflow:
    {
        char s[] = "Hello World";
        strErase(s, strlen(s), 0, 999);

        assert(*s == '\0');
    }

    // Count equal to zero is a no-op:
    {
        char s[] = "Hello World";
        strErase(s, strlen(s), 2, 0);

        assert(strlen(s) == 11);
        assert(strEquals(s, "Hello World"));
    }
}

int main(void)
{
    Test_strReplace();
    Test_strErase();
    printf("Passed all tests!\n");
}
\$\endgroup\$

2 Answers 2

4
\$\begingroup\$
  • Asserts are good in the test cases, but not in the actual code. First, in a release version they are no-ops, so the release doesn't check anything. Second, they result in a program termination, which is a bit drastic.

  • Interfaces are unclear. What is len? It may only represent the length of the character array (vs C string), i.e. you do not rely on a terminating 0, and this, in turn, annuls the claim that strErase is filling the end with null chars. If you do rely on a terminating 0, do not have len as a parameter, but calculate it.

  • Treating negative count is a somewhat unorthodox. Normally (if allowed at all) it means "count backwards", that is, erase a [startIndex + count, startIndex) range.

\$\endgroup\$
3
\$\begingroup\$

There's quite a bit of repeated code in your solution. All of the code that you've used to check specific invariants (string is not NULL, length is not 0 etc) can be moved into a separate function:

static bool check_invariants(const char *str, size_t len, size_t start_index, int count)
{
    assert(str != NULL);
    if (*str == '\0')
    {
        assert(len == 0); // This would indicate a parameter mismatch if not true.
        return false;
    }

    // If the string is not empty, a length must be provided.
    assert(len != 0);
    assert(startIndex < len && "Index out of range!");

    return count == 0;
}

You might want to consider returning the (potentially modified) str from strReplace, as this is more consistent with the C standard library interface.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.