Skip to content

Commit 567fc19

Browse files
committed
Fixed a bug in CsvReader where lines were mistakenly treated as empty and skipped when skipEmptyLines was set (default)
1 parent 7824099 commit 567fc19

File tree

8 files changed

+96
-44
lines changed

8 files changed

+96
-44
lines changed

CHANGELOG.md

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
# Changelog
2+
23
All notable changes to this project will be documented in this file.
34

4-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
56
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
67

78
## [Unreleased]
89
- Nothing yet
910

11+
## [3.3.1] - 2024-09-23
12+
### Fixed
13+
- Fixed a bug in CsvReader where lines were mistakenly treated as empty and skipped when skipEmptyLines was set (default). These affected lines made up solely of field separators, solely empty quoted fields, or fields rendered empty after applying optional field modifiers.
14+
1015
## [3.3.0] - 2024-09-19
1116
### Added
1217
- Implement `Flushable` interface for `CsvWriter` to allow flushing the underlying writer
@@ -126,7 +131,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
126131

127132
- Initial release
128133

129-
[Unreleased]: https://github.com/osiegmar/FastCSV/compare/v3.3.0...main
134+
[Unreleased]: https://github.com/osiegmar/FastCSV/compare/v3.3.1...main
135+
[3.3.0]: https://github.com/osiegmar/FastCSV/compare/v3.3.0...v3.3.1
130136
[3.3.0]: https://github.com/osiegmar/FastCSV/compare/v3.2.0...v3.3.0
131137
[3.2.0]: https://github.com/osiegmar/FastCSV/compare/v3.1.0...v3.2.0
132138
[3.1.0]: https://github.com/osiegmar/FastCSV/compare/v3.0.0...v3.1.0

lib/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ plugins {
1313
}
1414

1515
group = "de.siegmar"
16-
version = "3.3.0"
16+
version = "3.3.1"
1717

1818
project.base.archivesName = "fastcsv"
1919

lib/src/intTest/java/blackbox/reader/CsvReaderTest.java

-28
Original file line numberDiff line numberDiff line change
@@ -95,34 +95,6 @@ void readerToString() {
9595
+ "ignoreDifferentFieldCount=true]");
9696
}
9797

98-
// skipped record
99-
100-
@Test
101-
void singleRecordNoSkipEmpty() {
102-
crb.skipEmptyLines(false);
103-
assertThat(crb.ofCsvRecord("").iterator()).isExhausted();
104-
}
105-
106-
@Test
107-
void multipleRecordsNoSkipEmpty() {
108-
crb.skipEmptyLines(false);
109-
110-
assertThat(crb.ofCsvRecord("\n\na").iterator()).toIterable()
111-
.satisfiesExactly(
112-
item1 -> CsvRecordAssert.assertThat(item1).isStartingLineNumber(1).fields().containsExactly(""),
113-
item2 -> CsvRecordAssert.assertThat(item2).isStartingLineNumber(2).fields().containsExactly(""),
114-
item3 -> CsvRecordAssert.assertThat(item3).isStartingLineNumber(3).fields().containsExactly("a"));
115-
}
116-
117-
@Test
118-
void skippedRecords() {
119-
assertThat(crb.ofCsvRecord("\n\nfoo\n\nbar\n\n").stream())
120-
.satisfiesExactly(
121-
item1 -> CsvRecordAssert.assertThat(item1).isStartingLineNumber(3).fields().containsExactly("foo"),
122-
item2 -> CsvRecordAssert.assertThat(item2).isStartingLineNumber(5).fields().containsExactly("bar")
123-
);
124-
}
125-
12698
// different field count
12799

128100
@ParameterizedTest
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package blackbox.reader;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.ArrayList;
6+
import java.util.List;
7+
8+
import org.junit.jupiter.api.Test;
9+
import org.junit.jupiter.params.ParameterizedTest;
10+
import org.junit.jupiter.params.provider.ValueSource;
11+
12+
import de.siegmar.fastcsv.reader.AbstractBaseCsvCallbackHandler;
13+
import de.siegmar.fastcsv.reader.CsvReader;
14+
import de.siegmar.fastcsv.reader.CsvRecordHandler;
15+
import de.siegmar.fastcsv.reader.FieldModifiers;
16+
import de.siegmar.fastcsv.reader.RecordWrapper;
17+
import testutil.CsvRecordAssert;
18+
19+
class SkipRecordsTest {
20+
21+
private final CsvReader.CsvReaderBuilder crb = CsvReader.builder();
22+
23+
@Test
24+
void singleRecordNoSkipEmpty() {
25+
crb.skipEmptyLines(false);
26+
assertThat(crb.ofCsvRecord("").iterator()).isExhausted();
27+
}
28+
29+
@Test
30+
void multipleRecordsNoSkipEmpty() {
31+
crb.skipEmptyLines(false);
32+
33+
assertThat(crb.ofCsvRecord("\n\na").iterator()).toIterable()
34+
.satisfiesExactly(
35+
item1 -> CsvRecordAssert.assertThat(item1).isStartingLineNumber(1).fields().containsExactly(""),
36+
item2 -> CsvRecordAssert.assertThat(item2).isStartingLineNumber(2).fields().containsExactly(""),
37+
item3 -> CsvRecordAssert.assertThat(item3).isStartingLineNumber(3).fields().containsExactly("a"));
38+
}
39+
40+
@ParameterizedTest
41+
@ValueSource(strings = {",\nfoo\n", ",,\nfoo\n", "''\nfoo\n", "' '\nfoo\n"})
42+
void notEmpty(final String input) {
43+
crb.quoteCharacter('\'');
44+
final var cbh = new CsvRecordHandler(FieldModifiers.TRIM);
45+
assertThat(crb.build(cbh, input).stream()).hasSize(2);
46+
}
47+
48+
@ParameterizedTest
49+
@ValueSource(strings = {",\nfoo\n", ",,\nfoo\n", "''\nfoo\n", "' '\nfoo\n"})
50+
void notEmptyCustomCallback(final String input) {
51+
crb.quoteCharacter('\'');
52+
final AbstractBaseCsvCallbackHandler<String[]> cbh = new AbstractBaseCsvCallbackHandler<>() {
53+
private final List<String> fields = new ArrayList<>();
54+
55+
@Override
56+
protected void handleBegin(final long startingLineNumber) {
57+
fields.clear();
58+
}
59+
60+
@Override
61+
protected void handleField(final int fieldIdx, final char[] buf,
62+
final int offset, final int len, final boolean quoted) {
63+
fields.add(new String(buf, offset, len).trim());
64+
}
65+
66+
@Override
67+
protected RecordWrapper<String[]> buildRecord() {
68+
return wrapRecord(fields.toArray(new String[0]));
69+
}
70+
};
71+
assertThat(crb.build(cbh, input).stream()).hasSize(2);
72+
}
73+
74+
}

