LINQ – 拆分最大长度的字符串,但不要分开单词

我有一个简单的LINQ扩展方法…

public static IEnumerable SplitOnLength(this string input, int length) { int index = 0; while (index < input.Length) { if (index + length < input.Length) yield return input.Substring(index, length); else yield return input.Substring(index); index += length; } } 

这需要一个字符串,然后将其切换为不超过给定长度的字符串集合。

这很好 – 但是我想更进一步。 它把文字翻了一半。 我不需要它来理解任何复杂的东西,我只是希望它能够在早期切断一个字符串,如果切割它的length将在文本中间切割(基本上任何不是空白的东西)。

但是我很害羞LINQ,所以我想知道是否有人知道如何解决这个问题。 我知道我要做什么,但我不知道如何处理它。

所以我要说我有以下文字。

这是我将通过字符串拆分器的示例文本块。

我把这个方法SplitOnLength(6)我会得到以下结果。

  • 这个我
  • sa sa
  • mple b
  • 锁定o
  • f文字
  • 通过
  • hrough
  • s
  • 特林
  • splitt
  • ER。

我宁愿它足够智能停下来看起来更像..

  • 这个
  • 是一个
  • 样品

//坏例子,因为单个单词超过了最大长度,但在实际场景中长度会更大,接近200。

谁能帮我?

我将通过for循环解决这个问题:

 var ret1 = str.Split(' '); var ret2 = new List(); ret2.Add(""); int index = 0; foreach (var item in ret1) { if (item.Length + 1 + ret2[index].Length <= allowedLength) { ret2[index] += ' ' + item; if (ret2[index].Length >= allowedLength) { ret2.Add(""); index++; } } else { ret2.Add(item); index++; } } return ret2; 

首先我想到了Zip,但这里并不好。

和不同的执行版本与yield:

 public static IEnumerable SaeedsApproach(this string str, int allowedLength) { var ret1 = str.Split(' '); string current = ""; foreach (var item in ret1) { if (item.Length + 1 + current.Length <= allowedLength) { current += ' ' + item; if (current.Length >= allowedLength) { yield return current; current = ""; } } else { yield return current; current = ""; } } } 

更新2

我在Saeed的另一个回答中注意到他跳过了我的建议,因为它抛出了exception。 这是一个有趣的话题。 每当我们作为开发人员处理问题的解决方案时,我们需要考虑特殊情况 – 意外输入,无效状态等。我觉得,因为问题的要求是:

我只是希望它能够在早期切断一根绳子,如果切割它的长度将在文本中间切割(基本上任何不是空白的东西)。

… …如果这是不可能的 (即,为了返回指定长度的子字符串,有必要将单词切成两半,因为单词太长),抛出exception是合适的。 但显然这是一个主观问题。 然而,我意识到有不止一种方法可以使这只猫皮肤化,我已经更新了我的解决方案(在pastebin和下面)以采用WordPolicy枚举而不是简单的bool 。 这个枚举有三个值: None (相当于之前的false ), ThrowIfTooLong (相当于之前的true )和CutIfTooLong (如果必须剪切单词,只需剪切它)。

我还添加了一些其他答案的基准**,包括在下面。 请注意,这次我测试了不同length参数的多次运行(5,10,25,200,500,1000)。 对于最短的length (5),结果似乎有点均匀,Saeed的建议在顶部。 随着length增加,Saeed建议的表现越来越差。 在大输入的情况下,Simon和Jay的建议似乎更具可扩展性。

还要记住,OP已明确表示在实际情况下length值将“接近200”; 所以使用200作为输入并不是设计的。 这实际上是一个现实的案例。

新的基准

输入拆分大小:5

更长输入的结果:
赛义德(原创):2.8886毫秒
赛义德:3.2685毫秒
丹:3.3163毫秒
西蒙:7.4182毫秒
杰伊:36.7348毫秒

