Skip to content

Commit

Permalink
Fix #102: Quote configurable delimiter character, not just commas (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
lardieri authored Aug 28, 2022
1 parent 50a62e4 commit ada8ce6
Show file tree
Hide file tree
Showing 6 changed files with 72 additions and 36 deletions.
4 changes: 4 additions & 0 deletions SwiftCSV.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
DF94FE462898F3A3008FD3F9 /* utf8_with_bom.csv in Resources */ = {isa = PBXBuildFile; fileRef = DF94FE452898F3A3008FD3F9 /* utf8_with_bom.csv */; };
DF94FE472898F3A3008FD3F9 /* utf8_with_bom.csv in Resources */ = {isa = PBXBuildFile; fileRef = DF94FE452898F3A3008FD3F9 /* utf8_with_bom.csv */; };
DF94FE482898F3A3008FD3F9 /* utf8_with_bom.csv in Resources */ = {isa = PBXBuildFile; fileRef = DF94FE452898F3A3008FD3F9 /* utf8_with_bom.csv */; };
DFAD8B7B28B601EB0042BB56 /* Serializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFAD8B7A28B601EB0042BB56 /* Serializer.swift */; };
E46085921CCB1E8F00385286 /* large.csv in Resources */ = {isa = PBXBuildFile; fileRef = E46085911CCB1E8F00385286 /* large.csv */; };
E46085941CCB1F5C00385286 /* PerformanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46085931CCB1F5C00385286 /* PerformanceTest.swift */; };
F5C19F502283243C00920B06 /* ResourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C19F4F2283243C00920B06 /* ResourceHelper.swift */; };
Expand Down Expand Up @@ -153,6 +154,7 @@
BE6C86061CB5CE44009A351D /* QuotedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuotedTests.swift; sourceTree = "<group>"; };
BE9B02D71CBE57B8009FE424 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
DF94FE452898F3A3008FD3F9 /* utf8_with_bom.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = utf8_with_bom.csv; sourceTree = "<group>"; };
DFAD8B7A28B601EB0042BB56 /* Serializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Serializer.swift; sourceTree = "<group>"; };
E46085911CCB1E8F00385286 /* large.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = large.csv; sourceTree = "<group>"; };
E46085931CCB1F5C00385286 /* PerformanceTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceTest.swift; sourceTree = "<group>"; };
F5C19F4F2283243C00920B06 /* ResourceHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourceHelper.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -255,6 +257,7 @@
3D444BCC1C7D88290001C60C /* String+Lines.swift */,
BE9B02D71CBE57B8009FE424 /* Parser.swift */,
508975D61DBF34CF006F3DBE /* ParsingState.swift */,
DFAD8B7A28B601EB0042BB56 /* Serializer.swift */,
);
path = SwiftCSV;
sourceTree = "<group>";
Expand Down Expand Up @@ -588,6 +591,7 @@
buildActionMask = 2147483647;
files = (
50B3EEA4286F8A84007B3956 /* CSVDelimiter.swift in Sources */,
DFAD8B7B28B601EB0042BB56 /* Serializer.swift in Sources */,
508975D21DBB897A006F3DBE /* NamedCSVView.swift in Sources */,
508CA0FB2771F2E70084C8E8 /* CSV+DelimiterGuessing.swift in Sources */,
3DAAEE9C1C74C7EC00A933DB /* CSV.swift in Sources */,
Expand Down
8 changes: 0 additions & 8 deletions SwiftCSV/CSV.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,6 @@ extension CSV: CustomStringConvertible {
}
}

func enquoteContentsIfNeeded(cell: String) -> String {
// Add quotes if value contains a comma
if cell.contains(",") {
return "\"\(cell)\""
}
return cell
}

extension CSV {
/// Load a CSV file from `url`.
///
Expand Down
14 changes: 2 additions & 12 deletions SwiftCSV/EnumeratedCSVView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,9 @@ public struct Enumerated: CSVView {
}

public func serialize(header: [String], delimiter: CSVDelimiter) -> String {
let separator = String(delimiter.rawValue)

let head = header
.map(enquoteContentsIfNeeded(cell:))
.joined(separator: separator) + "\n"

let content = rows.map { row in
row.map(enquoteContentsIfNeeded(cell:))
.joined(separator: separator)
}.joined(separator: "\n")

return head + content
return Serializer.serialize(header: header, rows: rows, delimiter: delimiter)
}

}

