diff --git a/SwiftCSV/Serializer.swift b/SwiftCSV/Serializer.swift index ebb98ea..f5ac476 100644 --- a/SwiftCSV/Serializer.swift +++ b/SwiftCSV/Serializer.swift @@ -37,9 +37,9 @@ fileprivate extension String { static let quote = "\"" func enquoted(whenContaining separator: String) -> String { - // If value contains a delimiter or quotes, double any embedded quotes and surround with quotes. + // Quote fields containing delimiters, quotes or line breaks, and double embedded quotes. // For more information, see https://www.rfc-editor.org/rfc/rfc4180.html - if self.contains(separator) || self.contains(Self.quote) { + if self.contains(separator) || self.contains(Self.quote) || self.contains(where: { $0.isNewline }) { return Self.quote + self.replacingOccurrences(of: Self.quote, with: Self.quote + Self.quote) + Self.quote } else { return self diff --git a/SwiftCSVTests/SerializationNewlineTests.swift b/SwiftCSVTests/SerializationNewlineTests.swift new file mode 100644 index 0000000..540aac5 --- /dev/null +++ b/SwiftCSVTests/SerializationNewlineTests.swift @@ -0,0 +1,28 @@ +import XCTest +@testable import SwiftCSV + +class SerializationNewlineTests: XCTestCase { + func testLineBreaksRemainQuotedForEnumeratedRoundTrip() throws { + for newline in ["\n", "\r", "\r\n"] { + for delimiter: CSVDelimiter in [.comma, .semicolon, .tab, .character("|")] { + let separator = String(delimiter.rawValue) + let source = "\"first\(newline)name\"\(separator)second\n\"value\(newline)continued\"\(separator)other" + let original = try EnumeratedCSV(string: source, delimiter: delimiter) + XCTAssertEqual(original.serialized, source) + let decoded = try EnumeratedCSV(string: original.serialized, delimiter: delimiter) + XCTAssertEqual(decoded.header, original.header) + XCTAssertEqual(decoded.rows, original.rows) + } + } + } + + func testLineBreaksRemainQuotedForNamedRoundTrip() throws { + for newline in ["\n", "\r", "\r\n"] { + let source = "name,note\nAlice,\"first\(newline)second\"" + let original = try NamedCSV(string: source, delimiter: .comma) + XCTAssertEqual(original.serialized, source) + let decoded = try NamedCSV(string: original.serialized, delimiter: .comma) + XCTAssertEqual(decoded.rows, original.rows) + } + } +}