Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #102: Quote configurable delimiter character, not just commas #107

Merged
merged 6 commits into from
Aug 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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")
}
}