Skip to content

Commit 8dfa886

Browse files
committed
Improved CSVReader & CSVWriter API
1 parent 7111631 commit 8dfa886

25 files changed

+1138
-911
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ let reader = CSVReader(data: ...) {
108108

109109
<details><summary><code>CSVWriter</code>.</summary><p>
110110

111-
#warning("Complete me")
111+
#warning("TODO:")
112112

113113
</p></details>
114114
</ul>
@@ -163,7 +163,7 @@ decoder.decimalStratey = .custom {
163163

164164
<details><summary><code>CSVEncoder</code>.</summary><p>
165165

166-
#warning("Complete me")
166+
#warning("TODO:")
167167

168168
</p></details>
169169
</ul>
@@ -337,13 +337,13 @@ struct Student: Codable {
337337

338338
<details><summary>Configuration values and encoding/decoding strategies.</summary><p>
339339

340-
#warning("Complete me")
340+
#warning("TODO:")
341341

342342
</p></details>
343343

344344
<details><summary>Performance advices.</summary><p>
345345

346-
#warning("Complete me")
346+
#warning("TODO:")
347347

348348
</p></details>
349349
</ul>

Sources/Active/Reader/Reader.swift

Lines changed: 2 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -31,84 +31,13 @@ public final class CSVReader: IteratorProtocol, Sequence {
3131
/// - If a CSV file has a header, the first row after a header (i.e. the first actual data row) will be the integer zero.
3232
/// - If a CSV file doesn't have a header, the first row to parse will also be zero.
3333
public var rowIndex: Int { let r = self.count.rows; return self.headers.isEmpty ? r : r - 1 }
34-
35-
/// Creates a reader instance that will be used to parse the given `String`.
36-
/// - parameter string: A `String` containing CSV formatted data.
37-
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. encoding, delimiters, etc.).
38-
/// - throws: `CSVError<CSVReader>` exclusively.
39-
public convenience init(string: String, configuration: Configuration = .init()) throws {
40-
let buffer = ScalarBuffer(reservingCapacity: 8)
41-
let iterator = ScalarIterator(scalarIterator: string.unicodeScalars.makeIterator())
42-
try self.init(configuration: configuration, buffer: buffer, iterator: iterator)
43-
}
44-
45-
/// Creates a reader instance that will be used to parse the given data blob.
46-
///
47-
/// If the configuration's encoding hasn't been set and the input data doesn't contain a Byte Order Marker (BOM), UTF8 is presumed.
48-
/// - parameter data: A data blob containing CSV formatted data.
49-
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. encoding, delimiters, etc.).
50-
/// - throws: `CSVError<CSVReader>` exclusively.
51-
public convenience init(data: Data, configuration: Configuration = .init()) throws {
52-
if configuration.presample, let dataEncoding = configuration.encoding {
53-
// A. If the `presample` configuration has been set and the user has explicitly mark an encoding, then the data can parsed into a string.
54-
guard let string = String(data: data, encoding: dataEncoding) else { throw Error.mismatched(encoding: dataEncoding) }
55-
try self.init(string: string, configuration: configuration)
56-
} else {
57-
// B. Otherwise, start parsing byte-by-byte.
58-
let buffer = ScalarBuffer(reservingCapacity: 8)
59-
// B.1. Check whether the input data has a BOM.
60-
var dataIterator = data.makeIterator()
61-
let (inferredEncoding, unusedBytes) = String.Encoding.infer(from: &dataIterator)
62-
// B.2. Select the appropriate encoding depending from the user provided encoding (if any), and the BOM encoding (if any).
63-
let encoding = try String.Encoding.selectFrom(provided: configuration.encoding, inferred: inferredEncoding)
64-
// B.3. Create the scalar iterator producing all `Unicode.Scalar`s from the data bytes.
65-
let iterator = try ScalarIterator(iterator: dataIterator, encoding: encoding, firstBytes: unusedBytes)
66-
try self.init(configuration: configuration, buffer: buffer, iterator: iterator)
67-
}
68-
}
69-
70-
/// Creates a reader instance that will be used to parse the given CSV file.
71-
///
72-
/// If the configuration's encoding hasn't been set and the input data doesn't contain a Byte Order Marker (BOM), UTF8 is presumed.
73-
/// - parameter fileURL: The URL indicating the location of the file to be parsed.
74-
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. encoding, delimiters, etc.).
75-
/// - throws: `CSVError<CSVReader>` exclusively.
76-
public convenience init(fileURL: URL, configuration: Configuration = .init()) throws {
77-
if configuration.presample {
78-
// A. If the `presample` configuration has been set, the file can be completely load into memory.
79-
try self.init(data: try Data(contentsOf: fileURL), configuration: configuration); return
80-
} else {
81-
// B. Otherwise, create an input stream and start parsing byte-by-byte.
82-
guard let stream = InputStream(url: fileURL) else { throw Error.invalidFile(url: fileURL) }
83-
// B.1. Open the stream for usage.
84-
assert(stream.streamStatus == .notOpen)
85-
stream.open()
86-
87-
let (encoding, unusedBytes): (String.Encoding, [UInt8])
88-
do {
89-
// B.2. Check whether the input data has a BOM.
90-
let inferred = try String.Encoding.infer(from: stream)
91-
// B.3. Select the appropriate encoding depending from the user provided encoding (if any), and the BOM encoding (if any).
92-
encoding = try String.Encoding.selectFrom(provided: configuration.encoding, inferred: inferred.encoding)
93-
unusedBytes = inferred.unusedBytes
94-
} catch let error {
95-
if stream.streamStatus != .closed { stream.close() }
96-
throw error
97-
}
98-
99-
// B.5. Create the scalar buffer & iterator producing all `Unicode.Scalar`s from the data bytes.
100-
let buffer = ScalarBuffer(reservingCapacity: 8)
101-
let iterator = try ScalarIterator(stream: stream, encoding: encoding, chunk: 1024, firstBytes: unusedBytes)
102-
try self.init(configuration: configuration, buffer: buffer, iterator: iterator)
103-
}
104-
}
10534

10635
/// Designated initializer for the CSV reader.
10736
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. encoding, delimiters, etc.).
10837
/// - parameter buffer: A buffer storing in-flight `Unicode.Scalar`s.
10938
/// - parameter iterator: An iterator providing the CSV `Unicode.Scalar`s.
11039
/// - throws: `CSVError<CSVReader>` exclusively.
111-
private init(configuration: Configuration, buffer: ScalarBuffer, iterator: ScalarIterator) throws {
40+
internal init(configuration: Configuration, buffer: ScalarBuffer, iterator: ScalarIterator) throws {
11241
self.configuration = configuration
11342
self.settings = try Settings(configuration: configuration, iterator: iterator, buffer: buffer)
11443
(self.headers, self.headerLookup) = (.init(), nil)
@@ -127,7 +56,7 @@ public final class CSVReader: IteratorProtocol, Sequence {
12756
self.headers = headers
12857
self.count = (rows: 1, fields: headers.count)
12958
// case .unknown:
130-
// #warning("TODO")
59+
// #warning("TODO:")
13160
}
13261
}
13362
}
@@ -340,22 +269,6 @@ extension CSVReader {
340269
}
341270

342271
fileprivate extension CSVReader.Error {
343-
/// The given `String.Encoding` is not yet supported by the library.
344-
/// - parameter encoding: The desired byte representatoion.
345-
static func mismatched(encoding: String.Encoding) -> CSVError<CSVReader> {
346-
.init(.invalidConfiguration,
347-
reason: "The data blob didn't match the given string encoding.",
348-
help: "Let the reader infer the encoding or make sure the data blob is correctly formatted.",
349-
userInfo: ["Encoding": encoding])
350-
}
351-
/// Error raised when an input stream cannot be created to the indicated file URL.
352-
/// - parameter url: The URL address of the invalid file.
353-
static func invalidFile(url: URL) -> CSVError<CSVReader> {
354-
.init(.streamFailure,
355-
reason: "Creating an input stream to the given file URL failed.",
356-
help: "Make sure the URL is valid and you are allowed to access the file. Alternatively set the configuration's presample or load the file in a data blob and use the reader's data initializer.",
357-
userInfo: ["File URL": url])
358-
}
359272
/// Error raised when a header was required, but the line was empty.
360273
static func invalidEmptyHeader() -> CSVError<CSVReader> {
361274
.init(.invalidConfiguration,

0 commit comments

Comments
 (0)