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. <br> Here is the implementation that I use in my [tool library](https://github.com/ProphetLamb-Organistion/Groundbeef/blob/ab764b85b5815d5f1f4c3f1ed9aeb7d151b4c320/src/Text/StringHelper.cs#L827), it also validates the unicode symbols in the string: ```csharp [Pure] 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); } [Pure] 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; } [MethodImpl(MethodImplOptions.AggressiveInlining)] 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); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] 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); } } public static int CountTextSymbols(this ReadOnlySpan<char> self) => CountTextSymbols(self, true); public static unsafe int CountTextSymbols(this ReadOnlySpan<char> self, bool throwOnIllegalSymbol) { if (self.IsEmpty) return 0; int length; fixed (char* inPtr = &MemoryMarshal.GetReference(self)) { length = CountValidTextSymbols(inPtr, self.Length); } if (length > 0) return length; length = ~length; if (throwOnIllegalSymbol) throw new ArgumentException(string.Format(ExceptionResource.UNICODE_ILLEGAL_SYMBOL, length), nameof(self)); return length; } [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe int CountValidTextSymbols(char* c, int remainingChars) { int length = 0; int increment = 0; for (int i = 0; i + increment <= remainingChars; i += increment, length++) { increment = ValidateUnicodeSymbol(c + i); if (increment <= 0) { return ~length; } } return length; } /// <summary>Returns the number of 2byte characters that compose the next symbol. If the symbol is illegal returns the complement of the number of corrupted 2byte characters.</summary> [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe int ValidateUnicodeSymbol(char* c) { // Modified from source: https://stackoverflow.com/a/27084836/6401643 uint ch = *c; // Null terminator -> return zero if (ch == 0) return 0; if (ch < 0xD800) // Char is up to first high surrogate return 1; if (ch <= 0xDBFF) { // Found high surrogate -> check surrogate pair uint chLow = *(c + 1); if (chLow == 0) { // Fast char is high surrogate, so it is missing its pair return ~1; } if (!(chLow >= 0xDC00 && chLow <= 0xDFFF)) { // Did not find a low surrogate after the high surrogate return ~1; } // Convert to UTF32 - like in Char.ConvertToUtf32(highSurrogate, lowSurrogate) ch = ((ch - 0xD800) << 10) + (chLow - 0xDC00) + 0x10000; if ((ch & 0xFFFE) == 0xFFFE) // Other non-char found return ~2; // Found a good surrogate pair return 2; } if (ch <= 0xDFFF) // Unexpected low surrogate return ~1; if (ch >= 0xFDD0 && ch <= 0xFDEF) // Non-chars are considered invalid by System.Text.Encoding.GetBytes() and String.Normalize() return ~1; if ((ch & 0xFFFE) == 0xFFFE) // Other non-char found return ~1; return 1; } /// <summary> /// !Warning: Does not validate the symbol /// !Warning: Assumes that the pointer does not point to the null terminator. /// Returns the number of 2byte characters that compose the next symbol. /// </summary> [Pure] [MethodImpl(MethodImplOptions.AggressiveInlining)] private static unsafe int UnicodeSymbolCharLength(char* c) { // We can always check c and c + 1 (four bytes total) because of the null terminator. bool isTwoChars = (*(uint*)c & 0xFC00FC00) == 0xDC00D800; return 1 + *(byte*)(&isTwoChars); // Casting bool to byte is 0 when false, 1 when true; } ``` 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](https://www.ibm.com/support/producthub/db2/docs/content/SSEPGG_11.5.0/com.ibm.db2.luw.admin.nls.doc/doc/c0004816.html) `\u0301` only counts as one character. But I really am no expert so please educate me on that point.