Adjust csv parsing

This commit is contained in:
Daniel Saidi 2022-10-23 10:20:18 +02:00
parent 3d092ab2c2
commit 86753bf3ed
8 changed files with 211 additions and 122 deletions

View File

@ -5,13 +5,21 @@
This version adjusts the library for Xcode 14 and deprecates some things.
This version contains a few breaking changes, that should be easy to fix.
### ✨ New features
* `DateFormatter+Init` has a new convenience initializer.
* `CsvParser` can now parse CSV files at urls as well.
### 💡 Behavior changes
* `StandardCsvParser` now throws native errors for file parsing.
### 💥 Breaking changes
* Due to conflicting with TagKit, `String+Slugifies` has been removed.
* `CsvParserError` has a new convenience initializer.
* `String+Slugifies` has been removed due to conflicts with TagKit.

View File

@ -0,0 +1,59 @@
//
// CsvParser.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-10-23.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by classes that can handle
parsing of comma-separated value files and strings.
When parsing a csv file or string, every line will be split
up into components using the provided `componentSeparator`.
*/
public protocol CsvParser {
/**
Parse a csv file in a certain bundle.
- Parameters:
- fileName: The name of the file to parse.
- fileExtension: The extension of the file to parse.
- bundle: The bundle in which the file is located.
- componentSeparator: The separator that separates components on each line.
*/
func parseCsvFile(
named fileName: String,
withExtension fileExtension: String,
in bundle: Bundle,
componentSeparator: Character
) throws -> [[String]]
/**
Parse a csv file at a certain url.
- Parameters:
- url: The url of the file to parse.
- componentSeparator: The separator that separates components on each line.
*/
func parseCsvFile(
at url: URL,
componentSeparator: Character
) throws -> [[String]]
/**
Parse the provided csv string.
- Parameters:
- string: The string to parse.
- componentSeparator: The separator that separates components on each line.
*/
func parseCsvString(
_ string: String,
componentSeparator: Character
) -> [[String]]
}

View File

@ -0,0 +1,18 @@
//
// CsvParserError.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-10-23.
// Copyright © 2018 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This error can be thrown while parsing a csv string or file.
*/
public enum CsvParserError: Error {
/// The requested file doesn't exist.
case noFileWithName(_ fileName: String, andExtension: String, inBundle: Bundle)
}

View File

@ -0,0 +1,85 @@
//
// StandardCsvParser.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-10-23.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This class can be used to parse comma-separated value files
and strings.
When parsing a csv file or string, every line will be split
up into components using the provided `componentSeparator`.
*/
public class StandardCsvParser: CsvParser {
/**
Create a parser instance.
*/
public init(fileManager: FileManager = .default) {
self.fileManager = fileManager
}
private let fileManager: FileManager
/**
Parse a csv file in a certain bundle.
- Parameters:
- fileName: The name of the file to parse.
- fileExtension: The extension of the file to parse.
- bundle: The bundle in which the file is located.
- componentSeparator: The separator that separates components on each line.
*/
public func parseCsvFile(
named fileName: String,
withExtension ext: String,
in bundle: Bundle,
componentSeparator: Character
) throws -> [[String]] {
guard let path = bundle.path(forResource: fileName, ofType: ext) else {
throw CsvParserError.noFileWithName(fileName, andExtension: ext, inBundle: bundle)
}
let string = try String(contentsOfFile: path, encoding: .utf8)
return parseCsvString(string, componentSeparator: componentSeparator)
}
/**
Parse a csv file at a certain url.
- Parameters:
- url: The url of the file to parse.
- componentSeparator: The separator that separates components on each line.
*/
public func parseCsvFile(
at url: URL,
componentSeparator: Character
) throws -> [[String]] {
let string = try String(contentsOf: url, encoding: .utf8)
return parseCsvString(string, componentSeparator: componentSeparator)
}
/**
Parse the provided csv string.
- Parameters:
- string: The string to parse.
- componentSeparator: The separator that separates components on each line.
*/
public func parseCsvString(
_ string: String,
componentSeparator: Character
) -> [[String]] {
string
.components(separatedBy: .newlines)
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
.map { $0.split(separator: componentSeparator)
.map { String($0).trimmingCharacters(in: .whitespaces) }
}
}
}

View File

