Skip to content

Commit 90a2f8c

Browse files
authored
Fix #198: add CsvGenerator.Feature.ALWAYS_QUOTE_NUMBERS (#440)
1 parent f8bd56b commit 90a2f8c

File tree

5 files changed

+144
-22
lines changed

5 files changed

+144
-22
lines changed

csv/src/main/java/com/fasterxml/jackson/dataformat/csv/CsvGenerator.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ public enum Feature
7878
*/
7979
ALWAYS_QUOTE_EMPTY_STRINGS(false),
8080

81+
/**
82+
* Feature that determines whether values written as Nymbers (from {@code java.lang.Number}
83+
* valued POJO properties) should be forced to be quoted, regardless of whether they
84+
* actually need this.
85+
*
86+
* @since 2.16
87+
*/
88+
ALWAYS_QUOTE_NUMBERS(false),
89+
8190
/**
8291
* Feature that determines whether quote characters within quoted String values are escaped
8392
* using configured escape character, instead of being "doubled up" (that is: a quote character

csv/src/main/java/com/fasterxml/jackson/dataformat/csv/impl/CsvEncoder.java

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ public class CsvEncoder
113113

114114
protected boolean _cfgAlwaysQuoteEmptyStrings;
115115

116+
// @since 2.16
117+
protected boolean _cfgAlwaysQuoteNumbers;
118+
116119
protected boolean _cfgEscapeQuoteCharWithEscapeChar;
117120

118121
/**
@@ -218,6 +221,7 @@ public CsvEncoder(IOContext ctxt, int csvFeatures, Writer out, CsvSchema schema,
218221
_cfgIncludeMissingTail = !CsvGenerator.Feature.OMIT_MISSING_TAIL_COLUMNS.enabledIn(_csvFeatures);
219222
_cfgAlwaysQuoteStrings = CsvGenerator.Feature.ALWAYS_QUOTE_STRINGS.enabledIn(csvFeatures);
220223
_cfgAlwaysQuoteEmptyStrings = CsvGenerator.Feature.ALWAYS_QUOTE_EMPTY_STRINGS.enabledIn(csvFeatures);
224+
_cfgAlwaysQuoteNumbers = CsvGenerator.Feature.ALWAYS_QUOTE_NUMBERS.enabledIn(csvFeatures);
221225
_cfgEscapeQuoteCharWithEscapeChar = CsvGenerator.Feature.ESCAPE_QUOTE_CHAR_WITH_ESCAPE_CHAR.enabledIn(csvFeatures);
222226
_cfgEscapeControlCharWithEscapeChar = Feature.ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR.enabledIn(csvFeatures);
223227

@@ -257,6 +261,8 @@ public CsvEncoder(CsvEncoder base, CsvSchema newSchema)
257261
_cfgIncludeMissingTail = base._cfgIncludeMissingTail;
258262
_cfgAlwaysQuoteStrings = base._cfgAlwaysQuoteStrings;
259263
_cfgAlwaysQuoteEmptyStrings = base._cfgAlwaysQuoteEmptyStrings;
264+
_cfgAlwaysQuoteNumbers = base._cfgAlwaysQuoteNumbers;
265+
260266
_cfgEscapeQuoteCharWithEscapeChar = base._cfgEscapeQuoteCharWithEscapeChar;
261267
_cfgEscapeControlCharWithEscapeChar = base._cfgEscapeControlCharWithEscapeChar;
262268

@@ -329,6 +335,7 @@ public CsvEncoder overrideFormatFeatures(int feat) {
329335
_cfgIncludeMissingTail = !CsvGenerator.Feature.OMIT_MISSING_TAIL_COLUMNS.enabledIn(feat);
330336
_cfgAlwaysQuoteStrings = CsvGenerator.Feature.ALWAYS_QUOTE_STRINGS.enabledIn(feat);
331337
_cfgAlwaysQuoteEmptyStrings = CsvGenerator.Feature.ALWAYS_QUOTE_EMPTY_STRINGS.enabledIn(feat);
338+
_cfgAlwaysQuoteNumbers = CsvGenerator.Feature.ALWAYS_QUOTE_NUMBERS.enabledIn(feat);
332339
_cfgEscapeQuoteCharWithEscapeChar = CsvGenerator.Feature.ESCAPE_QUOTE_CHAR_WITH_ESCAPE_CHAR.enabledIn(feat);
333340
_cfgEscapeControlCharWithEscapeChar = Feature.ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR.enabledIn(feat);
334341
}
@@ -407,14 +414,20 @@ public final void write(int columnIndex, int value) throws IOException
407414
// easy case: all in order
408415
if (columnIndex == _nextColumnToWrite) {
409416
// inlined 'appendValue(int)'
410-
// up to 10 digits and possible minus sign, leading comma
411-
if ((_outputTail + 12) > _outputEnd) {
417+
// up to 10 digits and possible minus sign, leading comma, possible quotes
418+
if ((_outputTail + 14) > _outputEnd) {
412419
_flushBuffer();
413420
}
414421
if (_nextColumnToWrite > 0) {
415422
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
416423
}
424+
if (_cfgAlwaysQuoteNumbers) {
425+
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
426+
}
417427
_outputTail = NumberOutput.outputInt(value, _outputBuffer, _outputTail);
428+
if (_cfgAlwaysQuoteNumbers) {
429+
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
430+
}
418431
++_nextColumnToWrite;
419432
return;
420433
}
@@ -426,14 +439,20 @@ public final void write(int columnIndex, long value) throws IOException
426439
// easy case: all in order
427440
if (columnIndex == _nextColumnToWrite) {
428441
// inlined 'appendValue(int)'
429-
// up to 20 digits, minus sign, leading comma
430-
if ((_outputTail + 22) > _outputEnd) {
442+
// up to 20 digits, minus sign, leading comma, possible quotes
443+
if ((_outputTail + 24) > _outputEnd) {
431444
_flushBuffer();
432445
}
433446
if (_nextColumnToWrite > 0) {
434447
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
435448
}
449+
if (_cfgAlwaysQuoteNumbers) {
450+
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
451+
}
436452
_outputTail = NumberOutput.outputLong(value, _outputBuffer, _outputTail);
453+
if (_cfgAlwaysQuoteNumbers) {
454+
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
455+
}
437456
++_nextColumnToWrite;
438457
return;
439458
}
@@ -607,28 +626,40 @@ protected void appendRawValue(String value) throws IOException
607626

608627
protected void appendValue(int value) throws IOException
609628
{
610-
// up to 10 digits and possible minus sign, leading comma
611-
if ((_outputTail + 12) > _outputEnd) {
629+
// up to 10 digits and possible minus sign, leading comma, possible quotes
630+
if ((_outputTail + 14) > _outputEnd) {
612631
_flushBuffer();
613632
}
614633
if (_nextColumnToWrite > 0) {
615634
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
616635
}
636+
if (_cfgAlwaysQuoteNumbers) {
637+
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
638+
}
617639
_outputTail = NumberOutput.outputInt(value, _outputBuffer, _outputTail);
640+
if (_cfgAlwaysQuoteNumbers) {
641+
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
642+
}
618643
}
619644

620645
protected void appendValue(long value) throws IOException
621646
{
622-
// up to 20 digits, minus sign, leading comma
623-
if ((_outputTail + 22) > _outputEnd) {
647+
// up to 20 digits, minus sign, leading comma, possible quotes
648+
if ((_outputTail + 24) > _outputEnd) {
624649
_flushBuffer();
625650
}
626651
if (_nextColumnToWrite > 0) {
627652
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
628653
}
654+
if (_cfgAlwaysQuoteNumbers) {
655+
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
656+
}
629657
_outputTail = NumberOutput.outputLong(value, _outputBuffer, _outputTail);
658+
if (_cfgAlwaysQuoteNumbers) {
659+
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
660+
}
630661
}
631-
662+
632663
protected void appendValue(float value) throws IOException
633664
{
634665
String str = NumberOutput.toString(value, _cfgUseFastDoubleWriter);
@@ -639,7 +670,7 @@ protected void appendValue(float value) throws IOException
639670
if (_nextColumnToWrite > 0) {
640671
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
641672
}
642-
writeRaw(str);
673+
writeNumber(str);
643674
}
644675

645676
protected void appendValue(double value) throws IOException
@@ -652,11 +683,11 @@ protected void appendValue(double value) throws IOException
652683
if (_nextColumnToWrite > 0) {
653684
_outputBuffer[_outputTail++] = _cfgColumnSeparator;
654685
}
655-
writeRaw(str);
686+
writeNumber(str);
656687
}
657688

658689
// @since 2.16: pre-encoded BigInteger/BigDecimal value
659-
protected void appendNumberValue(String numValue) throws IOException
690+
protected void appendNumberValue(String numStr) throws IOException
660691
{
661692
// Same as "appendRawValue()", except may want quoting
662693
if (_outputTail >= _outputEnd) {
@@ -665,7 +696,7 @@ protected void appendNumberValue(String numValue) throws IOException
665696
if (_nextColumnToWrite > 0) {
666697
appendColumnSeparator();
667698
}
668-
writeRaw(numValue);
699+
writeNumber(numStr);
669700
}
670701

671702
protected void appendValue(boolean value) throws IOException {
@@ -702,7 +733,7 @@ protected void appendColumnSeparator() throws IOException {
702733
/* Output methods, unprocessed ("raw")
703734
/**********************************************************
704735
*/
705-
736+
706737
public void writeRaw(String text) throws IOException
707738
{
708739
// Nothing to check, can just output as is
@@ -788,6 +819,25 @@ private void writeRawLong(String text) throws IOException
788819
_outputTail = len;
789820
}
790821

