Skip to content

Commit

Permalink
Prepare 0.5.2 release
Browse files Browse the repository at this point in the history
  • Loading branch information
dehesa committed Mar 31, 2020
1 parent c9cbe41 commit 5ae5412
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 220 deletions.
2 changes: 1 addition & 1 deletion CodableCSV.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'CodableCSV'
s.version = '0.5.1'
s.version = '0.5.2'
s.summary = "Read and write CSV files row-by-row or through Swift's Codable interface."

s.homepage = 'https://github.com/dehesa/CodableCSV'
Expand Down
10 changes: 2 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,7 @@ let package = Package(
],
dependencies: [],
targets: [
.target(
name: "CodableCSV",
dependencies: [],
path: "sources"),
.testTarget(
name: "CodableCSVTests",
dependencies: ["CodableCSV"],
path: "tests"),
.target(name: "CodableCSV", dependencies: [], path: "sources"),
.testTarget(name: "CodableCSVTests", dependencies: ["CodableCSV"], path: "tests"),
]
)
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ A `CSVReadder` parses CSV data from a given input (`String`, or `Data`, or file)

```swift
let data: Data = ...
let result = try CSVReader.parse(input: data)
let result = try CSVReader.decode(input: data)

// `result` lets you access the CSV headers, all CSV rows, or access a specific row/record. For example:
let headers = result.headers // [String]
Expand All @@ -100,23 +100,23 @@ A `CSVReadder` parses CSV data from a given input (`String`, or `Data`, or file)
let reader = try CSVReader(input: string) { $0.headerStrategy = .firstLine }

let headers = reader.headers // ["numA", "numB", "numC"]
let rowA = try reader.parseRow() // ["1", "2", "3"]
let rowB = try reader.parseRow() // ["4", "5", "6"]
let rowA = try reader.readRow() // ["1", "2", "3"]
let rowB = try reader.readRow() // ["4", "5", "6"]
```

Alternatively you can use the `parseRecord()` function which also returns the next CSV row, but it wraps the result in a convenience structure. This structure lets you access each field with the header name (as long as the `headerStrategy` is marked with `.firstLine`).
Alternatively you can use the `readRecord()` function which also returns the next CSV row, but it wraps the result in a convenience structure. This structure lets you access each field with the header name (as long as the `headerStrategy` is marked with `.firstLine`).

```swift
let reader = try CSVReader(input: string) { $0.headerStrategy = .firstLine }

let headers = reader.headers // ["numA", "numB", "numC"]

let recordA = try reader.parseRecord()
let recordA = try reader.readRecord()
let rowA = recordA.row // ["1", "2", "3"]
let firstField = recordA[0] // "1"
let secondField = recordA["numB"] // "2"

let recordB = try reader.parseRecord()
let recordB = try reader.readRecord()
```

