Created aysnc CSV encoder

This commit is contained in:
Caleb Kleveter 2019-04-17 16:10:13 -05:00
parent 9896bd77df
commit a4444ffef3
No known key found for this signature in database
GPG Key ID: B38DBD5CF2C98D69
11 changed files with 337 additions and 46 deletions

View File

@ -12,9 +12,59 @@ public final class CSVCoder {
public func decode<T>(_ data: Data, to type: T.Type = T.self)throws -> [T] where T: Decodable {
return try _CSVDecoder(csv: Array(data), decodingOptions: self.decodingOptions).decode(T.self, from: data)
}
public func encode<T>(_ objects: [T], boolEncoding: BoolEncodingStrategy = .toString, stringEncoding: String.Encoding = .utf32)throws -> Data where T: Encodable {
return try Data(_CSVEncoder.encode(objects, boolEncoding: boolEncoding, stringEncoding: stringEncoding))
public func encode<T>(_ objects: [T])throws -> Data where T: Encodable {
return try Data(_CSVEncoder.encode(objects, encodingOptions: self.encodingOptions))
}
}
public final class CSVEncoder {
public var encodingOptions: CSVCodingOptions
public init(encodingOptions: CSVCodingOptions) {
self.encodingOptions = encodingOptions
}
public var sync: CSVSyncEncoder {
return CSVSyncEncoder(encodingOptions: self.encodingOptions)
}
public func async(_ onRow: @escaping ([UInt8]) -> ()) -> CSVAsyncEncoder {
return CSVAsyncEncoder(encodingOptions: self.encodingOptions, onRow: onRow)
}
}
public final class CSVSyncEncoder {
internal var encodingOptions: CSVCodingOptions
internal init(encodingOptions: CSVCodingOptions) {
self.encodingOptions = encodingOptions
}
public func encode<T>(_ objects: [T])throws -> Data where T: Encodable {
var rows: [[UInt8]] = []
rows.reserveCapacity(objects.count)
let encoder = AsyncEncoder(encodingOptions: self.encodingOptions) { row in
rows.append(row)
}
try objects.forEach(encoder.encode)
return Data(rows.joined(separator: [CSV.Delimiter.newLine]))
}
}
public final class CSVAsyncEncoder {
internal var encodingOptions: CSVCodingOptions
private var encoder: AsyncEncoder
internal init(encodingOptions: CSVCodingOptions, onRow: @escaping ([UInt8]) -> ()) {
self.encodingOptions = encodingOptions
self.encoder = AsyncEncoder(encodingOptions: encodingOptions, onRow: onRow)
}
public func encode<T>(_ object: T)throws where T: Encodable {
try self.encoder.encode(object)
}
}

View File

@ -0,0 +1,71 @@
import Foundation
final class AsyncEncoder: Encoder {
let codingPath: [CodingKey]
let userInfo: [CodingUserInfoKey : Any]
let container: DataContainer
let encodingOptions: CSVCodingOptions
let onRow: ([UInt8]) -> ()
init(
path: CodingPath = [],
info: [CodingUserInfoKey : Any] = [:],
encodingOptions: CSVCodingOptions,
onRow: @escaping ([UInt8]) -> ()
) {
self.codingPath = path
self.userInfo = info
self.container = DataContainer(section: .header)
self.encodingOptions = encodingOptions
self.onRow = onRow
}
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
let container = AsyncKeyedEncoder<Key>(path: self.codingPath, encoder: self)
return KeyedEncodingContainer(container)
}
func unkeyedContainer() -> UnkeyedEncodingContainer {
return AsyncUnkeyedEncoder(encoder: self)
}
func singleValueContainer() -> SingleValueEncodingContainer {
return AsyncSingleValueEncoder(path: self.codingPath, encoder: self)
}
func encode<T>(
_ object: T
)throws where T: Encodable {
switch self.container.section {
case .header:
try object.encode(to: self)
self.onRow(Array(self.container.cells.joined()))
self.container.section = .row
self.container.rowCount += 1
self.container.cells = []
fallthrough
case .row:
try object.encode(to: self)
self.onRow(Array(self.container.cells.joined()))
self.container.rowCount += 1
self.container.cells = []
}
}
enum EncodingSection {
case header
case row
}
final class DataContainer {
var cells: [[UInt8]]
var section: EncodingSection
var rowCount: Int
init(cells: [[UInt8]] = [], section: EncodingSection = .row) {
self.cells = cells
self.section = section
self.rowCount = 0
}
}
}