822+
// @since 2.16
823+
private void writeNumber(String text) throws IOException
824+
{
825+
final int len = text.length();
826+
if ((_outputTail + len + 2) > _outputEnd) {
827+
_flushBuffer();
828+
}
829+
830+
if (_cfgAlwaysQuoteNumbers) {
831+
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
832+
text.getChars(0, len, _outputBuffer, _outputTail);
833+
_outputTail += len;
834+
_outputBuffer[_outputTail++] = (char) _cfgQuoteCharacter;
835+
} else {
836+
text.getChars(0, len, _outputBuffer, _outputTail);
837+
_outputTail += len;
838+
}
839+
}
840+
791841
/*
792842
/**********************************************************
793843
/* Output methods, with quoting and escaping

csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/CSVGeneratorTest.java

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import java.io.File;
44
import java.io.StringWriter;
55
import java.math.BigDecimal;
6+
import java.math.BigInteger;
67

78
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
89

@@ -51,6 +52,19 @@ public Entry3(String id, BigDecimal amount, boolean enabled) {
5152
}
5253
}
5354

55+
@JsonPropertyOrder({"id", "amount"})
56+
static class NumberEntry<T> {
57+
public String id;
58+
public T amount;
59+
public boolean enabled;
60+
61+
public NumberEntry(String id, T amount, boolean enabled) {
62+
this.id = id;
63+
this.amount = amount;
64+
this.enabled = enabled;
65+
}
66+
}
67+
5468
/*
5569
/**********************************************************************
5670
/* Test methods
@@ -242,7 +256,7 @@ public void testForcedQuotingOfBigDecimal() throws Exception
242256
.writeValueAsString(new Entry3("xyz", BigDecimal.valueOf(1.5), false));
243257
assertEquals("xyz,1.5,false\n", result);
244258
}
245-
259+
246260
public void testForcedQuotingWithQuoteEscapedWithBackslash() throws Exception
247261
{
248262
CsvSchema schema = CsvSchema.builder()
@@ -370,12 +384,10 @@ public void testRawWrites() throws Exception
370384
public void testSerializationOfPrimitivesToCsv() throws Exception
371385
{
372386
CsvMapper mapper = new CsvMapper();
373-
/*
374387
testSerializationOfPrimitiveToCsv(mapper, String.class, "hello world", "\"hello world\"\n");
375388
testSerializationOfPrimitiveToCsv(mapper, Boolean.class, true, "true\n");
376389
testSerializationOfPrimitiveToCsv(mapper, Integer.class, 42, "42\n");
377390
testSerializationOfPrimitiveToCsv(mapper, Long.class, 42L, "42\n");
378-
*/
379391
testSerializationOfPrimitiveToCsv(mapper, Short.class, (short)42, "42\n");
380392
testSerializationOfPrimitiveToCsv(mapper, Double.class, 42.33d, "42.33\n");
381393
testSerializationOfPrimitiveToCsv(mapper, Float.class, 42.33f, "42.33\n");
@@ -390,6 +402,52 @@ private <T> void testSerializationOfPrimitiveToCsv(final CsvMapper mapper,
390402
assertEquals(expectedCsv, csv);
391403
}
392404

