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