lib/src/main/java/de/siegmar/fastcsv/reader/AbstractBaseCsvCallbackHandler.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ protected void handleBegin(final long startingLineNumber) {
9393
*/
9494
@Override
9595
protected final void addField(final char[] buf, final int offset, final int len, final boolean quoted) {
96-
emptyLine = emptyLine && len == 0;
96+
emptyLine = emptyLine && fieldCount == 0 && len == 0 && !quoted;
9797
handleField(fieldCount++, buf, offset, len, quoted);
9898
}
9999

lib/src/main/java/de/siegmar/fastcsv/reader/AbstractCsvCallbackHandler.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ abstract class AbstractCsvCallbackHandler<T> extends CsvCallbackHandler<T> {
4747
*/
4848
protected boolean comment;
4949

50+
/**
51+
* Whether the line is empty.
52+
*/
53+
protected boolean emptyLine;
54+
5055
/**
5156
* Constructs a new instance with an initial fields array of size {@value #INITIAL_FIELDS_SIZE}.
5257
*/
@@ -81,6 +86,7 @@ protected void beginRecord(final long startingLineNumber) {
8186
fieldIdx = 0;
8287
recordSize = 0;
8388
comment = false;
89+
emptyLine = true;
8490
}
8591

8692
/**
@@ -95,6 +101,7 @@ protected void addField(final char[] buf, final int offset, final int len, final
95101
if (recordSize + len > Limits.MAX_RECORD_SIZE) {
96102
throw new CsvParseException(maxRecordSizeExceededMessage(startingLineNumber));
97103
}
104+
emptyLine = emptyLine && fieldIdx == 0 && len == 0 && !quoted;
98105
addField(modifyField(materializeField(buf, offset, len), quoted), quoted);
99106
}
100107

@@ -175,8 +182,9 @@ protected void setComment(final char[] buf, final int offset, final int len) {
175182
* @throws CsvParseException if the addition exceeds the limit of record size.
176183
*/
177184
protected void setComment(final String value) {
178-
recordSize += value.length();
179185
comment = true;
186+
emptyLine = false;
187+
recordSize += value.length();
180188
fields[fieldIdx++] = value;
181189
}
182190

@@ -234,7 +242,7 @@ protected String[] compactFields() {
234242
* @throws NullPointerException if {@code rec} is {@code null}
235243
*/
236244
protected RecordWrapper<T> buildWrapper(final T rec) {
237-
return new RecordWrapper<>(comment, recordSize == 0, fieldIdx, rec);
245+
return new RecordWrapper<>(comment, emptyLine, fieldIdx, rec);
238246
}
239247

240248
}

lib/src/main/java/de/siegmar/fastcsv/reader/CsvReader.java

+2-7
Original file line numberDiff line numberDiff line change
@@ -380,13 +380,8 @@ public CsvReaderBuilder commentCharacter(final char commentCharacter) {
380380
/**
381381
* Defines whether empty lines should be skipped when reading data.
382382
* <p>
383-
* The default implementation interprets empty lines as lines that do not contain any data.
384-
* This includes lines that consist only of opening and closing quote characters.
385-
* <p>
386-
* A line that only contains whitespace characters is not considered empty.
387-
* However, the determination of empty lines is done after field modifiers have been applied.
388-
* If you use a field trimming modifier (like {@link FieldModifiers#TRIM}), lines that only contain whitespaces
389-
* are considered empty.
383+
* The default implementation interprets empty lines as lines that do not contain any data
384+
* (no whitespace, no quotes, nothing).
390385
* <p>
391386
* Commented lines are not considered empty lines. Use {@link #commentStrategy(CommentStrategy)} for handling
392387
* commented lines.

lib/src/main/java/de/siegmar/fastcsv/reader/FieldModifier.java

-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@
33
/**
44
* Implementations of this class are used within {@link CsvCallbackHandler} implementations to modify the fields of
55
* a CSV record before storing them in the resulting object.
6-
* <p>
7-
* Applying field modifiers might affect the behavior of skipping empty lines – see
8-
* {@link CsvReader.CsvReaderBuilder#skipEmptyLines(boolean)}.
96
*
107
* @see FieldModifiers
118
* @see SimpleFieldModifier

0 commit comments

Comments
 (0)