405+
// [dataformats-csv#198]: Verify quoting of Numbers
406+
public void testForcedQuotingOfNumbers() throws Exception
407+
{
408+
final CsvSchema schema = CsvSchema.builder()
409+
.addColumn("id")
410+
.addColumn("amount")
411+
.addColumn("enabled")
412+
.build();
413+
final CsvSchema reorderedSchema = CsvSchema.builder()
414+
.addColumn("amount")
415+
.addColumn("id")
416+
.addColumn("enabled")
417+
.build();
418+
ObjectWriter w = MAPPER.writer(schema);
419+
_testForcedQuotingOfNumbers(w, reorderedSchema,
420+
new NumberEntry<Integer>("id", Integer.valueOf(42), true));
421+
_testForcedQuotingOfNumbers(w, reorderedSchema,
422+
new NumberEntry<Long>("id", Long.MAX_VALUE, false));
423+
_testForcedQuotingOfNumbers(w, reorderedSchema,
424+
new NumberEntry<BigInteger>("id", BigInteger.valueOf(-37), true));
425+
_testForcedQuotingOfNumbers(w, reorderedSchema,
426+
new NumberEntry<Double>("id", 2.25, false));
427+
_testForcedQuotingOfNumbers(w, reorderedSchema,
428+
new NumberEntry<BigDecimal>("id", BigDecimal.valueOf(-10.5), true));
429+
}
430+
431+
private void _testForcedQuotingOfNumbers(ObjectWriter w, CsvSchema reorderedSchema,
432+
NumberEntry<?> bean) throws Exception
433+
{
434+
// First verify with quoting
435+
ObjectWriter w2 = w.with(CsvGenerator.Feature.ALWAYS_QUOTE_NUMBERS);
436+
assertEquals(String.format("%s,\"%s\",%s\n", bean.id, bean.amount, bean.enabled),
437+
w2.writeValueAsString(bean));
438+
439+
// And then dynamically disabled variant
440+
ObjectWriter w3 = w2.without(CsvGenerator.Feature.ALWAYS_QUOTE_NUMBERS);
441+
assertEquals(String.format("%s,%s,%s\n", bean.id, bean.amount, bean.enabled),
442+
w3.writeValueAsString(bean));
443+
444+
// And then quoted but reordered to force buffering
445+
ObjectWriter w4 = MAPPER.writer(reorderedSchema)
446+
.with(CsvGenerator.Feature.ALWAYS_QUOTE_NUMBERS);
447+
assertEquals(String.format("\"%s\",%s,%s\n", bean.amount, bean.id, bean.enabled),
448+
w4.writeValueAsString(bean));
449+
}
450+
393451
/*
394452
/**********************************************************************
395453
/* Secondary test methods

csv/src/test/java/com/fasterxml/jackson/dataformat/csv/ser/SchemaReorderTest.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ public class SchemaReorderTest extends ModuleTestBase
1010
{
1111
// should work ok since CsvMapper forces alphabetic ordering as default:
1212
static class Reordered {
13-
public int a, b, c, d;
13+
public int a;
14+
public long b;
15+
public long c;
16+
public int d;
1417
}
15-
18+
1619
private final CsvMapper MAPPER = new CsvMapper();
1720

1821
public void testSchemaWithOrdering() throws Exception
@@ -24,13 +27,13 @@ public void testSchemaWithOrdering() throws Exception
2427

2528
Reordered value = new Reordered();
2629
value.a = 1;
27-
value.b = 2;
28-
value.c = 3;
30+
value.b = Long.MIN_VALUE;
31+
value.c = Long.MAX_VALUE;
2932
value.d = 4;
3033

3134
schema = schema.withHeader();
3235
String csv = MAPPER.writer(schema).writeValueAsString(Arrays.asList(value));
33-
assertEquals("b,c,a,d\n2,3,1,4\n", csv);
36+
assertEquals("b,c,a,d\n"+Long.MIN_VALUE+","+Long.MAX_VALUE+",1,4\n", csv);
3437

3538
// _verifyLinks(schema);
3639
}

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Active Maintainers:
1616

1717
2.16.0 (not yet released)
1818

19+
#198: (csv) Support writing numbers as quoted Strings with
20+
`CsvGenerator.Feature.ALWAYS_QUOTE_NUMBERS`
1921
#422: (csv) Add `removeColumn()` method in `CsvSchema.Builder`
2022
#435: (yaml) Minor parsing validation miss: tagged as `int`, exception
2123
on underscore-only values

0 commit comments

Comments
 (0)