较短输入的结果:
赛义德(原创):0.031毫秒
赛义德:0.0357毫秒
丹:0.0514毫秒
西蒙:0.1047毫秒
杰伊:0.2885毫秒

再去一次?  ÿ

输入拆分大小:10

更长输入的结果:
丹:1.8798毫秒
赛义德:3.8205毫秒
赛义德(原创):4.0899毫秒
西蒙:4.9869毫秒
杰伊:14.9627毫秒

较短输入的结果:
丹:0.022毫秒
赛义德(原始):0.0396毫秒
赛义德:0.0466毫秒
西蒙:0.0483毫秒
杰伊:0.2022毫秒

再去一次?  ÿ

输入拆分大小:25

更长输入的结果:
丹:0.6713毫秒
西蒙:2.7506毫秒
赛义德:4.5075毫秒
赛义德(原创):4.7827毫秒
杰伊:6.3477毫秒

较短输入的结果:
丹:0.0131毫秒
西蒙:0.0301毫秒
赛义德(原始):0.0441毫秒
赛义德:0.0488毫秒
杰伊:0.1176毫秒

再去一次?  ÿ

输入拆分大小:200

更长输入的结果:
丹:0.1952毫秒
杰伊:1.5764毫秒
西蒙:1.8175毫秒
赛义德(原创):6.8025毫秒
赛义德:7.5221毫秒

较短输入的结果:
丹:0.0092毫秒
西蒙:0.0206毫秒
赛义德:0.0581毫秒
赛义德(原始):0.0586毫秒
杰伊:0.0812毫秒

再去一次?  ÿ

输入拆分大小:500

更长输入的结果:
丹:0.1463毫秒
杰伊:1.2923毫秒
西蒙:1.7326毫秒
赛义德(原创):8.686毫秒
赛义德:9.1726毫秒

较短输入的结果:
丹:0.0061毫秒
西蒙:0.0192毫秒
赛义德(原始):0.0664毫秒
赛义德:0.0748毫秒
杰伊:0.0832毫秒

再去一次?  ÿ

输入拆分大小:1000

更长输入的结果:
丹:0.1293毫秒
杰伊:1.1683毫秒
西蒙:1.7292毫秒
赛义德:11.7121毫秒
赛义德(原创):11.8752毫秒

较短输入的结果:
丹:0.0058毫秒
西蒙:0.0187毫秒
赛义德(原始):0.0765毫秒
杰伊:0.0801毫秒
赛义德:0.084毫秒

再去一次?  ñ

**不幸的是,对于“大”输入(简短的故事),我无法测试Jay的原始方法,这种方法非常昂贵 – 由于递归而导致令人难以置信的深度调用堆栈,以及由于以下数据导致的大量字符串分配string.Substring调用一个巨大的字符串


更新

我希望这不是防御性的,但我认为在评论和其他一些答案中有一些非常误导性的信息。 特别是Saeed的答案 ,即公认的答案,有一些明确的效率缺点。 这不会是一个大问题,除了Saeed在一些评论中声称其他答案(包括这个答案)的效率较低

性能比较

首先,数字不是谎言。 这是我编写的示例程序,用于测试下面的方法以及Saeed针对示例输入的方法,包括长和短。

以下是一些示例结果:

更长输入的结果:
 SplitOnLength:0.8073 ms
 SaeedsOriginalApproach:4.724毫秒
 SaeedsApproach:4.9095毫秒

较短输入的结果:
 SplitOnLength:0.0156毫秒
 SaeedsOriginalApproach:0.0522毫秒
 SaeedsApproach:0.046毫秒

SplitOnLength代表我的方法, SaeedsOriginalApproach代表Saeed答案中的第一个建议, SaeedsApproach代表Saeed使用延迟执行的更新答案。 测试输入是:

  • 弗兰兹卡夫卡的“在刑事殖民地”的全文(短篇小说输入)
  • OP问题文本的摘录(简短输入)
  • length参数为25(以容纳更长的单词)

