Группируйте строки во входном файле с помощью LINQ или другими средствами в C # - PullRequest
4 голосов
/ 11 августа 2011

Мне нужен совет о том, как я могу изменить реализацию некоторого кода. У меня есть входные файлы, которые содержат данные, которые читаются в классе:

This is line 1
This is line 2 
This is line 3

У меня есть код, который записывает эти данные в HTML

@foreach (var item in Model.NoteDetails)
{
    <li>@item</li>
} 

Теперь меня просят принять другой тип входного файла:

This is line 1
Data Line - two
Data Line - two two
Another line

Что мне нужно сделать, так это проверить код на наличие строк, начинающихся с одного и того же текста, за которым следует дефис. Тогда код должен сгруппироваться по этому. Таким образом, результат, полученный в результате этого, должен быть:

This is line 1

Data Line
- two
- two two

Another line

Я не уверен, возможно ли это с помощью LINQ. Может быть, лучше, если я использую массив или что-то. Проблема в том, что мне нужно иметь возможность смотреть в будущее, а затем, когда я обнаруживаю, что строки нет в группе, я распечатываю строки и создаю или не начинаю другую группу. Я думаю, что это больше, чем это возможно с LINQ. Я знаю, что есть много экспертов LINQ и буду признателен за любые советы.

Ответы [ 3 ]

2 голосов
/ 11 августа 2011

Используя метод расширения ChunkBy на MSDN (любой мой собственный простой метод расширения ToEnumerable), вы можете сделать это с помощью LINQ. Конечный продукт выглядит красиво, но есть много волшебных методов расширения, помогающих:

void Main()
{
    var data=
@"This is line 1
Data Line - two
Data Line - two two
Another line";
    var lines = data.Split(new[] {"\r\n", "\n"}, StringSplitOptions.None);
    var sep = " - ";
    var linesAndKeys=
        lines
            .Select(line => new {
                line, 
                parts = line.Split(new[] {sep}, StringSplitOptions.None)})
            .Select(x=>new {
                line = x.parts.Length>1
                    ? string.Join(sep, x.parts.Skip(1))
                    : x.line,
                key = x.parts.Length>1
                    ? x.parts[0]
                    : String.Empty
            });
    var transformedLines=
        linesAndKeys
            .ChunkBy(i => i.key)
            .Select(c =>
                c.Key == String.Empty
                    ? c.Select(s => s.line)
                    : c.Key.ToEnumerable().Concat(c.Select(s=>" - "+s.line)))
            .Interleave(() => Environment.NewLine.ToEnumerable())
            .SelectMany(x => x);

    var newString = string.Join(Environment.NewLine, transformedLines);

    Console.WriteLine(newString);

}

public static class MyExtensions
{
public static IEnumerable<T> 
    Interleave<T>(this IEnumerable<T> src, Func<T> separatorFactory)
{
    var srcArr = src.ToArray();
    for (int i = 0; i < srcArr.Length; i++)
    {
        yield return srcArr[i];
        if(i<srcArr.Length-1)
        {
            yield return separatorFactory();
        }
    }
}
    public static IEnumerable<T> ToEnumerable<T>(this T item)
    {
        yield return item;
    }
    public static IEnumerable<IGrouping<TKey, TSource>> ChunkBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
    {
        return source.ChunkBy(keySelector, EqualityComparer<TKey>.Default);
    }

    public static IEnumerable<IGrouping<TKey, TSource>> ChunkBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
    {
        // Flag to signal end of source sequence.
        const bool noMoreSourceElements = true;

        // Auto-generated iterator for the source array.       
        var enumerator = source.GetEnumerator();

        // Move to the first element in the source sequence.
        if (!enumerator.MoveNext()) yield break;

        // Iterate through source sequence and create a copy of each Chunk.
        // On each pass, the iterator advances to the first element of the next "Chunk"
        // in the source sequence. This loop corresponds to the outer foreach loop that
        // executes the query.
        Chunk<TKey, TSource> current = null;
        while (true)
        {
            // Get the key for the current Chunk. The source iterator will churn through
            // the source sequence until it finds an element with a key that doesn't match.
            var key = keySelector(enumerator.Current);

            // Make a new Chunk (group) object that initially has one GroupItem, which is a copy of the current source element.
            current = new Chunk<TKey, TSource>(key, enumerator, value => comparer.Equals(key, keySelector(value)));

            // Return the Chunk. A Chunk is an IGrouping<TKey,TSource>, which is the return value of the ChunkBy method.
            // At this point the Chunk only has the first element in its source sequence. The remaining elements will be
            // returned only when the client code foreach's over this chunk. See Chunk.GetEnumerator for more info.
            yield return current;

            // Check to see whether (a) the chunk has made a copy of all its source elements or 
            // (b) the iterator has reached the end of the source sequence. If the caller uses an inner
            // foreach loop to iterate the chunk items, and that loop ran to completion,
            // then the Chunk.GetEnumerator method will already have made
            // copies of all chunk items before we get here. If the Chunk.GetEnumerator loop did not
            // enumerate all elements in the chunk, we need to do it here to avoid corrupting the iterator
            // for clients that may be calling us on a separate thread.
            if (current.CopyAllChunkElements() == noMoreSourceElements)
            {
                yield break;
            }
        }
    }