View File

@ -0,0 +1,56 @@
import Foundation
final class AsyncKeyedEncoder<K>: KeyedEncodingContainerProtocol where K: CodingKey {
var codingPath: [CodingKey]
var encoder: AsyncEncoder
init(path: CodingPath, encoder: AsyncEncoder) {
self.codingPath = path
self.encoder = encoder
}
func _encode(_ value: [UInt8], for key: K) {
switch self.encoder.container.section {
case .header: self.encoder.container.cells.append(key.stringValue.bytes)
case .row: self.encoder.container.cells.append(value)
}
}
func encodeNil(forKey key: K) throws {
let value = self.encoder.encodingOptions.nilCodingStrategy.bytes()
self._encode(value, for: key)
}
func encode(_ value: Bool, forKey key: K) throws {
let value = self.encoder.encodingOptions.boolCodingStrategy.bytes(from: value)
self._encode(value, for: key)
}
func encode(_ value: Double, forKey key: K) throws { self._encode(value.bytes, for: key) }
func encode(_ value: Float, forKey key: K) throws { self._encode(value.bytes, for: key) }
func encode(_ value: Int, forKey key: K) throws { self._encode(value.bytes, for: key) }
func encode(_ value: String, forKey key: K) throws { self._encode(value.bytes, for: key) }
func encode<T>(_ value: T, forKey key: K) throws where T : Encodable {
let encoder = AsyncEncoder(encodingOptions: self.encoder.encodingOptions, onRow: self.encoder.onRow)
try value.encode(to: encoder)
self._encode(encoder.container.cells[0], for: key)
}
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: K) -> KeyedEncodingContainer<NestedKey>
where NestedKey : CodingKey
{
let container = AsyncKeyedEncoder<NestedKey>(path: self.codingPath + [key], encoder: self.encoder)
return KeyedEncodingContainer(container)
}
func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer {
return AsyncUnkeyedEncoder(encoder: self.encoder)
}
func superEncoder() -> Encoder {
return self.encoder
}
func superEncoder(forKey key: K) -> Encoder {
return encoder
}
}

View File

@ -0,0 +1,32 @@
import Foundation
final class AsyncSingleValueEncoder: SingleValueEncodingContainer {
let codingPath: [CodingKey]
let encoder: AsyncEncoder
init(path: CodingPath, encoder: AsyncEncoder) {
self.codingPath = path
self.encoder = encoder
}
func encodeNil() throws {
let value = self.encoder.encodingOptions.nilCodingStrategy.bytes()
self.encoder.container.cells.append(value)
}
func encode(_ value: Bool) throws {
let value = self.encoder.encodingOptions.boolCodingStrategy.bytes(from: value)
self.encoder.container.cells.append(value)
}
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<T>(_ value: T) throws where T : Encodable {
let column = self.codingPath.map { $0.stringValue }.joined(separator: ".")
throw EncodingError.invalidValue(value, EncodingError.Context(
codingPath: self.codingPath,
debugDescription: "Cannot encode nested data into cell in column '\(column)'"
))
}
}

View File