请注意,在Saeed的答案中建议的方法的一小部分时间内执行SplitOnLength 。 但是,我承认length参数可能会对此产生影响。 由于SaeedsOriginalApproachSaeedsApproach的性能显着提高,我不会感到惊讶。

说明

现在,我只想谈谈我的想法。 马上,Saeed的回答始于对string.Split的调用。 现在这里有一些有趣的微软对这种方法的看法 :

Split方法为返回的数组对象分配内存,为每个数组元素分配String对象。 如果您的应用程序需要最佳性能,或者在应用程序中管理内存分配至关重要,请考虑使用IndexOf或IndexOfAny方法以及可选的Compare方法来定位字符串中的子字符串[强调我的]

换句话说, string.Split方法未被Microsoft认可为实现最佳性能的适当方式。 我突出最后一行的原因是,Saeed似乎特别质疑使用string.Substring的答案的效率。 但这是解决这个问题的最有效方法

然后在Saeed建议的方法中,我们有如下代码:

 ret2[index] += ' ' + item; 

*这就是我认为Saeed方法的性能随着越来越高的长度参数而降低的原因。 正如我们这些在被知道之前被字符串连接性能所困扰的人一样,这是在每个追加上分配一个新的字符串对象,这是浪费。 随着length变长,问题变得更加严重。 请注意下面的SplitOnLength方法不会遇到此问题

另一种看待这种情况的方法就是分解需要发生的不同步骤。 下面,让N为输入字符串的长度, K为子字符串的指定最大长度。

赛义德的答案

  1. 首先, string.Split(' ') 。 这需要对整个字符串进行一次枚举,并为字符串中的每个单词分配一个字符串对象 – 这可能比我们需要的更多(我们几乎肯定要连接其中的一些) – 以及包含这些对象的数组。 让数组中的字符串数量为X.
  2. 然后对#1的X结果进行第二次枚举。 在此枚举期间,使用+=存在0-X字符串连接,在0-X个字符串对象之间分配。

SplitOnLength (下)

  1. 该字符串可能永远不会被完全枚举。 最多只进行了 N次比较,但在更现实的情况下,数字小于N.这是因为在每次迭代时,算法乐观地直接进入下一个“最佳”索引,并且只有在必要时才向后退步以找到最近的空白。
  2. 分配的唯一新字符串对象正是使用string.Substring所需的那些。

string.Substringstring.Substring非常快。 这可能是因为没有“额外”的工作要做; 子串的确切长度是事先已知的,因此在分配期间不会产生浪费(例如, +=运算符会发生)。

再次,我将这个巨大的更新添加到我的答案的原因是指出Saeed的建议中的性能问题,并在其他一些答案中对他所谓的效率问题提出质疑。


原始答案

我的本能方法是从你拥有的东西开始并略微增加它,添加一些代码:

  1. 检查index + length后面是否有空格; 如果不:
  2. 向后退到字符串直到找到空格。

这是修改后的SplitOnLength方法:

 enum WordPolicy { None, ThrowIfTooLong, CutIfTooLong } public static IEnumerable SplitOnLength(this string input, int length, WordPolicy wordPolicy) { int index = 0; while (index < input.Length) { int stepsBackward = 0; if (index + length < input.Length) { if (wordPolicy != WordPolicy.None) { yield return GetBiggestAllowableSubstring(input, index, length, wordPolicy, out stepsBackward); } else { yield return input.Substring(index, length); } } else { yield return input.Substring(index); } index += (length - stepsBackward); } } static string GetBiggestAllowableSubstring(string input, int index, int length, WordPolicy wordPolicy, out int stepsBackward) { stepsBackward = 0; int lastIndex = index + length - 1; if (!char.IsWhiteSpace(input[lastIndex + 1])) { int adjustedLastIndex = input.LastIndexOf(' ', lastIndex, length); stepsBackward = lastIndex - adjustedLastIndex; lastIndex = adjustedLastIndex; } if (lastIndex == -1) { if (wordPolicy == WordPolicy.ThrowIfTooLong) { throw new ArgumentOutOfRangeException("The input string contains at least one word greater in length than the specified length."); } else { stepsBackward = 0; lastIndex = index + length - 1; } } return input.Substring(index, lastIndex - index + 1); } 

