diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index 60bb6a5e7..d1e94ebe1 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -2514,11 +2514,12 @@ private void printWithQuotes(final Object object, final CharSequence charSeq, fi } } else { char c = charSeq.charAt(pos); - if (c <= Constants.COMMENT || isCommentMarkerSet() && c == commentMarker.charValue()) { + if (c <= Constants.COMMENT || Character.isWhitespace(c) || isCommentMarkerSet() && c == commentMarker.charValue()) { // Some other chars at the start of a value caused the parser to fail, so for now // encapsulate if we start in anything less than '#'. We are being conservative // by including the default comment char and any configured comment marker too, - // which the parser would otherwise read back as a comment line. + // which the parser would otherwise read back as a comment line. A leading Unicode + // whitespace above ' ' is stripped by ignoreSurroundingSpaces on read, so quote it too. quote = true; } else { while (pos < len) { @@ -2534,8 +2535,9 @@ private void printWithQuotes(final Object object, final CharSequence charSeq, fi pos = len - 1; c = charSeq.charAt(pos); // Some other chars at the end caused the parser to fail, so for now - // encapsulate if we end in anything less than ' ' - if (isTrimChar(c)) { + // encapsulate if we end in anything less than ' '. A trailing Unicode whitespace + // above ' ' is stripped by ignoreSurroundingSpaces on read, so quote it too. + if (isTrimChar(c) || Character.isWhitespace(c)) { quote = true; } else if (endsWithDelimiterPrefix(charSeq, delim, delimLength)) { // A trailing partial multi-character delimiter would merge with the following delimiter on read. diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index 70af43c1e..a34212924 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -1963,6 +1963,30 @@ void testQuoteValueEndingWithSupplementaryDelimiterPrefix() throws IOException { } } + @Test + void testQuoteValueWithLeadingOrTrailingUnicodeWhitespace() throws IOException { + // U+3000 IDEOGRAPHIC SPACE is Character.isWhitespace but greater than ' ', so ignoreSurroundingSpaces + // strips it on read. It must be quoted or the surrounding whitespace is lost on a round trip. + final String idSpace = " "; + final CSVFormat format = CSVFormat.DEFAULT.builder().setIgnoreSurroundingSpaces(true).get(); + final StringWriter sw = new StringWriter(); + try (CSVPrinter printer = new CSVPrinter(sw, format)) { + printer.printRecord(idSpace + "a" + idSpace, "b"); + // A value without surrounding whitespace stays unquoted. + printer.printRecord("ab", "c"); + } + final String string = sw.toString(); + assertEquals("\"" + idSpace + "a" + idSpace + "\",b" + RECORD_SEPARATOR + "ab,c" + RECORD_SEPARATOR, string); + try (CSVParser parser = CSVParser.parse(string, format)) { + final List records = parser.getRecords(); + assertEquals(2, records.size()); + assertEquals(idSpace + "a" + idSpace, records.get(0).get(0)); + assertEquals("b", records.get(0).get(1)); + assertEquals("ab", records.get(1).get(0)); + assertEquals("c", records.get(1).get(1)); + } + } + @Test void testRandomDefault() throws Exception { doRandom(CSVFormat.DEFAULT, ITERATIONS_FOR_RANDOM_TEST);