@ -0,0 +1,45 @@
import Foundation
final class AsyncUnkeyedEncoder: UnkeyedEncodingContainer {
var codingPath: [CodingKey]
var encoder: AsyncEncoder
init(encoder: AsyncEncoder) {
self.encoder = encoder
self.codingPath = []
}
var count: Int {
return 0
}
func fail(with value: Any) -> Error {
return EncodingError.invalidValue(value, EncodingError.Context(
codingPath: [],
debugDescription: "Cannot use CSV decoder to decode array values"
))
}
func encodeNil() throws { throw self.fail(with: Optional<Any>.none as Any) }
func encode(_ value: Bool) throws { throw self.fail(with: value) }
func encode(_ value: String) throws { throw self.fail(with: value) }
func encode(_ value: Double) throws { throw self.fail(with: value) }
func encode(_ value: Float) throws { throw self.fail(with: value) }
func encode(_ value: Int) throws { throw self.fail(with: value) }
func encode<T>(_ value: T) throws where T : Encodable { throw self.fail(with: value) }
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey>
where NestedKey : CodingKey
{
let container = AsyncKeyedEncoder<NestedKey>(path: self.codingPath, encoder: self.encoder)
return KeyedEncodingContainer(container)
}
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
return AsyncUnkeyedEncoder(encoder: self.encoder)
}
func superEncoder() -> Encoder {
return encoder
}
}

View File

