Skip to main content
3 of 11
Formatting

I personally advice against designing a library function, with the name of an existing framework function, but a fundamentally different functionality. That only leads to confusion. I would call the function Crop because, well it crops a string in evenly sized pieces.

Also implementing the functionality using an enumerator is ill advised, because we can calculate the exact amount of memory to allocate. Furthermore the function is so very light since all we do is call memcpy (Buffer.Memmov in in the string ctor) with a bunch of branching and integer comparisons, that you'd be hard pressed to gain any advantage from an enumerator, even, if you only need the first element cropped.
So here is a unmanaged implementation (its a bit hacky but gets the job done):

public static string[] Crop(this string? self, int chunkLength, bool strict) => Crop((self ?? throw new ArgumentNullException(nameof(self))).AsSpan(), chunkLength, strict);

public static string[] Crop(this ReadOnlySpan<char> self, int chunkLength, bool strict)
{
    if (strict && self.Length % chunkLength != 0)
        throw new ArgumentOutOfRangeException(nameof(chunkLength));
    return Crop(self, chunkLength);
}

public static string[] Crop(this string? self, int chunkLength) => Crop((self ?? throw new ArgumentNullException(nameof(self))).AsSpan(), chunkLength);

public static unsafe string[] Crop(this ReadOnlySpan<char> self, int chunkLength)
{
    int symbolCount = CountTextSymbols(self);
    if (symbolCount == 0)
    {
        if (chunkLength == 0)
            return new string[0];
        throw new ArgumentOutOfRangeException(nameof(self));
    }
    if (chunkLength > symbolCount || chunkLength <= 0)
        throw new ArgumentOutOfRangeException(nameof(chunkLength));
    if (symbolCount == chunkLength)
        return new []{ self.ToString() };

    string[] output = new string[(symbolCount - 1) / chunkLength + 1];
    fixed (char* ptr = &MemoryMarshal.GetReference(self))
    {
        if (symbolCount == self.Length)
        {
            CropASCII(ptr, symbolCount, chunkLength, output);
        }
        else
        {
            CropUnicode(ptr, symbolCount, chunkLength, output);   
        }
    }
    return output;
}

private static unsafe void CropASCII(char* source, int symbolCount, int chunkLength, string[] output)
{
    int outIndex = 0;
    int i;
    for (i = 0; i + chunkLength <= symbolCount; i += chunkLength)
    {
        output[outIndex++] = new string(source, i, chunkLength);
    }

    if (symbolCount != i)
    {
        output[outIndex] = new string(source, i, symbolCount - i);
    }
}

private static unsafe void CropUnicode(char* source, int symbolCount, int chunkLength, string[] output)
{
    int outIndex = 0;
    int i = 0;
    int blockCharCount;
    do
    {
        blockCharCount = 0;
        int increment;
        for (int blockSymbols = 0; i < symbolCount && blockSymbols < chunkLength; i++, blockSymbols++, blockCharCount += increment)
        {
            increment = UnicodeSymbolCharLength(source + blockCharCount);
        }

        output[outIndex++] = new string(source, 0, blockCharCount);
        source += blockCharCount;
    } while (i < symbolCount);

    if (symbolCount != i)
    {
        source -= blockCharCount;
        blockCharCount = 0;
        while (*(source + blockCharCount) != '\0')
            blockCharCount++;
        output[outIndex] = new string(source, 0, blockCharCount);
    }
}

[Pure]
public static unsafe int CountTextSymbols(ReadOnlySpan<char> source)
{
    if (source.IsEmpty)
        return 0;
    fixed (char* inPtr = &MemoryMarshal.GetReference(source))
    {
        return CountTextSymbols(inPtr, source.Length);
    }
}

[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static unsafe int CountTextSymbols(char* c, int remainingChars)
{
    int length = 0;
    int increment = 0;
    for (int i = 0; i + increment <= remainingChars; i += increment, length++)
    {
        increment = UnicodeSymbolCharLength(c + i);
    }
    return length;
}

[Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static unsafe int UnicodeSymbolCharLength(char* c)
{
    if ((*c & 0xFE00) == 0xD800 && (*(c + 1) & 0xFE00) == 0xDC00) // Can always check c + 1 because of the null terminator
        return 2;
    return 1;
}

It doesn't fulfill the 3rd test criteria SplitTestUnicodeVariant2 provided by @Heslacher. But I dont quite understand that criteria either since according to the UTF-16 binary pattern \u0301 only counts as one character. But I really am no expert so please educate me on that point.

Please note that I am not checking for a corrupted string, this should not be a problem in 99% of the cases, but I still think that we should validate the string, in the UnicodeSymbolCharLength-function or some function calling it, when calculating the number of symbols in the string.