Skip to content

Commit 82ee81e

Browse files
committed
Ignore empty lines for CSVs with multiple fields
1 parent 2fb1c65 commit 82ee81e

File tree

3 files changed

+98
-24
lines changed

3 files changed

+98
-24
lines changed

sources/imperative/reader/Reader.swift

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -107,31 +107,39 @@ extension CSVReader {
107107
case .failed(let e): throw e
108108
}
109109

110-
let result: [String]?
111-
do {
112-
result = try self._parseLine(rowIndex: self.count.rows)
113-
} catch let error {
114-
self.status = .failed(error as! CSVError<CSVReader>)
115-
throw error
116-
}
117-
118-
guard let numFields = result?.count else {
119-
self.status = .finished
120-
return nil
121-
}
122-
123-
if self.count.rows > 0 {
124-
guard self.count.fields == numFields else {
125-
let error = Error._invalidFieldCount(rowIndex: self.count.rows+1, parsed: numFields, expected: self.count.fields)
126-
self.status = .failed(error)
110+
loop: while true {
111+
let result: [String]?
112+
do {
113+
result = try self._parseLine(rowIndex: self.count.rows)
114+
} catch let error {
115+
self.status = .failed(error as! CSVError<CSVReader>)
127116
throw error
128117
}
129-
} else {
130-
self.count.fields = numFields
118+
// If no fields were parsed, the EOF has been reached.
119+
guard let fields = result else {
120+
self.status = .finished
121+
return nil
122+
}
123+
124+
let numFields = fields.count
125+
// If a single empty field is received, a white line has been parsed. Ignore empty lines for CSV files were several fields are expected.
126+
if numFields == 1, fields.first!.isEmpty, self.count.rows != 1 {
127+
continue loop
128+
}
129+
130+
if self.count.rows > 0 {
131+
guard self.count.fields == numFields else {
132+
let error = Error._invalidFieldCount(rowIndex: self.count.rows+1, parsed: numFields, expected: self.count.fields)
133+
self.status = .failed(error)
134+
throw error
135+
}
136+
} else {
137+
self.count.fields = numFields
138+
}
139+
140+
self.count.rows += 1
141+
return result
131142
}
132-
133-
self.count.rows += 1
134-
return result
135143
}
136144
}
137145

tests/declarative/DecodingBadInputTests.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ extension DecodingBadInputTests {
1717
}
1818

1919
extension DecodingBadInputTests {
20-
/// Tests bad quoting resulting in too many fields in a particular row
21-
func testBadQuoting() {
20+
/// Tests bad input, in which a row in not escaped resulting in too many fields in a particular row
21+
func testBadEscaping() {
2222
let input = """
2323
x,y
2424
A,A A
@@ -39,4 +39,16 @@ extension DecodingBadInputTests {
3939
let decoder = CSVDecoder { $0.headerStrategy = .firstLine }
4040
XCTAssertThrowsError(try decoder.decode([_Row].self, from: input))
4141
}
42+
43+
/// Tests a valid CSV file with an extra new line delimeter at the end of the file.
44+
func testExtraNewLine() throws {
45+
let input = """
46+
x,y
47+
A,AA
48+
B,BB
49+
\n
50+
"""
51+
let decoder = CSVDecoder { $0.headerStrategy = .firstLine }
52+
XCTAssertNoThrow(try decoder.decode([_Row].self, from: input))
53+
}
4254
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import XCTest
2+
import CodableCSV
3+
4+
/// Check support for handling bad input
5+
final class ReaderBadInputTests: XCTestCase {
6+
override func setUp() {
7+
self.continueAfterFailure = false
8+
}
9+
}
10+
11+
extension ReaderBadInputTests {
12+
/// Representation of a CSV row containing a couple of strings.
13+
private struct _Row: Codable, Equatable {
14+
var x: String
15+
var y: String
16+
}
17+
}
18+
19+
extension ReaderBadInputTests {
20+
/// Tests bad input, in which a row in not escaped resulting in too many fields in a particular row
21+
func testBadEscaping() throws {
22+
let input = """
23+
x,y
24+
A,A A
25+
C,C, C
26+
D,D D
27+
"""
28+
XCTAssertThrowsError(try CSVReader.decode(input: input) { $0.headerStrategy = .firstLine })
29+
}
30+
31+
/// Tests a CSV with a header with three fields (one of them being empty) and subsequent rows with two fields.
32+
func testIllFormedHeader() {
33+
let input = """
34+
x,y,
35+
A,A A
36+
B,"B, B"
37+
"""
38+
XCTAssertThrowsError(try CSVReader.decode(input: input) { $0.headerStrategy = .firstLine })
39+
}
40+
41+
/// Tests a valid CSV file with an extra new line delimeter at the end of the file.
42+
func testExtraNewLine() throws {
43+
let input = """
44+
x,y
45+
A,AA
46+
B,BB
47+
\n
48+
"""
49+
let reader = try CSVReader(input: input) { $0.headerStrategy = .firstLine }
50+
XCTAssertNotNil(try reader.readRow())
51+
XCTAssertNotNil(try reader.readRow())
52+
XCTAssertNil(try reader.readRow())
53+
}
54+
}

0 commit comments

Comments
 (0)