@ -4,38 +4,35 @@ final class _CSVEncoder: Encoder {
let codingPath: [CodingKey]
let userInfo: [CodingUserInfoKey : Any]
let container: DataContainer
let boolEncoding: BoolEncodingStrategy
let stringEncoding: String.Encoding
let encodingOptions: CSVCodingOptions
init(
container: DataContainer,
path: CodingPath = [],
info: [CodingUserInfoKey : Any] = [:],
boolEncoding: BoolEncodingStrategy = .toString,
stringEncoding: String.Encoding = .utf8
encodingOptions: CSVCodingOptions
) {
self.codingPath = path
self.userInfo = info
self.container = container
self.boolEncoding = boolEncoding
self.stringEncoding = stringEncoding
self.encodingOptions = encodingOptions
}
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
let container = _CSVKeyedEncoder<Key>(container: self.container, path: self.codingPath, boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
let container = _CSVKeyedEncoder<Key>(container: self.container, path: self.codingPath, encodingOptions: self.encodingOptions)
return KeyedEncodingContainer(container)
}
func unkeyedContainer() -> UnkeyedEncodingContainer {
return _CSVUnkeyedEncoder(container: self.container, path: self.codingPath, boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
return _CSVUnkeyedEncoder(container: self.container, path: self.codingPath, encodingOptions: self.encodingOptions)
}
func singleValueContainer() -> SingleValueEncodingContainer {
return _CSVSingleValueEncoder(container: self.container, path: self.codingPath, boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
return _CSVSingleValueEncoder(container: self.container, path: self.codingPath, encodingOptions: self.encodingOptions)
}
static func encode<T>(_ objects: [T], boolEncoding: BoolEncodingStrategy, stringEncoding: String.Encoding)throws -> [UInt8] where T: Encodable {
let encoder = _CSVEncoder(container: DataContainer(), boolEncoding: boolEncoding, stringEncoding: stringEncoding)
static func encode<T>(_ objects: [T], encodingOptions: CSVCodingOptions)throws -> [UInt8] where T: Encodable {
let encoder = _CSVEncoder(container: DataContainer(), encodingOptions: encodingOptions)
try objects.encode(to: encoder)
return encoder.container.data
}

View File

@ -1,16 +1,14 @@
import Foundation
final class _CSVKeyedEncoder<K>: KeyedEncodingContainerProtocol where K: CodingKey {
var codingPath: [CodingKey]
let codingPath: [CodingKey]
let container: DataContainer
let boolEncoding: BoolEncodingStrategy
let stringEncoding: String.Encoding
let encodingOptions: CSVCodingOptions
init(container: DataContainer, path: CodingPath, boolEncoding: BoolEncodingStrategy, stringEncoding: String.Encoding) {
init(container: DataContainer, path: CodingPath, encodingOptions: CSVCodingOptions) {
self.container = container
self.codingPath = path
self.boolEncoding = boolEncoding
self.stringEncoding = stringEncoding
self.encodingOptions = encodingOptions
}
func titleEncode(for key: K, converter: ()throws -> [UInt8])rethrows {
@ -24,8 +22,14 @@ final class _CSVKeyedEncoder<K>: KeyedEncodingContainerProtocol where K: CodingK
}
}
func encodeNil(forKey key: K) throws { self.titleEncode(for: key) { [] } }
func encode(_ value: Bool, forKey key: K) throws { self.titleEncode(for: key) { self.boolEncoding.convert(value) } }
func encodeNil(forKey key: K) throws {
let value = self.encodingOptions.nilCodingStrategy.bytes()
self.titleEncode(for: key) { value }
}
func encode(_ value: Bool, forKey key: K) throws {
let value = self.encodingOptions.boolCodingStrategy.bytes(from: value)
self.titleEncode(for: key) { value }
}
func encode(_ value: Double, forKey key: K) throws { self.titleEncode(for: key) { value.bytes } }
func encode(_ value: Float, forKey key: K) throws { self.titleEncode(for: key) { value.bytes } }
func encode(_ value: Int, forKey key: K) throws { self.titleEncode(for: key) { value.bytes } }
@ -33,26 +37,34 @@ final class _CSVKeyedEncoder<K>: KeyedEncodingContainerProtocol where K: CodingK
func encode<T>(_ value: T, forKey key: K) throws where T : Encodable {
try self.titleEncode(for: key) {
let encoder = _CSVEncoder(container: DataContainer(titles: true), path: self.codingPath, boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
let encoder = _CSVEncoder(
container: DataContainer(titles: true),
path: self.codingPath,
encodingOptions: self.encodingOptions
)
try value.encode(to: encoder)
return encoder.container.data
}
}
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: K) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
let container = _CSVKeyedEncoder<NestedKey>(container: self.container, path: self.codingPath + [key], boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
let container = _CSVKeyedEncoder<NestedKey>(
container: self.container,
path: self.codingPath + [key],
encodingOptions: self.encodingOptions
)
return KeyedEncodingContainer(container)
}
func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer {
return _CSVUnkeyedEncoder(container: self.container, path: self.codingPath, boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
return _CSVUnkeyedEncoder(container: self.container, path: self.codingPath, encodingOptions: self.encodingOptions)
}
func superEncoder() -> Encoder {
return _CSVEncoder(container: self.container, path: self.codingPath, boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
return _CSVEncoder(container: self.container, path: self.codingPath, encodingOptions: self.encodingOptions)
}
func superEncoder(forKey key: K) -> Encoder {
return _CSVEncoder(container: self.container, path: self.codingPath + [key], boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
return _CSVEncoder(container: self.container, path: self.codingPath + [key], encodingOptions: self.encodingOptions)
}
}

View File

@ -3,18 +3,20 @@ import Foundation
final class _CSVSingleValueEncoder: SingleValueEncodingContainer {
let codingPath: [CodingKey]
let container: DataContainer
let boolEncoding: BoolEncodingStrategy
let stringEncoding: String.Encoding
let encodingOptions: CSVCodingOptions
init(container: DataContainer, path: CodingPath, boolEncoding: BoolEncodingStrategy, stringEncoding: String.Encoding) {
init(container: DataContainer, path: CodingPath, encodingOptions: CSVCodingOptions) {
self.codingPath = path
self.container = container
self.boolEncoding = boolEncoding
self.stringEncoding = stringEncoding
self.encodingOptions = encodingOptions
}
func encodeNil() throws { self.container.data = [] }
func encode(_ value: Bool) throws { self.container.data = boolEncoding.convert(value) }
func encodeNil() throws {
self.container.data = self.encodingOptions.nilCodingStrategy.bytes()
}
func encode(_ value: Bool) throws {
self.container.data = self.encodingOptions.boolCodingStrategy.bytes(from: value)
}
func encode(_ value: String) throws { self.container.data = value.bytes }
func encode(_ value: Double) throws { self.container.data = value.bytes }
func encode(_ value: Float) throws { self.container.data = value.bytes }

View File

@ -3,14 +3,12 @@ import Foundation
final class _CSVUnkeyedEncoder: UnkeyedEncodingContainer {
var codingPath: [CodingKey]
let container: DataContainer
let boolEncoding: BoolEncodingStrategy
let stringEncoding: String.Encoding
let encodingOptions: CSVCodingOptions
init(container: DataContainer, path: CodingPath, boolEncoding: BoolEncodingStrategy, stringEncoding: String.Encoding) {
init(container: DataContainer, path: CodingPath, encodingOptions: CSVCodingOptions) {
self.container = container
self.codingPath = path
self.boolEncoding = boolEncoding
self.stringEncoding = stringEncoding
self.encodingOptions = encodingOptions
}
var count: Int {
@ -29,21 +27,29 @@ final class _CSVUnkeyedEncoder: UnkeyedEncodingContainer {
func encode(_ value: Int) throws { throw self.fail(with: value) }
func encode<T>(_ value: T) throws where T : Encodable {
let encoder = _CSVEncoder(container: DataContainer(titles: self.container.data.count > 0), path: self.codingPath, boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
let encoder = _CSVEncoder(
container: DataContainer(titles: self.container.data.count > 0),
path: self.codingPath,
encodingOptions: self.encodingOptions
)
try value.encode(to: encoder)
self.container.data.append(contentsOf: encoder.container.data.dropLast() + ["\n"])
}
func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
let container = _CSVKeyedEncoder<NestedKey>(container: self.container, path: self.codingPath, boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
let container = _CSVKeyedEncoder<NestedKey>(
container: self.container,
path: self.codingPath,
encodingOptions: self.encodingOptions
)
return KeyedEncodingContainer(container)
}
func nestedUnkeyedContainer() -> UnkeyedEncodingContainer {
return _CSVUnkeyedEncoder(container: self.container, path: self.codingPath, boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
return _CSVUnkeyedEncoder(container: self.container, path: self.codingPath, encodingOptions: self.encodingOptions)
}
func superEncoder() -> Encoder {
return _CSVEncoder(container: self.container, path: self.codingPath, boolEncoding: self.boolEncoding, stringEncoding: self.stringEncoding)
return _CSVEncoder(container: self.container, path: self.codingPath, encodingOptions: self.encodingOptions)
}
}

View File

@ -160,7 +160,7 @@ class CSVTests: XCTestCase {
let decoder = CSVCoder(decodingOptions: decodingOptions)
let fielders = try decoder.decode(data, to: Response.self)
_ = try decoder.encode(fielders, boolEncoding: .custom(true: "Yes".bytes, false: "No".bytes))
_ = try decoder.encode(fielders)
}
func testCSVEncodingSpeed()throws {
@ -172,14 +172,34 @@ class CSVTests: XCTestCase {
let fielders = try decoder.decode(data, to: Response.self)
// 7.391
// 9.477
measure {
do {
_ = try decoder.encode(fielders)
} catch { XCTFail(error.localizedDescription) }
}
}
func testCSVSyncEncodingSpeed() throws {
let url = URL(string: "file:/Users/calebkleveter/Development/developer_survey_2018.csv")!
let data = try Data(contentsOf: url)
let decodingOptions = CSVCodingOptions(boolCodingStrategy: .fuzzy, nilCodingStrategy: .custom("NA"))
let decoder = CSVDecoder(decodingOptions: decodingOptions)
let fielders = try decoder.sync.decode(Response.self, from: data)
let encodingOptions = CSVCodingOptions(boolCodingStrategy: .fuzzy, nilCodingStrategy: .custom("NA"))
let encoder = CSVEncoder(encodingOptions: encodingOptions)
// 5.908
measure {
do {
_ = try encoder.sync.encode(fielders)
} catch { XCTFail(error.localizedDescription) }
}
}
func testDataToIntSpeed() {
let bytes = "12495768014".bytes
measure {