Skip to content

Commit d83c1b8

Browse files
authored
Merge pull request #629 from rootvector2/quote-null-starting-record
Quote null value that starts a record in minimal quote mode
2 parents c6b4333 + 4026d3a commit d83c1b8

3 files changed

Lines changed: 57 additions & 4 deletions

File tree

src/main/java/org/apache/commons/csv/CSVFormat.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2134,6 +2134,15 @@ public boolean isEscapeCharacterSet() {
21342134
return escapeCharacter != null;
21352135
}
21362136

2137+
/**
2138+
* Tests whether the quote policy encapsulates values only when needed, which is also the policy when none is set.
2139+
*
2140+
* @return {@code true} if the {@link QuoteMode} is {@link QuoteMode#MINIMAL} or unset.
2141+
*/
2142+
private boolean isMinimalQuoteMode() {
2143+
return quoteMode == null || quoteMode == QuoteMode.MINIMAL;
2144+
}
2145+
21372146
/**
21382147
* Tests whether a null string has been defined.
21392148
*
@@ -2272,7 +2281,17 @@ private void print(final Object object, final CharSequence value, final Appendab
22722281
out.append(getDelimiterString());
22732282
}
22742283
if (object == null) {
2275-
out.append(value);
2284+
if (len == 0 && newRecord && isQuoteCharacterSet() && isMinimalQuoteMode()) {
2285+
// Encapsulate like printWithQuotes does for an empty value that starts a record: an
2286+
// unquoted one makes the whole line empty, and a parser with ignoreEmptyLines enabled
2287+
// then drops the record. ALL, ALL_NON_NULL, and NON_NUMERIC are excluded because they
2288+
// encode null as the bare empty field, distinct from a quoted empty string (CSV-203).
2289+
final char quoteChar = quoteCharacter.charValue(); // Explicit unboxing is intentional
2290+
out.append(quoteChar);
2291+
out.append(quoteChar);
2292+
} else {
2293+
out.append(value);
2294+
}
22762295
} else if (isQuoteCharacterSet()) {
22772296
// The original object is needed so can check for Number
22782297
printWithQuotes(object, value, out, newRecord);

src/test/java/org/apache/commons/csv/CSVFormatTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1557,7 +1557,7 @@ void testWithHeaderComments() {
15571557
assertNotEquals(csvFormat, csvFormatTwo); // CSV-244 - should not be equal
15581558

15591559
assertNotEquals(csvFormatTwo, csvFormat); // CSV-244 - should not be equal
1560-
assertEquals(",,,,,,,", string);
1560+
assertEquals("\"\",,,,,,,", string);
15611561

15621562
}
15631563

src/test/java/org/apache/commons/csv/CSVPrinterTest.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ private Connection getH2Connection() throws SQLException, ClassNotFoundException
188188
return DriverManager.getConnection("jdbc:h2:mem:my_test;", "sa", "");
189189
}
190190

191+
private String printNullRecord(final CSVFormat format) throws IOException {
192+
final StringWriter sw = new StringWriter();
193+
try (CSVPrinter printer = new CSVPrinter(sw, format)) {
194+
printer.printRecord((Object) null);
195+
}
196+
return sw.toString();
197+
}
198+
191199
private CSVPrinter printWithHeaderComments(final StringWriter sw, final Date now, final CSVFormat baseFormat) throws IOException {
192200
// Use withHeaderComments first to test CSV-145
193201
// @formatter:off
@@ -1721,6 +1729,31 @@ void testPrinter7() throws IOException {
17211729
}
17221730
}
17231731

1732+
@Test
1733+
void testPrintNullValueStartingRecord() throws IOException {
1734+
final StringWriter sw = new StringWriter();
1735+
try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) {
1736+
printer.printRecord("a");
1737+
printer.printRecord((Object) null);
1738+
printer.printRecord("b");
1739+
}
1740+
final String csv = sw.toString();
1741+
assertEquals("a" + RECORD_SEPARATOR + "\"\"" + RECORD_SEPARATOR + "b" + RECORD_SEPARATOR, csv);
1742+
try (CSVParser parser = CSVParser.parse(csv, CSVFormat.DEFAULT)) {
1743+
final List<CSVRecord> records = parser.getRecords();
1744+
assertEquals(3, records.size());
1745+
assertArrayEquals(new String[] { "" }, records.get(1).values());
1746+
}
1747+
// An explicit MINIMAL quote mode encapsulates it too.
1748+
assertEquals("\"\"" + RECORD_SEPARATOR, printNullRecord(CSVFormat.DEFAULT.builder().setQuoteMode(QuoteMode.MINIMAL).get()));
1749+
// ALL encodes null as the bare empty field, distinct from a quoted empty string (CSV-203).
1750+
assertEquals(RECORD_SEPARATOR, printNullRecord(CSVFormat.DEFAULT.builder().setQuoteMode(QuoteMode.ALL).get()));
1751+
// ALL_NON_NULL encodes null as the bare empty field, so it must stay unquoted.
1752+
assertEquals(RECORD_SEPARATOR, printNullRecord(CSVFormat.DEFAULT.builder().setQuoteMode(QuoteMode.ALL_NON_NULL).get()));
1753+
// Without a quote character there is nothing to encapsulate with.
1754+
assertEquals(RECORD_SEPARATOR, printNullRecord(CSVFormat.DEFAULT.builder().setQuote(null).get()));
1755+
}
1756+
17241757
@Test
17251758
void testPrintNullValues() throws IOException {
17261759
final StringWriter sw = new StringWriter();
@@ -1854,8 +1887,9 @@ void testPrintRecordsWithObjectArray() throws IOException {
18541887
printer.printRecords(objectArray);
18551888
assertEquals(objectArray.length, printer.getRecordCount());
18561889
}
1857-
assertEquals(6, charArrayWriter.size());
1858-
assertEquals("\n\n\n\n\n\n", charArrayWriter.toString());
1890+
final String expected = "\"\"\n\"\"\n\"\"\n\n\"\"\n\"\"\n";
1891+
assertEquals(expected.length(), charArrayWriter.size());
1892+
assertEquals(expected, charArrayWriter.toString());
18591893
}
18601894

18611895
@Test

0 commit comments

Comments
 (0)