Search code examples
c#listwritetofile

How to write a list to a text file. Write 50 items per line


I am working on a program where I read data from a file, store that data in a list and write that data to a new file in a special format. I have created the method to read the original file and store it in a list.

The file I read from is a list of numbers. One number per line.

I'm having issue with my method that writes to the new file. I'm having an issue with taking 50 items and writing them to a line and then taking the next 50 items and writing on the next line. The method is taking the first 50 items and writing them and repeating those 50 items on each line. I know this is because of my second for loop. Just not sure how to fix. any help would be appreciated. Below is my code:

public static void WriteFormattedTextToNewFile(List<string> groupedStrings)
{
    string file = @"C:\Users\e011691\Desktop\New folder\formatted.txt";
    StreamWriter sw = new StreamWriter(file, true);

    for (int i = 0; i < ReadFile.GroupedStrings.Count; i++)
    {
        sw.Write($"{DateTime.Now:yyyy MM dd  hh:mm:ss}\t\t");

        for (int j = 0; j < 50; j++)
        {
            sw.Write($"{ReadFile.GroupedStrings[j]}\t");
        }
        sw.WriteLine();
    }
    sw.Close();
}

Solution

  • I'll give you three options (and a bonus).

    First option. Use a custom Chunk(int) linq operator using iterator blocks. The trick is the inner method uses the same enumerator as the outer. Seems like a lot of code, but once you have the Chunk() method, you can use it anywhere. Also note this option doesn't even need a List<string>. It will work with any IEnumerable<T>, since we never reference any elements by index.

    public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> values, int chunkSize)
    {
        var e = values.GetEnumerator();
        while (e.MoveNext())
        {
            yield return innerChunk(e, chunkSize);
        }
    }
    
    private static IEnumerable<T> innerChunk<T>(IEnumerator<T> e, int chunkSize)
    {
        //If we're here, MoveNext() was already called once to advance between batches
        // Need to yield the value from that call.
        yield return e.Current;
    
        //start at 1, not 0, for the first yield above  
        int i = 1; 
        while(i++ < chunkSize && e.MoveNext()) //order of these conditions matters
        {
            yield return e.Current;
        }
    }
        
    public static void WriteFormattedTextToNewFile(IEnumerable<string> groupedStrings)
    {
        string file = @"C:\Users\e011691\Desktop\New folder\formatted.txt";
        using (var sw = new StreamWriter(file, true))
        {   
            foreach(var strings in groupedStrings.Chunk(50))
            {
                sw.Write($"{DateTime.Now:yyyy MM dd  hh:mm:ss}\t\t");
                foreach(var item in strings)
                {
                   sw.Write($"{item}\t");
                }
                sw.WriteLine();
            } 
        }
    }
    

    Here's a basic proof of concept Chunk() actually works.

    As a bonus option, here is another way to use the Chunk() method from the first option. Note how small and straight-forward the actual method becomes, but the construction of the long full-line strings likely makes this less efficient.

    public static void WriteFormattedTextToNewFile(IEnumerable<string> groupedStrings)
    {
        string file = @"C:\Users\e011691\Desktop\New folder\formatted.txt";
        using (var sw = new StreamWriter(file, true))
        {   
            foreach(var strings in groupedStrings.Chunk(50))
            {
                sw.Write($"{DateTime.Now:yyyy MM dd  hh:mm:ss}\t\t");
                sw.WriteLine(string.Join("\t", strings));
            } 
        }
    }
    

    Second option. Keep track using a separate integer/loop. Note the extra condition on the inner loop, still using the i value rather than j to reference the current position, and incrementing i in the inner loop. This is called a Control/Break loop. Note how we are able to write the end line and initial date value on each line such that they also appear in the correct order in the code: first the header, then the items, then the footer, and we do it without complicated conditional checks.

    public static void WriteFormattedTextToNewFile(List<string> groupedStrings)
    {
        string file = @"C:\Users\e011691\Desktop\New folder\formatted.txt";
        using (var sw = new StreamWriter(file, true))
        {   
            int i = 0;
            while(i < groupedStrings.Count)
            {
               sw.Write($"{DateTime.Now:yyyy MM dd  hh:mm:ss}\t\t");
               for(int j = 0; j < 50 && i < groupedStrings.Count; j++)
               {
                  sw.Write($"{groupedStrings[i]}\t");
                  i++;
               }
               sw.WriteLine();
            }
        }
    }
    

    Third option. Keep track using the modulus operator (%). This option (or a similar option using a second j value in the same loop) is where many would turn first, but beware; this option is deceptively difficult to get right, especially as the problem gets more complicated.

    public static void WriteFormattedTextToNewFile(List<string> groupedStrings)
    {
        string file = @"C:\Users\e011691\Desktop\New folder\formatted.txt";
        using (var sw = new StreamWriter(file, true))
        {
            for(int i = 0; i < groupedStrings.Count;i++)
            {
                if (i % 50 == 0)
                {
                    if (i > 0) sw.WriteLine();
                    sw.Write($"{DateTime.Now:yyyy MM dd  hh:mm:ss}\t\t");
                }
    
                sw.Write($"{groupedStrings[i]}\t");
            }
            sw.WriteLine();
        }
    }
    

    Update:

    A variant of Chunk() is now included out of the box for .Net 6.