Skip to content

Commit ac327b8

Browse files
authored
Fix serialization of embedded quotes. (#111)
Embedded quotes in values must be doubled, and the entire value surrounded with quotes.
1 parent 8244fd6 commit ac327b8

File tree

6 files changed

+60
-10
lines changed

6 files changed

+60
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Bugfixes:
1616

1717
- Strip byte order mark from Strings when importing so they don't become part of imported content's cells.
1818
See #97 for discussion. (#103) -- @lardieri
19+
- Respect alternate delimiters when serializing the CSV.
20+
See #102 for discussion. (#107) -- @lardieri
21+
- Escape any double-quotes embedded inside the field values when serializing the CSV.
22+
See #111 for discussion. -- @lardieri
1923

2024
Other:
2125

SwiftCSV.xcodeproj/project.pbxproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@
8686
DFAD8B8028BC8B6F0042BB56 /* Serializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFAD8B7A28B601EB0042BB56 /* Serializer.swift */; };
8787
DFAD8B8128BC8B700042BB56 /* Serializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFAD8B7A28B601EB0042BB56 /* Serializer.swift */; };
8888
DFAD8B8228BC8B710042BB56 /* Serializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFAD8B7A28B601EB0042BB56 /* Serializer.swift */; };
89+
DFAD8B8428BC91D10042BB56 /* wonderland.csv in Resources */ = {isa = PBXBuildFile; fileRef = DFAD8B8328BC91D10042BB56 /* wonderland.csv */; };
90+
DFAD8B8528BC91D10042BB56 /* wonderland.csv in Resources */ = {isa = PBXBuildFile; fileRef = DFAD8B8328BC91D10042BB56 /* wonderland.csv */; };
91+
DFAD8B8628BC91D10042BB56 /* wonderland.csv in Resources */ = {isa = PBXBuildFile; fileRef = DFAD8B8328BC91D10042BB56 /* wonderland.csv */; };
8992
E46085921CCB1E8F00385286 /* large.csv in Resources */ = {isa = PBXBuildFile; fileRef = E46085911CCB1E8F00385286 /* large.csv */; };
9093
E46085941CCB1F5C00385286 /* PerformanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46085931CCB1F5C00385286 /* PerformanceTest.swift */; };
9194
F5C19F502283243C00920B06 /* ResourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C19F4F2283243C00920B06 /* ResourceHelper.swift */; };
@@ -158,6 +161,7 @@
158161
BE9B02D71CBE57B8009FE424 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
159162
DF94FE452898F3A3008FD3F9 /* utf8_with_bom.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = utf8_with_bom.csv; sourceTree = "<group>"; };
160163
DFAD8B7A28B601EB0042BB56 /* Serializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Serializer.swift; sourceTree = "<group>"; };
164+
DFAD8B8328BC91D10042BB56 /* wonderland.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = wonderland.csv; sourceTree = "<group>"; };
161165
E46085911CCB1E8F00385286 /* large.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = large.csv; sourceTree = "<group>"; };
162166
E46085931CCB1F5C00385286 /* PerformanceTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceTest.swift; sourceTree = "<group>"; };
163167
F5C19F4F2283243C00920B06 /* ResourceHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourceHelper.swift; sourceTree = "<group>"; };
@@ -286,10 +290,11 @@
286290
BE06B67E1CB72680009578CC /* Res */ = {
287291
isa = PBXGroup;
288292
children = (
289-
DF94FE452898F3A3008FD3F9 /* utf8_with_bom.csv */,
290293
BE06B67C1CB7267B009578CC /* empty_fields.csv */,
291294
BE06B6811CB7287F009578CC /* quotes.csv */,
292295
E46085911CCB1E8F00385286 /* large.csv */,
296+
DF94FE452898F3A3008FD3F9 /* utf8_with_bom.csv */,
297+
DFAD8B8328BC91D10042BB56 /* wonderland.csv */,
293298
F5C19F4F2283243C00920B06 /* ResourceHelper.swift */,
294299
);
295300
name = Res;
@@ -536,6 +541,7 @@
536541
isa = PBXResourcesBuildPhase;
537542
buildActionMask = 2147483647;
538543
files = (
544+
DFAD8B8428BC91D10042BB56 /* wonderland.csv in Resources */,
539545
DF94FE462898F3A3008FD3F9 /* utf8_with_bom.csv in Resources */,
540546
BE06B67D1CB7267B009578CC /* empty_fields.csv in Resources */,
541547
BE06B6821CB7287F009578CC /* quotes.csv in Resources */,
@@ -554,6 +560,7 @@
554560
isa = PBXResourcesBuildPhase;
555561
buildActionMask = 2147483647;
556562
files = (
563+
DFAD8B8528BC91D10042BB56 /* wonderland.csv in Resources */,
557564
DF94FE472898F3A3008FD3F9 /* utf8_with_bom.csv in Resources */,
558565
5FB74BEA1CCB9325009DDBF1 /* empty_fields.csv in Resources */,
559566
5FB74BEB1CCB9325009DDBF1 /* quotes.csv in Resources */,
@@ -572,6 +579,7 @@
572579
isa = PBXResourcesBuildPhase;
573580
buildActionMask = 2147483647;
574581
files = (
582+
DFAD8B8628BC91D10042BB56 /* wonderland.csv in Resources */,
575583
DF94FE482898F3A3008FD3F9 /* utf8_with_bom.csv in Resources */,
576584
5FB74BED1CCB932B009DDBF1 /* empty_fields.csv in Resources */,
577585
5FB74BEE1CCB932B009DDBF1 /* quotes.csv in Resources */,

SwiftCSV/Serializer.swift

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@ enum Serializer {
3434

3535
fileprivate extension String {
3636

37+
static let quote = "\""
38+
3739
func enquoted(whenContaining separator: String) -> String {
38-
// Add quotes if value contains a delimiter
39-
if self.contains(separator) {
40-
return "\"\(self)\""
40+
// If value contains a delimiter or quotes, double any embedded quotes and surround with quotes.
41+
// For more information, see https://www.rfc-editor.org/rfc/rfc4180.html
42+
if self.contains(separator) || self.contains(Self.quote) {
43+
return Self.quote + self.replacingOccurrences(of: Self.quote, with: Self.quote + Self.quote) + Self.quote
44+
} else {
45+
return self
4146
}
42-
43-
return self
4447
}
4548

4649
}

SwiftCSVTests/QuotedTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,35 @@ class QuotedTests: XCTestCase {
3333
"age": "8"
3434
])
3535
}
36+
37+
func testEmbeddedQuotes() throws {
38+
let csvURL = ResourceHelper.url(forResource: "wonderland", withExtension: "csv")!
39+
csv = try CSV(url: csvURL)
40+
41+
/*
42+
The test file:
43+
44+
Character,Quote
45+
White Rabbit,"""Where shall I begin, please your Majesty?"" he asked."
46+
King,"""Begin at the beginning,"" the King said gravely, ""and go on till you come to the end: then stop."""
47+
March Hare,"""Do you mean that you think you can find out the answer to it?"" said the March Hare."
48+
49+
Notice there are no commas (delimiters) in the 3rd line.
50+
For more information, see https://www.rfc-editor.org/rfc/rfc4180.html
51+
*/
52+
53+
let expected = [
54+
[ "Character" : "White Rabbit" , "Quote" : #""Where shall I begin, please your Majesty?" he asked."# ],
55+
[ "Character" : "King" , "Quote" : #""Begin at the beginning," the King said gravely, "and go on till you come to the end: then stop.""# ],
56+
[ "Character" : "March Hare" , "Quote" : #""Do you mean that you think you can find out the answer to it?" said the March Hare."# ]
57+
]
58+
59+
for (index, row) in csv.rows.enumerated() {
60+
XCTAssertEqual(expected[index], row)
61+
}
62+
63+
let serialized = csv.serialized
64+
let read = try String(contentsOf: csvURL, encoding: .utf8)
65+
XCTAssertEqual(serialized, read)
66+
}
3667
}

SwiftCSVTests/URLTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ class URLTests: XCTestCase {
6161
}
6262
}
6363

64-
func testUTF8() {
64+
func testUTF8() throws {
6565
let csvURL = ResourceHelper.url(forResource: "utf8_with_bom", withExtension: "csv")!
66-
csv = try! CSV(url: csvURL)
66+
csv = try CSV(url: csvURL)
6767

6868
XCTAssertFalse(csv.header.first!.hasPrefix("\u{FEFF}"))
6969

@@ -80,9 +80,9 @@ class URLTests: XCTestCase {
8080
}
8181
}
8282

83-
func testUTF8Delimited() {
83+
func testUTF8Delimited() throws {
8484
let csvURL = ResourceHelper.url(forResource: "utf8_with_bom", withExtension: "csv")!
85-
csv = try! CSV(url: csvURL, delimiter: .comma)
85+
csv = try CSV(url: csvURL, delimiter: .comma)
8686

8787
XCTAssertFalse(csv.header.first!.hasPrefix("\u{FEFF}"))
8888

SwiftCSVTests/wonderland.csv

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Character,Quote
2+
White Rabbit,"""Where shall I begin, please your Majesty?"" he asked."
3+
King,"""Begin at the beginning,"" the King said gravely, ""and go on till you come to the end: then stop."""
4+
March Hare,"""Do you mean that you think you can find out the answer to it?"" said the March Hare."

0 commit comments

Comments
 (0)