    // A Chunk is a contiguous group of one or more source elements that have the same key. A Chunk 
    // has a key and a list of ChunkItem objects, which are copies of the elements in the source sequence.
    class Chunk<TKey, TSource> : IGrouping<TKey, TSource>
    {
        // INVARIANT: DoneCopyingChunk == true || 
        //   (predicate != null && predicate(enumerator.Current) && current.Value == enumerator.Current)

        // A Chunk has a linked list of ChunkItems, which represent the elements in the current chunk. Each ChunkItem
        // has a reference to the next ChunkItem in the list.
        class ChunkItem
        {
            public ChunkItem(TSource value)
            {
                Value = value;
            }
            public readonly TSource Value;
            public ChunkItem Next = null;
        }
        // The value that is used to determine matching elements
        private readonly TKey key;

        // Stores a reference to the enumerator for the source sequence
        private IEnumerator<TSource> enumerator;

        // A reference to the predicate that is used to compare keys.
        private Func<TSource, bool> predicate;

        // Stores the contents of the first source element that
        // belongs with this chunk.
        private readonly ChunkItem head;

        // End of the list. It is repositioned each time a new
        // ChunkItem is added.
        private ChunkItem tail;

        // Flag to indicate the source iterator has reached the end of the source sequence.
        internal bool isLastSourceElement = false;

        // Private object for thread syncronization
        private object m_Lock;

        // REQUIRES: enumerator != null && predicate != null
        public Chunk(TKey key, IEnumerator<TSource> enumerator, Func<TSource, bool> predicate)
        {
            this.key = key;
            this.enumerator = enumerator;
            this.predicate = predicate;

            // A Chunk always contains at least one element.
            head = new ChunkItem(enumerator.Current);

            // The end and beginning are the same until the list contains > 1 elements.
            tail = head;

            m_Lock = new object();
        }

        // Indicates that all chunk elements have been copied to the list of ChunkItems, 
        // and the source enumerator is either at the end, or else on an element with a new key.
        // the tail of the linked list is set to null in the CopyNextChunkElement method if the
        // key of the next element does not match the current chunk's key, or there are no more elements in the source.
        private bool DoneCopyingChunk { get { return tail == null; } }

        // Adds one ChunkItem to the current group
        // REQUIRES: !DoneCopyingChunk && lock(this)
        private void CopyNextChunkElement()
        {
            // Try to advance the iterator on the source sequence.
            // If MoveNext returns false we are at the end, and isLastSourceElement is set to true
            isLastSourceElement = !enumerator.MoveNext();

            // If we are (a) at the end of the source, or (b) at the end of the current chunk
            // then null out the enumerator and predicate for reuse with the next chunk.
            if (isLastSourceElement || !predicate(enumerator.Current))
            {
                enumerator = null;
                predicate = null;
            }
            else
            {
                tail.Next = new ChunkItem(enumerator.Current);
            }

            // tail will be null if we are at the end of the chunk elements
            // This check is made in DoneCopyingChunk.
            tail = tail.Next;
        }

        // Called after the end of the last chunk was reached. It first checks whether
        // there are more elements in the source sequence. If there are, it 
        // Returns true if enumerator for this chunk was exhausted.
        internal bool CopyAllChunkElements()
        {
            while (true)
            {
                lock (m_Lock)
                {
                    if (DoneCopyingChunk)
                    {
                        // If isLastSourceElement is false,
                        // it signals to the outer iterator
                        // to continue iterating.
                        return isLastSourceElement;
                    }
                    else
                    {
                        CopyNextChunkElement();
                    }
                }
            }
        }

        public TKey Key { get { return key; } }

