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")
+ }
+
+}