From 6a1950a6aa627b6bad7febd040f00cd19fd1d925 Mon Sep 17 00:00:00 2001 From: Christian Tietze Date: Fri, 1 Jul 2022 23:13:38 +0200 Subject: [PATCH] Generic CSV views (#76) Instead of mixing and matching CSV.namedRows with other enumerations, there's now distinct CSV and CSV types. These aren't interchangeable by accident, so as long as you know the type, you get safe access to your data. --- README.md | 69 +++--- SwiftCSV.xcodeproj/project.pbxproj | 120 +++++----- SwiftCSV/CSV+DelimiterGuessing.swift | 10 +- SwiftCSV/CSV.swift | 216 ++++++++---------- SwiftCSV/CSVDelimiter.swift | 37 +++ SwiftCSV/Description.swift | 30 --- SwiftCSV/EnumeratedCSVView.swift | 75 ++++++ SwiftCSV/EnumeratedView.swift | 43 ---- SwiftCSV/NamedCSVView.swift | 53 +++++ SwiftCSV/NamedView.swift | 32 --- SwiftCSV/Parser.swift | 6 +- SwiftCSV/String+Lines.swift | 4 +- .../CSV+DelimiterGuessingTests.swift | 81 ------- SwiftCSVTests/CSVDelimiterGuessingTests.swift | 86 +++++++ SwiftCSVTests/CSVDelimiterTests.swift | 16 +- SwiftCSVTests/CSVTests.swift | 130 ----------- SwiftCSVTests/EnumeratedCSVViewTests.swift | 87 +++++++ SwiftCSVTests/EnumeratedViewTests.swift | 59 ----- SwiftCSVTests/NamedCSVViewTests.swift | 134 +++++++++++ SwiftCSVTests/NewlineTests.swift | 63 +++++ SwiftCSVTests/ParserTests.swift | 4 - SwiftCSVTests/PerformanceTest.swift | 8 +- SwiftCSVTests/QuotedTests.swift | 13 +- SwiftCSVTests/ResourceHelper.swift | 2 +- SwiftCSVTests/TSVTests.swift | 16 +- SwiftCSVTests/URLTests.swift | 14 +- 26 files changed, 788 insertions(+), 620 deletions(-) create mode 100644 SwiftCSV/CSVDelimiter.swift delete mode 100644 SwiftCSV/Description.swift create mode 100644 SwiftCSV/EnumeratedCSVView.swift delete mode 100644 SwiftCSV/EnumeratedView.swift create mode 100644 SwiftCSV/NamedCSVView.swift delete mode 100644 SwiftCSV/NamedView.swift delete mode 100644 SwiftCSVTests/CSV+DelimiterGuessingTests.swift create mode 100644 SwiftCSVTests/CSVDelimiterGuessingTests.swift delete mode 100644 SwiftCSVTests/CSVTests.swift create mode 100644 SwiftCSVTests/EnumeratedCSVViewTests.swift delete mode 100644 SwiftCSVTests/EnumeratedViewTests.swift create mode 100644 SwiftCSVTests/NamedCSVViewTests.swift create mode 100644 SwiftCSVTests/NewlineTests.swift diff --git a/README.md b/README.md index bad6cb4..2ef0664 100644 --- a/README.md +++ b/README.md @@ -21,21 +21,21 @@ import SwiftCSV do { // As a string, guessing the delimiter - let csv: CSV = try CSV(string: "id,name,age\n1,Alice,18") + let csv: CSV = try CSV(string: "id,name,age\n1,Alice,18") // Specifying a custom delimiter - let tsv: CSV = try CSV(string: "id\tname\tage\n1\tAlice\t18", delimiter: "\t") + let tsv: CSV = try CSV(string: "id\tname\tage\n1\tAlice\t18", delimiter: .tab) // From a file (propagating error during file loading) - let csvFile: CSV = try CSV(url: URL(fileURLWithPath: "path/to/users.csv")) + let csvFile: CSV = try CSV(url: URL(fileURLWithPath: "path/to/users.csv")) // From a file inside the app bundle, with a custom delimiter, errors, and custom encoding. // Note the result is an optional. - let resource: CSV? = try CSV( + let resource: CSV? = try CSV( name: "users", extension: "tsv", bundle: .main, - delimiter: "\t", + delimiter: .character("🐠"), // Any character works! encoding: .utf8) } catch parseError as CSVParseError { // Catch errors from parsing invalid CSV @@ -52,22 +52,24 @@ The `CSV` class comes with initializers that are suited for loading files from U extension CSV { /// Load a CSV file from `url`. /// - /// - parameter url: URL of the file (will be passed to `String(contentsOfURL:encoding:)` to load) - /// - parameter delimiter: Character used to separate cells from one another in rows. - /// - parameter encoding: Character encoding to read file (default is `.utf8`) - /// - parameter loadColumns: Whether to populate the columns dictionary (default is `true`) - /// - throws: `CSVParseError` when parsing the contents of `url` fails, or file loading errors. + /// - Parameters: + /// - url: URL of the file (will be passed to `String(contentsOfURL:encoding:)` to load) + /// - delimiter: Character used to separate separate cells from one another in rows. + /// - encoding: Character encoding to read file (default is `.utf8`) + /// - loadColumns: Whether to populate the columns dictionary (default is `true`) + /// - Throws: `CSVParseError` when parsing the contents of `url` fails, or file loading errors. public convenience init(url: URL, - delimiter: Delimiter, + delimiter: CSVDelimiter, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws /// Load a CSV file from `url` and guess its delimiter from `CSV.recognizedDelimiters`, falling back to `.comma`. /// - /// - parameter url: URL of the file (will be passed to `String(contentsOfURL:encoding:)` to load) - /// - parameter encoding: Character encoding to read file (default is `.utf8`) - /// - parameter loadColumns: Whether to populate the columns dictionary (default is `true`) - /// - throws: `CSVParseError` when parsing the contents of `url` fails, or file loading errors. + /// - Parameters: + /// - url: URL of the file (will be passed to `String(contentsOfURL:encoding:)` to load) + /// - encoding: Character encoding to read file (default is `.utf8`) + /// - loadColumns: Whether to populate the columns dictionary (default is `true`) + /// - Throws: `CSVParseError` when parsing the contents of `url` fails, or file loading errors. public convenience init(url: URL, encoding: String.Encoding = .utf8, loadColumns: Bool = true) @@ -76,7 +78,7 @@ extension CSV { ### Delimiters -Delimiters are strongly typed. The recognized `CSV.Delimiter` cases are: `.comma`, `.semicolon`, and `.tab`. +Delimiters are strongly typed. The recognized `CSVDelimiter` cases are: `.comma`, `.semicolon`, and `.tab`. You can use convenience initializers that guess the delimiter from the recognized list for you. These initializers are available for loading CSV from URLs and strings. @@ -86,16 +88,16 @@ You can also use any other single-character delimiter when loading CSV data. A c ```swift // Recognized the comma delimiter automatically: -let csv = CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19") +let csv = CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19") csv.header //=> ["id", "name", "age"] -csv.namedRows //=> [["id": "1", "name": "Alice", "age": "18"], ["id": "2", "name": "Bob", "age": "19"]] -csv.namedColumns //=> ["id": ["1", "2"], "name": ["Alice", "Bob"], "age": ["18", "19"]] +csv.rows //=> [["id": "1", "name": "Alice", "age": "18"], ["id": "2", "name": "Bob", "age": "19"]] +csv.columns //=> ["id": ["1", "2"], "name": ["Alice", "Bob"], "age": ["18", "19"]] ``` The rows can also parsed and passed to a block on the fly, reducing the memory needed to store the whole lot in an array: ```swift -// Access each row as an array (array not guaranteed to be equal length to the header) +// Access each row as an array (inner array not guaranteed to always be equal length to the header) csv.enumerateAsArray { array in print(array.first) } @@ -107,17 +109,30 @@ csv.enumerateAsDict { dict in ### Skip Named Column Access for Large Data Sets -By default, the variants of `CSV.init` will populate its `namedColumns` and `enumeratedColumns` to provide access to the CSV data on a column-by-column basis. Think of this like a cross section: +Use `CSV` aka `NamedCSV` to access the CSV data on a column-by-column basis with named columns. Think of this like a cross section: ```swift -let csv = CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19") -csv.namedRows[0]["name"] //=> "Alice" -csv.namedColumns["name"] //=> ["Alice", "Bob"] +let csv = NamedCSV(string: "id,name,age\n1,Alice,18\n2,Bob,19") +csv.rows[0]["name"] //=> "Alice" +csv.columns["name"] //=> ["Alice", "Bob"] ``` -If you only want to access your data row-by-row, and not by-column, then you can set the `loadColumns` argument in any initializer to `false`. This will prevent the columnar data from being populated. +If you only want to access your data row-by-row, and not by-column, then you can use `CSV` or `EnumeratedCSV`: -Skipping this step can increase performance for lots of data. +```swift +let csv = EnumeratedCSV(string: "id,name,age\n1,Alice,18\n2,Bob,19") +csv.rows[0][1] //=> "Alice" +csv.columns?[0].header //=> "name" +csv.columns?[0].rows //=> ["Alice", "Bob"] +``` + +To speed things up, skip populating by-column access completely by passing `loadColums: false`. This will prevent the columnar data from being populated. For large data sets, this saves a lot of iterations (at quadratic runtime). + +```swift +let csv = EnumeratedCSV(string: "id,name,age\n1,Alice,18\n2,Bob,19", loadColumns: false) +csv.rows[0][1] //=> "Alice" +csv.columns //=> nil +``` ## Installation @@ -137,5 +152,5 @@ github "swiftcsv/SwiftCSV" ### SwiftPM ``` -.package(url: "https://github.com/swiftcsv/SwiftCSV.git", from: "0.6.1") +.package(url: "https://github.com/swiftcsv/SwiftCSV.git", from: "0.8.0") ``` diff --git a/SwiftCSV.xcodeproj/project.pbxproj b/SwiftCSV.xcodeproj/project.pbxproj index cc28289..2c2f56f 100644 --- a/SwiftCSV.xcodeproj/project.pbxproj +++ b/SwiftCSV.xcodeproj/project.pbxproj @@ -8,58 +8,62 @@ /* Begin PBXBuildFile section */ 3D1E59C01945FFAD001CF760 /* SwiftCSV.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3D1E59B41945FFAC001CF760 /* SwiftCSV.framework */; }; - 3D1E59C71945FFAD001CF760 /* CSVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1E59C61945FFAD001CF760 /* CSVTests.swift */; }; + 3D1E59C71945FFAD001CF760 /* NamedCSVViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1E59C61945FFAD001CF760 /* NamedCSVViewTests.swift */; }; 3D3749E3194D6DF7008F262A /* TSVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3749E2194D6DF7008F262A /* TSVTests.swift */; }; 3D444BCD1C7D88290001C60C /* String+Lines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D444BCC1C7D88290001C60C /* String+Lines.swift */; }; 3DAAEE9C1C74C7EC00A933DB /* CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAAEE9B1C74C7EC00A933DB /* CSV.swift */; }; 5015AD8A274BA20A0050F975 /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5015AD89274BA20A0050F975 /* ParserTests.swift */; }; 5015AD8B274BA20A0050F975 /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5015AD89274BA20A0050F975 /* ParserTests.swift */; }; 5015AD8C274BA20A0050F975 /* ParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5015AD89274BA20A0050F975 /* ParserTests.swift */; }; - 508975D21DBB897A006F3DBE /* NamedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D11DBB897A006F3DBE /* NamedView.swift */; }; - 508975D31DBB897A006F3DBE /* NamedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D11DBB897A006F3DBE /* NamedView.swift */; }; - 508975D41DBB897A006F3DBE /* NamedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D11DBB897A006F3DBE /* NamedView.swift */; }; - 508975D51DBB897A006F3DBE /* NamedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D11DBB897A006F3DBE /* NamedView.swift */; }; + 508975D21DBB897A006F3DBE /* NamedCSVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D11DBB897A006F3DBE /* NamedCSVView.swift */; }; + 508975D31DBB897A006F3DBE /* NamedCSVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D11DBB897A006F3DBE /* NamedCSVView.swift */; }; + 508975D41DBB897A006F3DBE /* NamedCSVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D11DBB897A006F3DBE /* NamedCSVView.swift */; }; + 508975D51DBB897A006F3DBE /* NamedCSVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D11DBB897A006F3DBE /* NamedCSVView.swift */; }; 508975D71DBF34CF006F3DBE /* ParsingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D61DBF34CF006F3DBE /* ParsingState.swift */; }; 508975D81DBF34CF006F3DBE /* ParsingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D61DBF34CF006F3DBE /* ParsingState.swift */; }; 508975D91DBF34CF006F3DBE /* ParsingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D61DBF34CF006F3DBE /* ParsingState.swift */; }; 508975DA1DBF34CF006F3DBE /* ParsingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975D61DBF34CF006F3DBE /* ParsingState.swift */; }; - 508975DC1DBF3B70006F3DBE /* EnumeratedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975DB1DBF3B70006F3DBE /* EnumeratedView.swift */; }; - 508975DD1DBF3B70006F3DBE /* EnumeratedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975DB1DBF3B70006F3DBE /* EnumeratedView.swift */; }; - 508975DE1DBF3B70006F3DBE /* EnumeratedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975DB1DBF3B70006F3DBE /* EnumeratedView.swift */; }; - 508975DF1DBF3B70006F3DBE /* EnumeratedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975DB1DBF3B70006F3DBE /* EnumeratedView.swift */; }; - 508975E11DBF3E51006F3DBE /* EnumeratedViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975E01DBF3E51006F3DBE /* EnumeratedViewTests.swift */; }; - 508975E21DBF3E51006F3DBE /* EnumeratedViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975E01DBF3E51006F3DBE /* EnumeratedViewTests.swift */; }; - 508975E31DBF3E51006F3DBE /* EnumeratedViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975E01DBF3E51006F3DBE /* EnumeratedViewTests.swift */; }; + 508975DC1DBF3B70006F3DBE /* EnumeratedCSVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975DB1DBF3B70006F3DBE /* EnumeratedCSVView.swift */; }; + 508975DD1DBF3B70006F3DBE /* EnumeratedCSVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975DB1DBF3B70006F3DBE /* EnumeratedCSVView.swift */; }; + 508975DE1DBF3B70006F3DBE /* EnumeratedCSVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975DB1DBF3B70006F3DBE /* EnumeratedCSVView.swift */; }; + 508975DF1DBF3B70006F3DBE /* EnumeratedCSVView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975DB1DBF3B70006F3DBE /* EnumeratedCSVView.swift */; }; + 508975E11DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975E01DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift */; }; + 508975E21DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975E01DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift */; }; + 508975E31DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508975E01DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift */; }; 508CA0FB2771F2E70084C8E8 /* CSV+DelimiterGuessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA0FA2771F2E70084C8E8 /* CSV+DelimiterGuessing.swift */; }; - 508CA0FD2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA0FC2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift */; }; - 508CA0FE2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA0FC2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift */; }; - 508CA0FF2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA0FC2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift */; }; + 508CA0FD2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA0FC2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift */; }; + 508CA0FE2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA0FC2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift */; }; + 508CA0FF2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA0FC2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift */; }; 508CA1002771F32C0084C8E8 /* CSV+DelimiterGuessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA0FA2771F2E70084C8E8 /* CSV+DelimiterGuessing.swift */; }; 508CA1022771F32D0084C8E8 /* CSV+DelimiterGuessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA0FA2771F2E70084C8E8 /* CSV+DelimiterGuessing.swift */; }; 508CA1032771F32E0084C8E8 /* CSV+DelimiterGuessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA0FA2771F2E70084C8E8 /* CSV+DelimiterGuessing.swift */; }; 508CA1052772039E0084C8E8 /* CSVDelimiterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA1042772039E0084C8E8 /* CSVDelimiterTests.swift */; }; 508CA1062772039E0084C8E8 /* CSVDelimiterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA1042772039E0084C8E8 /* CSVDelimiterTests.swift */; }; 508CA1072772039E0084C8E8 /* CSVDelimiterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508CA1042772039E0084C8E8 /* CSVDelimiterTests.swift */; }; + 50A2B23424894DC900B168A9 /* NewlineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A2B23324894DC900B168A9 /* NewlineTests.swift */; }; + 50A2B23524894DC900B168A9 /* NewlineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A2B23324894DC900B168A9 /* NewlineTests.swift */; }; + 50A2B23624894DC900B168A9 /* NewlineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A2B23324894DC900B168A9 /* NewlineTests.swift */; }; + 50B3EEA4286F8A84007B3956 /* CSVDelimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B3EEA3286F8A84007B3956 /* CSVDelimiter.swift */; }; + 50B3EEA5286F8AA3007B3956 /* CSVDelimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B3EEA3286F8A84007B3956 /* CSVDelimiter.swift */; }; + 50B3EEA6286F8AA4007B3956 /* CSVDelimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B3EEA3286F8A84007B3956 /* CSVDelimiter.swift */; }; + 50B3EEA7286F8AA5007B3956 /* CSVDelimiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B3EEA3286F8A84007B3956 /* CSVDelimiter.swift */; }; 5FB74B9B1CCB9274009DDBF1 /* SwiftCSV.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5FB74B911CCB9274009DDBF1 /* SwiftCSV.framework */; }; 5FB74BB71CCB929D009DDBF1 /* SwiftCSV.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5FB74BAD1CCB929D009DDBF1 /* SwiftCSV.framework */; }; 5FB74BD11CCB92E5009DDBF1 /* CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAAEE9B1C74C7EC00A933DB /* CSV.swift */; }; 5FB74BD21CCB92E5009DDBF1 /* String+Lines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D444BCC1C7D88290001C60C /* String+Lines.swift */; }; - 5FB74BD41CCB92E5009DDBF1 /* Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEE5461D1CBBB15900C0666F /* Description.swift */; }; 5FB74BD51CCB92E5009DDBF1 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9B02D71CBE57B8009FE424 /* Parser.swift */; }; 5FB74BD61CCB92EB009DDBF1 /* CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAAEE9B1C74C7EC00A933DB /* CSV.swift */; }; 5FB74BD71CCB92EB009DDBF1 /* String+Lines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D444BCC1C7D88290001C60C /* String+Lines.swift */; }; - 5FB74BD91CCB92EB009DDBF1 /* Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEE5461D1CBBB15900C0666F /* Description.swift */; }; 5FB74BDA1CCB92EB009DDBF1 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9B02D71CBE57B8009FE424 /* Parser.swift */; }; 5FB74BDB1CCB92F1009DDBF1 /* CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DAAEE9B1C74C7EC00A933DB /* CSV.swift */; }; 5FB74BDC1CCB92F1009DDBF1 /* String+Lines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D444BCC1C7D88290001C60C /* String+Lines.swift */; }; - 5FB74BDE1CCB92F1009DDBF1 /* Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEE5461D1CBBB15900C0666F /* Description.swift */; }; 5FB74BDF1CCB92F1009DDBF1 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9B02D71CBE57B8009FE424 /* Parser.swift */; }; - 5FB74BE01CCB9312009DDBF1 /* CSVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1E59C61945FFAD001CF760 /* CSVTests.swift */; }; + 5FB74BE01CCB9312009DDBF1 /* NamedCSVViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1E59C61945FFAD001CF760 /* NamedCSVViewTests.swift */; }; 5FB74BE11CCB9312009DDBF1 /* QuotedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C86061CB5CE44009A351D /* QuotedTests.swift */; }; 5FB74BE21CCB9312009DDBF1 /* TSVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3749E2194D6DF7008F262A /* TSVTests.swift */; }; 5FB74BE31CCB9312009DDBF1 /* URLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE06B67F1CB726B5009578CC /* URLTests.swift */; }; 5FB74BE41CCB9312009DDBF1 /* PerformanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46085931CCB1F5C00385286 /* PerformanceTest.swift */; }; - 5FB74BE51CCB931F009DDBF1 /* CSVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1E59C61945FFAD001CF760 /* CSVTests.swift */; }; + 5FB74BE51CCB931F009DDBF1 /* NamedCSVViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1E59C61945FFAD001CF760 /* NamedCSVViewTests.swift */; }; 5FB74BE61CCB931F009DDBF1 /* QuotedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C86061CB5CE44009A351D /* QuotedTests.swift */; }; 5FB74BE71CCB931F009DDBF1 /* TSVTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3749E2194D6DF7008F262A /* TSVTests.swift */; }; 5FB74BE81CCB931F009DDBF1 /* URLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE06B67F1CB726B5009578CC /* URLTests.swift */; }; @@ -75,7 +79,6 @@ BE06B6821CB7287F009578CC /* quotes.csv in Resources */ = {isa = PBXBuildFile; fileRef = BE06B6811CB7287F009578CC /* quotes.csv */; }; BE6C86071CB5CE44009A351D /* QuotedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE6C86061CB5CE44009A351D /* QuotedTests.swift */; }; BE9B02D81CBE57B8009FE424 /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE9B02D71CBE57B8009FE424 /* Parser.swift */; }; - BEE5461E1CBBB15900C0666F /* Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEE5461D1CBBB15900C0666F /* Description.swift */; }; E46085921CCB1E8F00385286 /* large.csv in Resources */ = {isa = PBXBuildFile; fileRef = E46085911CCB1E8F00385286 /* large.csv */; }; E46085941CCB1F5C00385286 /* PerformanceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E46085931CCB1F5C00385286 /* PerformanceTest.swift */; }; F5C19F502283243C00920B06 /* ResourceHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C19F4F2283243C00920B06 /* ResourceHelper.swift */; }; @@ -117,21 +120,25 @@ /* Begin PBXFileReference section */ 3D1E59B41945FFAC001CF760 /* SwiftCSV.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftCSV.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3D1E59BF1945FFAD001CF760 /* SwiftCSVTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftCSVTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3D1E59C61945FFAD001CF760 /* CSVTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVTests.swift; sourceTree = ""; }; + 3D1E59C61945FFAD001CF760 /* NamedCSVViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamedCSVViewTests.swift; sourceTree = ""; }; 3D3749E2194D6DF7008F262A /* TSVTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TSVTests.swift; sourceTree = ""; }; 3D444BCC1C7D88290001C60C /* String+Lines.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Lines.swift"; sourceTree = ""; }; 3DAAEE9B1C74C7EC00A933DB /* CSV.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = CSV.swift; sourceTree = ""; tabWidth = 4; }; 5015AD89274BA20A0050F975 /* ParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParserTests.swift; sourceTree = ""; }; 5034F4712272E0DC001C02D1 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 5034F4722272E0E4001C02D1 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; - 508975D11DBB897A006F3DBE /* NamedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NamedView.swift; sourceTree = ""; }; + 508975D11DBB897A006F3DBE /* NamedCSVView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NamedCSVView.swift; sourceTree = ""; }; 508975D61DBF34CF006F3DBE /* ParsingState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsingState.swift; sourceTree = ""; }; - 508975DB1DBF3B70006F3DBE /* EnumeratedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnumeratedView.swift; sourceTree = ""; }; - 508975E01DBF3E51006F3DBE /* EnumeratedViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnumeratedViewTests.swift; sourceTree = ""; }; + 508975DB1DBF3B70006F3DBE /* EnumeratedCSVView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnumeratedCSVView.swift; sourceTree = ""; }; + 508975E01DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnumeratedCSVViewTests.swift; sourceTree = ""; }; 508CA0FA2771F2E70084C8E8 /* CSV+DelimiterGuessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CSV+DelimiterGuessing.swift"; sourceTree = ""; }; - 508CA0FC2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CSV+DelimiterGuessingTests.swift"; sourceTree = ""; }; + 508CA0FC2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVDelimiterGuessingTests.swift; sourceTree = ""; }; 508CA1042772039E0084C8E8 /* CSVDelimiterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVDelimiterTests.swift; sourceTree = ""; }; + 50A2B23324894DC900B168A9 /* NewlineTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewlineTests.swift; sourceTree = ""; }; + 50B3EEA3286F8A84007B3956 /* CSVDelimiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVDelimiter.swift; sourceTree = ""; }; 50F241A4274BB8DB00520A69 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; + 50F241A5274BBDF000520A69 /* SwiftCSV.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = SwiftCSV.podspec; sourceTree = ""; }; + 50F241A6274BBDF000520A69 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 5FB74B911CCB9274009DDBF1 /* SwiftCSV.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftCSV.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5FB74B9A1CCB9274009DDBF1 /* SwiftCSVTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftCSVTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 5FB74BAD1CCB929D009DDBF1 /* SwiftCSV.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftCSV.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -142,7 +149,6 @@ BE06B6811CB7287F009578CC /* quotes.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = quotes.csv; sourceTree = ""; }; BE6C86061CB5CE44009A351D /* QuotedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QuotedTests.swift; sourceTree = ""; }; BE9B02D71CBE57B8009FE424 /* Parser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = ""; }; - BEE5461D1CBBB15900C0666F /* Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Description.swift; sourceTree = ""; }; E46085911CCB1E8F00385286 /* large.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = large.csv; sourceTree = ""; }; E46085931CCB1F5C00385286 /* PerformanceTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PerformanceTest.swift; sourceTree = ""; }; F5C19F4F2283243C00920B06 /* ResourceHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResourceHelper.swift; sourceTree = ""; }; @@ -210,6 +216,8 @@ 5034F4712272E0DC001C02D1 /* README.md */, 5034F4722272E0E4001C02D1 /* LICENSE */, 50F241A4274BB8DB00520A69 /* CHANGELOG.md */, + 50F241A6274BBDF000520A69 /* Package.swift */, + 50F241A5274BBDF000520A69 /* SwiftCSV.podspec */, 3D1E59B61945FFAC001CF760 /* SwiftCSV */, 3D1E59C31945FFAD001CF760 /* SwiftCSVTests */, 3D1E59B51945FFAC001CF760 /* Products */, @@ -236,11 +244,11 @@ isa = PBXGroup; children = ( 3DAAEE9B1C74C7EC00A933DB /* CSV.swift */, + 50B3EEA3286F8A84007B3956 /* CSVDelimiter.swift */, + 508975D11DBB897A006F3DBE /* NamedCSVView.swift */, + 508975DB1DBF3B70006F3DBE /* EnumeratedCSVView.swift */, 508CA0FA2771F2E70084C8E8 /* CSV+DelimiterGuessing.swift */, - 508975D11DBB897A006F3DBE /* NamedView.swift */, - 508975DB1DBF3B70006F3DBE /* EnumeratedView.swift */, 3D444BCC1C7D88290001C60C /* String+Lines.swift */, - BEE5461D1CBBB15900C0666F /* Description.swift */, BE9B02D71CBE57B8009FE424 /* Parser.swift */, 508975D61DBF34CF006F3DBE /* ParsingState.swift */, ); @@ -251,10 +259,11 @@ isa = PBXGroup; children = ( BE06B67E1CB72680009578CC /* Res */, - 3D1E59C61945FFAD001CF760 /* CSVTests.swift */, - 508CA0FC2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift */, + 3D1E59C61945FFAD001CF760 /* NamedCSVViewTests.swift */, + 508975E01DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift */, + 50A2B23324894DC900B168A9 /* NewlineTests.swift */, + 508CA0FC2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift */, 508CA1042772039E0084C8E8 /* CSVDelimiterTests.swift */, - 508975E01DBF3E51006F3DBE /* EnumeratedViewTests.swift */, BE6C86061CB5CE44009A351D /* QuotedTests.swift */, 3D3749E2194D6DF7008F262A /* TSVTests.swift */, BE06B67F1CB726B5009578CC /* URLTests.swift */, @@ -570,12 +579,12 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 508975D21DBB897A006F3DBE /* NamedView.swift in Sources */, + 50B3EEA4286F8A84007B3956 /* CSVDelimiter.swift in Sources */, + 508975D21DBB897A006F3DBE /* NamedCSVView.swift in Sources */, 508CA0FB2771F2E70084C8E8 /* CSV+DelimiterGuessing.swift in Sources */, - BEE5461E1CBBB15900C0666F /* Description.swift in Sources */, 3DAAEE9C1C74C7EC00A933DB /* CSV.swift in Sources */, BE9B02D81CBE57B8009FE424 /* Parser.swift in Sources */, - 508975DC1DBF3B70006F3DBE /* EnumeratedView.swift in Sources */, + 508975DC1DBF3B70006F3DBE /* EnumeratedCSVView.swift in Sources */, 508975D71DBF34CF006F3DBE /* ParsingState.swift in Sources */, 3D444BCD1C7D88290001C60C /* String+Lines.swift in Sources */, ); @@ -586,11 +595,12 @@ buildActionMask = 2147483647; files = ( E46085941CCB1F5C00385286 /* PerformanceTest.swift in Sources */, - 3D1E59C71945FFAD001CF760 /* CSVTests.swift in Sources */, - 508CA0FD2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift in Sources */, + 3D1E59C71945FFAD001CF760 /* NamedCSVViewTests.swift in Sources */, + 50A2B23424894DC900B168A9 /* NewlineTests.swift in Sources */, + 508CA0FD2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift in Sources */, F5C19F502283243C00920B06 /* ResourceHelper.swift in Sources */, + 508975E11DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift in Sources */, 508CA1052772039E0084C8E8 /* CSVDelimiterTests.swift in Sources */, - 508975E11DBF3E51006F3DBE /* EnumeratedViewTests.swift in Sources */, 5015AD8A274BA20A0050F975 /* ParserTests.swift in Sources */, 3D3749E3194D6DF7008F262A /* TSVTests.swift in Sources */, BE06B6801CB726B5009578CC /* URLTests.swift in Sources */, @@ -602,13 +612,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 508975D31DBB897A006F3DBE /* NamedView.swift in Sources */, + 50B3EEA5286F8AA3007B3956 /* CSVDelimiter.swift in Sources */, + 508975D31DBB897A006F3DBE /* NamedCSVView.swift in Sources */, 508CA1002771F32C0084C8E8 /* CSV+DelimiterGuessing.swift in Sources */, 5FB74BD11CCB92E5009DDBF1 /* CSV.swift in Sources */, 5FB74BD21CCB92E5009DDBF1 /* String+Lines.swift in Sources */, - 508975DD1DBF3B70006F3DBE /* EnumeratedView.swift in Sources */, + 508975DD1DBF3B70006F3DBE /* EnumeratedCSVView.swift in Sources */, 508975D81DBF34CF006F3DBE /* ParsingState.swift in Sources */, - 5FB74BD41CCB92E5009DDBF1 /* Description.swift in Sources */, 5FB74BD51CCB92E5009DDBF1 /* Parser.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -617,12 +627,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5FB74BE01CCB9312009DDBF1 /* CSVTests.swift in Sources */, + 5FB74BE01CCB9312009DDBF1 /* NamedCSVViewTests.swift in Sources */, 5FB74BE11CCB9312009DDBF1 /* QuotedTests.swift in Sources */, - 508CA0FE2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift in Sources */, + 50A2B23524894DC900B168A9 /* NewlineTests.swift in Sources */, + 508CA0FE2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift in Sources */, F5C19F512283C0C100920B06 /* ResourceHelper.swift in Sources */, + 508975E21DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift in Sources */, 508CA1062772039E0084C8E8 /* CSVDelimiterTests.swift in Sources */, - 508975E21DBF3E51006F3DBE /* EnumeratedViewTests.swift in Sources */, 5015AD8B274BA20A0050F975 /* ParserTests.swift in Sources */, 5FB74BE21CCB9312009DDBF1 /* TSVTests.swift in Sources */, 5FB74BE31CCB9312009DDBF1 /* URLTests.swift in Sources */, @@ -634,13 +645,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 508975D41DBB897A006F3DBE /* NamedView.swift in Sources */, + 50B3EEA6286F8AA4007B3956 /* CSVDelimiter.swift in Sources */, + 508975D41DBB897A006F3DBE /* NamedCSVView.swift in Sources */, 508CA1022771F32D0084C8E8 /* CSV+DelimiterGuessing.swift in Sources */, 5FB74BD61CCB92EB009DDBF1 /* CSV.swift in Sources */, 5FB74BD71CCB92EB009DDBF1 /* String+Lines.swift in Sources */, - 508975DE1DBF3B70006F3DBE /* EnumeratedView.swift in Sources */, + 508975DE1DBF3B70006F3DBE /* EnumeratedCSVView.swift in Sources */, 508975D91DBF34CF006F3DBE /* ParsingState.swift in Sources */, - 5FB74BD91CCB92EB009DDBF1 /* Description.swift in Sources */, 5FB74BDA1CCB92EB009DDBF1 /* Parser.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -649,12 +660,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5FB74BE51CCB931F009DDBF1 /* CSVTests.swift in Sources */, + 5FB74BE51CCB931F009DDBF1 /* NamedCSVViewTests.swift in Sources */, 5FB74BE61CCB931F009DDBF1 /* QuotedTests.swift in Sources */, - 508CA0FF2771F3260084C8E8 /* CSV+DelimiterGuessingTests.swift in Sources */, + 50A2B23624894DC900B168A9 /* NewlineTests.swift in Sources */, + 508CA0FF2771F3260084C8E8 /* CSVDelimiterGuessingTests.swift in Sources */, F5C19F522283C0C300920B06 /* ResourceHelper.swift in Sources */, + 508975E31DBF3E51006F3DBE /* EnumeratedCSVViewTests.swift in Sources */, 508CA1072772039E0084C8E8 /* CSVDelimiterTests.swift in Sources */, - 508975E31DBF3E51006F3DBE /* EnumeratedViewTests.swift in Sources */, 5015AD8C274BA20A0050F975 /* ParserTests.swift in Sources */, 5FB74BE71CCB931F009DDBF1 /* TSVTests.swift in Sources */, 5FB74BE81CCB931F009DDBF1 /* URLTests.swift in Sources */, @@ -666,13 +678,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 508975D51DBB897A006F3DBE /* NamedView.swift in Sources */, + 50B3EEA7286F8AA5007B3956 /* CSVDelimiter.swift in Sources */, + 508975D51DBB897A006F3DBE /* NamedCSVView.swift in Sources */, 508CA1032771F32E0084C8E8 /* CSV+DelimiterGuessing.swift in Sources */, 5FB74BDB1CCB92F1009DDBF1 /* CSV.swift in Sources */, 5FB74BDC1CCB92F1009DDBF1 /* String+Lines.swift in Sources */, - 508975DF1DBF3B70006F3DBE /* EnumeratedView.swift in Sources */, + 508975DF1DBF3B70006F3DBE /* EnumeratedCSVView.swift in Sources */, 508975DA1DBF34CF006F3DBE /* ParsingState.swift in Sources */, - 5FB74BDE1CCB92F1009DDBF1 /* Description.swift in Sources */, 5FB74BDF1CCB92F1009DDBF1 /* Parser.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SwiftCSV/CSV+DelimiterGuessing.swift b/SwiftCSV/CSV+DelimiterGuessing.swift index 8a98d4d..e892220 100644 --- a/SwiftCSV/CSV+DelimiterGuessing.swift +++ b/SwiftCSV/CSV+DelimiterGuessing.swift @@ -8,12 +8,12 @@ import Foundation -extension CSV { - public static let recognizedDelimiters: [Delimiter] = [.comma, .tab, .semicolon] +extension CSVDelimiter { + public static let recognized: [CSVDelimiter] = [.comma, .tab, .semicolon] /// - Returns: Delimiter between cells based on the first line in the CSV. Falls back to `.comma`. - public static func guessedDelimiter(string: String) -> Delimiter { - let recognizedDelimiterCharacters = recognizedDelimiters.map(\.rawValue) + public static func guessed(string: String) -> CSVDelimiter { + let recognizedDelimiterCharacters = CSVDelimiter.recognized.map(\.rawValue) // Trim newline and spaces, but keep tabs (as delimiters) var trimmedCharacters = CharacterSet.whitespacesAndNewlines @@ -39,7 +39,7 @@ extension CSV { index = line.endIndex } case _ where recognizedDelimiterCharacters.contains(character): - return Delimiter(rawValue: character) + return CSVDelimiter(rawValue: character) default: index = line.index(after: index) } diff --git a/SwiftCSV/CSV.swift b/SwiftCSV/CSV.swift index 2d65902..e304024 100644 --- a/SwiftCSV/CSV.swift +++ b/SwiftCSV/CSV.swift @@ -9,113 +9,75 @@ import Foundation public protocol CSVView { - associatedtype Rows + associatedtype Row associatedtype Columns - var rows: Rows { get } - var columns: Columns { get } + var rows: [Row] { get } - init(header: [String], text: String, delimiter: CSV.Delimiter, limitTo: Int?, loadColumns: Bool) throws -} + /// Is `nil` if `loadColumns` was set to `false`. + var columns: Columns? { get } -open class CSV { - public enum Delimiter: Equatable, ExpressibleByUnicodeScalarLiteral { - public typealias UnicodeScalarLiteralType = Character + init(header: [String], text: String, delimiter: CSVDelimiter, loadColumns: Bool, rowLimit: Int?) throws - case comma, semicolon, tab - case character(Character) + func serialize(header: [String], delimiter: CSVDelimiter) -> String +} - public init(unicodeScalarLiteral: Character) { - self.init(rawValue: unicodeScalarLiteral) - } +/// CSV variant for which unique column names are assumed. +/// +/// Example: +/// +/// let csv = NamedCSV(...) +/// let allIDs = csv.columns["id"] +/// let firstEntry = csv.rows[0] +/// let fullName = firstEntry["firstName"] + " " + firstEntry["lastName"] +/// +public typealias NamedCSV = CSV + +/// CSV variant that exposes columns and rows as arrays. +/// Example: +/// +/// let csv = EnumeratedCSV(...) +/// let allIds = csv.columns.filter { $0.header == "id" }.rows +/// +public typealias EnumeratedCSV = CSV + +/// For convenience, there's `EnumeratedCSV` to access fields in rows by their column index, +/// and `NamedCSV` to access fields by their column names as defined in a header row. +open class CSV { - init(rawValue: Character) { - switch rawValue { - case ",": self = .comma - case ";": self = .semicolon - case "\t": self = .tab - default: self = .character(rawValue) - } - } - - public var rawValue: Character { - switch self { - case .comma: return "," - case .semicolon: return ";" - case .tab: return "\t" - case .character(let character): return character - } - } - } - public let header: [String] - lazy var _namedView: NamedView = { - return try! NamedView( - header: self.header, - text: self.text, - delimiter: self.delimiter, - loadColumns: self.loadColumns) - }() - - lazy var _enumeratedView: EnumeratedView = { - return try! EnumeratedView( - header: self.header, - text: self.text, - delimiter: self.delimiter, - loadColumns: self.loadColumns) - }() - - var text: String - var delimiter: Delimiter - - let loadColumns: Bool - - /// List of dictionaries that contains the CSV data - public var namedRows: [[String : String]] { - return _namedView.rows - } - - /// Dictionary of header name to list of values in that column - /// Will not be loaded if loadColumns in init is false - public var namedColumns: [String : [String]] { - return _namedView.columns - } + /// Unparsed contents. + public let text: String - /// Collection of column fields that contain the CSV data - public var enumeratedRows: [[String]] { - return _enumeratedView.rows - } - - /// Collection of columns with metadata. - /// Will not be loaded if loadColumns in init is false - public var enumeratedColumns: [EnumeratedView.Column] { - return _enumeratedView.columns - } + /// Used delimiter to parse `text` and to serialize the data again. + public let delimiter: CSVDelimiter + /// Underlying data representation of the CSV contents. + public let content: DataView - @available(*, unavailable, renamed: "namedRows") - public var rows: [[String : String]] { - return namedRows + public var rows: [DataView.Row] { + return content.rows } - @available(*, unavailable, renamed: "namedColumns") - public var columns: [String : [String]] { - return namedColumns + /// Is `nil` if `loadColumns` was set to `false` during initialization. + public var columns: DataView.Columns? { + return content.columns } - /// Load CSV data from a string. /// - /// - parameter string: CSV contents to parse. - /// - parameter delimiter: Character used to separate cells from one another in rows. - /// - parameter loadColumns: Whether to populate the `columns` dictionary (default is `true`) - /// - throws: `CSVParseError` when parsing `string` fails. - public init(string: String, delimiter: Delimiter, loadColumns: Bool = true) throws { + /// - Parameters: + /// - string: CSV contents to parse. + /// - delimiter: Character used to separate cells from one another in rows. + /// - loadColumns: Whether to populate the `columns` dictionary (default is `true`) + /// - rowLimit: Amount of rows to parse (default is `nil`). + /// - Throws: `CSVParseError` when parsing `string` fails. + public init(string: String, delimiter: CSVDelimiter, loadColumns: Bool = true, rowLimit: Int? = nil) throws { self.text = string self.delimiter = delimiter - self.loadColumns = loadColumns self.header = try Parser.array(text: string, delimiter: delimiter, rowLimit: 1).first ?? [] + self.content = try DataView(header: header, text: text, delimiter: delimiter, loadColumns: loadColumns, rowLimit: rowLimit) } /// Load CSV data from a string and guess its delimiter from `CSV.recognizedDelimiters`, falling back to `.comma`. @@ -124,25 +86,46 @@ open class CSV { /// - parameter loadColumns: Whether to populate the `columns` dictionary (default is `true`) /// - throws: `CSVParseError` when parsing `string` fails. public convenience init(string: String, loadColumns: Bool = true) throws { - let delimiter = CSV.guessedDelimiter(string: string) + let delimiter = CSVDelimiter.guessed(string: string) try self.init(string: string, delimiter: delimiter, loadColumns: loadColumns) } /// Turn the CSV data into NSData using a given encoding open func dataUsingEncoding(_ encoding: String.Encoding) -> Data? { - return description.data(using: encoding) + return serialized.data(using: encoding) + } + + /// Serialized form of the CSV data; depending on the View used, this may + /// perform additional normalizations. + open var serialized: String { + return self.content.serialize(header: self.header, delimiter: self.delimiter) + } +} + +extension CSV: CustomStringConvertible { + public var description: String { + return self.serialized + } +} + +func enquoteContentsIfNeeded(cell: String) -> String { + // Add quotes if value contains a comma + if cell.contains(",") { + return "\"\(cell)\"" } + return cell } extension CSV { /// Load a CSV file from `url`. /// - /// - parameter url: URL of the file (will be passed to `String(contentsOfURL:encoding:)` to load) - /// - parameter delimiter: Character used to separate cells from one another in rows. - /// - parameter encoding: Character encoding to read file (default is `.utf8`) - /// - parameter loadColumns: Whether to populate the columns dictionary (default is `true`) - /// - throws: `CSVParseError` when parsing the contents of `url` fails, or file loading errors. - public convenience init(url: URL, delimiter: Delimiter, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws { + /// - Parameters: + /// - url: URL of the file (will be passed to `String(contentsOfURL:encoding:)` to load) + /// - delimiter: Character used to separate separate cells from one another in rows. + /// - encoding: Character encoding to read file (default is `.utf8`) + /// - loadColumns: Whether to populate the columns dictionary (default is `true`) + /// - Throws: `CSVParseError` when parsing the contents of `url` fails, or file loading errors. + public convenience init(url: URL, delimiter: CSVDelimiter, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws { let contents = try String(contentsOf: url, encoding: encoding) try self.init(string: contents, delimiter: delimiter, loadColumns: loadColumns) @@ -150,10 +133,11 @@ extension CSV { /// Load a CSV file from `url` and guess its delimiter from `CSV.recognizedDelimiters`, falling back to `.comma`. /// - /// - parameter url: URL of the file (will be passed to `String(contentsOfURL:encoding:)` to load) - /// - parameter encoding: Character encoding to read file (default is `.utf8`) - /// - parameter loadColumns: Whether to populate the columns dictionary (default is `true`) - /// - throws: `CSVParseError` when parsing the contents of `url` fails, or file loading errors. + /// - Parameters: + /// - url: URL of the file (will be passed to `String(contentsOfURL:encoding:)` to load) + /// - encoding: Character encoding to read file (default is `.utf8`) + /// - loadColumns: Whether to populate the columns dictionary (default is `true`) + /// - Throws: `CSVParseError` when parsing the contents of `url` fails, or file loading errors. public convenience init(url: URL, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws { let contents = try String(contentsOf: url, encoding: encoding) @@ -164,15 +148,16 @@ extension CSV { extension CSV { /// Load a CSV file as a named resource from `bundle`. /// - /// - parameter name: Name of the file resource inside `bundle`. - /// - parameter ext: File extension of the resource; use `nil` to load the first file matching the name (default is `nil`) - /// - parameter bundle: `Bundle` to use for resource lookup (default is `.main`) - /// - parameter delimiter: Character used to separate cells from one another in rows. - /// - parameter encoding: encoding used to read file (default is `.utf8`) - /// - parameter loadColumns: Whether to populate the columns dictionary (default is `true`) - /// - throws: `CSVParseError` when parsing the contents of the resource fails, or file loading errors. - /// - returns: `nil` if the resource could not be found - public convenience init?(name: String, extension ext: String? = nil, bundle: Bundle = .main, delimiter: Delimiter, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws { + /// - Parameters: + /// - name: Name of the file resource inside `bundle`. + /// - ext: File extension of the resource; use `nil` to load the first file matching the name (default is `nil`) + /// - bundle: `Bundle` to use for resource lookup (default is `.main`) + /// - delimiter: Character used to separate separate cells from one another in rows. + /// - encoding: encoding used to read file (default is `.utf8`) + /// - loadColumns: Whether to populate the columns dictionary (default is `true`) + /// - Throws: `CSVParseError` when parsing the contents of the resource fails, or file loading errors. + /// - Returns: `nil` if the resource could not be found + public convenience init?(name: String, extension ext: String? = nil, bundle: Bundle = .main, delimiter: CSVDelimiter, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws { guard let url = bundle.url(forResource: name, withExtension: ext) else { return nil } @@ -181,14 +166,15 @@ extension CSV { /// Load a CSV file as a named resource from `bundle` and guess its delimiter from `CSV.recognizedDelimiters`, falling back to `.comma`. /// - /// - parameter name: Name of the file resource inside `bundle`. - /// - parameter ext: File extension of the resource; use `nil` to load the first file matching the name (default is `nil`) - /// - parameter bundle: `Bundle` to use for resource lookup (default is `.main`) - /// - parameter encoding: encoding used to read file (default is `.utf8`) - /// - parameter loadColumns: Whether to populate the columns dictionary (default is `true`) - /// - throws: `CSVParseError` when parsing the contents of the resource fails, or file loading errors. - /// - returns: `nil` if the resource could not be found - public convenience init?(name: String, extension ext: String? = nil, bundle: Bundle = .main, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws { + /// - Parameters: + /// - name: Name of the file resource inside `bundle`. + /// - ext: File extension of the resource; use `nil` to load the first file matching the name (default is `nil`) + /// - bundle: `Bundle` to use for resource lookup (default is `.main`) + /// - encoding: encoding used to read file (default is `.utf8`) + /// - loadColumns: Whether to populate the columns dictionary (default is `true`) + /// - Throws: `CSVParseError` when parsing the contents of the resource fails, or file loading errors. + /// - Returns: `nil` if the resource could not be found + public convenience init?(name: String, extension ext: String? = nil, bundle: Bundle = .main, encoding: String.Encoding = .utf8, loadColumns: Bool = true) throws { guard let url = bundle.url(forResource: name, withExtension: ext) else { return nil } diff --git a/SwiftCSV/CSVDelimiter.swift b/SwiftCSV/CSVDelimiter.swift new file mode 100644 index 0000000..69551d7 --- /dev/null +++ b/SwiftCSV/CSVDelimiter.swift @@ -0,0 +1,37 @@ +// +// CSVDelimiter.swift +// SwiftCSV +// +// Created by Christian Tietze on 01.07.22. +// Copyright © 2022 SwiftCSV. All rights reserved. +// + +public enum CSVDelimiter: Equatable, ExpressibleByUnicodeScalarLiteral { + + public typealias UnicodeScalarLiteralType = Character + + case comma, semicolon, tab + case character(Character) + + public init(unicodeScalarLiteral: Character) { + self.init(rawValue: unicodeScalarLiteral) + } + + init(rawValue: Character) { + switch rawValue { + case ",": self = .comma + case ";": self = .semicolon + case "\t": self = .tab + default: self = .character(rawValue) + } + } + + public var rawValue: Character { + switch self { + case .comma: return "," + case .semicolon: return ";" + case .tab: return "\t" + case .character(let character): return character + } + } +} diff --git a/SwiftCSV/Description.swift b/SwiftCSV/Description.swift deleted file mode 100644 index d5c1d7c..0000000 --- a/SwiftCSV/Description.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Description.swift -// SwiftCSV -// -// Created by Will Richardson on 11/04/16. -// Copyright © 2016 Naoto Kaneko. All rights reserved. -// - -import Foundation - -extension CSV: CustomStringConvertible { - public var description: String { - let head = header.joined(separator: ",") + "\n" - let cont = namedRows.map { row in - return header.map { key -> String in - let value = row[key]! - - // Add quotes if value contains a comma - if value.contains(",") { - return "\"\(value)\"" - } - return value - - }.joined(separator: ",") - - }.joined(separator: "\n") - return head + cont - } -} - diff --git a/SwiftCSV/EnumeratedCSVView.swift b/SwiftCSV/EnumeratedCSVView.swift new file mode 100644 index 0000000..1586f0f --- /dev/null +++ b/SwiftCSV/EnumeratedCSVView.swift @@ -0,0 +1,75 @@ +// +// EnumeratedCSVView.swift +// SwiftCSV +// +// Created by Christian Tietze on 25/10/16. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +import Foundation + +public struct Enumerated: CSVView { + + public struct Column: Equatable { + public let header: String + public let rows: [String] + } + + public typealias Row = [String] + public typealias Columns = [Column] + + public private(set) var rows: [Row] + public private(set) var columns: Columns? + + public init(header: [String], text: String, delimiter: CSVDelimiter, loadColumns: Bool = false, rowLimit: Int? = nil) throws { + + self.rows = try { + var rows: [Row] = [] + try Parser.enumerateAsArray(text: text, delimiter: delimiter, startAt: 1, rowLimit: rowLimit) { fields in + rows.append(fields) + } + + // Fill in gaps at the end of rows that are too short. + return makingRectangular(rows: rows) + }() + + self.columns = { + guard loadColumns else { return nil } + return header.enumerated().map { (index: Int, header: String) -> Column in + return Column( + header: header, + rows: rows.map { $0[safe: index] ?? "" }) + } + }() + } + + public func serialize(header: [String], delimiter: CSVDelimiter) -> String { + let separator = String(delimiter.rawValue) + + let head = header + .map(enquoteContentsIfNeeded(cell:)) + .joined(separator: separator) + "\n" + + let content = rows.map { row in + row.map(enquoteContentsIfNeeded(cell:)) + .joined(separator: separator) + }.joined(separator: "\n") + + return head + content + } +} + +extension Collection { + subscript (safe index: Self.Index) -> Self.Iterator.Element? { + return index < endIndex ? self[index] : nil + } +} + +fileprivate func makingRectangular(rows: [[String]]) -> [[String]] { + let cellsPerRow = rows.map { $0.count }.max() ?? 0 + return rows.map { row -> [String] in + let missingCellCount = cellsPerRow - row.count + let appendix = Array(repeating: "", count: missingCellCount) + return row + appendix + } +} diff --git a/SwiftCSV/EnumeratedView.swift b/SwiftCSV/EnumeratedView.swift deleted file mode 100644 index eddc297..0000000 --- a/SwiftCSV/EnumeratedView.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// EnumeratedView.swift -// SwiftCSV -// -// Created by Christian Tietze on 25/10/16. -// Copyright © 2016 Naoto Kaneko. All rights reserved. -// - -import Foundation - -public struct EnumeratedView: CSVView { - - public struct Column { - public let header: String - public let rows: [String] - } - - public private(set) var rows: [[String]] - public private(set) var columns: [Column] - - public init(header: [String], text: String, delimiter: CSV.Delimiter, limitTo: Int? = nil, loadColumns: Bool = false) throws { - - var rows = [[String]]() - var columns: [EnumeratedView.Column] = [] - - try Parser.enumerateAsArray(text: text, delimiter: delimiter, startAt: 1, rowLimit: limitTo.map { $0 - 1 }) { fields in - rows.append(fields) - } - - if loadColumns { - columns = header.enumerated().map { (index: Int, header: String) -> EnumeratedView.Column in - - return EnumeratedView.Column( - header: header, - rows: rows.map { $0[index] }) - } - } - - self.rows = rows - self.columns = columns - } - -} diff --git a/SwiftCSV/NamedCSVView.swift b/SwiftCSV/NamedCSVView.swift new file mode 100644 index 0000000..af3ad46 --- /dev/null +++ b/SwiftCSV/NamedCSVView.swift @@ -0,0 +1,53 @@ +// +// NamedCSVView.swift +// SwiftCSV +// +// Created by Christian Tietze on 22/10/16. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +public struct Named: CSVView { + + public typealias Row = [String : String] + public typealias Columns = [String : [String]] + + public var rows: [Row] + public var columns: Columns? + + public init(header: [String], text: String, delimiter: CSVDelimiter, loadColumns: Bool = false, rowLimit: Int? = nil) throws { + + self.rows = try { + var rows: [Row] = [] + try Parser.enumerateAsDict(header: header, content: text, delimiter: delimiter, rowLimit: rowLimit) { dict in + rows.append(dict) + } + return rows + }() + + self.columns = { + guard loadColumns else { return nil } + var columns: Columns = [:] + for field in header { + columns[field] = rows.map { $0[field] ?? "" } + } + return columns + }() + } + + public func serialize(header: [String], delimiter: CSVDelimiter) -> String { + let separator = String(delimiter.rawValue) + + let head = header + .map(enquoteContentsIfNeeded(cell:)) + .joined(separator: separator) + "\n" + + let content = rows.map { row in + header + .map { cellID in row[cellID]! } + .map(enquoteContentsIfNeeded(cell:)) + .joined(separator: separator) + }.joined(separator: "\n") + + return head + content + } +} diff --git a/SwiftCSV/NamedView.swift b/SwiftCSV/NamedView.swift deleted file mode 100644 index 585a053..0000000 --- a/SwiftCSV/NamedView.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// NamedView.swift -// SwiftCSV -// -// Created by Christian Tietze on 22/10/16. -// Copyright © 2016 Naoto Kaneko. All rights reserved. -// - -public struct NamedView: CSVView { - - public var rows: [[String : String]] - public var columns: [String : [String]] - - public init(header: [String], text: String, delimiter: CSV.Delimiter, limitTo: Int? = nil, loadColumns: Bool = false) throws { - - var rows = [[String: String]]() - var columns = [String: [String]]() - - try Parser.enumerateAsDict(header: header, content: text, delimiter: delimiter, rowLimit: limitTo.map { $0 + 1 }) { dict in - rows.append(dict) - } - - if loadColumns { - for field in header { - columns[field] = rows.map { $0[field] ?? "" } - } - } - - self.rows = rows - self.columns = columns - } -} diff --git a/SwiftCSV/Parser.swift b/SwiftCSV/Parser.swift index 30d25fc..ac329ff 100644 --- a/SwiftCSV/Parser.swift +++ b/SwiftCSV/Parser.swift @@ -34,7 +34,7 @@ extension CSV { enum Parser { - static func array(text: String, delimiter: CSV.Delimiter, startAt offset: Int = 0, rowLimit: Int? = nil) throws -> [[String]] { + static func array(text: String, delimiter: CSVDelimiter, startAt offset: Int = 0, rowLimit: Int? = nil) throws -> [[String]] { var rows = [[String]]() @@ -57,7 +57,7 @@ enum Parser { /// - rowCallback: Callback invoked for every parsed row between `startAt` and `limitTo` in `text`. /// - Throws: `CSVParseError` static func enumerateAsArray(text: String, - delimiter: CSV.Delimiter, + delimiter: CSVDelimiter, startAt offset: Int = 0, rowLimit: Int? = nil, rowCallback: @escaping ([String]) -> ()) throws { @@ -123,7 +123,7 @@ enum Parser { } } - static func enumerateAsDict(header: [String], content: String, delimiter: CSV.Delimiter, rowLimit: Int? = nil, block: @escaping ([String : String]) -> ()) throws { + static func enumerateAsDict(header: [String], content: String, delimiter: CSVDelimiter, rowLimit: Int? = nil, block: @escaping ([String : String]) -> ()) throws { let enumeratedHeader = header.enumerated() diff --git a/SwiftCSV/String+Lines.swift b/SwiftCSV/String+Lines.swift index d824d3f..a3aa944 100644 --- a/SwiftCSV/String+Lines.swift +++ b/SwiftCSV/String+Lines.swift @@ -18,6 +18,8 @@ extension String { extension Character { internal var isNewline: Bool { - return self == "\n" || self == "\r\n" || self == "\r" + return self == "\n" + || self == "\r\n" + || self == "\r" } } diff --git a/SwiftCSVTests/CSV+DelimiterGuessingTests.swift b/SwiftCSVTests/CSV+DelimiterGuessingTests.swift deleted file mode 100644 index dd7a626..0000000 --- a/SwiftCSVTests/CSV+DelimiterGuessingTests.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// CSV+DelimiterGuessingTests.swift -// SwiftCSV -// -// Created by Christian Tietze on 21.12.21. -// Copyright © 2021 Naoto Kaneko. All rights reserved. -// - -import XCTest -@testable import SwiftCSV - -class CSV_DelimiterGuessingTests: XCTestCase { - func testGuessDelimiter_InvalidInput_FallbackToComma() throws { - XCTAssertEqual(CSV.guessedDelimiter(string: ""), .comma) - XCTAssertEqual(CSV.guessedDelimiter(string: " "), .comma) - XCTAssertEqual(CSV.guessedDelimiter(string: "fallback"), .comma) - XCTAssertEqual(CSV.guessedDelimiter(string: #""opened;quote;never;closed"#), .comma) - XCTAssertEqual(CSV.guessedDelimiter(string: "just a single line of text"), .comma) - XCTAssertEqual(CSV.guessedDelimiter(string: "\n"), .comma) - } - - func testGuessDelimiter_Comma() throws { - XCTAssertEqual(CSV.guessedDelimiter(string: "header,"), .comma) - XCTAssertEqual(CSV.guessedDelimiter(string: "id,name,age"), .comma) - XCTAssertEqual(CSV.guessedDelimiter(string: #""a","b","c""#), .comma) - XCTAssertEqual(CSV.guessedDelimiter(string: #""a;","b\t","c""#), .comma, - "Prioritizes separator between quotations over first occurrence") - } - - func testGuessDelimiter_Semicolon() throws { - XCTAssertEqual(CSV.guessedDelimiter(string: "header;"), .semicolon) - XCTAssertEqual(CSV.guessedDelimiter(string: "id;name;age"), .semicolon) - XCTAssertEqual(CSV.guessedDelimiter(string: #""a";"b";"c""#), .semicolon) - XCTAssertEqual(CSV.guessedDelimiter(string: #""a,";"b\t";"c""#), .semicolon, - "Prioritizes separator between quotations over first occurrence") - } - - func testGuessDelimiter_Tab() throws { - XCTAssertEqual(CSV.guessedDelimiter(string: "header\t"), .tab) - XCTAssertEqual(CSV.guessedDelimiter(string: "id\tname\tage"), .tab) - // We cannot use #"..."# string delimiters here because \t doesn't work inside these. - XCTAssertEqual(CSV.guessedDelimiter(string: "\"a\"\t\"b\"\t\"c\""), .tab) - XCTAssertEqual(CSV.guessedDelimiter(string: "\"a,\"\t\"b;\"\t\"c\""), .tab, - "Prioritizes separator between quotations over first occurrence") - } - - func testGuessDelimiter_IgnoresEmptyLeadingLines() throws { - XCTAssertEqual(CSV.guessedDelimiter(string: "\na,b,c"), .comma) - XCTAssertEqual(CSV.guessedDelimiter(string: "\n\n\na,b,c"), .comma) - XCTAssertEqual(CSV.guessedDelimiter(string: "\n \n \na,b,c"), .comma) - - XCTAssertEqual(CSV.guessedDelimiter(string: "\na;b;c"), .semicolon) - XCTAssertEqual(CSV.guessedDelimiter(string: "\n \n \na;b;c"), .semicolon) - - XCTAssertEqual(CSV.guessedDelimiter(string: "\na\tb\tc"), .tab) - XCTAssertEqual(CSV.guessedDelimiter(string: "\n \n \na\tb\tc"), .tab) - } - - - func testInitWithGuessedDelimiter() { - let semicolonCSV = try! CSV(string: "id;name;age\n1;Alice;18\n2;Bob;19\n3;Charlie") - let expectedSemicolonCSV = [ - ["id": "1", "name": "Alice", "age": "18"], - ["id": "2", "name": "Bob", "age": "19"], - ["id": "3", "name": "Charlie", "age": ""] - ] - for (index, row) in semicolonCSV.namedRows.enumerated() { - XCTAssertEqual(expectedSemicolonCSV[index], row) - } - - let tabCSV = try! CSV(string: "id\tname\tage\n1\tAlice\t18\n2\tBob\t19\n3\tCharlie") - let expectedTabCSV = [ - ["id": "1", "name": "Alice", "age": "18"], - ["id": "2", "name": "Bob", "age": "19"], - ["id": "3", "name": "Charlie", "age": ""] - ] - for (index, row) in tabCSV.namedRows.enumerated() { - XCTAssertEqual(expectedTabCSV[index], row) - } - } -} diff --git a/SwiftCSVTests/CSVDelimiterGuessingTests.swift b/SwiftCSVTests/CSVDelimiterGuessingTests.swift new file mode 100644 index 0000000..a605240 --- /dev/null +++ b/SwiftCSVTests/CSVDelimiterGuessingTests.swift @@ -0,0 +1,86 @@ +// +// CSVDelimiterGuessingTests.swift +// SwiftCSV +// +// Created by Christian Tietze on 21.12.21. +// Copyright © 2021 Naoto Kaneko. All rights reserved. +// + +import XCTest +@testable import SwiftCSV + +class CSVDelimiterGuessingTests: XCTestCase { + func testGuessDelimiter_InvalidInput_FallbackToComma() throws { + XCTAssertEqual(CSVDelimiter.guessed(string: ""), .comma) + XCTAssertEqual(CSVDelimiter.guessed(string: " "), .comma) + XCTAssertEqual(CSVDelimiter.guessed(string: "fallback"), .comma) + XCTAssertEqual(CSVDelimiter.guessed(string: #""opened;quote;never;closed"#), .comma) + XCTAssertEqual(CSVDelimiter.guessed(string: "just a single line of text"), .comma) + XCTAssertEqual(CSVDelimiter.guessed(string: "\n"), .comma) + } + + func testGuessDelimiter_Comma() throws { + XCTAssertEqual(CSVDelimiter.guessed(string: "header,"), .comma) + XCTAssertEqual(CSVDelimiter.guessed(string: "id,name,age"), .comma) + XCTAssertEqual(CSVDelimiter.guessed(string: #""a","b","c""#), .comma) + XCTAssertEqual(CSVDelimiter.guessed(string: #""a;","b\t","c""#), .comma, + "Prioritizes separator between quotations over first occurrence") + } + + func testGuessDelimiter_Semicolon() throws { + XCTAssertEqual(CSVDelimiter.guessed(string: "header;"), .semicolon) + XCTAssertEqual(CSVDelimiter.guessed(string: "id;name;age"), .semicolon) + XCTAssertEqual(CSVDelimiter.guessed(string: #""a";"b";"c""#), .semicolon) + XCTAssertEqual(CSVDelimiter.guessed(string: #""a,";"b\t";"c""#), .semicolon, + "Prioritizes separator between quotations over first occurrence") + + XCTAssertEqual(CSVDelimiter.guessed(string: """ +"Test";"Test_1" +"Test";"Test_2" +"""), .semicolon) + } + + func testGuessDelimiter_Tab() throws { + XCTAssertEqual(CSVDelimiter.guessed(string: "header\t"), .tab) + XCTAssertEqual(CSVDelimiter.guessed(string: "id\tname\tage"), .tab) + // We cannot use #"..."# string delimiters here because \t doesn't work inside these. + XCTAssertEqual(CSVDelimiter.guessed(string: "\"a\"\t\"b\"\t\"c\""), .tab) + XCTAssertEqual(CSVDelimiter.guessed(string: "\"a,\"\t\"b;\"\t\"c\""), .tab, + "Prioritizes separator between quotations over first occurrence") + } + + func testGuessDelimiter_IgnoresEmptyLeadingLines() throws { + XCTAssertEqual(CSVDelimiter.guessed(string: "\na,b,c"), .comma) + XCTAssertEqual(CSVDelimiter.guessed(string: "\n\n\na,b,c"), .comma) + XCTAssertEqual(CSVDelimiter.guessed(string: "\n \n \na,b,c"), .comma) + + XCTAssertEqual(CSVDelimiter.guessed(string: "\na;b;c"), .semicolon) + XCTAssertEqual(CSVDelimiter.guessed(string: "\n \n \na;b;c"), .semicolon) + + XCTAssertEqual(CSVDelimiter.guessed(string: "\na\tb\tc"), .tab) + XCTAssertEqual(CSVDelimiter.guessed(string: "\n \n \na\tb\tc"), .tab) + } + + + func testInitWithGuessedDelimiter() throws { + let semicolonCSV = try NamedCSV(string: "id;name;age\n1;Alice;18\n2;Bob;19\n3;Charlie") + let expectedSemicolonCSV = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "Bob", "age": "19"], + ["id": "3", "name": "Charlie", "age": ""] + ] + for (index, row) in semicolonCSV.rows.enumerated() { + XCTAssertEqual(expectedSemicolonCSV[index], row) + } + + let tabCSV = try NamedCSV(string: "id\tname\tage\n1\tAlice\t18\n2\tBob\t19\n3\tCharlie") + let expectedTabCSV = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "Bob", "age": "19"], + ["id": "3", "name": "Charlie", "age": ""] + ] + for (index, row) in tabCSV.rows.enumerated() { + XCTAssertEqual(expectedTabCSV[index], row) + } + } +} diff --git a/SwiftCSVTests/CSVDelimiterTests.swift b/SwiftCSVTests/CSVDelimiterTests.swift index e8cfd45..c9e04f0 100644 --- a/SwiftCSVTests/CSVDelimiterTests.swift +++ b/SwiftCSVTests/CSVDelimiterTests.swift @@ -11,16 +11,16 @@ import XCTest class CSVDelimiterTests: XCTestCase { func testRawValue() { - XCTAssertEqual(CSV.Delimiter.comma.rawValue, ",") - XCTAssertEqual(CSV.Delimiter.semicolon.rawValue, ";") - XCTAssertEqual(CSV.Delimiter.tab.rawValue, "\t") - XCTAssertEqual(CSV.Delimiter.character("x").rawValue, "x") + XCTAssertEqual(CSVDelimiter.comma.rawValue, ",") + XCTAssertEqual(CSVDelimiter.semicolon.rawValue, ";") + XCTAssertEqual(CSVDelimiter.tab.rawValue, "\t") + XCTAssertEqual(CSVDelimiter.character("x").rawValue, "x") } func testLiteralInitializer() { - XCTAssertEqual(CSV.Delimiter.comma, ",") - XCTAssertEqual(CSV.Delimiter.semicolon, ";") - XCTAssertEqual(CSV.Delimiter.tab, "\t") - XCTAssertEqual(CSV.Delimiter.character("x"), "x") + XCTAssertEqual(CSVDelimiter.comma, ",") + XCTAssertEqual(CSVDelimiter.semicolon, ";") + XCTAssertEqual(CSVDelimiter.tab, "\t") + XCTAssertEqual(CSVDelimiter.character("x"), "x") } } diff --git a/SwiftCSVTests/CSVTests.swift b/SwiftCSVTests/CSVTests.swift deleted file mode 100644 index b4a7519..0000000 --- a/SwiftCSVTests/CSVTests.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// CSVTests.swift -// CSVTests -// -// Created by naoty on 2014/06/09. -// Copyright (c) 2014年 Naoto Kaneko. All rights reserved. -// - -import XCTest -@testable import SwiftCSV - -class CSVTests: XCTestCase { - var csv: CSV! - - override func setUp() { - csv = try! CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20", delimiter: .comma) - } - - func testInit_makesHeader() { - XCTAssertEqual(csv.header, ["id", "name", "age"]) - } - - func testInit_makesRows() { - let expected = [ - ["id": "1", "name": "Alice", "age": "18"], - ["id": "2", "name": "Bob", "age": "19"], - ["id": "3", "name": "Charlie", "age": "20"] - ] - for (index, row) in csv.namedRows.enumerated() { - XCTAssertEqual(expected[index], row) - } - } - - func testInit_whenThereAreIncompleteRows_makesRows() { - csv = try! CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie", delimiter: .comma) - let expected = [ - ["id": "1", "name": "Alice", "age": "18"], - ["id": "2", "name": "Bob", "age": "19"], - ["id": "3", "name": "Charlie", "age": ""] - ] - for (index, row) in csv.namedRows.enumerated() { - XCTAssertEqual(expected[index], row) - } - } - - func testInit_whenThereAreextraCarriageReturns() throws { - csv = try CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie\r\n") - let expected = [ - ["id": "1", "name": "Alice", "age": "18"], - ["id": "2", "name": "Bob", "age": "19"], - ["id": "3", "name": "Charlie", "age": ""] - ] - for (index, row) in csv.namedRows.enumerated() { - XCTAssertEqual(expected[index], row) - } - } - - func testInit_whenThereAreCRLFs_makesRows() { - csv = try! CSV(string: "id,name,age\r\n1,Alice,18\r\n2,Bob,19\r\n3,Charlie,20\r\n", delimiter: .comma) - let expected = [ - ["id": "1", "name": "Alice", "age": "18"], - ["id": "2", "name": "Bob", "age": "19"], - ["id": "3", "name": "Charlie", "age": "20"] - ] - for (index, row) in csv.namedRows.enumerated() { - XCTAssertEqual(expected[index], row) - } - } - - func testInit_makesColumns() { - let expected = [ - "id": ["1", "2", "3"], - "name": ["Alice", "Bob", "Charlie"], - "age": ["18", "19", "20"] - ] - XCTAssertEqual(Set(csv.namedColumns.keys), Set(expected.keys)) - for (key, value) in csv.namedColumns { - XCTAssertEqual(expected[key] ?? [], value) - } - } - - func testDescription() { - XCTAssertEqual(csv.description, "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20") - } - - func testDescriptionWithDoubleQuotes() { - csv = try! CSV(string: "id,name,age\n1,\"Alice, In, Wonderland\",18\n2,Bob,19\n3,Charlie,20", delimiter: .comma) - XCTAssertEqual(csv.description, "id,name,age\n1,\"Alice, In, Wonderland\",18\n2,Bob,19\n3,Charlie,20") - } - - func testEnumerate() throws { - let expected = [ - ["id": "1", "name": "Alice", "age": "18"], - ["id": "2", "name": "Bob", "age": "19"], - ["id": "3", "name": "Charlie", "age": "20"] - ] - var index = 0 - try csv.enumerateAsDict { row in - XCTAssertEqual(row, expected[index]) - index += 1 - } - } - - func testIgnoreColumns() { - csv = try! CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20", delimiter: .comma, loadColumns: false) - XCTAssertEqual(csv.namedColumns.isEmpty, true) - let expected = [ - ["id": "1", "name": "Alice", "age": "18"], - ["id": "2", "name": "Bob", "age": "19"], - ["id": "3", "name": "Charlie", "age": "20"] - ] - for (index, row) in csv.namedRows.enumerated() { - XCTAssertEqual(expected[index], row) - } - } - - func testInit_ParseFileWithQuotesAndWhitespaces() { - let tab = "\t" - let paragraphSeparator = "\u{2029}" - let ideographicSpace = "\u{3000}" - - let failingCsv = """ - "a" \(tab) , \(paragraphSeparator) "b" - "A" \(ideographicSpace) , \(tab) "B" - """ - let csv = try! CSV(string: failingCsv, delimiter: .comma) - - XCTAssertEqual(csv.namedRows, [["b": "B", "a": "A"]]) - } -} diff --git a/SwiftCSVTests/EnumeratedCSVViewTests.swift b/SwiftCSVTests/EnumeratedCSVViewTests.swift new file mode 100644 index 0000000..20e6cc0 --- /dev/null +++ b/SwiftCSVTests/EnumeratedCSVViewTests.swift @@ -0,0 +1,87 @@ +// +// EnumeratedViewTests.swift +// SwiftCSV +// +// Created by Christian Tietze on 2016-10-25. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +import XCTest +@testable import SwiftCSV + +class EnumeratedViewTests: XCTestCase { + let string = "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20" + var csv: CSV! + + override func setUpWithError() throws { + csv = try CSV(string: string, delimiter: ",", loadColumns: true) + } + + override func tearDownWithError() throws { + csv = nil + } + + func testRows() { + let expected = [ + ["1", "Alice", "18"], + ["2", "Bob", "19"], + ["3", "Charlie", "20"] + ] + XCTAssertEqual(csv.rows, expected) + } + + func testRows_WithLimit() throws { + csv = try CSV(string: string, delimiter: ",", rowLimit: 2) + let expected = [ + ["1", "Alice", "18"], + ["2", "Bob", "19"] + ] + XCTAssertEqual(csv.rows, expected) + } + + func testColumns() { + let expected = [ + Enumerated.Column(header: "id", rows: ["1", "2", "3"]), + Enumerated.Column(header: "name", rows: ["Alice", "Bob", "Charlie"]), + Enumerated.Column(header: "age", rows: ["18", "19", "20"]) + ] + XCTAssertEqual(csv.columns, expected) + } + + func testColumns_WithLimit() throws { + csv = try CSV(string: string, delimiter: ",", rowLimit: 2) + let expected = [ + Enumerated.Column(header: "id", rows: ["1", "2"]), + Enumerated.Column(header: "name", rows: ["Alice", "Bob"]), + Enumerated.Column(header: "age", rows: ["18", "19"]) + ] + XCTAssertEqual(csv.columns, expected) + } + + func testFillsIncompleteRows() throws { + csv = try CSV(string: "id,name,age\n1,Alice,18\n2\n3,Charlie", delimiter: ",", loadColumns: true) + + let expectedRows = [ + ["1", "Alice", "18"], + ["2", "", ""], + ["3", "Charlie", ""] + ] + XCTAssertEqual(csv.rows, expectedRows) + + let expectedColumns = [ + Enumerated.Column(header: "id", rows: ["1", "2", "3"]), + Enumerated.Column(header: "name", rows: ["Alice", "", "Charlie"]), + Enumerated.Column(header: "age", rows: ["18", "", ""]) + ] + XCTAssertEqual(csv.columns, expectedColumns) + } + + func testSerialization() { + XCTAssertEqual(csv.serialized, "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20") + } + + func testSerializationWithDoubleQuotes() throws { + csv = try CSV(string: "id,\"the, name\",age\n1,\"Alice, In, Wonderland\",18\n2,Bob,19\n3,Charlie,20") + XCTAssertEqual(csv.serialized, "id,\"the, name\",age\n1,\"Alice, In, Wonderland\",18\n2,Bob,19\n3,Charlie,20") + } +} diff --git a/SwiftCSVTests/EnumeratedViewTests.swift b/SwiftCSVTests/EnumeratedViewTests.swift deleted file mode 100644 index f41e2dc..0000000 --- a/SwiftCSVTests/EnumeratedViewTests.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// EnumeratedViewTests.swift -// SwiftCSV -// -// Created by Christian Tietze on 2016-10-25. -// Copyright © 2016 Naoto Kaneko. All rights reserved. -// - -import XCTest -@testable import SwiftCSV - -class EnumeratedViewTests: XCTestCase { - - var csv: CSV! - - override func setUp() { - super.setUp() - - csv = try! CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20", delimiter: ",", loadColumns: true) - } - - func testExposesRows() { - let expected: [[String]] = [ - ["1", "Alice", "18"], - ["2", "Bob", "19"], - ["3", "Charlie", "20"] - ] - let actual = csv.enumeratedRows - - // Abort if counts don't match to not raise index-out-of-bounds exception - guard actual.count == expected.count else { - XCTFail("expected actual.count (\(actual.count)) to equal expected.count (\(expected.count))") - return - } - - for i in actual.indices { - XCTAssertEqual(actual[i], expected[i]) - } - } - - func testExposesColumns() { - let actual = csv.enumeratedColumns - - // Abort if counts don't match to not raise index-out-of-bounds exception - guard actual.count == 3 else { - XCTFail("expected actual.count to equal 3") - return - } - - XCTAssertEqual(actual[0].header, "id") - XCTAssertEqual(actual[0].rows, ["1", "2", "3"]) - - XCTAssertEqual(actual[1].header, "name") - XCTAssertEqual(actual[1].rows, ["Alice", "Bob", "Charlie"]) - - XCTAssertEqual(actual[2].header, "age") - XCTAssertEqual(actual[2].rows, ["18", "19", "20"]) - } -} diff --git a/SwiftCSVTests/NamedCSVViewTests.swift b/SwiftCSVTests/NamedCSVViewTests.swift new file mode 100644 index 0000000..f3fdad2 --- /dev/null +++ b/SwiftCSVTests/NamedCSVViewTests.swift @@ -0,0 +1,134 @@ +// +// NamedViewTests.swift +// CSVTests +// +// Created by naoty on 2014/06/09. +// Copyright (c) 2014年 Naoto Kaneko. All rights reserved. +// + +import XCTest +@testable import SwiftCSV + +class NamedViewTests: XCTestCase { + let string = "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20" + var csv: CSV! + + override func setUpWithError() throws { + csv = try CSV(string: string) + } + + override func tearDownWithError() throws { + csv = nil + } + + func testHeader() { + XCTAssertEqual(csv.header, ["id", "name", "age"]) + } + + func testRows() { + let expected = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "Bob", "age": "19"], + ["id": "3", "name": "Charlie", "age": "20"] + ] + XCTAssertEqual(csv.rows, expected) + } + + func testRows_WithLimit() throws { + csv = try CSV(string: string, delimiter: ",", rowLimit: 2) + let expected = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "Bob", "age": "19"], + ] + XCTAssertEqual(csv.rows, expected) + } + + func testColumns() { + let expected = [ + "id": ["1", "2", "3"], + "name": ["Alice", "Bob", "Charlie"], + "age": ["18", "19", "20"] + ] + XCTAssertEqual(csv.columns, expected) + } + + func testColumns_WithLimit() throws { + csv = try CSV(string: string, delimiter: ",", rowLimit: 2) + let expected = [ + "id": ["1", "2"], + "name": ["Alice", "Bob"], + "age": ["18", "19"] + ] + XCTAssertEqual(csv.columns, expected) + } + + func testFillsIncompleteRows() throws { + csv = try CSV(string: "id,name,age\n1,Alice,18\n2,,19\n3,Charlie") + + let expectedRows = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "", "age": "19"], + ["id": "3", "name": "Charlie", "age": ""] + ] + XCTAssertEqual(csv.rows, expectedRows) + + let expectedColumns = [ + "id": ["1", "2", "3"], + "name": ["Alice", "", "Charlie"], + "age": ["18", "19", ""] + ] + XCTAssertEqual(csv.columns, expectedColumns) + } + + func testSerialization() { + XCTAssertEqual(csv.serialized, "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20") + } + + func testSerializationWithDoubleQuotes() throws { + csv = try CSV(string: "id,\"the, name\",age\n1,\"Alice, In, Wonderland\",18\n2,Bob,19\n3,Charlie,20") + XCTAssertEqual(csv.serialized, "id,\"the, name\",age\n1,\"Alice, In, Wonderland\",18\n2,Bob,19\n3,Charlie,20") + } + + func testEnumerate() throws { + let expected = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "Bob", "age": "19"], + ["id": "3", "name": "Charlie", "age": "20"] + ] + var index = 0 + try csv.enumerateAsDict { row in + XCTAssertEqual(row, expected[index]) + index += 1 + } + } + + func testIgnoreColumns() throws { + csv = try CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20", delimiter: ",", loadColumns: false) + XCTAssertNil(csv.columns) + let expected = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "Bob", "age": "19"], + ["id": "3", "name": "Charlie", "age": "20"] + ] + for (index, row) in csv.rows.enumerated() { + XCTAssertEqual(expected[index], row) + } + } + + func testInit_ParseCommaSeparatedFileWithQuotesAndWhitespaces() throws { + let tab = "\t" + let paragraphSeparator = "\u{2029}" + let ideographicSpace = "\u{3000}" + + let failingCsv = """ + "a" \(tab) , \(paragraphSeparator) "b" + "A" \(ideographicSpace) , \(tab) "B" + """ + let csv = try CSV(string: failingCsv, delimiter: .comma) + + XCTAssertEqual(csv.rows, [["b": "B", "a": "A"]]) + + XCTAssertThrowsError(try CSV(string: failingCsv), + "Delimiter guessing should fail here.") + } +} diff --git a/SwiftCSVTests/NewlineTests.swift b/SwiftCSVTests/NewlineTests.swift new file mode 100644 index 0000000..5780da4 --- /dev/null +++ b/SwiftCSVTests/NewlineTests.swift @@ -0,0 +1,63 @@ +// +// NewlineTests.swift +// SwiftCSV +// +// Created by Christian Tietze on 05/12/16. +// Copyright © 2016 Naoto Kaneko. All rights reserved. +// + +import XCTest +@testable import SwiftCSV + +class NewlineTests: XCTestCase { + func testInit_withCR() throws { + let csv = try CSV(string: "id,name,age\r1,Alice,18\r2,Bob,19\r3,Charlie,20") + XCTAssertEqual(csv.header, ["id", "name", "age"]) + let expectedRows = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "Bob", "age": "19"], + ["id": "3", "name": "Charlie", "age": "20"] + ] + for (index, row) in csv.rows.enumerated() { + XCTAssertEqual(expectedRows[index], row) + } + } + + func testInit_withLF() throws { + let csv = try CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie,20") + XCTAssertEqual(csv.header, ["id", "name", "age"]) + let expectedRows = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "Bob", "age": "19"], + ["id": "3", "name": "Charlie", "age": "20"] + ] + for (index, row) in csv.rows.enumerated() { + XCTAssertEqual(expectedRows[index], row) + } + } + + func testInit_withCRLF() throws { + let csv = try CSV(string: "id,name,age\r\n1,Alice,18\r\n2,Bob,19\r\n3,Charlie,20") + XCTAssertEqual(csv.header, ["id", "name", "age"]) + let expectedRows = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "Bob", "age": "19"], + ["id": "3", "name": "Charlie", "age": "20"] + ] + for (index, row) in csv.rows.enumerated() { + XCTAssertEqual(expectedRows[index], row) + } + } + + func testInit_whenThereIsExtraCarriageReturnAtTheEnd() throws { + let csv = try CSV(string: "id,name,age\n1,Alice,18\n2,Bob,19\n3,Charlie\r\n") + let expected = [ + ["id": "1", "name": "Alice", "age": "18"], + ["id": "2", "name": "Bob", "age": "19"], + ["id": "3", "name": "Charlie", "age": ""] + ] + for (index, row) in csv.rows.enumerated() { + XCTAssertEqual(expected[index], row) + } + } +} diff --git a/SwiftCSVTests/ParserTests.swift b/SwiftCSVTests/ParserTests.swift index ea0e2c9..14ce1dd 100644 --- a/SwiftCSVTests/ParserTests.swift +++ b/SwiftCSVTests/ParserTests.swift @@ -13,7 +13,6 @@ class ParserTests: XCTestCase { func testParseArray_RowLimitAndOffset() throws { let string = "id,name\n1,foo\n2,bar\n3,baz" - XCTAssertEqual(try Parser.array(text: string, delimiter: ",", startAt: 1, rowLimit: nil), [["1","foo"], ["2","bar"], ["3","baz"]]) XCTAssertEqual(try Parser.array(text: string, delimiter: ",", startAt: 2, rowLimit: nil), @@ -23,7 +22,6 @@ class ParserTests: XCTestCase { XCTAssertEqual(try Parser.array(text: string, delimiter: ",", startAt: 4, rowLimit: nil), []) - XCTAssertEqual(try Parser.array(text: string, delimiter: ",", startAt: 0, rowLimit: nil), [["id","name"], ["1","foo"], ["2","bar"], ["3","baz"]]) XCTAssertEqual(try Parser.array(text: string, delimiter: ",", startAt: 0, rowLimit: -1), @@ -43,13 +41,11 @@ class ParserTests: XCTestCase { XCTAssertEqual(try Parser.array(text: string, delimiter: ",", startAt: 0, rowLimit: 999), [["id","name"], ["1","foo"], ["2","bar"], ["3","baz"]]) - XCTAssertEqual(try Parser.array(text: string, delimiter: ",", startAt: 1, rowLimit: 1), [["1","foo"]]) XCTAssertEqual(try Parser.array(text: string, delimiter: ",", startAt: 1, rowLimit: 2), [["1","foo"], ["2","bar"]]) - XCTAssertEqual(try Parser.array(text: string, delimiter: ",", startAt: 2, rowLimit: 1), [["2","bar"]]) XCTAssertEqual(try Parser.array(text: string, delimiter: ",", startAt: 2, rowLimit: 2), diff --git a/SwiftCSVTests/PerformanceTest.swift b/SwiftCSVTests/PerformanceTest.swift index 1465f3e..ca2e4c9 100644 --- a/SwiftCSVTests/PerformanceTest.swift +++ b/SwiftCSVTests/PerformanceTest.swift @@ -10,16 +10,16 @@ import XCTest @testable import SwiftCSV class PerformanceTest: XCTestCase { - var csv: CSV! + var csv: CSV! - override func setUp() { + override func setUpWithError() throws { let csvURL = ResourceHelper.url(forResource: "large", withExtension: "csv")! - csv = try! CSV(url: csvURL) + csv = try CSV(url: csvURL) } func testParsePerformance() { measure { - _ = self.csv.namedRows + _ = self.csv.rows } } } diff --git a/SwiftCSVTests/QuotedTests.swift b/SwiftCSVTests/QuotedTests.swift index 3446f24..94f2397 100644 --- a/SwiftCSVTests/QuotedTests.swift +++ b/SwiftCSVTests/QuotedTests.swift @@ -10,23 +10,18 @@ import XCTest import SwiftCSV class QuotedTests: XCTestCase { - var csv: CSV! + var csv: CSV! - override func setUp() { - super.setUp() - csv = try! CSV(string: "id,\"name, person\",age\n\"5\",\"Smith, John\",67\n8,Joe Bloggs,\"8\"") + override func setUpWithError() throws { + csv = try CSV(string: "id,\"name, person\",age\n\"5\",\"Smith, John\",67\n8,Joe Bloggs,\"8\"") } - override func tearDown() { - super.tearDown() - } - func testQuotedHeader() { XCTAssertEqual(csv.header, ["id", "name, person", "age"]) } func testQuotedContent() { - let cols = csv.namedRows + let cols = csv.rows XCTAssertEqual(cols[0], [ "id": "5", "name, person": "Smith, John", diff --git a/SwiftCSVTests/ResourceHelper.swift b/SwiftCSVTests/ResourceHelper.swift index 663de93..8b5c378 100644 --- a/SwiftCSVTests/ResourceHelper.swift +++ b/SwiftCSVTests/ResourceHelper.swift @@ -4,7 +4,7 @@ import Foundation // This is a workaround for SwiftPM, becasue SwiftPM is not yet support for include resources with targets.(https://bugs.swift.org/browse/SR-2866) struct ResourceHelper { static func url(forResource name: String, withExtension type: String) -> URL? { - let bundle = Bundle(for: CSVTests.self) + let bundle = Bundle(for: NamedViewTests.self) if let url = bundle.url(forResource: name, withExtension: type) { return url } else if let realBundle = Bundle(path: "\(bundle.bundlePath)/../../../../SwiftCSVTests") { diff --git a/SwiftCSVTests/TSVTests.swift b/SwiftCSVTests/TSVTests.swift index ec5009e..0cc4128 100644 --- a/SwiftCSVTests/TSVTests.swift +++ b/SwiftCSVTests/TSVTests.swift @@ -11,10 +11,10 @@ import Foundation @testable import SwiftCSV class TSVTests: XCTestCase { - var tsv: CSV! + var tsv: CSV! - override func setUp() { - tsv = try! CSV(string: "id\tname\tage\n1\tAlice\t18\n2\tBob\t19\n3\tCharlie\t20", delimiter: "\t") + override func setUpWithError() throws { + tsv = try CSV(string: "id\tname\tage\n1\tAlice\t18\n2\tBob\t19\n3\tCharlie\t20", delimiter: "\t") } func testInit_makesHeader() { @@ -27,19 +27,21 @@ class TSVTests: XCTestCase { ["id": "2", "name": "Bob", "age": "19"], ["id": "3", "name": "Charlie", "age": "20"] ] - for (index, row) in tsv.namedRows.enumerated() { + for (index, row) in tsv.rows.enumerated() { XCTAssertEqual(expected[index], row) } } - func testInit_makesColumns() { + func testInit_makesColumns() throws { let expected = [ "id": ["1", "2", "3"], "name": ["Alice", "Bob", "Charlie"], "age": ["18", "19", "20"] ] - XCTAssertEqual(Set(tsv.namedColumns.keys), Set(expected.keys)) - for (key, value) in tsv.namedColumns { + XCTAssertEqual( + Set(try XCTUnwrap(tsv.columns).keys), + Set(expected.keys)) + for (key, value) in try XCTUnwrap(tsv.columns) { XCTAssertEqual(expected[key] ?? [], value) } } diff --git a/SwiftCSVTests/URLTests.swift b/SwiftCSVTests/URLTests.swift index aaaddf9..1407334 100644 --- a/SwiftCSVTests/URLTests.swift +++ b/SwiftCSVTests/URLTests.swift @@ -10,11 +10,11 @@ import XCTest import SwiftCSV class URLTests: XCTestCase { - var csv: CSV! + var csv: CSV! - func testEmptyFields() { + func testEmptyFields() throws { let csvURL = ResourceHelper.url(forResource: "empty_fields", withExtension: "csv")! - csv = try! CSV(url: csvURL) + csv = try CSV(url: csvURL) let expected = [ ["id": "1", "name": "John", "age": "23"], ["id": "2", "name": "James", "age": "32"], @@ -23,14 +23,14 @@ class URLTests: XCTestCase { ["id": "", "name": "", "age": ""], ["id": "", "name": "Tom", "age": ""] ] - for (index, row) in csv.namedRows.enumerated() { + for (index, row) in csv.rows.enumerated() { XCTAssertEqual(expected[index], row) } } - func testQuotes() { + func testQuotes() throws { let csvURL = ResourceHelper.url(forResource: "quotes", withExtension: "csv")! - csv = try! CSV(url: csvURL) + csv = try CSV(url: csvURL) let expected = [ ["id": "4", "name, first": "Alex", "name, last": "Smith"], ["id": "5", "name, first": "Joe", "name, last": "Bloggs"], @@ -56,7 +56,7 @@ class URLTests: XCTestCase { ], [:] ] - for (index, row) in csv.namedRows.enumerated() { + for (index, row) in csv.rows.enumerated() { XCTAssertEqual(expected[index], row) } }