@ -1,37 +0,0 @@
//
// CsvParser.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-10-23.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
/**
This protocol can be implemented by classes that can handle
parsing of comma-separated value files and strings.
*/
public protocol CsvParser {
/**
Parse a CVS file with a certain name and extension in a
certain bundle.
The file content will be parsed line by line, splitting
each line using the provided `separator`.
*/
func parseCvsFile(named fileName: String, withExtension ext: String, in bundle: Bundle, separator: Character) throws -> [[String]]
/**
Parse a CVS string line by line, splitting up each line
using the provided `separator`.
*/
func parseCvsString(_ string: String, separator: Character) -> [[String]]
}
public enum CsvParserError: Error {
case invalidDataInFile(_ fileName: String, fileExtension: String)
case noSuchFile(_ fileName: String, fileExtension: String)
}

View File

@ -1,45 +0,0 @@
//
// StandardCsvParser.swift
// SwiftKit
//
// Created by Daniel Saidi on 2018-10-23.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Foundation
public class StandardCsvParser: CsvParser {
public init() {}
/**
Parse a CVS file with a certain name and extension in a
certain bundle.
The file content will be parsed line by line, splitting
each line using the provided `separator`.
*/
public func parseCvsFile(
named fileName: String,
withExtension ext: String,
in bundle: Bundle,
separator: Character) throws -> [[String]] {
let missingFile = CsvParserError.noSuchFile(fileName, fileExtension: ext)
let invalidData = CsvParserError.invalidDataInFile(fileName, fileExtension: ext)
guard let path = bundle.path(forResource: fileName, ofType: ext) else { throw missingFile }
guard let string = try? String(contentsOfFile: path, encoding: .utf8) else { throw invalidData }
return parseCvsString(string, separator: separator)
}
/**
Parse a CVS string line by line, splitting up each line
using the provided `separator`.
*/
public func parseCvsString(
_ string: String,
separator: Character) -> [[String]] {
let allRows = string.components(separatedBy: .newlines)
let rows = allRows.filter { $0.trimmingCharacters(in: .whitespaces).count > 0 }
return rows.map { $0.split(separator: separator).map { String($0) } }
}
}

View File

@ -0,0 +1,40 @@
//
// StandardCsvParserTests.swift
// SwiftKitTests
//
// Created by Daniel Saidi on 2018-10-23.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import SwiftKit
import XCTest
class StandardCsvParserTests: XCTestCase {
let parser = StandardCsvParser()
func testCanParseSemicolonSeparatedString() {
let result = parser.parseCsvString("foo;bar;baz\nenough", componentSeparator: ";")
XCTAssertEqual(result.count, 2)
XCTAssertEqual(result[0], ["foo", "bar", "baz"])
XCTAssertEqual(result[1], ["enough"])
}
func testCanParseCommaSeparatedString() {
let result = parser.parseCsvString("a,b,c", componentSeparator: ",")
XCTAssertEqual(result.count, 1)
XCTAssertEqual(result[0], ["a", "b", "c"])
}
func testTrimsComponents() {
let result = parser.parseCsvString(" a , b , c ", componentSeparator: ",")
XCTAssertEqual(result.count, 1)
XCTAssertEqual(result[0], ["a", "b", "c"])
}
func testIncludesEmptyComponents() {
let result = parser.parseCsvString(" a , , c ", componentSeparator: ",")
XCTAssertEqual(result.count, 1)
XCTAssertEqual(result[0], ["a", "", "c"])
}
}

View File

@ -1,39 +0,0 @@
//
// StandardCsvParserTests.swift
// SwiftKitTests
//
// Created by Daniel Saidi on 2018-10-23.
// Copyright © 2020 Daniel Saidi. All rights reserved.
//
import Quick
import Nimble
import SwiftKit
class StandardCsvParserTests: QuickSpec {
override func spec() {
var parser: CsvParser!
beforeEach {
parser = StandardCsvParser()
}
describe("parsing csv strings") {
it("can parse semicolon-separated string") {
let result = parser.parseCvsString("foo;bar;baz\nenough", separator: ";")
expect(result.count).to(equal(2))
expect(result[0]).to(equal(["foo", "bar", "baz"]))
expect(result[1]).to(equal(["enough"]))
}
it("can parse comma-separated string") {
let result = parser.parseCvsString("a,b,c", separator: ",")
expect(result.count).to(equal(1))
expect(result[0]).to(equal(["a", "b", "c"]))
}
}
}
}