diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..0cbf169 Binary files /dev/null and b/.DS_Store differ diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 0952f49..65cd6bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ Bugfixes: - Strip byte order mark from all input strings, not just when loading files (#128) -- @Diggory +## 0.8.2 + +Bugfixes: + +- Throw an error when encountering duplicate column names in CSV headers (#136) -- @TomorrowMC + ## 0.8.1 Bugfixes: diff --git a/SwiftCSV/Array+duplicates.swift b/SwiftCSV/Array+duplicates.swift new file mode 100644 index 0000000..63bc027 --- /dev/null +++ b/SwiftCSV/Array+duplicates.swift @@ -0,0 +1,13 @@ +// +// Array+duplicates.swift +// +// +// Created by 胡逸飞 on 2024/4/26. +// + +extension Array where Element: Hashable { + func duplicates() -> [Element] { + let counts = self.reduce(into: [:]) { counts, element in counts[element, default: 0] += 1 } + return counts.filter { $0.value > 1 }.map { $0.key } + } +} diff --git a/SwiftCSV/Parser.swift b/SwiftCSV/Parser.swift index ac329ff..1bfc336 100644 --- a/SwiftCSV/Parser.swift +++ b/SwiftCSV/Parser.swift @@ -126,6 +126,12 @@ enum Parser { static func enumerateAsDict(header: [String], content: String, delimiter: CSVDelimiter, rowLimit: Int? = nil, block: @escaping ([String : String]) -> ()) throws { let enumeratedHeader = header.enumerated() + + // Check for duplicate column names + let duplicateColumns = header.duplicates() + if !duplicateColumns.isEmpty { + throw CSVParseError.duplicateColumns(columnNames: duplicateColumns) + } // Start after the header try enumerateAsArray(text: content, delimiter: delimiter, startAt: 1, rowLimit: rowLimit) { fields in diff --git a/SwiftCSV/ParsingState.swift b/SwiftCSV/ParsingState.swift index ed37ce0..74c0e95 100644 --- a/SwiftCSV/ParsingState.swift +++ b/SwiftCSV/ParsingState.swift @@ -9,8 +9,10 @@ public enum CSVParseError: Error { case generic(message: String) case quotation(message: String) + case duplicateColumns(columnNames: [String]) + + } - /// State machine of parsing CSV contents character by character. struct ParsingState { diff --git a/SwiftCSVTests/DuplicateColumnNameHandlingTests.swift b/SwiftCSVTests/DuplicateColumnNameHandlingTests.swift new file mode 100644 index 0000000..6e539ee --- /dev/null +++ b/SwiftCSVTests/DuplicateColumnNameHandlingTests.swift @@ -0,0 +1,64 @@ +// +// DuplicateColumnNameHandlingTests.swift +// +// +// Created by 胡逸飞 on 2024/4/27. +// + +import Foundation +import XCTest +@testable import SwiftCSV + +class DuplicateColumnNameHandlingTests: XCTestCase { + + func testErrorOnDuplicateColumnNames() throws { + let csvString = """ + id,name,age,name + 1,John,23,John Doe + 2,Jane,25,Jane Doe + """ + + XCTAssertThrowsError(try CSV(string: csvString)) { error in + switch error as? CSVParseError { + case .duplicateColumns(let columnNames): + XCTAssertEqual(["name"], columnNames) + default: + XCTFail("Expected CSVParseError.duplicateColumns") + } + } + } + + func testNoDuplicateColumnNames() throws { + let csvString = """ + id,name,age + 1,John,23 + 2,Jane,25 + """ + + let csvError = try CSV(string: csvString) + let csvRandom = try CSV(string: csvString) + + XCTAssertEqual(csvError.header, ["id", "name", "age"]) + XCTAssertEqual(csvRandom.header, ["id", "name", "age"]) + + XCTAssertEqual(csvError.rows.count, 2) + XCTAssertEqual(csvRandom.rows.count, 2) + + XCTAssertEqual(csvError.rows[0]["id"], "1") + XCTAssertEqual(csvError.rows[0]["name"], "John") + XCTAssertEqual(csvError.rows[0]["age"], "23") + + XCTAssertEqual(csvRandom.rows[0]["id"], "1") + XCTAssertEqual(csvRandom.rows[0]["name"], "John") + XCTAssertEqual(csvRandom.rows[0]["age"], "23") + + XCTAssertEqual(csvError.rows[1]["id"], "2") + XCTAssertEqual(csvError.rows[1]["name"], "Jane") + XCTAssertEqual(csvError.rows[1]["age"], "25") + + XCTAssertEqual(csvRandom.rows[1]["id"], "2") + XCTAssertEqual(csvRandom.rows[1]["name"], "Jane") + XCTAssertEqual(csvRandom.rows[1]["age"], "25") + } + +}