0

I need to create a single string named CSVOutput, and load all keys and values from the ConcurrentHashMap<String, Integer>, counts into this file separated by commas. So I'll have two columns in the resulting csv file, one is for the keys and the other one for the values.

I have written this code for my method OutputCountsAsCSV:

private void OutputCountsAsCSV(ConcurrentHashMap<String, Integer> counts, String filename) {
    String CSVOutput = new String("");
    for (Entry<String, Integer> entry : counts.entrySet()) {
        String rowText = String.format("%s,%d\n", entry.getKey(), entry.getValue());  # defines new lines/rows
        CSVOutput += rowText;
        try (FileWriter writer = new FileWriter(filename)) {
            writer.write(CSVOutput);
        }
        catch (Exception e) {
            System.out.println("Saving CSV to file failed...");
        }
    }
    System.out.println("CSV File saved successfully...");
}

However, I'm told I should have followed this order:

  1. create and prepare file
  2. for each entry, write data to file
  3. close the file (and print that it happened successfully).

Indeed, my System.out.println("CSV File saved successfully...") seems to be inside the loop and is being printed many times.

How can I do it correctly?

I am a beginner in Java, so I'm very grateful for any help.

Abra
  • 19,142
  • 7
  • 29
  • 41
Bluetail
  • 1,093
  • 2
  • 13
  • 27
  • 1
    remove `{` before the `try` block. An also one of `}` before `System.out.println()` – Jens Aug 16 '22 at 13:57
  • yes, I have. the result is still the same. – Bluetail Aug 16 '22 at 13:59
  • Note: if any of the strings contain a comma or a newline, the CSV will be invalid. Also if they contain double quotes. This is why it's best to use a CSV library and not just roll your own. – David Conrad Aug 16 '22 at 14:00
  • so how should I be doing it? – Bluetail Aug 16 '22 at 14:05
  • 2
    Using a library, but for now I assume you are just learning. It seems you already know what to do: open the file first (so the try block should be the outermost one), then produce the output in the for loop. Don't concatenate it, just write each line to the FileWriter. – David Conrad Aug 16 '22 at 14:07
  • thanks. how do I find this library if I wanted to use it? is Apache Commons CSV one of them? – Bluetail Aug 16 '22 at 14:15
  • Yes. Commons csv is a standard one. ```String rowText = String.format("%s,%d%n", entry.getKey(), entry.getValue());``` is what you want as line separators are platform-specific. You're hard-coding one for Unix – g00se Aug 16 '22 at 15:08
  • @g00se Actually, the [RFC 4180](https://datatracker.ietf.org/doc/html/rfc4180) standard for CSV requires CRLF as the line terminator. Not platform-specific. – Basil Bourque Aug 16 '22 at 15:44
  • @BasilBourque interesting. And if that's the case then the original code is of course even wronger. So if you're rolling your own, you'd be doing ```String rowText = String.format("%s,%d\r\n", entry.getKey(), entry.getValue());``` – g00se Aug 16 '22 at 15:52

2 Answers2

2

Here is a better approach (do your own research about the finally keywords)

private void OutputCountsAsCSV(ConcurrentHashMap<String, Integer> counts, String filename) {
            
    try (FileWriter writer = new FileWriter(filename)) {
        
        for (Entry<String, Integer> entry : counts.entrySet()) {
            
            String rowText = String.format("%s,%d\n", entry.getKey(), entry.getValue());  
            writer.append(rowText);
        }
    
    } catch (Exception e) {
        System.out.println("Saving CSV to file failed...");
    } finally {
        System.out.println("CSV File saved successfully...");      
    } 
}
qdoot
  • 132
  • 1
  • 10
1

The Answer by qdoot seems correct. I'll add a few points:

  • Use the more general Map as the type of your parameter. See another Question.
  • Use NIO.2 in modern Java for working with files. See Wikipedia.
  • The RFC 4180 standard specification for CSV, Common Format and MIME Type for Comma-Separated Values (CSV) Files, requires CRLF (Carriage Return, Linefeed) as the line terminator.
  • Note that a Map such as ConcurrentHashMap may iterate in any arbitrary order. If you care about order, use a NavigableMap such as TreeMap.

Core code:

try (
        final Writer writer = Files.newBufferedWriter( path , StandardCharsets.UTF_8 ) ;
)
{
    for ( Map.Entry < String, Integer > entry : nameToNumberMap.entrySet() )
    {
        String line = String.format( "%s,%d" , entry.getKey() , entry.getValue() );
        writer.write( line.concat( CRLF ) );
    }
}
catch ( IOException e )
{
    System.out.println( "ERROR - Failed when opening or writing to file. " + Instant.now() + " Message # da8fc3ac-1f6b-4722-bbf9-a86f2acbb77f." );
    throw new RuntimeException( e );
}
System.out.println( "INFO - Done writing CSV file. " + Instant.now() + " Message # fa26f37e-70b7-42e8-a8cf-bf72ad0b2da8." );

Full example app.

package work.basil.example.csv;

import java.io.IOException;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.Map;

public class App
{
    public static void main ( String[] args )
    {
        App app = new App();
        app.demo();
    }

    private void demo ( )
    {
        Map < String, Integer > nameToNumberMap =
                Map.of(
                        "Alice" , 1 ,
                        "Bob" , 2 ,
                        "Carol" , 3
                );
        Path path = Paths.get( "/Users/basil_dot_work" , "demo.csv" );
        System.out.println( path.toAbsolutePath() );
        this.writeCsv( nameToNumberMap , path );
    }

    private void writeCsv ( final Map < String, Integer > nameToNumberMap , final Path path )
    {
        // The RFC 4180 standard for CSV requires a CRLF (Carriage Return, Linefeed) as the line terminator. *Not* platform-specific, not Unix-style Linefeed.
        final String CRLF = Character.toString( 13 ) + Character.toString( 10 );

        // Open file.
        try (
                final Writer writer = Files.newBufferedWriter( path , StandardCharsets.UTF_8 ) ;
        )
        {
            for ( Map.Entry < String, Integer > entry : nameToNumberMap.entrySet() )
            {
                String line = String.format( "%s,%d" , entry.getKey() , entry.getValue() );
                writer.write( line.concat( CRLF ) );
            }
        }
        catch ( IOException e )
        {
            System.out.println( "ERROR - Failed when opening or writing to file. " + Instant.now() + " Message # da8fc3ac-1f6b-4722-bbf9-a86f2acbb77f." );
            throw new RuntimeException( e );
        }
        finally
        {
            System.out.println( "INFO - Done writing CSV file. " + Instant.now() + " Message # fa26f37e-70b7-42e8-a8cf-bf72ad0b2da8." );
        }
    }
}

When run.

/Users/some_user/demo.csv
INFO - Done writing CSV file. 2022-08-16T20:06:07.610196Z Message # fa26f37e-70b7-42e8-a8cf-bf72ad0b2da8.

screenshot of tabular data stared in CSV

Note to other readers: We used try-with-resources syntax to make sure the file is always closed gracefully, even when encountering exceptions. The code in the Question does the same. See The Java Tutorials by Oracle Corp.

This Question & Answer is for learning purposes. In real work, I would not generate CSV text. I would use one of the several good libraries available in the Java ecosystem for handling CSV text. Example: Apache Commons CSV.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • thank you for taking the time to write this more detailed explanation. the Map.of() method did not work for me unfortunately (so I tried using HashMap<> instead and then .put()). I'm using SEJava-1.8 in Eclipse. – Bluetail Aug 18 '22 at 12:03
  • @Bluetail `Map.of` methods arrived in Java 9. But, no matter. Those methods are just a convenience here. You could just as well use conventional maps like `HashMap`. – Basil Bourque Aug 18 '22 at 17:46