Skip to content

Commit f4741b2

Browse files
committed
Reenable Decodable tests and new convenience parsing result added
1 parent 4ed8f45 commit f4741b2

File tree

11 files changed

+599
-412
lines changed

11 files changed

+599
-412
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ There are two ways to use [CodableCSV](https://github.com/dehesa/CodableCSV):
3333

3434
The _active entities_ provide _imperative_ control on how to read or write CSV data.
3535

36+
<ul>
3637
<details><summary><code>CSVReader</code>.</summary><p>
3738

3839
A `CSVReadder` parses CSV data from an input and returns you each CSV row as an array of strings.
@@ -106,11 +107,13 @@ let reader = CSVReader(data: ...) {
106107
#warning("Complete me")
107108

108109
</p></details>
110+
</ul>
109111

110112
## Swift's `Codable`
111113

112114
The encoders/decoders provided by this library let you use Swift's `Codable` declarative approach to encode/decode CSV data.
113115

116+
<ul>
114117
<details><summary><code>CSVDecoder</code>.</summary><p>
115118

116119
`CSVDecoder` transforms CSV data into a Swift type conforming to `Decodable`. The decoding process is very simple and it only requires creating a decoding instance and call its `decode` function passing the `Decodable` type and the input data.
@@ -159,11 +162,13 @@ decoder.decimalStratey = .custom {
159162
#warning("Complete me")
160163

161164
</p></details>
165+
</ul>
162166

163167
## Tips Using `Codable`
164168

165169
`Codable` is fairly easy to use and most Swift standard library types already conform to it. However, sometimes it is tricky to get custom types to comply to `Codable` for very specific functionality. That is why I am leaving here some tips and advices concerning its usage:
166170

171+
<ul>
167172
<details><summary>Basic adoption.</summary><p>
168173

169174
`Codable` is just a type alias for `Decodable` and `Encodable`. When a custom type conforms to `Codable`, the type is stating that it has the ability to decode itself from and encode itself to a external representation. Which representation depends on the decoder or encoder chosen. Foundation provides support for [JSON and Property Lists](https://developer.apple.com/documentation/foundation/archives_and_serialization), but the community provide many other formats, such as: [YAML](https://github.com/jpsim/Yams), [XML](https://github.com/MaxDesiatov/XMLCoder), [BSON](https://github.com/OpenKitten/BSON), and CSV (through this library).
@@ -295,7 +300,7 @@ extension Pet.Gender {
295300
}
296301
}
297302

298-
private RowKeys: Int, CodingKey {
303+
private CustomKeys: Int, CodingKey {
299304
case name = 0
300305
case age = 1
301306
case nickname = 2
@@ -335,6 +340,7 @@ struct Student: Codable {
335340
#warning("Complete me")
336341

337342
</p></details>
343+
</ul>
338344

339345
# Roadmap
340346

Sources/Active/Reader/Reader.swift

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public final class CSVReader: IteratorProtocol, Sequence {
1111
///
1212
/// If empty, the file contained no headers.
1313
private(set) public var headers: [String]
14+
/// Lookup dictionary providing fast index discovery for header names.
15+
private(set) internal var headerLookup: [Int:Int]?
1416
/// Unicode scalar buffer to keep scalars that hasn't yet been analysed.
1517
private let buffer: ScalarBuffer
1618
/// The unicode scalar iterator providing all input data.
@@ -109,7 +111,7 @@ public final class CSVReader: IteratorProtocol, Sequence {
109111
private init(configuration: Configuration, buffer: ScalarBuffer, iterator: ScalarIterator) throws {
110112
self.configuration = configuration
111113
self.settings = try Settings(configuration: configuration, iterator: iterator, buffer: buffer)
112-
self.headers = .init()
114+
(self.headers, self.headerLookup) = (.init(), nil)
113115
self.buffer = buffer
114116
self.iterator = iterator
115117
self.isFieldDelimiter = CSVReader.makeMatcher(delimiter: self.settings.delimiters.field, buffer: self.buffer, iterator: self.iterator)
@@ -134,10 +136,30 @@ extension CSVReader {
134136
/// Advances to the next row and returns it, or `nil` if no next row exists.
135137
/// - warning: If the CSV file being parsed contains invalid characters, this function will crash. For safer parsing use `parseRow()`.
136138
/// - seealso: parseRow()
137-
public func next() -> [String]? {
139+
@inlinable public func next() -> [String]? {
138140
return try! self.parseRow()
139141
}
140142

143+
/// Parses a CSV row and wraps it in a convenience structure giving accesses to fields through header titles/names.
144+
///
145+
/// Since CSV parsing is sequential, if a previous call of this function encountered an error, subsequent calls will throw the same error.
146+
/// - throws: `CSVReader.Error` exclusively.
147+
/// - returns: A record structure or `nil` if there isn't anything else to parse. If a record is returned there shall always be at least one field.
148+
/// - seealso: parseRow()
149+
public func parseRecord() throws -> Record? {
150+
guard let row = try self.parseRow() else { return nil }
151+
152+
let lookup: [Int:Int]
153+
if let l = self.headerLookup {
154+
lookup = l
155+
} else {
156+
lookup = try self.makeHeaderLookup()
157+
self.headerLookup = lookup
158+
}
159+
160+
return .init(row: row, lookup: lookup)
161+
}
162+
141163
/// Parses a CSV row.
142164
///
143165
/// Since CSV parsing is sequential, if a previous call of this function encountered an error, subsequent calls will throw the same error.
@@ -175,7 +197,19 @@ extension CSVReader {
175197
}
176198
}
177199

178-
fileprivate extension CSVReader {
200+
extension CSVReader {
201+
/// Creates the lookup dictionary from the headers row.
202+
internal func makeHeaderLookup() throws -> [Int:Int] {
203+
var result: [Int:Int] = .init(minimumCapacity: self.headers.count)
204+
for (index, header) in self.headers.enumerated() {
205+
let hash = header.hashValue
206+
guard case .none = result.updateValue(index, forKey: hash) else {
207+
throw CSVReader.Error.invalidHashableHeader()
208+
}
209+
}
210+
return result
211+
}
212+
179213
/// Parses a CSV row.
180214
/// - throws: `CSVReader.Error.invalidInput` exclusively.
181215
/// - returns: The row's fields or `nil` if there isn't anything else to parse. The row will never be an empty array.
@@ -327,6 +361,12 @@ fileprivate extension CSVReader.Error {
327361
reason: "A header line was expected, but an empty line was found instead.",
328362
help: "Make sure there is a header line at the very beginning of the file or mark the configuration as 'no header'.")
329363
}
364+
/// Error raised when a record is fetched, but the there are header names which has the same hash value (i.e. they have the same name).
365+
static func invalidHashableHeader() -> CSVReader.Error {
366+
.init(.invalidInput,
367+
reason: "The header row contain two fields with the same value.",
368+
help: "Request a row instead of a record.")
369+
}
330370
/// Error raised when the number of fields are not kept constant between CSV rows.
331371
/// - parameter rowIndex: The location of the row which generated the error.
332372
/// - parameter parsed: The number of parsed fields.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import Foundation
2+
3+
extension CSVReader {
4+
/// Creates a reader instance that will be used to parse the given `String`.
5+
/// - parameter string: A `String` containing CSV formatted data.
6+
/// - parameter configuration: Closure receiving the default parsing configuration values and letting you change them.
7+
/// - throws: `CSVReader.Error` exclusively.
8+
@inlinable public convenience init(string: String, configuration: (inout Configuration)->Void) throws {
9+
var config = Configuration()
10+
configuration(&config)
11+
try self.init(string: string, configuration: config)
12+
}
13+
14+
/// Creates a reader instance that will be used to parse the given data blob.
15+
/// - parameter data: A data blob containing CSV formatted data.
16+
/// - parameter configuration: Closure receiving the default parsing configuration values and letting you change them.
17+
/// - throws: `CSVReader.Error` exclusively.
18+
@inlinable public convenience init(data: Data, configuration: (inout Configuration)->Void) throws {
19+
var config = Configuration()
20+
configuration(&config)
21+
try self.init(data: data, configuration: config)
22+
}
23+
24+
/// Creates a reader instance that will be used to parse the given CSV file.
25+
/// - parameter fileURL: The URL indicating the location of the file to be parsed.
26+
/// - parameter configuration: Closure receiving the default parsing configuration values and letting you change them.
27+
/// - throws: `CSVReader.Error` exclusively.
28+
@inlinable public convenience init(fileURL: URL, configuration: (inout Configuration)->Void) throws {
29+
var config = Configuration()
30+
configuration(&config)
31+
try self.init(fileURL: fileURL, configuration: config)
32+
}
33+
}
34+
35+
extension CSVReader {
36+
/// Reads the Swift String and returns the CSV headers (if any) and all the records.
37+
/// - parameter string: A `String` value containing CSV formatted data.
38+
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. delimiters, date strategy, etc.).
39+
/// - throws: `CSVReader.Error` exclusively.
40+
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
41+
public static func parse(string: String, configuration: Configuration = .init()) throws -> Output {
42+
let reader = try CSVReader(string: string, configuration: configuration)
43+
let lookup = try reader.makeHeaderLookup()
44+
45+
var result: [[String]] = .init()
46+
while let row = try reader.parseRow() {
47+
result.append(row)
48+
}
49+
50+
return .init(headers: reader.headers, rows: result, lookup: lookup)
51+
}
52+
53+
/// Reads a blob of data using the encoding provided as argument and returns the CSV headers (if any) and all the CSV records.
54+
/// - parameter data: A blob of data containing CSV formatted data.
55+
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. delimiters, date strategy, etc.).
56+
/// - throws: `CSVReader.Error` exclusively.
57+
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
58+
public static func parse(data: Data, configuration: Configuration = .init()) throws -> Output {
59+
let reader = try CSVReader(data: data, configuration: configuration)
60+
let lookup = try reader.makeHeaderLookup()
61+
62+
var result: [[String]] = .init()
63+
while let row = try reader.parseRow() {
64+
result.append(row)
65+
}
66+
67+
return .init(headers: reader.headers, rows: result, lookup: lookup)
68+
}
69+
70+
/// Reads a CSV file using the provided encoding and returns the CSV headers (if any) and all the CSV records.
71+
/// - parameter fileURL: The URL indicating the location of the file to be parsed.
72+
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. delimiters, date strategy, etc.).
73+
/// - throws: `CSVReader.Error` exclusively.
74+
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
75+
public static func parse(fileURL: URL, configuration: Configuration = .init()) throws -> Output {
76+
let reader = try CSVReader(fileURL: fileURL, configuration: configuration)
77+
let lookup = try reader.makeHeaderLookup()
78+
79+
var result: [[String]] = .init()
80+
while let row = try reader.parseRow() {
81+
result.append(row)
82+
}
83+
84+
return .init(headers: reader.headers, rows: result, lookup: lookup)
85+
}
86+
}
87+
88+
extension CSVReader {
89+
/// Reads the Swift String and returns the CSV headers (if any) and all the records.
90+
/// - parameter string: A `String` value containing CSV formatted data.
91+
/// - parameter configuration: Closure receiving the default parsing configuration values and letting you change them.
92+
/// - throws: `CSVReader.Error` exclusively.
93+
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
94+
@inlinable public static func parse(string: String, configuration: (inout Configuration)->Void) throws -> Output {
95+
var config = Configuration()
96+
configuration(&config)
97+
return try CSVReader.parse(string: string, configuration: config)
98+
}
99+
100+
/// Reads a blob of data using the encoding provided as argument and returns the CSV headers (if any) and all the CSV records.
101+
/// - parameter data: A blob of data containing CSV formatted data.
102+
/// - parameter configuration: Closure receiving the default parsing configuration values and letting you change them.
103+
/// - throws: `CSVReader.Error` exclusively.
104+
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
105+
@inlinable public static func parse(data: Data, configuration: (inout Configuration)->Void) throws -> Output {
106+
var config = Configuration()
107+
configuration(&config)
108+
return try CSVReader.parse(data: data, configuration: config)
109+
}
110+
111+
/// Reads a CSV file using the provided encoding and returns the CSV headers (if any) and all the CSV records.
112+
/// - parameter fileURL: The URL indicating the location of the file to be parsed.
113+
/// - parameter configuration: Closure receiving the default parsing configuration values and letting you change them.
114+
/// - throws: `CSVReader.Error` exclusively.
115+
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
116+
@inlinable public static func parse(fileURL: URL, configuration: (inout Configuration)->Void) throws -> Output {
117+
var config = Configuration()
118+
configuration(&config)
119+
return try CSVReader.parse(fileURL: fileURL, configuration: config)
120+
}
121+
}

0 commit comments

Comments
 (0)