6
\$\begingroup\$

In an attempt to summarize the question and all the good answers to this question, I decided to try to write a pair of string extensions that could

  1. split a string in respect to number of slices
  2. split a string in respect to number of text elements in each slice.

The effort resulted in the code below.

  public static class Extensions
  {

    public static IEnumerable<string> SliceByNumber(this string value, int sliceCount, bool strict = false)
    {
      if (value == null) throw new ArgumentNullException(nameof(value));
      if (sliceCount <= 0) throw new ArgumentException("Number of slices is invalid.", nameof(sliceCount));

      StringInfo stringInfo = new StringInfo(value);
      return stringInfo.Slice(stringInfo.LengthInTextElements / sliceCount, sliceCount, strict);
    }

    public static IEnumerable<string> SliceBySize(this string value, int sliceSize, bool strict = false)
    {
      if (value == null) throw new ArgumentNullException(nameof(value));
      if (sliceSize <= 0) throw new ArgumentException("The size of the slices is invalid.", nameof(sliceSize));

      StringInfo stringInfo = new StringInfo(value);
      return stringInfo.Slice(sliceSize, stringInfo.LengthInTextElements / sliceSize, strict);
    }

    private static IEnumerable<string> Slice(this StringInfo stringInfo, int sliceSize, int sliceCount, bool strict)
    {
      stringInfo.CheckSliceValidity(sliceSize, sliceCount, strict);

      int sliceStart = 0;
      for (; sliceStart < (sliceCount - 1) * sliceSize; sliceStart += sliceSize)
      {
        yield return stringInfo.SubstringByTextElements(sliceStart, sliceSize);
      }

      yield return stringInfo.SubstringByTextElements(sliceStart);
    }

    private static void CheckSliceValidity(this StringInfo stringInfo, int sliceSize, int sliceCount, bool strict)
    {
      if (stringInfo.LengthInTextElements < sliceSize)
        throw new ArgumentOutOfRangeException(nameof(sliceSize), "Number of elements in a slice is greather than the number of characters in the string value.");
      if (stringInfo.LengthInTextElements < sliceCount)
        throw new ArgumentOutOfRangeException(nameof(sliceSize), "Number of slices is greather than the number of characters in the string value.");
      if (strict && sliceSize * sliceCount != stringInfo.LengthInTextElements)
        throw new ArgumentException("Unable to slice the string into a number of strings of equal length.", nameof(sliceSize));

      // Maybe other precautions?
    }
  }

static void SliceTest()
{
  int slicesOrSize= 5;
  string data = "OnceUponATime";

  foreach (string slice in data.SliceByNumber(slicesOrSize, false))
  {
    Console.WriteLine(slice);
  }
  Console.WriteLine();

  foreach (string slice in data.SliceBySize(slicesOrSize, false))
  {
    Console.WriteLine(slice);
  }
}

Any comments or improvements are welcome.

\$\endgroup\$
  • 1
    \$\begingroup\$ I'd replace ArgumentException with ArgumentOutOfRangeException and change boolean parameter to an enum \$\endgroup\$ – Adriano Repetti Mar 31 '18 at 8:45
3
\$\begingroup\$

Specification

SliceBySize

I have verified your method against that of the OP. The OP splits slices in chunks of desired length, with any remainder in a smaller chunk.

[TestMethod()]
public void SplitTestNotEqual()
{
    string input = "12345";
    string[] expected = new string[] { "123", "45" };
    string[] actual = input.Split(3);

    CollectionAssert.AreEqual(expected, actual);
}

I expected your SliceBySize to have equivalent behavior. Instead, you add the remainder to the last chunk.

[TestMethod()]
public void SplitTestNotEqual()
{
    string input = "12345";
    string[] expected = new string[] { "123", "45" };
    string[] actual = input.SliceBySize(3, false);      // <- yields "12345"

    CollectionAssert.AreEqual(expected, actual);        // <- assertion failure
}

It is not clear from your question whether this is as designed.

SliceByNumber

SliceByNumber has similar behavior that no chunks are smallar than the calculated chunk size. For instance, "OnceUponATime" split in 5 numbers yields

  • "On", "ce", "Up", "on", "ATime"

while I would expect it to return

  • "Onc", "eUp", "onA", "Tim", "e"

Use Cases

These extensions only make sense when working with special characters and extended unicode, as described in the original question (linked in your question) and comes at a performance cost. I would not use these extensions for day to day string manipulations.

I would provide extensions for simple strings (used in most cases):

namespace MyNamespace.Text
{
    public static class StringExtensions 
    { 
        // .. slice simple strings (1 character = 1 code point)
    }
}

And specialized extensions, in case you need the behavior of StringInfo.

namespace MyNamespace.Text.Specialized
{
    public static class StringExtensions 
    { 
        // .. slice exotic strings (extended unicode, diacritics)
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ SliceByNymber("OnceUponATime") couldn't it equally be: "Onc", "eUp", "onA", "Ti", "me"? \$\endgroup\$ – Henrik Hansen Aug 5 at 12:16
  • \$\begingroup\$ @HenrikHansen Yes, that's another option, if you want the distribution to be as 'fair' as possible. I'd say it can be eager (like OP), lazy (like your question) or fair (like you just suggested). \$\endgroup\$ – dfhwze Aug 5 at 12:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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