- `Sequence` syntax parsing.
Expand All @@ -128,7 +128,7 @@ A `CSVReadder` parses CSV data from a given input (`String`, or `Data`, or file)
}
```

Please note the `Sequence` syntax (i.e. `IteratorProtocol`) doesn't throw errors; therefore if the CSV data is invalid, the previous code will crash. If you don't control the CSV data origin, use `parseRow()` instead.
Please note the `Sequence` syntax (i.e. `IteratorProtocol`) doesn't throw errors; therefore if the CSV data is invalid, the previous code will crash. If you don't control the CSV data origin, use `readRow()` instead.

### Reader configuration

Expand Down Expand Up @@ -315,11 +315,11 @@ let result = try decoder.decode(CustomType.self, from: data)
`CSVDecoder` can decode CSVs represented as a `Data` blob, a `String`, or an actual file in the file system.

```swift
let decoder = CSVDecoder { $0.bufferingStrategy = .assembled }
let decoder = CSVDecoder { $0.bufferingStrategy = .sequential }
let content: [Student] = try decoder([Student].self, from: URL("~/Desktop/Student.csv"))
```

If you are dealing with a big CSV file, it is preferred to used direct file decoding, a `.sequential` or `.assembled` buffering strategy, and set *presampling* to false; since then memory usage is drastically reduced.
If you are dealing with a big CSV file, it is preferred to used direct file decoding, a `.sequential` or `.unrequested` buffering strategy, and set *presampling* to false; since then memory usage is drastically reduced.

### Decoder configuration

Expand Down
14 changes: 7 additions & 7 deletions sources/Active/Reader/Reader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,20 @@ public final class CSVReader: IteratorProtocol, Sequence {

extension CSVReader {
/// Advances to the next row and returns it, or `nil` if no next row exists.
/// - warning: If the CSV file being parsed contains invalid characters, this function will crash. For safer parsing use `parseRow()`.
/// - seealso: parseRow()
/// - warning: If the CSV file being parsed contains invalid characters, this function will crash. For safer parsing use `readRow()`.
/// - seealso: readRow()
@inlinable public func next() -> [String]? {
return try! self.parseRow()
return try! self.readRow()
}

/// Parses a CSV row and wraps it in a convenience structure giving accesses to fields through header titles/names.
///
/// Since CSV parsing is sequential, if a previous call of this function encountered an error, subsequent calls will throw the same error.
/// - throws: `CSVError<CSVReader>` exclusively.
/// - 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.
/// - seealso: parseRow()
public func parseRecord() throws -> Record? {
guard let row = try self.parseRow() else { return nil }
/// - seealso: readRow()
public func readRecord() throws -> Record? {
guard let row = try self.readRow() else { return nil }

let lookup: [Int:Int]
if let l = self.headerLookup {
Expand All @@ -96,7 +96,7 @@ extension CSVReader {
/// Since CSV parsing is sequential, if a previous call of this function encountered an error, subsequent calls will throw the same error.
/// - throws: `CSVError<CSVReader>` exclusively.
/// - returns: The row's fields or `nil` if there isn't anything else to parse. The row will never be an empty array.
public func parseRow() throws -> [String]? {
public func readRow() throws -> [String]? {
switch self.status {
case .active: break
case .finished: return nil
Expand Down
80 changes: 62 additions & 18 deletions sources/Active/Reader/ReaderAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ extension CSVReader {
let decoder = CSVReader.makeDecoder(from: input.unicodeScalars.makeIterator())
try self.init(configuration: configuration, buffer: buffer, decoder: decoder)
}

/// Creates a reader instance that will be used to parse the given data blob.
///
/// If the configuration's encoding hasn't been set and the input data doesn't contain a Byte Order Marker (BOM), UTF8 is presumed.
Expand All @@ -35,7 +35,7 @@ extension CSVReader {
try self.init(configuration: configuration, buffer: buffer, decoder: decoder)
}
}

/// Creates a reader instance that will be used to parse the given CSV file.
///
/// If the configuration's encoding hasn't been set and the input data doesn't contain a Byte Order Marker (BOM), UTF8 is presumed.
Expand Down Expand Up @@ -84,7 +84,7 @@ extension CSVReader {
setter(&configuration)
try self.init(input: input, configuration: configuration)
}

/// Creates a reader instance that will be used to parse the given data blob.
/// - parameter input: A data blob containing CSV formatted data.
/// - parameter setter: Closure receiving the default parsing configuration values and letting you change them.
Expand All @@ -95,7 +95,7 @@ extension CSVReader {
setter(&configuration)
try self.init(input: input, configuration: configuration)
}

/// Creates a reader instance that will be used to parse the given CSV file.
/// - parameter input: The URL indicating the location of the file to be parsed.
/// - parameter setter: Closure receiving the default parsing configuration values and letting you change them.
Expand All @@ -116,46 +116,46 @@ extension CSVReader {
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. delimiters, date strategy, etc.).
/// - throws: `CSVError<CSVReader>` exclusively.
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
public static func parse<S>(input: S, configuration: Configuration = .init()) throws -> Output where S:StringProtocol {
public static func decode<S>(input: S, configuration: Configuration = .init()) throws -> Output where S:StringProtocol {
let reader = try CSVReader(input: input, configuration: configuration)
let lookup = try reader.headers.lookupDictionary(onCollision: Error.invalidHashableHeader)

var result: [[String]] = .init()
while let row = try reader.parseRow() {
while let row = try reader.readRow() {
result.append(row)
}

return .init(headers: reader.headers, rows: result, lookup: lookup)
}

/// Reads a blob of data using the encoding provided as argument and returns the CSV headers (if any) and all the CSV records.
/// - parameter input: A blob of data containing CSV formatted data.
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. delimiters, date strategy, etc.).
/// - throws: `CSVError<CSVReader>` exclusively.
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
public static func parse(input: Data, configuration: Configuration = .init()) throws -> Output {
public static func decode(input: Data, configuration: Configuration = .init()) throws -> Output {
let reader = try CSVReader(input: input, configuration: configuration)
let lookup = try reader.headers.lookupDictionary(onCollision: Error.invalidHashableHeader)

var result: [[String]] = .init()
while let row = try reader.parseRow() {
while let row = try reader.readRow() {
result.append(row)
}

return .init(headers: reader.headers, rows: result, lookup: lookup)
}

/// Reads a CSV file using the provided encoding and returns the CSV headers (if any) and all the CSV records.
/// - parameter input: The URL indicating the location of the file to be parsed.
/// - parameter configuration: Recipe detailing how to parse the CSV data (i.e. delimiters, date strategy, etc.).
/// - throws: `CSVError<CSVReader>` exclusively.
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
public static func parse(input: URL, configuration: Configuration = .init()) throws -> Output {
public static func decode(input: URL, configuration: Configuration = .init()) throws -> Output {
let reader = try CSVReader(input: input, configuration: configuration)
let lookup = try reader.headers.lookupDictionary(onCollision: Error.invalidHashableHeader)

var result: [[String]] = .init()
while let row = try reader.parseRow() {
while let row = try reader.readRow() {
result.append(row)
}

Expand All @@ -170,10 +170,10 @@ extension CSVReader {
/// - parameter configuration: Default configuration values for the `CSVReader`.
/// - throws: `CSVError<CSVReader>` exclusively.
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
@inlinable public static func parse<S>(input: S, setter: (_ configuration: inout Configuration)->Void) throws -> Output where S:StringProtocol {
@inlinable public static func decode<S>(input: S, setter: (_ configuration: inout Configuration)->Void) throws -> Output where S:StringProtocol {
var configuration = Configuration()
setter(&configuration)
return try CSVReader.parse(input: input, configuration: configuration)
return try CSVReader.decode(input: input, configuration: configuration)
}

/// Reads a blob of data using the encoding provided as argument and returns the CSV headers (if any) and all the CSV records.
Expand All @@ -182,10 +182,10 @@ extension CSVReader {
/// - parameter configuration: Default configuration values for the `CSVReader`.
/// - throws: `CSVError<CSVReader>` exclusively.
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
@inlinable public static func parse(input: Data, setter: (_ configuration: inout Configuration)->Void) throws -> Output {
@inlinable public static func decode(input: Data, setter: (_ configuration: inout Configuration)->Void) throws -> Output {
var configuration = Configuration()
setter(&configuration)
return try CSVReader.parse(input: input, configuration: configuration)
return try CSVReader.decode(input: input, configuration: configuration)
}

/// Reads a CSV file using the provided encoding and returns the CSV headers (if any) and all the CSV records.
Expand All @@ -194,10 +194,10 @@ extension CSVReader {
/// - parameter configuration: Default configuration values for the `CSVReader`.
/// - throws: `CSVError<CSVReader>` exclusively.
/// - returns: Tuple with the CSV headers (empty if none) and all records within the CSV file.
@inlinable public static func parse(input: URL, setter: (_ configuration: inout Configuration)->Void) throws -> Output {
@inlinable public static func decode(input: URL, setter: (_ configuration: inout Configuration)->Void) throws -> Output {
var configuration = Configuration()
setter(&configuration)
return try CSVReader.parse(input: input, configuration: configuration)
return try CSVReader.decode(input: input, configuration: configuration)
}
}

Expand Down Expand Up @@ -227,3 +227,47 @@ fileprivate extension CSVReader.Error {
help: "Request a row instead of a record.")
}
}

// MARK: - Deprecations

extension CSVReader {
@available(*, deprecated, renamed: "readRecord()")
public func parseRecord() throws -> Record? {
try self.readRecord()
}

@available(*, deprecated, renamed: "readRow()")
public func parseRow() throws -> [String]? {
try self.readRow()
}

@available(*, deprecated, renamed: "decode(input:configuration:)")
public static func parse<S>(input: S, configuration: Configuration = .init()) throws -> Output where S:StringProtocol {
try self.decode(input: input, configuration: configuration)
}

@available(*, deprecated, renamed: "decode(rows:into:configuration:)")
public static func parse(input: Data, configuration: Configuration = .init()) throws -> Output {
try self.decode(input: input, configuration: configuration)
}

@available(*, deprecated, renamed: "decode(rows:into:configuration:)")
public static func parse(input: URL, configuration: Configuration = .init()) throws -> Output {
try self.decode(input: input, configuration: configuration)
}

@available(*, deprecated, renamed: "decode(rows:setter:)")
public static func parse<S>(input: S, setter: (_ configuration: inout Configuration)->Void) throws -> Output where S:StringProtocol {
try self.decode(input: input, setter: setter)
}

@available(*, deprecated, renamed: "decode(rows:into:setter:)")
public static func parse(input: Data, setter: (_ configuration: inout Configuration)->Void) throws -> Output {
try self.decode(input: input, setter: setter)
}

@available(*, deprecated, renamed: "decode(rows:into:append:setter:)")
public static func parse(input: URL, setter: (_ configuration: inout Configuration)->Void) throws -> Output {
try self.decode(input: input, setter: setter)
}
}
2 changes: 1 addition & 1 deletion sources/Active/Writer/WriterAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ fileprivate extension CSVWriter.Error {
}
}

// MARK: -
// MARK: - Deprecations

extension CSVWriter {
@available(*, deprecated, renamed: "encode(rows:configuration:)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,17 +237,17 @@ private extension ShadowDecoder.SingleValueContainer {

switch self.focus {
case .field(let rowIndex, let fieldIndex):
let string = try source.field(at: rowIndex, fieldIndex)
let string = try source.field(rowIndex, fieldIndex)
return try transform(string) ?! DecodingError.invalid(type: T.self, string: string, codingPath: self.codingPath)
case .row(let rowIndex):
// Values are only allowed to be decoded directly from a single value container in "row level" if the CSV has single column rows.
guard source.numExpectedFields == 1 else { throw DecodingError.invalidNestedRequired(codingPath: self.codingPath) }
let string = try source.field(at: rowIndex, 0)
let string = try source.field(rowIndex, 0)
return try transform(string) ?! DecodingError.invalid(type: T.self, string: string, codingPath: self.codingPath + [IndexKey(0)])
case .file:
// Values are only allowed to be decoded directly from a single value container in "file level" if the CSV file has a single row with a single column.
if source.isRowAtEnd(index: 1), source.numExpectedFields == 1 {
let string = try self.decoder.source.field(at: 0, 0)
let string = try self.decoder.source.field(0, 0)
return try transform(string) ?! DecodingError.invalid(type: T.self, string: string, codingPath: self.codingPath + [IndexKey(0), IndexKey(0)])
} else {
throw DecodingError.invalidNestedRequired(codingPath: self.codingPath)
Expand Down

0 comments on commit 5ae5412

Please sign in to comment.