这种方法的优点是不需要做更多的工作; 请注意没有任何string.Split调用以及string.Substring只在实际返回结果的地方调用的事实。 换句话说,使用此方法不会创建多余的字符串对象 - 只有您真正想要的对象。

尝试使用String.Split(' ')将单个字符串转换为单个单词的数组。 然后,遍历它们,构建最长的字符串(重新添加空格)小于限制,附加换行符和yield。

我也很喜欢LINQ :-),但是这里的代码可以在不分配任何内容的情况下工作(当然除了输出字),删除空格,删除空字符串,修剪字符串,永不断言(这是设计选择) – 我有兴趣看到完整的LINQ等价物:

 public static IEnumerable SplitOnLength(this string input, int length) { if (input == null) yield break; string chunk; int current = 0; int lastSep = -1; for (int i = 0; i < input.Length; i++) { if (char.IsSeparator(input[i])) { lastSep = i; continue; } if ((i - current) >= length) { if (lastSep < 0) // big first word case continue; chunk = input.Substring(current, lastSep - current).Trim(); if (chunk.Length > 0) yield return chunk; current = lastSep; } } chunk = input.Substring(current).Trim(); if (chunk.Length > 0) yield return chunk; } 

我不得不提出一个答案,因为我觉得其他答案过于依赖索引和复杂的逻辑。 我认为我的答案相当简单。

 public static IEnumerable SplitOnLength(this string input, int length) { var words = input.Split(new [] { " ", }, StringSplitOptions.None); var result = words.First(); foreach (var word in words.Skip(1)) { if (result.Length + word.Length > length) { yield return result; result = word; } else { result += " " + word; } } yield return result; } 

OP中提供的样本字符串的结果是:

 This is a sample block of text that I would pass through the string splitter. 
 public static IEnumerable SplitOnLength(this string source,int maxLength) { //check parameters' validity and then int currentIndex = 0; while (currentIndex + maxLength < source.Length) { int prevIndex = currentIndex; currentIndex += maxLength; while (currentIndex >= 0 && source[currentIndex] != ' ') currentIndex--; if (currentIndex <= prevIndex) throw new ArgumentException("invalid maxLength"); yield return source.Substring(prevIndex, currentIndex - prevIndex); currentIndex++; } yield return source.Substring(currentIndex); } 

测试用例:

 "this is a test".SplitOnLength(5).ToList() .ForEach(x => Console.WriteLine("|" + x + "|")); 

输出:

 |this| |is a| |test| 

好吧,我测试了各种方式(jay RegEx方式,而不是LINQ方式)当我为所有位置设置一个维护词时,我也得到了Dan taos的例外,所以我跳过它。

这就是我所做的:

 List smallStrings = new List(); List mediomStrings = new List(); List largeStrings = new List(); for (int i = 0; i < 10; i++) { string strSmallTest = "This is a small string test for different approachs provided here."; smallStrings.Add(Approachs(strSmallTest, "small")); string mediomSize = "Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe." + "Windows 7, Windows Vista SP1 or later, Windows XP SP3, Windows Server 2008 (Server Core Role not supported), Windows Server 2008 R2 " + "(Server Core Role not supported), Windows Server 2003 SP2" + " .NET Framework does not support all versions of every platform. For a list of the supported versions, see .NET Framework System Requirements. "; mediomStrings.Add(Approachs(mediomSize, "Mediom")); string largeSize = "This is a question that I get very frequently, and I always tried to dodged the bullet, but I get it so much that I feel that I have to provide an answer. Obviously, I am (not so) slightly biased toward NHibernate, so while you read it, please keep it in mind." + "EF 4.0 has done a lot to handle the issues that were raised with the previous version of EF. Thinks like transparent lazy loading, POCO classes, code only, etc. EF 4.0 is a much nicer than EF 1.0." + "The problem is that it is still a very young product, and the changes that were added only touched the surface. I already talked about some of my problems with the POCO model in EF, so I won't repeat that, or my reservations with the Code Only model. But basically, the major problem that I have with those two is that there seems to be a wall between what experience of the community and what Microsoft is doing. Both of those features shows much of the same issues that we have run into with NHibernate and Fluent NHibernate. Issues that were addressed and resolved, but show up in the EF implementations." + "Nevertheless, even ignoring my reservations about those, there are other indications that NHibernate's maturity makes itself known. I run into that several times while I was writing the guidance for EF Prof, there are things that you simple can't do with EF, that are a natural part of NHibernate." + "I am not going to try to do a point by point list of the differences, but it is interesting to look where we do find major differences between the capabilities of NHibernate and EF 4.0. Most of the time, it is in the ability to fine tune what the framework is actually doing. Usually, this is there to allow you to gain better performance from the system without sacrificing the benefits of using an OR/M in the first place."; largeStrings.Add(Approachs(largeSize, "Large")); Console.WriteLine(); } Console.WriteLine("/////////////////////////"); Console.WriteLine("average small for saeed: {0}", smallStrings.Average(x => x.saeed)); Console.WriteLine("average small for Jay: {0}", smallStrings.Average(x => x.Jay)); Console.WriteLine("average small for Simmon: {0}", smallStrings.Average(x => x.Simmon)); Console.WriteLine("/////////////////////////"); Console.WriteLine("average mediom for saeed: {0}", mediomStrings.Average(x => x.saeed)); Console.WriteLine("average mediom for Jay: {0}", mediomStrings.Average(x => x.Jay)); Console.WriteLine("average mediom for Simmon: {0}", mediomStrings.Average(x => x.Simmon)); Console.WriteLine("/////////////////////////"); Console.WriteLine("average large for saeed: {0}", largeStrings.Average(x => x.saeed)); Console.WriteLine("average large for Jay: {0}", largeStrings.Average(x => x.Jay)); Console.WriteLine("average large for Simmon: {0}", largeStrings.Average(x => x.Simmon)); 

和:

 private static DifferentTypes Approachs(string stringToDecompose, string text2Write) { DifferentTypes differentTypes; Stopwatch sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 1000; i++) { var strs = stringToDecompose.SaeedsApproach(10); foreach (var item in strs) { } } sw.Stop(); Console.WriteLine("Saeed's Approach takes {0} millisecond for {1} strings", sw.ElapsedMilliseconds, text2Write); differentTypes.saeed = sw.ElapsedMilliseconds; sw.Restart(); for (int i = 0; i < 1000; i++) { var strs = stringToDecompose.JaysApproach(10); foreach (var item in strs) { } } sw.Stop(); Console.WriteLine("Jay's Approach takes {0} millisecond for {1} strings", sw.ElapsedMilliseconds, text2Write); differentTypes.Jay = sw.ElapsedMilliseconds; sw.Restart(); for (int i = 0; i < 1000; i++) { var strs = stringToDecompose.SimmonsApproach(10); foreach (var item in strs) { } } sw.Stop(); Console.WriteLine("Simmon's Approach takes {0} millisecond for {1} strings", sw.ElapsedMilliseconds, text2Write); differentTypes.Simmon = sw.ElapsedMilliseconds; return differentTypes; } 

结果:

 average small for saeed: 4.6 average small for Jay: 33.9 average small for Simmon: 5.6 average mediom for saeed: 28.7 average mediom for Jay: 173.9 average mediom for Simmon: 38.7 average large for saeed: 115.3 average large for Jay: 594.2 average large for Simmon: 138.7 

只需在您的PC上进行测试,就可以编辑它以保留测试结果或改善当前function。 我敢肯定,如果我们用更大的弦测试它,我们可以看到我的方法与你的方法之间存在很大差异。

编辑:我编辑了使用foreach和yield的方法,请参阅上面的代码。 结果是:

 average small for saeed: 6.5 average small for Jay: 34.5 average small for Simmon: 5.9 average mediom for saeed: 30.6 average mediom for Jay: 157.9 average mediom for Simmon: 35 average large for saeed: 122.4 average large for Jay: 584 average large for Simmon: 157 

这是我(杰伊)的测试:

 class Program { static void Main() { var s = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit, " + "sed diam nonummy nibh euismod tincidunt ut laoreet dolore " + "magna aliquam erat volutpat. Ut wisi enim ad minim veniam, " + "quis nostrud exerci tation ullamcorper suscipit lobortis nisl " + "ut aliquip ex ea commodo consequat. Duis autem " + "vel eum iriure dolor in hendrerit in vulputate velit " + "esse molestie consequat, vel illum dolore eu feugiat " + "nulla facilisis at vero eros et accumsan et iusto " + "odio dignissim qui blandit praesent luptatum zzril delenit augue " + "duis dolore te feugait nulla facilisi. Nam liber tempor " + "cum soluta nobis eleifend option congue nihil imperdiet doming id " + "quod mazim placerat facer possim assum. Typi non habent " + "claritatem insitam; est usus legentis in iis qui facit " + "eorum claritatem. Investigationes demonstraverunt lectores legere me lius quod " + "ii legunt saepius. Claritas est etiam processus dynamicus, " + "qui sequitur mutationem consuetudium lectorum. Mirum est notare quam " + "littera gothica, quam nunc putamus parum claram, anteposuerit " + "litterarum formas humanitatis per seacula quarta decima et quinta decima" + ". Eodem modo typi, qui nunc nobis videntur parum clari" + ", fiant sollemnes in futurum."; s += s; s += s; s += s; var watch = new Stopwatch(); watch.Start(); for (int i = 1; i <= 10000; i++) s.JaysApproach(200).ToList(); watch.Stop(); Console.WriteLine("Jay: {0}", watch.ElapsedTicks / 10000); watch.Reset(); watch.Start(); for (int i = 1; i <= 10000; i++) s.SaeedsApproach(200); watch.Stop(); Console.WriteLine("Saeed: {0}", watch.ElapsedTicks / 10000); watch.Reset(); watch.Start(); for (int i = 1; i <= 10000; i++) s.SimonsApproach(200).ToList(); watch.Stop(); Console.WriteLine("Simon: {0}", watch.ElapsedTicks / 10000); Console.ReadLine(); } } 

结果:

 4 lorem ipsums (as shown): Jay: 317 Saeed: 1069 Simon: 599 3 lorems ipsums: Jay: 283 Saeed: 862 Simon: 465 2 lorem ipsums: Jay: 189 Saeed: 417 Simon: 236 1 lorem ipsum: Jay: 113 Saeed: 204 Simon: 118 

更新:

单线:

  public static IEnumerable SplitOnLength(this string s, int length) { return Regex.Split(s, @"(.{0," + length + @"}) ") .Where(x => x != string.Empty); } 

我已经将这个与已接受的答案进行了分析,其中~9,300个字符源(lorem ipsum x4)在200个字符之前或之前分割。

10,000次通过:
– 循环需要大约4,200毫秒
– 我的需要大约1,200毫秒

原答案:

这个方法会缩短结果以避免破坏单词,除非单词超过指定的长度,在这种情况下它会破坏它。

 public static IEnumerable SplitOnLength(this string s, int length) { var pattern = @"^.{0," + length + @"}\W"; var result = Regex.Match(s, pattern).Groups[0].Value; if (result == string.Empty) { if (s == string.Empty) yield break; result = s.Substring(0, length); } yield return result; foreach (var subsequent_result in SplitOnLength(s.Substring(result.Length), length)) { yield return subsequent_result; } }