commit
9ed6d6bf07
155
README.md
155
README.md
|
@ -1,3 +1,156 @@
|
|||
# CSV
|
||||
|
||||
A description of this package.
|
||||
A pure Swift CSV parser and serializer, with related encoders and decoders for types that conform to `Codable`.
|
||||
|
||||
## 📦 Swift Package Manager
|
||||
|
||||
The `skelpo/CSV` package can be installed to any project that has an SPM manifest. Add the `.package` instance to the `dependencies` array:
|
||||
|
||||
```swift
|
||||
.package(url: "https://github.com/skelpo/CSV.git", from: "1.0.0")
|
||||
```
|
||||
|
||||
And add the `CSV` target to the dependencies of any target you want to use the package in:
|
||||
|
||||
```swift
|
||||
.target(name: "App", dependencies: ["CSV"])
|
||||
```
|
||||
|
||||
Then run `swift package update` and `swift package generate-xcodeproj` (if you are using Xcode).
|
||||
|
||||
## 🛠 API
|
||||
|
||||
You can find the generated API documentation [here](http://www.skelpo.codes/CSV/). In the mean time, here is a rundown of how the different methods if parsing and serializing work:
|
||||
|
||||
### Parser
|
||||
|
||||
Each type has a basic async version, and a sync wrapper around that. `Parser` is the core async implementation, while `SyncParser` is the wrapper for sync operations.
|
||||
|
||||
To create a `Parser`, you need to pass in a handler for header data and cell data. The header handler takes in a single parameter, which is a byte array (`[UInt8]`) of the header's contents. The cell handler takes in two parameters, the header for the cell, and the cell's contents. These are also both byte arrays. Both of these handlers allowing throwing.
|
||||
|
||||
**Note:** For you to be able to call `.parse(_:length:)`, your parser must be a variable. The method mutates internal state that you can't access. Anoying, I know. Hopefully this gets fixed in the future.
|
||||
|
||||
You can parse the CSV data by passing chunks of data into the `.parser(_:length:)` method, along with the total length of the CSV file that will be parsed. This allows us to parse the last chunk properly instead of never handling it.
|
||||
|
||||
As the data is parsed, the handlers for the parser will be called with the parsed data. The parsing method returns a type `Result<Void, ErrorList>`. This method has been marked `@discarableResult`, so you can ignore the returned value. An `ErrorList` is just a wrapper for an array of errors, which will be the errors thrown from the handler functions, if you ever throw anything from them.
|
||||
|
||||
Here is an example `Parser` instance:
|
||||
|
||||
```swift
|
||||
var data: [String: [String]] = [:]
|
||||
|
||||
var parser = Parser(
|
||||
onHeader: { header in
|
||||
let title = String(decoding: header, as: UTF8.self)
|
||||
data[title] = []
|
||||
},
|
||||
onCell: { header, cell in
|
||||
let title = String(decoding: header, as: UTF8.self)
|
||||
let contents = String(decoding: cell, as: UTF8.self)
|
||||
data[title, default: []].append(contents)
|
||||
}
|
||||
)
|
||||
|
||||
let length = chunks.reduce(0) { $0 + $1.count }
|
||||
for chunk in chunks {
|
||||
parser.parse(chunk, length: length)
|
||||
}
|
||||
```
|
||||
|
||||
If you want to parse a whole CSV document synchronously, you can use `SyncParser` instead. This type has two methods that both take in a byte array and return a dictionary that uses the headers as the keys and the columns are arrays of the cell data. One variation of the method returns the data as byte arrays, and the other returns strings.
|
||||
|
||||
```swift
|
||||
let parser = SyncParser()
|
||||
let data: [String: [String]] = parser.parse(csv)
|
||||
```
|
||||
|
||||
### Serializer
|
||||
|
||||
List the parser types, there is an async `Serializer` type and a corrosponding `SyncSerializer` type. The `Serializer` initializer takes in a row handler, that is called when a row is serialized from the data passed in. This is used for both the header and cell rows.
|
||||
|
||||
The `Serializer.serialize(_:)` method takes in data in the form of a `KeyedCollection` with variaous generic constrainst. You can just pass in a dictionary of type `[String: [String]]` or `[[UInt8]: [[UInt8]]]`. The protocol is mostly for testing purposes within the package test suite.
|
||||
|
||||
If you serialize chunks of parsed data, you will need to make sure that the columns are all the same length in the data passed in, or the method will trap. That's another opprotunity for a PR if you want 😄.
|
||||
|
||||
Here is what an example `Serializer` might look like:
|
||||
|
||||
```swift
|
||||
let parsedData = ...
|
||||
var rows: [[UInt8]] = []
|
||||
|
||||
var serializer = Serializer { row in
|
||||
rows.append(row)
|
||||
}
|
||||
serializer.serialize(parsedData)
|
||||
|
||||
let document = rows.joined(separator: UInt8(ascii: "\n"))
|
||||
```
|
||||
|
||||
The `SyncSerializer` takes in data just like the `Serializer`, and returns the whole serialized document as a byte array:
|
||||
|
||||
```swift
|
||||
let parsedData = ...
|
||||
let serializer = SyncSerializer()
|
||||
|
||||
let document = serializer.serialize(parsedData)
|
||||
```
|
||||
|
||||
### Decoder
|
||||
|
||||
The `CSVDecoder` handles decoding CSV data into `Decodable` types in a sync or async way. You start by creating a `CSVDecoder` instance with a `CSVCodingOptions`. This defaults to the `.default` instance of the `CSVCodingOptions` type. Once you have a `CSVDecoder` instance, you can get `CSVSyncDecoder` or `CSVAsyncDecoder` based on your needs.
|
||||
|
||||
To get an async decoder, you can use the `.async(for:length:_:)` method. This method takes in the type that the CSV rows will be decoded to, the total length of the CSV document that will be decoded, and a handler that is called when a row is decoded. Then you can call `.decode(_:)` on the `CSVAsyncDecoder` instance with the data to decode. This method throws any errors that occur when decoding the data.
|
||||
|
||||
Here is an example of a `CSVAsyncDecoder`:
|
||||
|
||||
```swift
|
||||
let length = chunks.reduce(0) { $0 + $1.count }
|
||||
let decoder = CSVDecoder().async(for: [String: String].self, length: length) { row in
|
||||
database.insert(into: "table").values(row).run()
|
||||
}
|
||||
|
||||
for chunk in chunks {
|
||||
try decoder.decode(chunk)
|
||||
}
|
||||
```
|
||||
|
||||
There is also the `CSVSyncDecoder` that works like most of the other decoders you encounter. You can create an instance with the `.sync` computed property on the `CSVDecoder` type. The sync decoder has a `.decode(_:from:)` method that takes in the type to decode the data to and the CSV data to decode. The method then returns an array of the type passed in.
|
||||
|
||||
Here is an example of a `CSVSyncDecoder`:
|
||||
|
||||
```swift
|
||||
let decoder = CSVDecoder().sync
|
||||
let data = try decoder.decode([String: String].self, from: data)
|
||||
```
|
||||
|
||||
### Encoder
|
||||
|
||||
Like the `CSVDecoder`, you can create a `CSVEncoder` with a `CSVCodingOptions` instance and then get a sync or async version for handling your data.
|
||||
|
||||
The `CSVEncoder.async(_:)` method takes in 1 parameter, which is a callback function that takes in an encoded CSV row. Then when you encode your Swift type instance, they become CSV rows and you can what you want with them.
|
||||
|
||||
Here is an example `CSVAsyncEncoder`:
|
||||
|
||||
```swift
|
||||
var rows: [[UInt8]] = []
|
||||
let encoder = CSVEncoder().async { row in
|
||||
rows.append(row)
|
||||
}
|
||||
|
||||
for data in encodables {
|
||||
try encoder.encode(data)
|
||||
}
|
||||
|
||||
let document = rows.joined(separator: UInt8(ascii: "\n"))
|
||||
```
|
||||
|
||||
The sync encoder, as expected, takes in an array of an `Encodable` type and encodes it to a CSV document:
|
||||
|
||||
```swift
|
||||
let encoder = CSVEncoder().sync
|
||||
let document = try encoder.encode(parsedData)
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
This package and anything it contains is under the [MIT license agreement](https://github.com/skelpo/CSV/blob/master/LICENSE).
|
||||
|
|
|
@ -201,7 +201,7 @@ public final class CSVSyncDecoder {
|
|||
/// - Throws: Errors that occur during the decoding proccess.
|
||||
public func decode<D>(_ type: D.Type = D.self, from data: Data)throws -> [D] where D: Decodable {
|
||||
var result: [D] = []
|
||||
result.reserveCapacity(data.lazy.split(separator: "\n").count)
|
||||
result.reserveCapacity(data.lazy.split(separator: 10).count)
|
||||
|
||||
let decoder = AsyncDecoder(decoding: type, path: [], decodingOptions: self.decodingOptions) { decoded in
|
||||
guard let typed = decoded as? D else {
|
||||
|
|
|
@ -54,8 +54,8 @@ public enum BoolCodingStrategy: Hashable {
|
|||
/// - Returns: The bytes value for the bool passed in.
|
||||
public func bytes(from bool: Bool) -> [UInt8] {
|
||||
switch self {
|
||||
case .integer: return bool ? "1" : "0"
|
||||
case .string, .fuzzy: return bool ? "true" : "false"
|
||||
case .integer: return bool ? [49] : [48]
|
||||
case .string, .fuzzy: return bool ? [116, 114, 117, 101] : [102, 97, 108, 115, 101]
|
||||
case let .custom(`true`, `false`): return bool ? `true` : `false`
|
||||
}
|
||||
}
|
||||
|
@ -66,10 +66,10 @@ public enum BoolCodingStrategy: Hashable {
|
|||
/// - Returns: The `Bool` value for the bytes passed in, or `nil` if no match is found.
|
||||
public func bool(from bytes: [UInt8]) -> Bool? {
|
||||
switch (self, bytes) {
|
||||
case (.integer, ["0"]): return false
|
||||
case (.integer, ["1"]): return true
|
||||
case (.string, "false"): return false
|
||||
case (.string, "true"): return true
|
||||
case (.integer, [49]): return true
|
||||
case (.integer, [48]): return false
|
||||
case (.string, [116, 114, 117, 101]): return true
|
||||
case (.string, [102, 97, 108, 115, 101]): return false
|
||||
case (let .custom(`true`, `false`), _):
|
||||
switch bytes {
|
||||
case `false`: return false
|
||||
|
|
|
@ -12,10 +12,10 @@ final class AsyncKeyedEncoder<K>: KeyedEncodingContainerProtocol where K: Coding
|
|||
func _encode(_ value: [UInt8], for key: K) {
|
||||
switch self.encoder.container.section {
|
||||
case .header:
|
||||
let bytes = Array([[34], key.stringValue.bytes, [34]].joined())
|
||||
let bytes = Array([[34], key.stringValue.bytes.escaped, [34]].joined())
|
||||
self.encoder.container.cells.append(bytes)
|
||||
case .row:
|
||||
let bytes = Array([[34], value, [34]].joined())
|
||||
let bytes = Array([[34], value.escaped, [34]].joined())
|
||||
self.encoder.container.cells.append(bytes)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,16 +11,16 @@ final class AsyncSingleValueEncoder: SingleValueEncodingContainer {
|
|||
|
||||
func encodeNil() throws {
|
||||
let value = self.encoder.encodingOptions.nilCodingStrategy.bytes()
|
||||
self.encoder.container.cells.append(value)
|
||||
self.encoder.container.cells.append(value.escaped)
|
||||
}
|
||||
func encode(_ value: Bool) throws {
|
||||
let value = self.encoder.encodingOptions.boolCodingStrategy.bytes(from: value)
|
||||
self.encoder.container.cells.append(value)
|
||||
self.encoder.container.cells.append(value.escaped)
|
||||
}
|
||||
func encode(_ value: String) throws { self.encoder.container.cells.append(value.bytes) }
|
||||
func encode(_ value: Double) throws { self.encoder.container.cells.append(value.bytes) }
|
||||
func encode(_ value: Float) throws { self.encoder.container.cells.append(value.bytes) }
|
||||
func encode(_ value: Int) throws { self.encoder.container.cells.append(value.bytes) }
|
||||
func encode(_ value: String) throws { self.encoder.container.cells.append(value.bytes.escaped) }
|
||||
func encode(_ value: Double) throws { self.encoder.container.cells.append(value.bytes.escaped) }
|
||||
func encode(_ value: Float) throws { self.encoder.container.cells.append(value.bytes.escaped) }
|
||||
func encode(_ value: Int) throws { self.encoder.container.cells.append(value.bytes.escaped) }
|
||||
|
||||
func encode<T>(_ value: T) throws where T : Encodable {
|
||||
let column = self.codingPath.map { $0.stringValue }.joined(separator: ".")
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
// MARK: - String <-> Bytes conversion
|
||||
extension CustomStringConvertible {
|
||||
var bytes: [UInt8] {
|
||||
return Array(self.description.utf8)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
init(_ bytes: [UInt8]) {
|
||||
self = String(decoding: bytes, as: UTF8.self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Coding Key Interactions
|
||||
extension Dictionary where Key == String {
|
||||
func value(for key: CodingKey)throws -> Value {
|
||||
guard let value = self[key.stringValue] else {
|
||||
throw DecodingError.keyNotFound(key, DecodingError.Context.init(codingPath: [key], debugDescription: "No value found for key '\(key.stringValue)'"))
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
extension UInt8: ExpressibleByUnicodeScalarLiteral {
|
||||
public init(unicodeScalarLiteral value: UnicodeScalar) {
|
||||
self = UInt8(ascii: value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array: ExpressibleByUnicodeScalarLiteral where Element == UInt8 {
|
||||
public init(unicodeScalarLiteral value: UnicodeScalar) {
|
||||
self = [UInt8(ascii: value)]
|
||||
}
|
||||
}
|
||||
|
||||
extension Array: ExpressibleByExtendedGraphemeClusterLiteral where Element == UInt8 {
|
||||
public init(extendedGraphemeClusterLiteral value: Character) {
|
||||
self = value.unicodeScalars.map(UInt8.init(ascii:))
|
||||
}
|
||||
}
|
||||
|
||||
extension Array: ExpressibleByStringLiteral where Element == UInt8 {
|
||||
public init(stringLiteral value: String) {
|
||||
self = value.unicodeScalars.map(UInt8.init(ascii:))
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ public protocol KeyedCollection: Collection where Self.Element == (key: Key, val
|
|||
|
||||
extension String: BytesRepresentable {
|
||||
|
||||
/// The string's UTF-* view converted to an `Array`.
|
||||
/// The string's UTF-8 view converted to an `Array`.
|
||||
public var bytes: [UInt8] {
|
||||
return Array(self.utf8)
|
||||
}
|
||||
|
@ -82,7 +82,7 @@ public struct Serializer {
|
|||
/// first time it is called.
|
||||
///
|
||||
/// - Note: When you pass a dictionary into this method, each value collection is expect to contain the same
|
||||
//// number of elements, and will crash with `index out of bounds` if that assumption is broken.
|
||||
/// number of elements, and will crash with `index out of bounds` if that assumption is broken.
|
||||
///
|
||||
/// - Parameter data: The dictionary (or other object) to parse.
|
||||
/// - Returns: A `Result` instance with a `.failure` case with all the errors from the the `.onRow` callback calls.
|
||||
|
@ -96,8 +96,8 @@ public struct Serializer {
|
|||
guard data.count > 0 else { return errors.result }
|
||||
|
||||
if !self.serializedHeaders {
|
||||
let headers = data.keys.map { title in Array([[34], title.bytes, [34]].joined()) }
|
||||
do { try self.onRow(Array(headers.joined(separator: [10]))) }
|
||||
let headers = data.keys.map { title in Array([[34], title.bytes.escaped, [34]].joined()) }
|
||||
do { try self.onRow(Array(headers.joined(separator: [44]))) }
|
||||
catch let error { errors.errors.append(error) }
|
||||
self.serializedHeaders = true
|
||||
}
|
||||
|
@ -105,9 +105,9 @@ public struct Serializer {
|
|||
guard let first = data.first?.value else { return errors.result }
|
||||
(first.startIndex..<first.endIndex).forEach { index in
|
||||
let cells = data.values.map { column -> [UInt8] in
|
||||
return Array([[34], column[index].bytes, [34]].joined())
|
||||
return Array([[34], column[index].bytes.escaped, [34]].joined())
|
||||
}
|
||||
do { try onRow(Array(cells.joined(separator: [10]))) }
|
||||
do { try onRow(Array(cells.joined(separator: [44]))) }
|
||||
catch let error { errors.errors.append(error) }
|
||||
}
|
||||
|
||||
|
@ -125,7 +125,7 @@ public struct SyncSerializer {
|
|||
/// `[BytesRepresentable: [BytesRepresentable]], but it can be any type you conform to the proper protocols.
|
||||
///
|
||||
/// - Note: When you pass a dictionary into this method, each value collection is expect to contain the same
|
||||
//// number of elements, and will crash with `index out of bounds` if that assumption is broken.
|
||||
/// number of elements, and will crash with `index out of bounds` if that assumption is broken.
|
||||
///
|
||||
/// - Parameter data: The dictionary (or other object) to parse.
|
||||
/// - Returns: The serialized CSV data.
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import Foundation
|
||||
|
||||
// MARK: - String <-> Bytes conversion
|
||||
extension CustomStringConvertible {
|
||||
var bytes: [UInt8] {
|
||||
return Array(self.description.utf8)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
init(_ bytes: [UInt8]) {
|
||||
self = String(decoding: bytes, as: UTF8.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == UInt8 {
|
||||
var escaped: [UInt8] {
|
||||
return self.contains(34) ? Array(self.split(separator: 34).joined(separator: [34, 34])) : self
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Coding Key Interactions
|
||||
extension Dictionary where Key == String {
|
||||
func value(for key: CodingKey)throws -> Value {
|
||||
guard let value = self[key.stringValue] else {
|
||||
throw DecodingError.keyNotFound(key, DecodingError.Context.init(codingPath: [key], debugDescription: "No value found for key '\(key.stringValue)'"))
|
||||
}
|
||||
return value
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
let orderedData: OrderedKeyedCollection = [
|
||||
"first name": ["Caleb", "Benjamin", "Doc", "Grace", "Anne", "TinTin"],
|
||||
"last_name": ["Kleveter", "Franklin", "Holliday", "Hopper", "Shirley", nil],
|
||||
"age": ["18", "269", "174", "119", "141", "16"],
|
||||
"gender": ["M", "M", "M", "F", "F", "M"],
|
||||
"tagLine": [
|
||||
"😜", "A penny saved is a penny earned", "Bang", nil,
|
||||
"God's in His heaven,\nall's right with the world", "Great snakes!"
|
||||
]
|
||||
]
|
||||
|
||||
let orderedChunks: [OrderedKeyedCollection<String, Array<String?>>] = [
|
||||
["first name": ["Caleb"], "last_name": ["Kleveter"], "age": ["18"], "gender": ["M"], "tagLine": ["😜"]],
|
||||
[
|
||||
"first name": ["Benjamin"], "last_name": ["Franklin"], "age": ["269"], "gender": ["M"],
|
||||
"tagLine": ["A penny saved is a penny earned"]
|
||||
],
|
||||
["first name": ["Doc"], "last_name": ["Holliday"], "age": ["174"], "gender": ["M"], "tagLine": ["Bang"]],
|
||||
["first name": ["Grace"], "last_name": ["Hopper"], "age": ["119"], "gender": ["F"], "tagLine": [nil]],
|
||||
[
|
||||
"first name": ["Anne"], "last_name": ["Shirley"], "age": ["141"], "gender": ["F"],
|
||||
"tagLine": ["God's in His heaven,\nall's right with the world"]
|
||||
],
|
||||
["first name": ["TinTin"], "last_name": [nil], "age": ["16"], "gender": ["M"], "tagLine": ["Great snakes!"]]
|
||||
]
|
||||
|
||||
let chunks: [String] = [
|
||||
"first name,last_name,age",
|
||||
",gender,tagLine\nCaleb,Kleveter,18,M,",
|
||||
"😜\r\nBenjamin,Franklin,269,M,A penny saved is a ",
|
||||
"penny earned\n\"",
|
||||
#"Doc","Holliday","174","M",Bang\#r\#n"#,
|
||||
"Grace,Hopper,119,F,",
|
||||
#"\#nAnne,Shirley,141,F,"God's in His heaven,\#n"#,
|
||||
#"all's right with the world""#,
|
||||
"\nTinTin,,16,M,Great snakes!"
|
||||
]
|
||||
|
||||
let data = """
|
||||
first name,last_name,age,gender,tagLine
|
||||
Caleb,Kleveter,18,M,😜\r
|
||||
Benjamin,Franklin,269,M,A penny saved is a penny earned
|
||||
"Doc","Holliday","174","M",Bang\r
|
||||
Grace,Hopper,119,F,
|
||||
Anne,Shirley,141,F,"God's in His heaven,
|
||||
all's right with the world"
|
||||
TinTin,,16,M,Great snakes!
|
||||
"""
|
||||
|
||||
let expected = """
|
||||
"first name","last_name","age","gender","tagLine"
|
||||
"Caleb","Kleveter","18","M","😜"
|
||||
"Benjamin","Franklin","269","M","A penny saved is a penny earned"
|
||||
"Doc","Holliday","174","M","Bang"
|
||||
"Grace","Hopper","119","F",""
|
||||
"Anne","Shirley","141","F","God's in His heaven,
|
||||
all's right with the world"
|
||||
"TinTin","","16","M","Great snakes!"
|
||||
"""
|
|
@ -123,26 +123,3 @@ fileprivate struct Person: Codable, Equatable {
|
|||
case tagLine
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let data = """
|
||||
first name,last_name,age,gender,tagLine
|
||||
Caleb,Kleveter,18,M,😜\r
|
||||
Benjamin,Franklin,269,M,A penny saved is a penny earned
|
||||
"Doc","Holliday","174","M",Bang\r
|
||||
Grace,Hopper,119,F,
|
||||
Anne,Shirley,141,F,"God's in His heaven,
|
||||
all's right with the world"
|
||||
TinTin,,16,M,Great snakes!
|
||||
"""
|
||||
|
||||
fileprivate let chunks: [String] = [
|
||||
"first name,last_name,age",
|
||||
",gender,tagLine\nCaleb,Kleveter,18,M,",
|
||||
"😜\r\nBenjamin,Franklin,269,M,A penny saved is a ",
|
||||
"penny earned\n\"",
|
||||
#"Doc","Holliday","174","M",Bang\#r\#n"#,
|
||||
"Grace,Hopper,119,F,",
|
||||
#"\#nAnne,Shirley,141,F,"God's in His heaven,\#n"#,
|
||||
#"all's right with the world""#,
|
||||
"\nTinTin,,16,M,Great snakes!"
|
||||
]
|
||||
|
|
|
@ -16,7 +16,7 @@ final class EncoderTests: XCTestCase {
|
|||
|
||||
func testMeasureAsyncEncode() {
|
||||
|
||||
// 0.502
|
||||
// 0.543
|
||||
measure {
|
||||
for _ in 0..<10_000 {
|
||||
let encoder = CSVEncoder().async { _ in return }
|
||||
|
@ -35,15 +35,15 @@ final class EncoderTests: XCTestCase {
|
|||
|
||||
func testSyncEncode() throws {
|
||||
let encoder = CSVEncoder().sync
|
||||
let data = try encoder.encode(people)
|
||||
let string = String(decoding: data, as: UTF8.self)
|
||||
let encoded = try encoder.encode(people)
|
||||
let string = String(decoding: encoded, as: UTF8.self)
|
||||
|
||||
XCTAssertEqual(string, expected)
|
||||
}
|
||||
|
||||
func testMeasureSyncEncode() {
|
||||
|
||||
// 0.583
|
||||
// 0.621
|
||||
measure {
|
||||
for _ in 0..<10_000 {
|
||||
let encoder = CSVEncoder().sync
|
||||
|
@ -93,14 +93,3 @@ fileprivate let people = [
|
|||
),
|
||||
Person(firstName: "TinTin", lastName: nil, age: 16, gender: .male, tagLine: "Great snakes!")
|
||||
]
|
||||
|
||||
fileprivate let expected = """
|
||||
"first name","last_name","age","gender","tagLine"
|
||||
"Caleb","Kleveter","18","M","😜"
|
||||
"Benjamin","Franklin","269","M","A penny saved is a penny earned"
|
||||
"Doc","Holliday","174","M","Bang"
|
||||
"Grace","Hopper","119","F",""
|
||||
"Anne","Shirley","141","F","God's in His heaven,
|
||||
all's right with the world"
|
||||
"TinTin","","16","M","Great snakes!"
|
||||
"""
|
||||
|
|
|
@ -110,26 +110,3 @@ final class ParserTests: XCTestCase {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let data = """
|
||||
first name,last_name,age,gender,tagLine
|
||||
Caleb,Kleveter,18,M,😜\r
|
||||
Benjamin,Franklin,269,M,A penny saved is a penny earned
|
||||
"Doc","Holliday","174","M",Bang\r
|
||||
Grace,Hopper,119,F,
|
||||
Anne,Shirley,141,F,"God's in His heaven,
|
||||
all's right with the world"
|
||||
TinTin,,16,M,Great snakes!
|
||||
"""
|
||||
|
||||
fileprivate let chunks: [String] = [
|
||||
"first name,last_name,age",
|
||||
",gender,tagLine\nCaleb,Kleveter,18,M,",
|
||||
"😜\r\nBenjamin,Franklin,269,M,A penny saved is a ",
|
||||
"penny earned\n\"",
|
||||
#"Doc","Holliday","174","M",Bang\#r\#n"#,
|
||||
"Grace,Hopper,119,F,",
|
||||
#"\#nAnne,Shirley,141,F,"God's in His heaven,\#n"#,
|
||||
#"all's right with the world""#,
|
||||
"\nTinTin,,16,M,Great snakes!"
|
||||
]
|
||||
|
|
|
@ -13,10 +13,10 @@ final class SerializerTests: XCTestCase {
|
|||
func testMeasuerSyncSerializer() {
|
||||
let serializer = SyncSerializer()
|
||||
|
||||
// 5.786
|
||||
// 6.679
|
||||
measure {
|
||||
for _ in 0..<100_000 {
|
||||
_ = serializer.serialize(data)
|
||||
_ = serializer.serialize(orderedData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,78 +35,15 @@ final class SerializerTests: XCTestCase {
|
|||
func testMeasureChunkedSerialize() {
|
||||
var serializer = Serializer { _ in return }
|
||||
|
||||
// 5.504
|
||||
// 5.896
|
||||
measure {
|
||||
for _ in 0..<100_000 {
|
||||
chunks.forEach { chunk in serializer.serialize(chunk) }
|
||||
orderedChunks.forEach { chunk in serializer.serialize(chunk) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate let orderedData: OrderedKeyedCollection = [
|
||||
"first name": ["Caleb", "Benjamin", "Doc", "Grace", "Anne", "TinTin"],
|
||||
"last_name": ["Kleveter", "Franklin", "Holliday", "Hopper", "Shirley", nil],
|
||||
"age": ["18", "269", "174", "119", "141", "16"],
|
||||
"gender": ["M", "M", "M", "F", "F", "M"],
|
||||
"tagLine": [
|
||||
"😜", "A penny saved is a penny earned", "Bang", nil,
|
||||
"God's in His heaven,\nall's right with the world", "Great snakes!"
|
||||
]
|
||||
]
|
||||
|
||||
fileprivate let data = [
|
||||
"first name": ["Caleb", "Benjamin", "Doc", "Grace", "Anne", "TinTin"],
|
||||
"last_name": ["Kleveter", "Franklin", "Holliday", "Hopper", "Shirley", nil],
|
||||
"age": ["18", "269", "174", "119", "141", "16"],
|
||||
"gender": ["M", "M", "M", "F", "F", "M"],
|
||||
"tagLine": [
|
||||
"😜", "A penny saved is a penny earned", "Bang", nil,
|
||||
"God's in His heaven,\nall's right with the world", "Great snakes!"
|
||||
]
|
||||
]
|
||||
|
||||
fileprivate let orderedChunks: [OrderedKeyedCollection<String, Array<String?>>] = [
|
||||
["first name": ["Caleb"], "last_name": ["Kleveter"], "age": ["18"], "gender": ["M"], "tagLine": ["😜"]],
|
||||
[
|
||||
"first name": ["Benjamin"], "last_name": ["Franklin"], "age": ["269"], "gender": ["M"],
|
||||
"tagLine": ["A penny saved is a penny earned"]
|
||||
],
|
||||
["first name": ["Doc"], "last_name": ["Holliday"], "age": ["174"], "gender": ["M"], "tagLine": ["Bang"]],
|
||||
["first name": ["Grace"], "last_name": ["Hopper"], "age": ["119"], "gender": ["F"], "tagLine": [nil]],
|
||||
[
|
||||
"first name": ["Anne"], "last_name": ["Shirley"], "age": ["141"], "gender": ["F"],
|
||||
"tagLine": ["God's in His heaven,\nall's right with the world"]
|
||||
],
|
||||
["first name": ["TinTin"], "last_name": [nil], "age": ["16"], "gender": ["M"], "tagLine": ["Great snakes!"]]
|
||||
]
|
||||
|
||||
fileprivate let chunks = [
|
||||
["first name": ["Caleb"], "last_name": ["Kleveter"], "age": ["18"], "gender": ["M"], "tagLine": ["😜"]],
|
||||
[
|
||||
"first name": ["Benjamin"], "last_name": ["Franklin"], "age": ["269"], "gender": ["M"],
|
||||
"tagLine": ["A penny saved is a penny earned"]
|
||||
],
|
||||
["first name": ["Doc"], "last_name": ["Holliday"], "age": ["174"], "gender": ["M"], "tagLine": ["Bang"]],
|
||||
["first name": ["Grace"], "last_name": ["Hopper"], "age": ["119"], "gender": ["F"], "tagLine": [nil]],
|
||||
[
|
||||
"first name": ["Anne"], "last_name": ["Shirley"], "age": ["141"], "gender": ["F"],
|
||||
"tagLine": ["God's in His heaven,\nall's right with the world"]
|
||||
],
|
||||
["first name": ["TinTin"], "last_name": [nil], "age": ["16"], "gender": ["M"], "tagLine": ["Great snakes!"]]
|
||||
]
|
||||
|
||||
fileprivate let expected = """
|
||||
"first name","last_name","age","gender","tagLine"
|
||||
"Caleb","Kleveter","18","M","😜"
|
||||
"Benjamin","Franklin","269","M","A penny saved is a penny earned"
|
||||
"Doc","Holliday","174","M","Bang"
|
||||
"Grace","Hopper","119","F",""
|
||||
"Anne","Shirley","141","F","God's in His heaven,
|
||||
all's right with the world"
|
||||
"TinTin","","16","M","Great snakes!"
|
||||
"""
|
||||
|
||||
internal struct OrderedKeyedCollection<K, V>: KeyedCollection, ExpressibleByDictionaryLiteral where K: Hashable {
|
||||
typealias Index = Int
|
||||
typealias Element = (key: Key, value: Value)
|
||||
|
|
|
@ -69,6 +69,10 @@ final class StressTests: XCTestCase {
|
|||
{ _ in return }
|
||||
)
|
||||
try async.decode(data)
|
||||
} catch let error as EncodingError {
|
||||
XCTFail(error.failureReason ?? "No failure reason")
|
||||
error.errorDescription.map { print($0) }
|
||||
error.recoverySuggestion.map { print($0) }
|
||||
} catch let error {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
|
@ -122,17 +126,17 @@ final class StressTests: XCTestCase {
|
|||
}
|
||||
|
||||
fileprivate struct Response: Codable, Equatable {
|
||||
let Respondent: Int
|
||||
let Hobby: Bool
|
||||
let OpenSource: Bool
|
||||
let Country: String
|
||||
let Student: String
|
||||
let Employment: String
|
||||
let FormalEducation: String
|
||||
let Respondent: Int?
|
||||
let Hobby: Bool?
|
||||
let OpenSource: Bool?
|
||||
let Country: String?
|
||||
let Student: String?
|
||||
let Employment: String?
|
||||
let FormalEducation: String?
|
||||
let UndergradMajor: String?
|
||||
let CompanySize: String
|
||||
let DevType: String
|
||||
let YearsCoding: String
|
||||
let CompanySize: String?
|
||||
let DevType: String?
|
||||
let YearsCoding: String?
|
||||
let YearsCodingProf: String?
|
||||
let JobSatisfaction: String?
|
||||
let CareerSatisfaction: String?
|
||||
|
@ -250,5 +254,5 @@ fileprivate struct Response: Codable, Equatable {
|
|||
let Dependents: Bool?
|
||||
let MilitaryUS: Bool?
|
||||
let SurveyTooLong: String?
|
||||
let SurveyEasy: String
|
||||
let SurveyEasy: String?
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue