Skip to content

Commit 49e077d

Browse files
committed
Fix #17 Encoding optionals
1 parent 53de172 commit 49e077d

File tree

5 files changed

+219
-120
lines changed

5 files changed

+219
-120
lines changed

README.md

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -649,18 +649,13 @@ To configure the encoding/decoding process, you need to set the configuration va
649649
decoder.decode([Pets].self, from: url2)
650650
```
651651
652-
The strategies labeled with `.custom` let you insert behavior into the encoding/decoding process without forcing you to manually conform to `init(from:)` and `encode(to:)`. When set, they will reference the targeted type for the whole process. For example, if you want to decode a CSV file where empty fields are marked with the word `null` (for some reason). You could do the following:
652+
The strategies labeled with `.custom` let you insert behavior into the encoding/decoding process without forcing you to manually conform to `init(from:)` and `encode(to:)`. When set, they will reference the targeted type for the whole process. For example, if you want to encode a CSV file where empty fields are marked with the word `null` (for some reason). You could do the following:
653653
654654
```swift
655655
let decoder = CSVDecoder()
656-
decoder.nilStrategy = .custom({ (decoder) -> Bool in
657-
do {
658-
let container = try decoder.singleValueContainer()
659-
let field = try container.decode(String.self)
660-
return field == "null"
661-
} catch let error {
662-
return false
663-
}
656+
decoder.nilStrategy = .custom({ (encoder) in
657+
var container = encoder.singleValueContainer()
658+
try container.encode("null")
664659
})
665660
```
666661
@@ -692,6 +687,6 @@ If `CodableCSV` is not of your liking, the Swift community has developed other C
692687
- [SwiftCSVExport](https://github.com/vigneshuvi/SwiftCSVExport) reads/writes CSV imperatively with Objective-C support.
693688
- [swift-csv](https://github.com/brutella/swift-csv) offers an imperative CSV reader/writer based on Foundation's streams.
694689
- [CSV](https://github.com/skelpo/CSV) offers synchronous and asynchronous imperative CSV reader/writer and encoders/decoders.
695-
- [CommonCoding](https://github.com/Lantua/CommonCoding) provides CSV encoder/decoder conforming to the [RFC4180](https://tools.ietf.org/html/rfc4180) standard.
690+
- [CommonCoding](https://github.com/Lantua/CommonCoding) provides a CSV encoder/decoder conforming to the [RFC4180](https://tools.ietf.org/html/rfc4180) standard.
696691
697692
There are many good tools outside the Swift community. Since writing them all would be a hard task, I will just point you to the great [AwesomeCSV](https://github.com/secretGeek/awesomeCSV) github repo. Take it a look! There are a lot of treasures to be found there.

sources/declarative/encodable/containers/KeyedEncodingContainer.swift

Lines changed: 91 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -192,67 +192,97 @@ extension ShadowEncoder.KeyedContainer {
192192
// }
193193
}
194194

195-
//extension ShadowEncoder.KeyedContainer {
196-
// mutating func encodeIfPresent(_ value: String?, forKey key: Key) throws {
197-
// fatalError()
198-
// }
199-
//
200-
// mutating func encodeIfPresent(_ value: Bool?, forKey key: Key) throws {
201-
// fatalError()
202-
// }
203-
//
204-
// mutating func encodeIfPresent(_ value: Double?, forKey key: Key) throws {
205-
// fatalError()
206-
// }
207-
//
208-
// mutating func encodeIfPresent(_ value: Float?, forKey key: Key) throws {
209-
// fatalError()
210-
// }
211-
//
212-
// mutating func encodeIfPresent(_ value: Int?, forKey key: Key) throws {
213-
// fatalError()
214-
// }
215-
//
216-
// mutating func encodeIfPresent(_ value: Int8?, forKey key: Key) throws {
217-
// fatalError()
218-
// }
219-
//
220-
// mutating func encodeIfPresent(_ value: Int16?, forKey key: Key) throws {
221-
// fatalError()
222-
// }
223-
//
224-
// mutating func encodeIfPresent(_ value: Int32?, forKey key: Key) throws {
225-
// fatalError()
226-
// }
227-
//
228-
// mutating func encodeIfPresent(_ value: Int64?, forKey key: Key) throws {
229-
// fatalError()
230-
// }
231-
//
232-
// mutating func encodeIfPresent(_ value: UInt?, forKey key: Key) throws {
233-
// fatalError()
234-
// }
235-
//
236-
// mutating func encodeIfPresent(_ value: UInt8?, forKey key: Key) throws {
237-
// fatalError()
238-
// }
239-
//
240-
// mutating func encodeIfPresent(_ value: UInt16?, forKey key: Key) throws {
241-
// fatalError()
242-
// }
243-
//
244-
// mutating func encodeIfPresent(_ value: UInt32?, forKey key: Key) throws {
245-
// fatalError()
246-
// }
247-
//
248-
// mutating func encodeIfPresent(_ value: UInt64?, forKey key: Key) throws {
249-
// fatalError()
250-
// }
251-
//
252-
// mutating func encodeIfPresent<T>(_ value: T?, forKey key: Key) throws where T:Encodable {
253-
// fatalError()
254-
// }
255-
//}
195+
extension ShadowEncoder.KeyedContainer {
196+
mutating func encodeIfPresent(_ value: String?, forKey key: Key) throws {
197+
var container = try self.fieldContainer(forKey: key)
198+
guard let value = value else { return try container.encodeNil() }
199+
try container.encode(value)
200+
}
201+
202+
mutating func encodeIfPresent(_ value: Bool?, forKey key: Key) throws {
203+
var container = try self.fieldContainer(forKey: key)
204+
guard let value = value else { return try container.encodeNil() }
205+
try container.encode(value)
206+
}
207+
208+
mutating func encodeIfPresent(_ value: Double?, forKey key: Key) throws {
209+
var container = try self.fieldContainer(forKey: key)
210+
guard let value = value else { return try container.encodeNil() }
211+
try container.encode(value)
212+
}
213+
214+
mutating func encodeIfPresent(_ value: Float?, forKey key: Key) throws {
215+
var container = try self.fieldContainer(forKey: key)
216+
guard let value = value else { return try container.encodeNil() }
217+
try container.encode(value)
218+
}
219+
220+
mutating func encodeIfPresent(_ value: Int?, forKey key: Key) throws {
221+
var container = try self.fieldContainer(forKey: key)
222+
guard let value = value else { return try container.encodeNil() }
223+
try container.encode(value)
224+
}
225+
226+
mutating func encodeIfPresent(_ value: Int8?, forKey key: Key) throws {
227+
var container = try self.fieldContainer(forKey: key)
228+
guard let value = value else { return try container.encodeNil() }
229+
try container.encode(value)
230+
}
231+
232+
mutating func encodeIfPresent(_ value: Int16?, forKey key: Key) throws {
233+
var container = try self.fieldContainer(forKey: key)
234+
guard let value = value else { return try container.encodeNil() }
235+
try container.encode(value)
236+
}
237+
238+
mutating func encodeIfPresent(_ value: Int32?, forKey key: Key) throws {
239+
var container = try self.fieldContainer(forKey: key)
240+
guard let value = value else { return try container.encodeNil() }
241+
try container.encode(value)
242+
}
243+
244+
mutating func encodeIfPresent(_ value: Int64?, forKey key: Key) throws {
245+
var container = try self.fieldContainer(forKey: key)
246+
guard let value = value else { return try container.encodeNil() }
247+
try container.encode(value)
248+
}
249+
250+
mutating func encodeIfPresent(_ value: UInt?, forKey key: Key) throws {
251+
var container = try self.fieldContainer(forKey: key)
252+
guard let value = value else { return try container.encodeNil() }
253+
try container.encode(value)
254+
}
255+
256+
mutating func encodeIfPresent(_ value: UInt8?, forKey key: Key) throws {
257+
var container = try self.fieldContainer(forKey: key)
258+
guard let value = value else { return try container.encodeNil() }
259+
try container.encode(value)
260+
}
261+
262+
mutating func encodeIfPresent(_ value: UInt16?, forKey key: Key) throws {
263+
var container = try self.fieldContainer(forKey: key)
264+
guard let value = value else { return try container.encodeNil() }
265+
try container.encode(value)
266+
}
267+
268+
mutating func encodeIfPresent(_ value: UInt32?, forKey key: Key) throws {
269+
var container = try self.fieldContainer(forKey: key)
270+
guard let value = value else { return try container.encodeNil() }
271+
try container.encode(value)
272+
}
273+
274+
mutating func encodeIfPresent(_ value: UInt64?, forKey key: Key) throws {
275+
var container = try self.fieldContainer(forKey: key)
276+
guard let value = value else { return try container.encodeNil() }
277+
try container.encode(value)
278+
}
279+
280+
mutating func encodeIfPresent<T>(_ value: T?, forKey key: Key) throws where T:Encodable {
281+
var container = try self.fieldContainer(forKey: key)
282+
guard let value = value else { return try container.encodeNil() }
283+
try container.encode(value)
284+
}
285+
}
256286

257287
// MARK: -
258288

sources/imperative/writer/Writer.swift

Lines changed: 40 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -187,59 +187,51 @@ extension CSVWriter {
187187
/// - parameter field: The field to be checked for characters to escape and subsequently written.
188188
/// - throws: `CSVError<CSVWriter>` exclusively.
189189
private func lowlevelWrite(field: String) throws {
190-
var result: [Unicode.Scalar]
190+
// 1. If the field is empty, don't write anything.
191+
guard !field.isEmpty else { return }
191192

192-
// 1. If the field is empty, just write two escaping scalars.
193-
if field.isEmpty {
194-
switch self.settings.escapingScalar {
195-
case let s?: result = .init(repeating: s, count: 2)
196-
case .none: result = .init()
193+
let input: [Unicode.Scalar] = .init(field.unicodeScalars)
194+
// 2. Reserve space for all field scalars plus a bit more in case escaping is needed.
195+
var result: [Unicode.Scalar] = .init()
196+
result.reserveCapacity(input.count + 3)
197+
198+
// 3.A. If escaping is allowed.
199+
if let escapingScalar = self.settings.escapingScalar {
200+
var (index, needsEscaping) = (0, false)
201+
// 4. Iterate through all the input's Unicode scalars.
202+
while index < input.endIndex {
203+
let scalar = input[index]
204+
// 5. If the escaping character appears, the field needs escaping, but also the escaping character is duplicated.
205+
if scalar == escapingScalar {
206+
needsEscaping = true
207+
result.append(escapingScalar)
208+
// 6. If there is a field or row delimiter, the field needs escaping.
209+
} else if self.isFieldDelimiter(input, &index, &result) || self.isRowDelimiter(input, &index, &result) {
210+
needsEscaping = true
211+
continue
212+
}
213+
214+
result.append(scalar)
215+
index += 1
197216
}
198-
// 2. If the field contains characters...
199-
} else {
200-
let input: [Unicode.Scalar] = .init(field.unicodeScalars)
201-
result = .init()
202-
// 3. Reserve space for all field scalars plus a bit more in case escaping is needed.
203-
result.reserveCapacity(input.count + 3)
204217

205-
// 4.A. If escaping is allowed.
206-
if let escapingScalar = self.settings.escapingScalar {
207-
var (index, needsEscaping) = (0, false)
208-
// 5. Iterate through all the input's Unicode scalars.
209-
while index < input.endIndex {
210-
let scalar = input[index]
211-
// 6. If the escaping character appears, the field needs escaping, but also the escaping character is duplicated.
212-
if scalar == escapingScalar {
213-
needsEscaping = true
214-
result.append(escapingScalar)
215-
// 7. If there is a field or row delimiter, the field needs escaping.
216-
} else if self.isFieldDelimiter(input, &index, &result) || self.isRowDelimiter(input, &index, &result) {
217-
needsEscaping = true
218-
continue
219-
}
220-
221-
result.append(scalar)
222-
index += 1
218+
// 7. If the field needed escaping, insert the escaping escalar at the beginning and end of the field.
219+
if needsEscaping {
220+
result.insert(escapingScalar, at: result.startIndex)
221+
result.append(escapingScalar)
222+
}
223+
// 3.B. If escaping is not allowed.
224+
} else {
225+
var index = 0
226+
// 4. Iterate through all the input's Unicode scalars.
227+
while index < input.endIndex {
228+
// 5. If the input data contains a delimiter, throw an error.
229+
guard !self.isFieldDelimiter(input, &index, &result), !self.isRowDelimiter(input, &index, &result) else {
230+
throw Error.invalidPriviledgeCharacter(on: field)
223231
}
224232

225-
// 8. If the field needed escaping, insert the escaping escalar at the beginning and end of the field.
226-
if needsEscaping {
227-
result.insert(escapingScalar, at: result.startIndex)
228-
result.append(escapingScalar)
229-
}
230-
// 4.B. If escaping is not allowed.
231-
} else {
232-
var index = 0
233-
// 5. Iterate through all the input's Unicode scalars.
234-
while index < input.endIndex {
235-
// 6. If the input data contains a delimiter, through an error.
236-
guard !self.isFieldDelimiter(input, &index, &result), !self.isRowDelimiter(input, &index, &result) else {
237-
throw Error.invalidPriviledgeCharacter(on: field)
238-
}
239-
240-
result.append(input[index])
241-
index += 1
242-
}
233+
result.append(input[index])
234+
index += 1
243235
}
244236
}
245237

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import XCTest
2+
import CodableCSV
3+
4+
/// Tests checking the regular encoding usage.
5+
final class EncodingOptionalsTests: XCTestCase {
6+
override func setUp() {
7+
self.continueAfterFailure = false
8+
}
9+
}
10+
11+
extension EncodingOptionalsTests {
12+
/// Tests writting `nil` values on a keyed containers with named coding keys.
13+
func testOptionalNamedFields() throws {
14+
struct Student: Encodable {
15+
let name: String, age: Int?, country: String?, hasPet: Bool?
16+
}
17+
18+
let students: [Student] = [
19+
.init(name: "Marcos", age: 1, country: "Spain", hasPet: true),
20+
.init(name: "Anaïs", age: nil, country: "France", hasPet: false),
21+
.init(name: "Alex", age: 3, country: nil, hasPet: false),
22+
.init(name: "家豪", age: nil, country: "China", hasPet: nil),
23+
.init(name: "Дэниел", age: 5, country: nil, hasPet: nil),
24+
.init(name: "ももこ", age: nil, country: nil, hasPet: nil)
25+
]
26+
27+
let encoder = CSVEncoder { $0.headers = ["name", "age", "country", "hasPet"] }
28+
let result = try encoder.encode(students, into: String.self)
29+
XCTAssertFalse(result.isEmpty)
30+
}
31+
32+
/// Tests writting `nil` values on a keyed containers with integer coding keys.
33+
func testOptionalIntegerFields() throws {
34+
struct Student: Encodable {
35+
let name: String, age: Int?, country: String?, hasPet: Bool?
36+
private enum CodingKeys: Int, CodingKey {
37+
case name=0, age, country, hasPet
38+
}
39+
}
40+
41+
let students: [Student] = [
42+
.init(name: "Marcos", age: 1, country: "Spain", hasPet: true),
43+
.init(name: "Anaïs", age: nil, country: "France", hasPet: false),
44+
.init(name: "Alex", age: 3, country: nil, hasPet: false),
45+
.init(name: "家豪", age: nil, country: "China", hasPet: nil),
46+
.init(name: "Дэниел", age: 5, country: nil, hasPet: nil),
47+
.init(name: "ももこ", age: nil, country: nil, hasPet: nil)
48+
]
49+
50+
let encoder = CSVEncoder()
51+
let result = try encoder.encode(students, into: String.self)
52+
XCTAssertFalse(result.isEmpty)
53+
XCTAssertTrue(result.hasPrefix("Marcos,1,Spain,true\nAnaïs,,France,false"))
54+
}
55+
56+
/// Tests writting `nil` values on a keyed containers with named coding keys.
57+
func testOptionalNamedFieldsWithCustomStrategy() throws {
58+
struct Student: Encodable {
59+
let name: String, age: Int?, country: String?, hasPet: Bool?
60+
}
61+
62+
let students: [Student] = [
63+
.init(name: "Marcos", age: 1, country: "Spain", hasPet: true),
64+
.init(name: "Anaïs", age: nil, country: "France", hasPet: false),
65+
.init(name: "Alex", age: 3, country: nil, hasPet: false),
66+
.init(name: "家豪", age: nil, country: "China", hasPet: nil),
67+
.init(name: "Дэниел", age: 5, country: nil, hasPet: nil),
68+
.init(name: "ももこ", age: nil, country: nil, hasPet: nil)
69+
]
70+
71+
let encoder = CSVEncoder {
72+
$0.headers = ["name", "age", "country", "hasPet"]
73+
$0.nilStrategy = .custom({
74+
var container = $0.singleValueContainer()
75+
try container.encode("null")
76+
})
77+
}
78+
let result = try encoder.encode(students, into: String.self)
79+
XCTAssertFalse(result.isEmpty)
80+
XCTAssertTrue(result.contains("null"))
81+
}
82+
}

tests/CodableTests/EncodingRegularUsageTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ extension EncodingRegularUsageTests {
118118
let encoder = CSVEncoder(configuration: configuration)
119119
let data = try encoder.encode(value)
120120
let string = String(data: data, encoding: encoding)!
121-
XCTAssertEqual(string, "\"\"\(delimiters.row.rawValue)")
121+
XCTAssertEqual(string, "\(delimiters.row.rawValue)")
122122
}
123123
}
124124

0 commit comments

Comments
 (0)