        // Invoked by the inner foreach loop. This method stays just one step ahead
        // of the client requests. It adds the next element of the chunk only after
        // the clients requests the last element in the list so far.
        public IEnumerator<TSource> GetEnumerator()
        {
            //Specify the initial element to enumerate.
            ChunkItem current = head;

            // There should always be at least one ChunkItem in a Chunk.
            while (current != null)
            {
                // Yield the current item in the list.
                yield return current.Value;

                // Copy the next item from the source sequence, 
                // if we are at the end of our local list.
                lock (m_Lock)
                {
                    if (current == tail)
                    {
                        CopyNextChunkElement();
                    }
                }

                // Move to the next ChunkItem in the list.
                current = current.Next;
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}

EDIT

Я добавил другой метод расширения (Interleave). Это используется для чередования новых строк, где это необходимо. Вывод теперь полностью соответствует вашим требованиям.

Как это работает:

Сначала нам нужны ключи. Если есть тире, используйте то, что находится перед первым тире, в противном случае используйте string.Empty.

Это дает нам:

line           | key 
--------------------------
This is line 1 |
two            | Data Line   
two two        | Data Line   
Another line   |

Тогда, когда мы ChunkBy, у нас есть 3 группы (строка 1), (строка 2, строка 3) и (строка 4). У каждой группы также есть Ключ.

Теперь мы можем использовать эту информацию для повторной сборки данных в требуемом формате.

1 голос
/ 11 августа 2011

Должно ли это быть LINQ?Простая итеративная функция может сделать работу.(Извините, Ex в Java, у меня нет VS передо мной)

    //im using string here, but easy enough to convert to file reader
    String[] l = str.split("\r\n");
    String curr = l[0]; //current line 
    for (int i = 1; i < l.length; i++) {
        //next line
        String peek = l[i];     

        String[] lcurr = curr.split("-");
        String[] lpeek = peek.split("-");
        int c = 0; //count of grouped lines
        if (lcurr.length == 2 && lpeek.length == 2 && lcurr[0].equals(lpeek[0])) {
            //print group header prepended with blank line
            System.out.println("\r\n" + lcurr[0]);
            //print first grouped item
            System.out.println("- " + lcurr[1]);
            c++;

            //advance position
            lcurr = lpeek;
            //as long as there is one ahead to peek
            if (i < l.length - 1) {
                lpeek = l[++i].split("-"); 
                //continue printing remaining groups
                while (i < l.length && lcurr.length == 2 && lpeek.length == 2 && lcurr[0].equals(lpeek[0])) {
                    System.out.println("- " + lcurr[1]);
                    lcurr = lpeek;
                    lpeek = l[++i].split("-");
                    c++;
                }   
            }
        }

        //print last grouped item
        if (c > 0) {
            System.out.println("- " + lcurr[1] + "\r\n");
        }
        else
            System.out.println(curr);

        curr = l[i];
    }
1 голос
/ 11 августа 2011

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

// let IEnumerable<string> lines be
// a list of lines in the input.
string s = lines
    .Aggregate(
    // seed accumulator with anonymous class 
    // including StringBuilder and the last
    // header group
    new {
        builder = new StringBuilder(),
        lastheader = new string[1] 
    },
    // accumulator function for each item
    (acc, line) =>
    {
        var parts = line.Split(new char[] {'-'}, 2);
        string headerpart = parts[0];
        string itempart = parts.Length == 1 ? null : line.Substring(line.IndexOf('-'));
        bool firstline = acc.builder.Length == 0;
        // Case 1: The line contains no hyphen
        if (parts.Length == 1)
        {
            if (!firstline) acc.builder.AppendLine();
            acc.builder.AppendLine(line);
            acc.lastheader[0] = null;
        }
        // Case 2: The line contains a hyphen, and
        // the header is the same as the header on the previous
        // line.  Only output the item.
        else if (acc.lastheader[0] == parts[0])
        {
             acc.builder.AppendLine(itempart);
        }
        // Case 3: The line contains a hyphen, and
        // the header is not the same as the header on the previous
        // line.  Output the header on one line, and then the item
        // on the subsequent line.
        else
        {
            acc.lastheader[0] = headerpart;
            if (!firstline) acc.builder.AppendLine();
            acc.builder.AppendLine(headerpart);
            acc.builder.AppendLine(itempart);
        }
        // Finally, return the mutated accumulator.
        return acc;
    },
    // When finished, convert the builder to a string
    // and return the complete string.
    acc => acc.builder.ToString());
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...