Skip to content

Commit

Permalink
Merge pull request #14 from skelpo/develop
Browse files Browse the repository at this point in the history
Fix Cell Order When Encoding Data to CSV Format
  • Loading branch information
calebkleveter authored Aug 11, 2020
2 parents ca28521 + 7bc2dec commit 05d10cb
Show file tree
Hide file tree
Showing 9 changed files with 187 additions and 16 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
/*.xcodeproj
/build
/DerivedData
.swiftpm/
10 changes: 8 additions & 2 deletions Sources/CSV/Coder/Encoder/AsyncEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ final class AsyncEncoder: Encoder {
self.onRow(Array(self.container.cells.joined(separator: [self.configuration.cellSeparator])))
self.container.section = .row
self.container.rowCount += 1
self.container.cells = []
self.container.cells = Array(repeating: [], count: self.container.headers.count)
fallthrough
case .row:
try object.encode(to: self)
self.onRow(Array(self.container.cells.joined(separator: [self.configuration.cellSeparator])))
self.container.rowCount += 1
self.container.cells = []
self.container.cells = Array(repeating: [], count: self.container.headers.count)
}
}

Expand All @@ -63,12 +63,18 @@ final class AsyncEncoder: Encoder {
final class DataContainer {
var cells: [[UInt8]]
var section: EncodingSection

var rowCount: Int
var columnCount: Int
var headers: [String]

init(cells: [[UInt8]] = [], section: EncodingSection = .row) {
self.cells = cells
self.section = section

self.rowCount = 0
self.columnCount = 0
self.headers = []
}
}
}
12 changes: 9 additions & 3 deletions Sources/CSV/Coder/Encoder/AsyncKeyedEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ final class AsyncKeyedEncoder<K>: KeyedEncodingContainerProtocol where K: Coding
case .header:
let bytes = key.stringValue.bytes.escaping(self.delimiter)
self.encoder.container.cells.append(bytes)
self.encoder.container.headers.append(key.stringValue)
case .row:
let bytes = value.escaping(self.delimiter)
self.encoder.container.cells.append(bytes)
let column = self.encoder.container.headers.firstIndex(of: key.stringValue)!
self.encoder.container.cells[column] = bytes
}
}
func _encode(_ value: [UInt8]?, for key: K)throws {
Expand All @@ -46,15 +48,19 @@ final class AsyncKeyedEncoder<K>: KeyedEncodingContainerProtocol where K: Coding

func encode<T>(_ value: T, forKey key: K) throws where T : Encodable {
switch self.encoder.container.section {
case .header: self.encoder.container.cells.append(key.stringValue.bytes.escaping(self.delimiter))
case .header:
self.encoder.container.cells.append(key.stringValue.bytes.escaping(self.delimiter))
self.encoder.container.headers.append(key.stringValue)
case .row:
let encoder = AsyncEncoder(
encodingOptions: self.encoder.encodingOptions,
configuration: self.encoder.configuration,
onRow: self.encoder.onRow
)
try value.encode(to: encoder)
self.encoder.container.cells.append(encoder.container.cells[0])

let column = self.encoder.container.headers.firstIndex(of: key.stringValue)!
self.encoder.container.cells[column] = encoder.container.cells[0]
}
}

Expand Down
2 changes: 0 additions & 2 deletions Sources/CSV/Parser.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Foundation

// '\t' => 9
// '\n' => 10
// '\r' => 13
Expand Down
2 changes: 0 additions & 2 deletions Sources/CSV/Seralizer.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Foundation

/// The type where an instance can be represented by an array of bytes (`UInt8`).
public protocol BytesRepresentable {

Expand Down
32 changes: 29 additions & 3 deletions Sources/CSV/Utilities.swift
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
import Foundation

// MARK: - String <-> Bytes conversion
extension CustomStringConvertible {

/// Converts the value's string description to its byte representation.
var bytes: [UInt8] {
return Array(self.description.utf8)
}
}

extension String {

/// Creats a new `String` instance from an array of bytes.
///
/// - Parameter bytes: The byte array that will be the new string value.
init(_ bytes: [UInt8]) {
self = String(decoding: bytes, as: UTF8.self)
}
}

extension Array where Element == UInt8 {

/// Escapes raw escape chraacters in a byte array.
///
/// The characters are escaped by each escape charcter in the array being replaced with 2 escape charcters, so a string like this:
///
/// "\o/"
///
/// If escaped with the `\` chracter, would become:
///
/// "\\o/"
///
/// - Parameter character: The byte that represents the character to escape.
/// - Returns: The bytes of the string, with each raw escaped character being escaped.
func escaping(_ character: UInt8?) -> [UInt8] {
guard let code = character else {
return self
Expand All @@ -28,9 +45,18 @@ extension Array where Element == UInt8 {

// MARK: - Coding Key Interactions
extension Dictionary where Key == String {

/// Extracts the value from a dictinary, using a `CodingKey` for the key.
///
/// - Parameter key: The coding key to use as the key in the dictionary.
/// - Returns: The value for the key passed in.
/// - Throws: `DecodingError.valueNotFound` if the value for the given key is missing,
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)'"))
throw DecodingError.valueNotFound(Value.self, .init(
codingPath: [key],
debugDescription: "No value found for key '\(key.stringValue)'"
))
}
return value
}
Expand Down
39 changes: 39 additions & 0 deletions Tests/CSVTests/EncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,45 @@ final class EncoderTests: XCTestCase {
try XCTAssertEqual(quoteEncoder.encode([quotePerson]), Data(quoteResult.utf8))
try XCTAssertEqual(hashEncoder.encode([hashPerson]), Data(hashResult.utf8))
}

func testEncodingColumnValues() throws {
let data = [
["a": "hello", "b": "true", "c": "1"],
["a": "world", "b": "false", "c": "2"],
["a": "fizz", "b": "false", "c": "3"],
["a": "buzz", "b": "true", "c": "5"],
["a": "foo", "b": "false", "c": "8"],
["a": "bar", "b": "true", "c": "13"],
]

var header = Optional<Array<String>>.none
var index = 0

let encoder = CSVEncoder().async { row in
guard let keys = header else {
header = String(decoding: row, as: UTF8.self).split(separator: ",").map {
String($0).trimmingCharacters(in: .punctuationCharacters)
}

return
}

let object = data[index]
let cells = String(decoding: row, as: UTF8.self).split(separator: ",").map {
String($0).trimmingCharacters(in: .punctuationCharacters)
}

keys.enumerated().forEach { offset, name in
XCTAssertEqual(
object[name], cells[offset],
"Row \(index), column '\(name)' has value '\(cells[offset])'. Expected '\(object[name] ?? "<null>")'"
)
}

index += 1
}
try data.forEach(encoder.encode(_:))
}
}

fileprivate struct Person: Codable, Equatable {
Expand Down
95 changes: 95 additions & 0 deletions Tests/CSVTests/XCTestManifests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#if !canImport(ObjectiveC)
import XCTest

extension DecoderTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__DecoderTests = [
("testAsyncDecode", testAsyncDecode),
("testAsyncDecodeSpeed", testAsyncDecodeSpeed),
("testSyncDecode", testSyncDecode),
("testSyncDecodeSpeed", testSyncDecodeSpeed),
]
}

extension EncoderTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__EncoderTests = [
("testAsyncEncode", testAsyncEncode),
("testEscapingDelimiters", testEscapingDelimiters),
("testMeasureAsyncEncode", testMeasureAsyncEncode),
("testMeasureSyncEncode", testMeasureSyncEncode),
("testSyncEncode", testSyncEncode),
]
}

extension ParserTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__ParserTests = [
("testChunkedParsing", testChunkedParsing),
("testMeasureChunkedParse", testMeasureChunkedParse),
("testMeasureFullParse", testMeasureFullParse),
("testParserInit", testParserInit),
("testParserParse", testParserParse),
]
}

extension SerializerTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__SerializerTests = [
("testChunkedSerialize", testChunkedSerialize),
("testEscapedDelimiter", testEscapedDelimiter),
("testMeasuerSyncSerializer", testMeasuerSyncSerializer),
("testMeasureChunkedSerialize", testMeasureChunkedSerialize),
("testSyncSerialize", testSyncSerialize),
]
}

extension StressTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__StressTests = [
("testMeasureAsyncDecoding", testMeasureAsyncDecoding),
("testMeasureAsyncEncoding", testMeasureAsyncEncoding),
("testMeasureAsyncParsing", testMeasureAsyncParsing),
("testMeasureAsyncSerialize", testMeasureAsyncSerialize),
("testMeasureSyncDecoding", testMeasureSyncDecoding),
("testMeasureSyncEncoding", testMeasureSyncEncoding),
("testMeasureSyncParsing", testMeasureSyncParsing),
("testMeasureSyncSerialize", testMeasureSyncSerialize),
]
}

extension UtilityTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__UtilityTests = [
("testBytesToDouble", testBytesToDouble),
("testBytesToFloat", testBytesToFloat),
("testBytesToInt", testBytesToInt),
("testMeasureBytesToDouble", testMeasureBytesToDouble),
("testMeasureBytesToFloat", testMeasureBytesToFloat),
("testMeasureBytesToInt", testMeasureBytesToInt),
]
}

public func __allTests() -> [XCTestCaseEntry] {
return [
testCase(DecoderTests.__allTests__DecoderTests),
testCase(EncoderTests.__allTests__EncoderTests),
testCase(ParserTests.__allTests__ParserTests),
testCase(SerializerTests.__allTests__SerializerTests),
testCase(StressTests.__allTests__StressTests),
testCase(UtilityTests.__allTests__UtilityTests),
]
}
#endif
10 changes: 6 additions & 4 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import XCTest
@testable import CSVTests

XCTMain([
testCase(CSVTests.allTests),
])
import CSVTests

var tests = [XCTestCaseEntry]()
tests += CSVTests.__allTests()

XCTMain(tests)

0 comments on commit 05d10cb

Please sign in to comment.