2013-08-08 2 views
19

Я знаю, что концепция String.Split была рассмотрена ранее множеством различных подходов, но меня особенно интересует решение LINQ по этому вопросу.Разделите строку на список строк N-длины, используя LINQ

Я попытался написать класс расширения для обработки split, но обе попытки имеют некоторые серьезные проблемы. Так следующее:

string s = "ABCDEFGHIJKLMNOPQRSTUVWX"; 
var results = s.SplitEvery(4); 

Я хотел бы список как: { "ABCD", "EFGH", "IJKL", "MNOP", "QRST", "UVWX"}

Здесь мой класс расширения:

public static class Extensions 
{ 
    public static List<string> SplitEvery(this string s, int n) 
    { 
     List<string> list = new List<string>(); 

     var Attempt1 = s.Select((c, i) => i % n== 0 ? s.Substring(i, n) : "|").Where(x => x != "|").ToList(); 

     var Attempt2 = s.Where((c, i) => i % n== 0).Select((c, i) => s.Substring(i, n)).ToList(); 

     return list; 
    } 
} 

Попытка 1 вставляет фиктивную строку "|" каждый раз, когда условие не выполняется, затем удаляет все экземпляры фиктивной строки для создания окончательного списка. Он работает, но создание плохих строк кажется ненужным дополнительным шагом. Кроме того, эта попытка не выполняется, если строка не делится на n.

Попытка 2 пыталась выбрать только подстроки, где индекс был делимым на N, но значение «i» в выражении Select не соответствует значению «i» в инструкции Where, поэтому я получаю результаты например: {«ABCD», «BCDE» и т. д.)

Я чувствую, что я близок к хорошему решению, но могу использовать полезный толчок в правильном направлении. Какие-либо предложения?

[Редактировать]

Я в конечном итоге происходит с комбинацией предложений, чтобы справиться с моей строковое разветвитель. Это может быть не самый быстрый, но, как новичок в LINQ, эта реализация была наиболее кратким и легким для меня.

public static List<string> SplitEvery(this string s, int size) 
{ 
    return s.Select((x, i) => i) 
     .Where(i => i % size == 0) 
     .Select(i => String.Concat(s.Skip(i).Take(size))).ToList(); 
} 

Спасибо за все замечательные предложения.

+0

Сторона примечания: было бы неплохо указать, каковы ваши «лучшие» критерии. То естьв этом случае это, по-видимому, «запрос, читаемый новичком пользователя LINQ, который соответствует описанию как можно ближе, предпочитает« перечислимые »методы по всем соображениям производительности». В этом Lite 'Concat' с' Take' действительно будет выглядеть как лучший подход. –

+0

Приношу свои извинения, это справедливая оценка. Меня в основном интересовал чистый, однострочный подход, похожий на мои первоначальные попытки выше. В моем случае читаемость была для меня более важной, чем масштабируемость. Надеюсь, никто не попытается выгрузить огромный текстовый файл в мою строку. :) – MadHenchbot

+1

(Мой комментарий выше - чистое предложение - ничего не извиниться за). Еще одна случайная нота, которую нужно отслеживать в LINQ - ваш окончательный подход повторяет последовательность несколько раз. Это нормально для строки, но не будет работать для «одноразовых» последовательностей, таких как результат SQL-запроса или «File.ReadAllLines». Существует несколько ответов (т. Е. С возвратом доходности), которые демонстрируют подходы, которые повторяют сборку один раз. –

ответ

8

Вот еще одно решение:

var result = s.Select((x, i) => i) 
       .Where(i => i % 4 == 0) 
       .Select(i => s.Substring(i, s.Length - i >= 4 ? 4 : s.Length - i)); 
+0

ДА. Это именно то, что я пытался выбраться оттуда. Для меня это был самый простой и понятный вариант, поскольку мои знания LINQ довольно ограничены. Спасибо! – MadHenchbot

24
string s = "ABCDEFGHIJKLMNOPQRSTUVWX"; 
var results = s.Select((c, i) => new { c, i }) 
      .GroupBy(x => x.i/4) 
      .Select(g => String.Join("",g.Select(y=>y.c))) 
      .ToList(); 

Вы также можете использовать morelinq's batch

var res = s.Batch(4).Select(x => String.Join("", x)).ToList(); 

Если вы не возражаете против использования побочных эффектов, это тоже возможно

var res2 = s.SplitEvery(4).ToList(); 

public static IEnumerable<string> SplitEvery(this string s, int n) 
{ 
    int index = 0; 
    return s.GroupBy(_=> index++/n).Select(g => new string(g.ToArray())); 
} 

И конечно каждая операция вопрос строка заслуживает Regex ответ :)

var res3 = Regex.Split(s, @"(?<=\G.{4})"); 
+2

+1 для регулярных выражений. ;-p – Macke

+0

Отлично! Спасибо за так много хороших примеров. :) – MadHenchbot

+0

Думайте, что решение regex должно быть в верхней части этого ответа, так как это быстрее (из моих тестов) и короче, чем другие решения здесь. – Kamarey

3

Это похоже на работу:

public static IEnumerable<string> SplitEvery(this string s, int n) { 
    var enumerators = Enumerable.Repeat(s.GetEnumerator(), n); 
    while (true) { 
     var chunk = string.Concat(enumerators 
      .Where(e => e.MoveNext()) 
      .Select(e => e.Current)); 
     if (chunk == "") yield break; 
     yield return chunk; 
    } 
} 
1

Вот несколько LINQy способов сделать это:

public static IEnumerable<string> SplitEvery(this IEnumerable<char> s , int n) 
{ 
    StringBuilder sb = new StringBuilder(n) ; 
    foreach (char c in s) 
    { 
    if (sb.Length == n) 
    { 
     yield return sb.ToString() ; 
     sb.Length = 0 ; 
    } 
    sb.Append(c) ; 
    } 
} 

или

public static IEnumerable<string> SplitEvery(this string s , int n) 
{ 
    int limit = s.Length - (s.Length % n) ; 
    int i = 0 ; 

    while (i < limit) 
    { 
    yield return s.Substring(i,n) ; 
    i+=n ; 
    } 

    if (i < s.Length) 
    { 
    yield return s.Substring(i) ; 
    } 

} 
+5

Любопытно, как они "LINQy"? –

+2

Чтобы быть LINQy, вы должны использовать LINQ. – recursive

+1

Это методы расширения LINQ. Возможно, вы захотите прочитать [как расширить LINQ] (http://msdn.microsoft.com/en-us/library/cc981895.aspx) –

4

Substring должно быть в порядке, чтобы выбрать 4-символьные части строки. Вам просто нужно быть осторожным с последней частью:

new Func<string, int, IEnumerable<string>>(
     (string s, int n) => 
      Enumerable.Range(0, (s.Length + n-1)/n) 
      .Select(i => s.Substring(i*n, Math.Min(n, s.Length - i*n)))) 
("ABCDEFGHIJKLMNOPQRSTUVWX", 4) 

Примечание: если этот ответ преобразуется в действие на общем перечислима ей придется перебирать коллекцию несколько раз (Count() и Substring преобразованы в Skip(i*n).Take(n)).

6
public static IEnumerable<string> SplitEvery(this string s, int length) 
{ 
    return s.Where((c, index) => index % length == 0) 
      .Select((c, index) => String.Concat(
       s.Skip(index * length).Take(length) 
      ) 
      ); 
} 

Жюри из того, будет ли new String(chars.ToArray()) быть быстрее или медленнее, чем это String.Concat(chars).

Вы можете, конечно, добавить .ToList(), чтобы вернуть список, а не IEnumerable.

+0

Я был обеспокоен окончательным .Take (length) выкинет ошибку индекса, но похоже, что все это обрабатывается внутри метода. Отличное решение! – MadHenchbot

+1

Да, есть немного обманчивости с точки зрения читаемости, которая показывает, что было уже поздно, когда я написал это ... То есть, результат вызова 'Where' (=' char' в каждом разделяющем индексе) никогда не используется напрямую - только там, чтобы ограничить количество результатов, возвращаемых следующим «Select». Единственное исключение «Take» должно когда-либо бросать, насколько я помню, если источник, на который вы ссылаетесь, является «null». В остальное время это делает The Sensible Thing. – JimmiTh

+0

... другими словами, 's.Where' может быть заменен на' Enumerable.Range (0, x) ', где' x' будет вычисленным числом разделенных индексов. См. @ Ответ Алексея Левенкова. Это будет более четко сообщать о намерениях. – JimmiTh

9

Вы можете использовать этот метод расширения, который реализован с получением простой подстроки (я считаю, что это быстрее, чем перечисляя над персонажами и объединить их в строки):

public static IEnumerable<string> SplitEvery(this string s, int length) 
{ 
    int index = 0; 
    while (index + length < s.Length) 
    { 
     yield return s.Substring(index, length); 
     index += length;     
    } 

    if (index < s.Length) 
     yield return s.Substring(index, s.Length - index); 
} 
1

Это также работает, но требует 'разворачивания' IGrouping<x,y>:

public static IEnumerable<String> Split(this String me,int SIZE) { 
    //Works by mapping the character index to a 'modulo Staircase' 
    //and then grouping by that 'stair step' value 
    return me.Select((c, i) => new { 
    step = i - i % SIZE, 
    letter = c.ToString() 
    }) 
    .GroupBy(kvp => kvp.step) 
    .Select(grouping => grouping 
    .Select(g => g.letter) 
    .Aggregate((a, b) => a + b) 
); 
} 

EDIT: Использование ленивые механизмы оценки Linq (в yield return) вы также можете достичь этого, используя рекурсию

public static IEnumerable<String> Split(this String me, int SIZE) {  
    if (me.Length > SIZE) { 
    var head = me.Substring(0,SIZE); 
    var tail = me.Substring(SIZE,me.Length-SIZE); 
    yield return head;   
    foreach (var item in tail.Split(SIZE)) { 
     yield return item; 
    } 
    } else { 
    yield return me; 
    } 
} 

Хотя, лично я держаться подальше от Substring, потому что она поощряет государственную FUL код (счетчики, индексы и т.д. в родительский или глобальный охват).

+0

Читая ответы, этот метод почти идентичен первому ответу от @ I4V, за исключением того, что он не имеет ни пол-целое-деление, ни объединение пустых строк. – theoski

Смежные вопросы