From 3b3c73fc64a9f788171e1e2ab692e27bb87c84e4 Mon Sep 17 00:00:00 2001 From: JamesWidman <627773+JamesWidman@users.noreply.github.com> Date: Fri, 1 Apr 2022 22:05:14 -0700 Subject: [PATCH] Added new writer configuration option 'quoteAll' So you can now do: let encoder = CSVEncoder { // ... $0.quoteAll = true } ...with the effect that each field is quoted in the output regardless of whether it needs to be escaped. This may be useful for round-trip testing where the input file contains redundant quotation marks. --- sources/imperative/writer/Writer.swift | 2 +- .../writer/WriterConfiguration.swift | 3 +++ .../writer/internal/WriterInternals.swift | 3 +++ .../declarative/CodableNumericBoolTests.swift | 20 +++++++++++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/sources/imperative/writer/Writer.swift b/sources/imperative/writer/Writer.swift index 46f2538..c6aef83 100644 --- a/sources/imperative/writer/Writer.swift +++ b/sources/imperative/writer/Writer.swift @@ -200,7 +200,7 @@ extension CSVWriter { // 3.A. If escaping is allowed. if let escapingScalar = self.settings.escapingScalar { - var (index, needsEscaping) = (0, false) + var (index, needsEscaping) = (0, self.settings.quoteAll) // 4. Iterate through all the input's Unicode scalars. while index < input.endIndex { let scalar = input[index] diff --git a/sources/imperative/writer/WriterConfiguration.swift b/sources/imperative/writer/WriterConfiguration.swift index 7f7b0ae..d9ee9c9 100644 --- a/sources/imperative/writer/WriterConfiguration.swift +++ b/sources/imperative/writer/WriterConfiguration.swift @@ -13,6 +13,8 @@ extension CSVWriter { public var delimiters: Delimiter.Pair /// The strategy to allow/disable escaped fields and how. public var escapingStrategy: Strategy.Escaping + /// Whether to quote all fields (as opposed to quoting only fields that need to be escaped) + public var quoteAll: Bool /// The row of headers to write at the beginning of the CSV data. /// /// If empty, no row will be written. @@ -25,6 +27,7 @@ extension CSVWriter { self.delimiters = (field: ",", row: "\n") self.escapingStrategy = .doubleQuote self.headers = Array() + self.quoteAll = false } } } diff --git a/sources/imperative/writer/internal/WriterInternals.swift b/sources/imperative/writer/internal/WriterInternals.swift index a989588..f23c861 100644 --- a/sources/imperative/writer/internal/WriterInternals.swift +++ b/sources/imperative/writer/internal/WriterInternals.swift @@ -32,6 +32,8 @@ extension CSVWriter { let delimiters: (field: [Unicode.Scalar], row: [Unicode.Scalar]) /// The unicode scalar used as encapsulator and escaping character (when printed two times). let escapingScalar: Unicode.Scalar? + /// Whether to quote all fields (as opposed to quoting only fields that need to be escaped) + let quoteAll: Bool /// Boolean indicating whether the received CSV contains a header row or not. let headers: [String] /// The encoding used to identify the underlying data. @@ -52,6 +54,7 @@ extension CSVWriter { }!) // 2. Copy all other values. self.escapingScalar = configuration.escapingStrategy.scalar + self.quoteAll = configuration.quoteAll self.headers = configuration.headers self.encoding = encoding } diff --git a/tests/declarative/CodableNumericBoolTests.swift b/tests/declarative/CodableNumericBoolTests.swift index d896e41..1887868 100644 --- a/tests/declarative/CodableNumericBoolTests.swift +++ b/tests/declarative/CodableNumericBoolTests.swift @@ -52,6 +52,26 @@ extension CodableNumericBoolTests { XCTAssertEqual(rows, try decoder.decode([_Row].self, from: shuffledString.data(using: .utf8)!)) } + /// Tests the quoteAll configuration option + func testQuoteAll() throws { + let rows = [_Row(a: true, b: false, c: nil), + _Row(a: true, b: true, c: false), + _Row(a: false, b: false, c: true)] + let string = """ + "a","b","c" + "1","0", + "1","1","0" + "0","0","1" + """.appending("\n") + + let encoder = CSVEncoder { + $0.headers = ["a", "b", "c"] + $0.boolStrategy = .numeric + $0.quoteAll = true + } + XCTAssertEqual(string, try encoder.encode(rows, into: String.self)) + } + /// Tests the error throwing/handling. func testThrows() throws { // b = nil on 2nd row, must throw an exception.