extension Collection {
Expand Down
18 changes: 5 additions & 13 deletions SwiftCSV/NamedCSVView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,11 @@ public struct Named: CSVView {
}

public func serialize(header: [String], delimiter: CSVDelimiter) -> String {
let separator = String(delimiter.rawValue)
let rowsOrderingCellsByHeader = rows.map { row in
header.map { cellID in row[cellID]! }
}

let head = header
.map(enquoteContentsIfNeeded(cell:))
.joined(separator: separator) + "\n"

let content = rows.map { row in
header
.map { cellID in row[cellID]! }
.map(enquoteContentsIfNeeded(cell:))
.joined(separator: separator)
}.joined(separator: "\n")

return head + content
return Serializer.serialize(header: header, rows: rowsOrderingCellsByHeader, delimiter: delimiter)
}

}
46 changes: 46 additions & 0 deletions SwiftCSV/Serializer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//
// Serializer.swift
// SwiftCSV
//

import Foundation

enum Serializer {

static let newline = "\n"

static func serialize(header: [String], rows: [[String]], delimiter: CSVDelimiter) -> String {
let head = serializeRow(row: header, delimiter: delimiter) + newline

let content = rows.map { row in
serializeRow(row: row, delimiter: delimiter)
}.joined(separator: newline)

return head + content
}


static func serializeRow(row: [String], delimiter: CSVDelimiter) -> String {
let separator = String(delimiter.rawValue)

let content = row.map { cell in
cell.enquoted(whenContaining: separator)
}.joined(separator: separator)

return content
}

}

fileprivate extension String {

func enquoted(whenContaining separator: String) -> String {
// Add quotes if value contains a delimiter
if self.contains(separator) {
return "\"\(self)\""
}

return self
}

}
18 changes: 15 additions & 3 deletions SwiftCSVTests/EnumeratedCSVViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,24 @@ class EnumeratedViewTests: XCTestCase {
XCTAssertEqual(csv.columns, expectedColumns)
}

func testSerialization() {
func testSerialization() throws {
// Comma-separated values.
XCTAssertEqual(csv.serialized, "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20")
}

func testSerializationWithDoubleQuotes() throws {
// Comma-separated values with double quotes and embedded delimiters in cells.
csv = try CSV<Enumerated>(string: "id,\"the, name\",age\n1,\"Alice, In, Wonderland\",18\n2,Bob,19\n3,Charlie,20")
XCTAssertEqual(csv.serialized, "id,\"the, name\",age\n1,\"Alice, In, Wonderland\",18\n2,Bob,19\n3,Charlie,20")

// Tab-separated values with implicit delimiter (delimiter guessing).
csv = try CSV<Enumerated>(string: "id\tname\tage\n1\tAlice\t18\n2\tBob\t19\n3\tCharlie\t20")
XCTAssertEqual(csv.serialized, "id\tname\tage\n1\tAlice\t18\n2\tBob\t19\n3\tCharlie\t20")

// Tab-separated values with double quotes and embedded delimiters in cells.
csv = try CSV<Enumerated>(string: "id\t\"the\t name\"\tage\n1\t\"Alice\t In\t Wonderland\"\t18\n2\tBob\t19\n3\tCharlie\t20")
XCTAssertEqual(csv.serialized, "id\t\"the\t name\"\tage\n1\t\"Alice\t In\t Wonderland\"\t18\n2\tBob\t19\n3\tCharlie\t20")

// Tab-separated values with explicit alternate delimiter (tab) and embedded default delimiters (commas) in cells.
csv = try CSV<Enumerated>(string: "id\tthe, name,age\n1\tAlice, In, Wonderland\t18\n2\tBob\t19\n3\tCharlie\t20", delimiter: .tab)
XCTAssertEqual(csv.serialized, "id\tthe, name,age\n1\tAlice, In, Wonderland\t18\n2\tBob\t19\n3\tCharlie\t20")
}
}

0 comments on commit ada8ce6

Please sign in to comment.