Skip to content

Commit 5cfe55b

Browse files
committed
LazyEncoder added
1 parent 2db5f56 commit 5cfe55b

14 files changed

+561
-60
lines changed

README.md

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ A `CSVWriter` encodes CSV information into a specified target (i.e. a `String`,
213213
for row in input {
214214
try writer.write(row: row)
215215
}
216-
try writer.endFile()
216+
try writer.endEncoding()
217217
```
218218

219219
Alternatively, you may write directly to a buffer in memory and access its `Data` representation.
@@ -223,7 +223,7 @@ A `CSVWriter` encodes CSV information into a specified target (i.e. a `String`,
223223
for row in input.dropFirst() {
224224
try writer.write(row: row)
225225
}
226-
try writer.endFile()
226+
try writer.endEncoding()
227227
let result = try writer.data()
228228
```
229229

@@ -241,7 +241,7 @@ A `CSVWriter` encodes CSV information into a specified target (i.e. a `String`,
241241
try writer.write(fields: input[2])
242242
try writer.endRow()
243243

244-
try writer.endFile()
244+
try writer.endEncoding()
245245
```
246246

247247
`CSVWriter` has a wealth of low-level imperative APIs, that let you write one field, several fields at a time, end a row, write an empty row, etc.
@@ -330,7 +330,7 @@ let decoder = CSVDecoder()
330330
let result = try decoder.decode(CustomType.self, from: data)
331331
```
332332
333-
`CSVDecoder` can decode CSVs represented as a `Data` blob, a `String`, or an actual file in the file system.
333+
`CSVDecoder` can decode CSVs represented as a `Data` blob, a `String`, an actual file in the file system, or an `InputStream` (e.g. `stdin`).
334334
335335
```swift
336336
let decoder = CSVDecoder { $0.bufferingStrategy = .sequential }
@@ -377,33 +377,33 @@ decoder.decimalStrategy = .custom { (decoder) in
377377
378378
</p></details>
379379
380-
<details><summary><code>CSVDecoder.LazySequence</code>.</summary><p>
380+
<details><summary><code>CSVDecoder.LazyDecoder</code>.</summary><p>
381381
382382
A CSV input can be decoded _on demand_ with the decoder's `lazy(from:)` function.
383383
384384
```swift
385-
var sequence = CSVDecoder().lazy(from: fileURL)
385+
let lazyDecoder = CSVDecoder().lazy(from: fileURL)
386386
while let row = sequence.next() {
387387
let student = try row.decode(Student.self)
388388
// Do something here
389389
}
390390
```
391391
392-
`LazySequence` conforms to Swift's [`Sequence` protocol](https://developer.apple.com/documentation/swift/sequence), letting you use functionality such as `map()`, `allSatisfy()`, etc. Please note, `LazySequence` cannot be used for repeated access. It _consumes_ the input CSV.
392+
`LazyDecoder` conforms to Swift's [`Sequence` protocol](https://developer.apple.com/documentation/swift/sequence), letting you use functionality such as `map()`, `allSatisfy()`, etc. Please note, `LazyDecoder` cannot be used for repeated access. It _consumes_ the input CSV.
393393
394394
```swift
395-
var sequence = decoder.lazy(from: fileData)
396-
let students = try sequence.map { try $0.decode(Student.self) }
395+
let lazyDecoder = CSVDecoder(configuration: config).lazy(from: fileData)
396+
let students = try lazyDecoder.map { try $0.decode(Student.self) }
397397
```
398398
399399
A nice benefit of using the _lazy_ operation, is that it lets you switch how a row is decoded at any point. For example:
400400
```swift
401-
var sequence = decoder.lazy(from: fileString)
402-
let students = zip( 0..<100, sequence) { (_, row) in row.decode(Student.self) }
403-
let teachers = zip(100..<110, sequence) { (_, row) in row.decode(Teacher.self) }
401+
let lazyDecoder = decoder.lazy(from: fileString)
402+
let students = ( 0..<100).map { _ in try lazyDecoder.decode(Student.self) }
403+
let teachers = (100..<110).map { _ in try lazyDecoder.decode(Teacher.self) }
404404
```
405405
406-
Since `LazySequence` exclusively provides sequential access; setting the buffering strategy to `.sequential` will reduce the decoder's memory usage.
406+
Since `LazyDecoder` exclusively provides sequential access; setting the buffering strategy to `.sequential` will reduce the decoder's memory usage.
407407
408408
```swift
409409
let decoder = CSVDecoder {
@@ -420,7 +420,7 @@ let decoder = CSVDecoder {
420420
421421
```swift
422422
let encoder = CSVEncoder()
423-
let data: Data = try encoder.encode(value)
423+
let data = try encoder.encode(value, into: Data.self)
424424
```
425425
426426
The `Encoder`'s `encode()` function creates a CSV file as a `Data` blob, a `String`, or an actual file in the file system.
@@ -472,6 +472,45 @@ encoder.dataStrategy = .custom { (data, encoder) in
472472
473473
> The `.headers` configuration is required if you are using keyed encoding container.
474474
475+
</p></details>
476+
477+
<details><summary><code>CSVEncoder.LazyEncoder</code>.</summary><p>
478+
479+
A series of codable types can be encoded _on demand_ with the encoder's `lazy(into:)` function.
480+
481+
```swift
482+
let lazyEncoder = CSVEncoder().lazy(into: Data.self)
483+
for student in students {
484+
try lazyEncoder.encode(student)
485+
}
486+
let data = try lazyEncoder.endEncoding()
487+
```
488+
489+
Call `endEncoding()` once there is no more values to be encoded. The function will return the encoded CSV.
490+
```swift
491+
let lazyEncoder = CSVEncoder().lazy(into: String.self)
492+
students.forEach {
493+
try lazyEncoder.encode($0)
494+
}
495+
let string = try lazyEncoder.endEncoding()
496+
```
497+
498+
A nice benefit of using the _lazy_ operation, is that it lets you switch how a row is encoded at any point. For example:
499+
```swift
500+
let lazyEncoder = CSVEncoder(configuration: config).lazy(into: fileURL)
501+
students.forEach { try lazyEncoder.encode($0) }
502+
teachers.forEach { try lazyEncoder.encode($0) }
503+
try lazyEncoder.endEncoding()
504+
```
505+
506+
Since `LazyEncoder` exclusively provides sequential encoding; setting the buffering strategy to `.sequential` will reduce the encoder's memory usage.
507+
508+
```swift
509+
let lazyEncoder = CSVEncoder {
510+
$0.bufferingStrategy = .sequential
511+
}.lazy(into: String.self)
512+
```
513+
475514
</p></details>
476515
</ul>
477516

sources/Delimiter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ public enum Delimiter {
77
}
88

99
extension Delimiter {
10-
/// The delimiter between fields/vlaues.
10+
/// The delimiter between fields/values.
1111
public struct Field: ExpressibleByNilLiteral, ExpressibleByStringLiteral, RawRepresentable {
1212
public let rawValue: String.UnicodeScalarView
1313

sources/Deprecated.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,21 @@ extension CSVWriter {
7575
public static func serialize<S:Sequence,C:Collection>(row: S, into fileURL: URL, append: Bool, setter: (_ configuration: inout Configuration) -> Void) throws where S.Element==C, C.Element==String {
7676
try self.encode(rows: row, into: fileURL, append: append, setter: setter)
7777
}
78+
79+
@available(*, deprecated, renamed: "endEncoding()")
80+
public func endFile() throws {
81+
try self.endEncoding()
82+
}
83+
}
84+
85+
extension CSVEncoder {
86+
@available(*, deprecated, renamed: "CSVDecoder.LazyDecoder")
87+
typealias LazySequence = CSVDecoder.LazyDecoder
88+
}
89+
90+
extension CSVEncoder {
91+
@available(*, deprecated, renamed: "encode(_:into:)")
92+
open func encode<T:Encodable>(_ value: T) throws -> Data {
93+
try self.encode(value, into: Data.self)
94+
}
7895
}

sources/declarative/decodable/Decoder.swift

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,31 +69,31 @@ extension CSVDecoder {
6969
}
7070

7171
extension CSVDecoder {
72-
/// Returns a sequence for decoding each row from a CSV file (given as a `Data` blob).
72+
/// Returns a sequence for decoding row-by-row from a CSV file (given as a `Data` blob).
7373
/// - parameter data: The data blob representing a CSV file.
7474
/// - throws: `CSVError<CSVReader>` exclusively.
75-
open func lazy(from data: Data) throws -> LazySequence {
75+
open func lazy(from data: Data) throws -> LazyDecoder {
7676
let reader = try CSVReader(input: data, configuration: self._configuration.readerConfiguration)
7777
let source = ShadowDecoder.Source(reader: reader, configuration: self._configuration, userInfo: self.userInfo)
78-
return LazySequence(source: source)
78+
return LazyDecoder(source: source)
7979
}
8080

81-
/// Returns a sequence for decoding each row from a CSV file (given as a `String`).
81+
/// Returns a sequence for decoding row-by-row from a CSV file (given as a `String`).
8282
/// - parameter string: A Swift string representing a CSV file.
8383
/// - throws: `CSVError<CSVReader>` exclusively.
84-
open func lazy(from string: String) throws -> LazySequence {
84+
open func lazy(from string: String) throws -> LazyDecoder {
8585
let reader = try CSVReader(input: string, configuration: self._configuration.readerConfiguration)
8686
let source = ShadowDecoder.Source(reader: reader, configuration: self._configuration, userInfo: self.userInfo)
87-
return LazySequence(source: source)
87+
return LazyDecoder(source: source)
8888
}
8989

90-
/// Returns a sequence for decoding each row from a CSV file (being pointed by `url`).
90+
/// Returns a sequence for decoding row-by-row from a CSV file (being pointed by `url`).
9191
/// - parameter url: The URL pointing to the file to decode.
9292
/// - throws: `CSVError<CSVReader>` exclusively.
93-
open func lazy(from url: URL) throws -> LazySequence {
93+
open func lazy(from url: URL) throws -> LazyDecoder {
9494
let reader = try CSVReader(input: url, configuration: self._configuration.readerConfiguration)
9595
let source = ShadowDecoder.Source(reader: reader, configuration: self._configuration, userInfo: self.userInfo)
96-
return LazySequence(source: source)
96+
return LazyDecoder(source: source)
9797
}
9898
}
9999

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,47 @@
11
extension CSVDecoder {
2-
/// Swift sequence type giving access to all the "undecoded" CSV rows.
2+
/// Lazy decoder allowing declarative row-by-row decoding.
33
///
44
/// The CSV rows are read _on-demand_ and only decoded when explicitly told so (unlike the default _decode_ functions).
5-
public struct LazySequence: IteratorProtocol, Sequence {
5+
public final class LazyDecoder: IteratorProtocol, Sequence {
66
/// The source of the CSV data.
77
private let _source: ShadowDecoder.Source
88
/// The row to be read (not decoded) next.
9-
private var _currentIndex: Int = 0
9+
private var _currentIndex: Int
10+
/// A dictionary you use to customize the decoding process by providing contextual information.
11+
public var userInfo: [CodingUserInfoKey:Any] { self._source.userInfo }
12+
1013
/// Designated initalizer passing all the required components.
1114
/// - parameter source: The data source for the decoder.
1215
internal init(source: ShadowDecoder.Source) {
1316
self._source = source
17+
self._currentIndex = 0
18+
}
19+
20+
/// Returns a value of the type you specify, decoded from a CSV row.
21+
///
22+
/// This function will throw an error if the file has reached the end. If you are unsure where the CSV file ends, use the `next()` function instead.
23+
/// - parameter type: The type of the value to decode from the supplied file.
24+
/// - returns: A CSV row decoded as a type `T`.
25+
public func decode<T:Decodable>(_ type: T.Type) throws -> T {
26+
guard let rowDecoder = self.next() else { throw CSVDecoder.Error._unexpectedEnd() }
27+
return try rowDecoder.decode(type)
28+
}
29+
30+
/// Returns a value of the type you specify, decoded from a CSV row (if there are still rows to be decoded in the file).
31+
/// - parameter type: The type of the value to decode from the supplied file.
32+
/// - returns: A CSV row decoded as a type `T` or `nil` if the CSV file doesn't contain any more rows.
33+
public func decodeIfPresent<T:Decodable>(_ type: T.Type) throws -> T? {
34+
guard let rowDecoder = self.next() else { return nil }
35+
return try rowDecoder.decode(type)
36+
}
37+
38+
/// Ignores the subsequent row.
39+
public func ignoreRow() {
40+
let _ = self.next()
1441
}
1542

16-
/// Advances to the next row and returns a `LazySequence.Row`, or `nil` if no next row exists.
17-
public mutating func next() -> RowDecoder? {
43+
/// Advances to the next row and returns a `LazyDecoder.Row`, or `nil` if no next row exists.
44+
public func next() -> RowDecoder? {
1845
guard !self._source.isRowAtEnd(index: self._currentIndex) else { return nil }
1946

2047
defer { self._currentIndex += 1 }
@@ -24,7 +51,7 @@ extension CSVDecoder {
2451
}
2552
}
2653

27-
extension CSVDecoder.LazySequence {
54+
extension CSVDecoder.LazyDecoder {
2855
/// Pointer to a row within a CSV file that is able to decode it to a custom type.
2956
public struct RowDecoder {
3057
/// The representation of the decoding process point-in-time.
@@ -36,10 +63,21 @@ extension CSVDecoder.LazySequence {
3663
self._decoder = decoder
3764
}
3865

39-
/// Returns a value of the type you specify, decoded from CSV row.
66+
/// Returns a value of the type you specify, decoded from a CSV row.
4067
/// - parameter type: The type of the value to decode from the supplied file.
4168
@inline(__always) public func decode<T:Decodable>(_ type: T.Type) throws -> T {
4269
return try T(from: self._decoder)
4370
}
4471
}
4572
}
73+
74+
// MARK: -
75+
76+
fileprivate extension CSVDecoder.Error {
77+
/// Error raised when the end of the file has been reached unexpectedly.
78+
static func _unexpectedEnd() -> CSVError<CSVDecoder> {
79+
.init(.invalidPath,
80+
reason: "There are no more rows to decode. The file is at the end.",
81+
help: "Use next() or decodeIfPresent(_:) instead of decode(_:) if you are unsure where the file ends.")
82+
}
83+
}

sources/declarative/encodable/Encoder.swift

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ import Foundation
3333
extension CSVEncoder {
3434
/// Returns a CSV-encoded representation of the value you supply.
3535
/// - parameter value: The value to encode as CSV.
36+
/// - parameter type: The Swift type for a data blob.
3637
/// - returns: `Data` blob with the CSV representation of `value`.
37-
open func encode<T:Encodable>(_ value: T) throws -> Data {
38+
open func encode<T:Encodable>(_ value: T, into type: Data.Type) throws -> Data {
3839
let writer = try CSVWriter(configuration: self._configuration.writerConfiguration)
3940
let sink = try ShadowEncoder.Sink(writer: writer, configuration: self._configuration, userInfo: self.userInfo)
4041
try value.encode(to: ShadowEncoder(sink: sink, codingPath: []))
@@ -44,9 +45,10 @@ extension CSVEncoder {
4445

4546
/// Returns a CSV-encoded representation of the value you supply.
4647
/// - parameter value: The value to encode as CSV.
48+
/// - parameter type: The Swift type for a string.
4749
/// - returns: `String` with the CSV representation of `value`.
48-
open func encode<T:Encodable>(_ value: T, into: String.Type) throws -> String {
49-
let data = try self.encode(value)
50+
open func encode<T:Encodable>(_ value: T, into type: String.Type) throws -> String {
51+
let data = try self.encode(value, into: Data.self)
5052
let encoding = self._configuration.writerConfiguration.encoding ?? .utf8
5153
return String(data: data, encoding: encoding)!
5254
}
@@ -63,6 +65,36 @@ extension CSVEncoder {
6365
}
6466
}
6567

68+
extension CSVEncoder {
69+
/// Returns an instance to encode row-by-row the feeded values.
70+
/// - parameter type: The Swift type for a data blob.
71+
/// - returns: Instance used for _on demand_ encoding.
72+
open func lazy(into type: Data.Type) throws -> LazyEncoder<Data> {
73+
let writer = try CSVWriter(configuration: self._configuration.writerConfiguration)
74+
let sink = try ShadowEncoder.Sink(writer: writer, configuration: self._configuration, userInfo: self.userInfo)
75+
return LazyEncoder<Data>(sink: sink)
76+
}
77+
78+
/// Returns an instance to encode row-by-row the feeded values.
79+
/// - parameter type: The Swift type for a data blob.
80+
/// - returns: Instance used for _on demand_ encoding.
81+
open func lazy(into type: String.Type) throws -> LazyEncoder<String> {
82+
let writer = try CSVWriter(configuration: self._configuration.writerConfiguration)
83+
let sink = try ShadowEncoder.Sink(writer: writer, configuration: self._configuration, userInfo: self.userInfo)
84+
return LazyEncoder<String>(sink: sink)
85+
}
86+
87+
/// Returns an instance to encode row-by-row the feeded values.
88+
/// - parameter fileURL: The file receiving the encoded values.
89+
/// - parameter append: In case an existing file is under the given URL, this Boolean indicates that the information will be appended to the file (`true`), or the file will be overwritten (`false`).
90+
/// - returns: Instance used for _on demand_ encoding.
91+
open func lazy(into fileURL: URL, append: Bool = false) throws -> LazyEncoder<URL> {
92+
let writer = try CSVWriter(fileURL: fileURL, append: append, configuration: self._configuration.writerConfiguration)
93+
let sink = try ShadowEncoder.Sink(writer: writer, configuration: self._configuration, userInfo: self.userInfo)
94+
return LazyEncoder<URL>(sink: sink)
95+
}
96+
}
97+
6698
#if canImport(Combine)
6799
import Combine
68100

0 commit comments

Comments
 (0)