├── .gitignore ├── CSV ├── CSV.h └── Info.plist ├── LICENSE ├── README.md ├── Tests ├── CSVParserTest.swift ├── CSVWriterTest.swift ├── Info.plist └── Reading Test Documents │ ├── postico-issues.csv │ └── semi-big-file.csv ├── swift-csv.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── CSV.xcscheme └── xcuserdata │ └── mah.xcuserdatad │ └── xcschemes │ ├── swift-csv.xcscheme │ └── xcschememanagement.plist └── swift-csv ├── CSV.swift └── main.swift /.gitignore: -------------------------------------------------------------------------------- 1 | ### Xcode ### 2 | # Xcode 3 | # 4 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 5 | 6 | ## Build generated 7 | build/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xccheckout 24 | *.xcscmblueprint 25 | 26 | ### Xcode Patch ### 27 | *.xcodeproj/* 28 | !*.xcodeproj/project.pbxproj 29 | !*.xcodeproj/xcshareddata/ 30 | !*.xcworkspace/contents.xcworkspacedata 31 | /*.gcno -------------------------------------------------------------------------------- /CSV/CSV.h: -------------------------------------------------------------------------------- 1 | // 2 | // CSV.h 3 | // CSV 4 | // 5 | // Created by Matthias Hochgatterer on 23.10.19. 6 | // Copyright © 2019 Matthias Hochgatterer. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for CSV. 12 | FOUNDATION_EXPORT double CSVVersionNumber; 13 | 14 | //! Project version string for CSV. 15 | FOUNDATION_EXPORT const unsigned char CSVVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /CSV/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matthias 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swift-csv 2 | 3 | `swift-csv` is a stream based CSV library written in Swift. It uses `InputStream` to parse a CSV file and `OutputStream` to write CSV data to a file. 4 | This way it doesn't keep everything in memory while working with big CSV files. It also supports [BOM](https://en.wikipedia.org/wiki/Byte_order_mark) for UTF-8, UTF-16 and UTF-32 text encodings. 5 | 6 | `swift-csv` is battle tested in the CSV import and export of [Finances](https://hochgatterer.me/finances/). 7 | 8 | # Features 9 | 10 | - Stream based CSV parser and writer 11 | - Complete documentation 12 | - Unit tested 13 | 14 | # Usage 15 | 16 | ## Parser 17 | 18 | ```swift 19 | let stream = InputStream(...) 20 | 21 | // Define the delimeter and encoding of the CSV file 22 | let configuration = CSV.Configuration(delimiter: ",", encoding: .utf8) 23 | 24 | let parser = CSV.Parser(inputStream: stream, configuration: configuration) 25 | try parser.parse() 26 | ``` 27 | 28 | If you don't know the delimiter and encoding of the data, you can automatically detect it. 29 | 30 | ```swift 31 | let url = ... 32 | guard let configuration = CSV.Configuration.detectConfigurationForContentsOfURL(url) else { 33 | return 34 | } 35 | ``` 36 | 37 | ### Example 38 | 39 | Parsing this CSV file 40 | 41 | ``` 42 | a,b 43 | 1,"ha ""ha"" ha" 44 | 3,4 45 | ``` 46 | 47 | will result in the following delegate calls. 48 | 49 | ``` 50 | parserDidBeginDocument 51 | parser(_:didBeginLineAt:) 0 52 | parser(_:didReadFieldAt:value:) a 53 | parser(_:didReadFieldAt:value:) b 54 | parser(_:didEndLineAt:) 0 55 | parser(_:didBeginLineAt:) 1 56 | parser(_:didReadFieldAt:value:) 1 57 | parser(_:didReadFieldAt:value:) ha "ha" ha 58 | parser(_:didEndLineAt:) 1 59 | parser(_:didBeginLineAt:) 2 60 | parser(_:didReadFieldAt:value:) 3 61 | parser(_:didReadFieldAt:value:) 4 62 | parser(_:didEndLineAt:) 2 63 | parserDidEndDocument 64 | ``` 65 | 66 | ## Writer 67 | 68 | ```swift 69 | let stream = OutputStream(...) 70 | let configuration = CSV.Configuration(delimiter: ",", encoding: .utf8) 71 | 72 | let writer = CSV.Writer(outputStream: stream, configuration: configuration) 73 | try writer.writeLine(of: ["a", "b", "c"]) 74 | try writer.writeLine(of: ["1", "2", "3"]) 75 | ``` 76 | 77 | The code above produces the following output. 78 | 79 | ``` 80 | a,b,c 81 | 1,2,3 82 | ``` 83 | 84 | # TODOs 85 | 86 | - [ ] Support comments in CSV file 87 | 88 | # Contact 89 | 90 | Matthias Hochgatterer 91 | 92 | Github: [https://github.com/brutella/](https://github.com/brutella/) 93 | 94 | Twitter: [https://twitter.com/brutella](https://twitter.com/brutella) 95 | 96 | 97 | # License 98 | 99 | swift-csv is available under the MIT license. See the LICENSE file for more info. 100 | -------------------------------------------------------------------------------- /Tests/CSVParserTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CSVParserTest.swift 3 | // swift-csv 4 | // 5 | // Created by Matthias Hochgatterer on 02/06/2017. 6 | // Copyright © 2017 Matthias Hochgatterer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class TestParserDelegate: ParserDelegate { 12 | 13 | internal var didBeginDocument: Bool = false 14 | internal var didEndDocument: Bool = false 15 | internal var didBeginLineIndex: UInt? 16 | 17 | internal var content = Array<[String]>() 18 | internal var currentFieldValues = Array() 19 | 20 | func parserDidBeginDocument(_ parser: CSV.Parser) { 21 | didBeginDocument = true 22 | print(#function) 23 | } 24 | 25 | func parserDidEndDocument(_ parser: CSV.Parser) { 26 | didEndDocument = true 27 | print(#function) 28 | } 29 | 30 | func parser(_ parser: CSV.Parser, didBeginLineAt index: UInt) { 31 | didBeginLineIndex = index 32 | currentFieldValues.removeAll() 33 | print("\(#function) \(index)") 34 | } 35 | 36 | func parser(_ parser: CSV.Parser, didEndLineAt index: UInt) { 37 | guard let beginLineIndex = didBeginLineIndex else { 38 | XCTFail() 39 | return 40 | } 41 | 42 | XCTAssertEqual(beginLineIndex, index) 43 | content.append(currentFieldValues) 44 | print("\(#function) \(index)") 45 | } 46 | 47 | func parser(_ parser: CSV.Parser, didReadFieldAt index: UInt, value: String) { 48 | currentFieldValues.append(value) 49 | print("\(#function) \(value)") 50 | } 51 | } 52 | 53 | class PerformanceParserDelegate: ParserDelegate { 54 | 55 | internal var didBeginDocument: Bool = false 56 | internal var didEndDocument: Bool = false 57 | internal var didBeginLineIndex: UInt? 58 | 59 | func parserDidBeginDocument(_ parser: CSV.Parser) { 60 | didBeginDocument = true 61 | } 62 | 63 | func parserDidEndDocument(_ parser: CSV.Parser) { 64 | didEndDocument = true 65 | } 66 | 67 | func parser(_ parser: CSV.Parser, didBeginLineAt index: UInt) { 68 | didBeginLineIndex = index 69 | } 70 | 71 | func parser(_ parser: CSV.Parser, didEndLineAt index: UInt) { 72 | guard let beginLineIndex = didBeginLineIndex else { 73 | XCTFail() 74 | return 75 | } 76 | XCTAssertEqual(beginLineIndex, index) 77 | } 78 | 79 | func parser(_ parser: CSV.Parser, didReadFieldAt index: UInt, value: String) { 80 | // do nothing 81 | // we want the parser to run as fast as possible for testing 82 | } 83 | } 84 | 85 | 86 | class CSVParser_Test: XCTestCase { 87 | 88 | func testQuotedFields() throws { 89 | let string = "\"aaa\",\"b \r\nbb\",\"ccc\" \r\nzzz,yyy,xxx" 90 | let parser = CSV.Parser(string: string, configuration: CSV.Configuration(delimiter: ",")) 91 | let testDelegate = TestParserDelegate() 92 | parser.delegate = testDelegate 93 | try parser.parse() 94 | 95 | XCTAssertTrue(testDelegate.didBeginDocument) 96 | XCTAssertTrue(testDelegate.didEndDocument) 97 | 98 | let line0 = ["aaa", "b \r\nbb","ccc"] 99 | let line1 = ["zzz", "yyy", "xxx"] 100 | XCTAssertEqual(testDelegate.content[0], line0) 101 | XCTAssertEqual(testDelegate.content[1], line1) 102 | } 103 | 104 | func testQuoteInQuotedFields() throws { 105 | let string = "\"z\"\"zz\";;xxx;" 106 | let parser = CSV.Parser(string: string, configuration: CSV.Configuration(delimiter: ";")) 107 | let testDelegate = TestParserDelegate() 108 | parser.delegate = testDelegate 109 | try parser.parse() 110 | 111 | XCTAssertEqual(testDelegate.content[0], ["z\"zz", "", "xxx", ""]) 112 | } 113 | 114 | func testEmptyFields() throws { 115 | let string = "zzz;;xxx;" 116 | let parser = CSV.Parser(string: string, configuration: CSV.Configuration(delimiter: ";")) 117 | let testDelegate = TestParserDelegate() 118 | parser.delegate = testDelegate 119 | try parser.parse() 120 | 121 | XCTAssertEqual(testDelegate.content[0], ["zzz", "", "xxx", ""]) 122 | } 123 | 124 | func testSemicolonDelimiter() throws { 125 | let string = "zzz;yyy;xxx" 126 | let parser = CSV.Parser(string: string, configuration: CSV.Configuration(delimiter: ";")) 127 | let testDelegate = TestParserDelegate() 128 | parser.delegate = testDelegate 129 | try parser.parse() 130 | 131 | XCTAssertEqual(testDelegate.content[0], ["zzz", "yyy", "xxx"]) 132 | } 133 | 134 | func testCR() throws { 135 | let string = "First name,Last name\rJohn,Doe" 136 | let parser = CSV.Parser(string: string, configuration: CSV.Configuration(delimiter: ",")) 137 | 138 | let testDelegate = TestParserDelegate() 139 | parser.delegate = testDelegate 140 | try parser.parse() 141 | 142 | let line0 = ["First name", "Last name"] 143 | let line1 = ["John","Doe"] 144 | XCTAssertEqual(testDelegate.content[0], line0) 145 | XCTAssertEqual(testDelegate.content[1], line1) 146 | } 147 | 148 | func testLF() throws { 149 | let string = "First name,Last name\nJohn,Doe" 150 | let parser = CSV.Parser(string: string, configuration: CSV.Configuration(delimiter: ",")) 151 | 152 | let testDelegate = TestParserDelegate() 153 | parser.delegate = testDelegate 154 | try parser.parse() 155 | 156 | let line0 = ["First name", "Last name"] 157 | let line1 = ["John","Doe"] 158 | XCTAssertEqual(testDelegate.content[0], line0) 159 | XCTAssertEqual(testDelegate.content[1], line1) 160 | } 161 | 162 | func testCRLF() throws { 163 | let string = "First name,Last name\r\nJohn,Doe" 164 | let parser = CSV.Parser(string: string, configuration: CSV.Configuration(delimiter: ",")) 165 | 166 | let testDelegate = TestParserDelegate() 167 | parser.delegate = testDelegate 168 | try parser.parse() 169 | 170 | let line0 = ["First name", "Last name"] 171 | let line1 = ["John","Doe"] 172 | XCTAssertEqual(testDelegate.content[0], line0) 173 | XCTAssertEqual(testDelegate.content[1], line1) 174 | } 175 | 176 | func testCSVSpectrumFiles() throws { 177 | let data: [String: Array<[String]>] = [ 178 | "first,last,address,city,zip\nJohn,Doe,120 any st.,\"Anytown, WW\",08123": [["first", "last", "address", "city", "zip"],["John","Doe","120 any st.","Anytown, WW","08123"]], 179 | "a,b,c\n1,\"\",\"\"\n2,3,4": [["a","b","c"],["1","",""], ["2","3","4"]], 180 | "a,b\n1,\"ha \"\"ha\"\" ha\"\n3,4": [["a","b"],["1","ha \"ha\" ha"], ["3","4"]], 181 | "key,val\n1,\"{\"\"type\"\": \"\"Point\"\", \"\"coordinates\"\": [102.0, 0.5]}\"": [["key","val"],["1","{\"type\": \"Point\", \"coordinates\": [102.0, 0.5]}"]], 182 | "a,b,c\n1,2,3\n\"Once upon\na time\",5,6\n7,8,9": [["a", "b", "c"], ["1", "2", "3"], ["Once upon\na time", "5", "6"], ["7", "8", "9"]], 183 | "a,b,c\n1,2,3": [["a","b","c"],["1","2","3"]], 184 | "a,b,c\r\n1,2,3": [["a","b","c"],["1","2","3"]], 185 | "a,b,c\r\n1,2,3\n4,5,ʤ": [["a","b","c"],["1","2","3"], ["4","5","ʤ"]], 186 | ] 187 | 188 | for (key, value) in data { 189 | let parser = CSV.Parser(string: key, configuration: CSV.Configuration(delimiter: ",")) 190 | 191 | let testDelegate = TestParserDelegate() 192 | parser.delegate = testDelegate 193 | try parser.parse() 194 | 195 | guard testDelegate.content.count == value.count else { 196 | XCTFail("Invalid number of lines") 197 | return 198 | } 199 | for (index, line) in value.enumerated() { 200 | XCTAssertEqual(line, testDelegate.content[index]) 201 | } 202 | } 203 | } 204 | 205 | func testPerformance() { 206 | self.measure { 207 | let fileURL = Bundle(for: type(of: self)).url(forResource: "Reading Test Documents/semi-big-file", withExtension: "csv")! 208 | let parser = CSV.Parser(url: fileURL, configuration: CSV.Configuration(delimiter: ","))! 209 | let testDelegate = PerformanceParserDelegate() 210 | parser.delegate = testDelegate 211 | try! parser.parse() 212 | 213 | } 214 | } 215 | 216 | func testPerformanceIssues() { 217 | self.measure { 218 | let fileURL = Bundle(for: type(of: self)).url(forResource: "Reading Test Documents/postico-issues", withExtension: "csv")! 219 | let parser = CSV.Parser(url: fileURL, configuration: CSV.Configuration(delimiter: ","))! 220 | let testDelegate = PerformanceParserDelegate() 221 | parser.delegate = testDelegate 222 | try! parser.parse() 223 | } 224 | } 225 | 226 | 227 | } 228 | -------------------------------------------------------------------------------- /Tests/CSVWriterTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CSVWriter+Test.swift 3 | // swift-csv 4 | // 5 | // Created by Matthias Hochgatterer on 02/06/2017. 6 | // Copyright © 2017 Matthias Hochgatterer. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class CSVWriter_Test: XCTestCase { 12 | 13 | var stream: OutputStream! 14 | var writer: CSV.Writer! 15 | 16 | override func setUp() { 17 | super.setUp() 18 | 19 | stream = OutputStream(toMemory: ()) 20 | let configuration = CSV.Configuration(delimiter: ",") 21 | writer = CSV.Writer(outputStream: stream, configuration: configuration) 22 | } 23 | 24 | func testQuotedFields() throws { 25 | try writer.writeLine(of: ["aaa", "b \r\nbb", "ccc"]) 26 | try writer.writeLine(of: ["zzz", "yyy", "xxx"]) 27 | guard let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else { 28 | XCTFail("Could not retrieve data") 29 | return 30 | } 31 | 32 | if let string = String(data: data, encoding: .utf8) { 33 | let expected = "aaa,\"b \r\nbb\",ccc\nzzz,yyy,xxx" 34 | XCTAssertEqual(string, expected) 35 | } else { 36 | XCTFail("Invalid data \(data)") 37 | } 38 | } 39 | 40 | func testEmptyFields() throws { 41 | try writer.writeLine(of: ["zzz", "", "xxx", ""]) 42 | guard let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else { 43 | XCTFail("Could not retrieve data") 44 | return 45 | } 46 | 47 | if let string = String(data: data, encoding: .utf8) { 48 | let expected = "zzz,,xxx," 49 | XCTAssertEqual(string, expected) 50 | } else { 51 | XCTFail("Invalid data \(data)") 52 | } 53 | } 54 | 55 | func testSemicolonDelimiter() throws { 56 | let writer = CSV.Writer(outputStream: stream, configuration: CSV.Configuration(delimiter: ";")) 57 | try writer.writeLine(of: ["zzz", "yyy", "xxx"]) 58 | guard let data = stream.property(forKey: .dataWrittenToMemoryStreamKey) as? Data else { 59 | XCTFail("Could not retrieve data") 60 | return 61 | } 62 | 63 | if let string = String(data: data, encoding: .utf8) { 64 | let expected = "zzz;yyy;xxx" 65 | XCTAssertEqual(string, expected) 66 | } else { 67 | XCTFail("Invalid data \(data)") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /swift-csv.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 10175E5A23604822004F48C4 /* CSV.h in Headers */ = {isa = PBXBuildFile; fileRef = 10175E5823604822004F48C4 /* CSV.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | 10175E5E2360482A004F48C4 /* CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106FEA141EE14F970061918B /* CSV.swift */; }; 12 | 106FE9F21EE14F340061918B /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106FE9F11EE14F340061918B /* main.swift */; }; 13 | 106FEA121EE14F900061918B /* CSVParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106FEA101EE14F900061918B /* CSVParserTest.swift */; }; 14 | 106FEA131EE14F900061918B /* CSVWriterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106FEA111EE14F900061918B /* CSVWriterTest.swift */; }; 15 | 106FEA151EE14F970061918B /* CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106FEA141EE14F970061918B /* CSV.swift */; }; 16 | 106FEA161EE14F9B0061918B /* CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106FEA141EE14F970061918B /* CSV.swift */; }; 17 | 364316631EE1C34800C879E6 /* Reading Test Documents in Resources */ = {isa = PBXBuildFile; fileRef = 364316621EE1C34800C879E6 /* Reading Test Documents */; }; 18 | /* End PBXBuildFile section */ 19 | 20 | /* Begin PBXCopyFilesBuildPhase section */ 21 | 106FE9EC1EE14F340061918B /* CopyFiles */ = { 22 | isa = PBXCopyFilesBuildPhase; 23 | buildActionMask = 2147483647; 24 | dstPath = /usr/share/man/man1/; 25 | dstSubfolderSpec = 0; 26 | files = ( 27 | ); 28 | runOnlyForDeploymentPostprocessing = 1; 29 | }; 30 | /* End PBXCopyFilesBuildPhase section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 10175E5623604822004F48C4 /* CSV.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CSV.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 10175E5823604822004F48C4 /* CSV.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CSV.h; sourceTree = ""; }; 35 | 10175E5923604822004F48C4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | 106FE9EE1EE14F340061918B /* swift-csv */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "swift-csv"; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 106FE9F11EE14F340061918B /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 38 | 106FEA081EE14F630061918B /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 39 | 106FEA0C1EE14F630061918B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 40 | 106FEA101EE14F900061918B /* CSVParserTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSVParserTest.swift; sourceTree = ""; }; 41 | 106FEA111EE14F900061918B /* CSVWriterTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSVWriterTest.swift; sourceTree = ""; }; 42 | 106FEA141EE14F970061918B /* CSV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSV.swift; sourceTree = ""; }; 43 | 364316621EE1C34800C879E6 /* Reading Test Documents */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "Reading Test Documents"; sourceTree = ""; }; 44 | /* End PBXFileReference section */ 45 | 46 | /* Begin PBXFrameworksBuildPhase section */ 47 | 10175E5323604822004F48C4 /* Frameworks */ = { 48 | isa = PBXFrameworksBuildPhase; 49 | buildActionMask = 2147483647; 50 | files = ( 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | 106FE9EB1EE14F340061918B /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ); 59 | runOnlyForDeploymentPostprocessing = 0; 60 | }; 61 | 106FEA051EE14F630061918B /* Frameworks */ = { 62 | isa = PBXFrameworksBuildPhase; 63 | buildActionMask = 2147483647; 64 | files = ( 65 | ); 66 | runOnlyForDeploymentPostprocessing = 0; 67 | }; 68 | /* End PBXFrameworksBuildPhase section */ 69 | 70 | /* Begin PBXGroup section */ 71 | 10175E5723604822004F48C4 /* CSV */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 10175E5823604822004F48C4 /* CSV.h */, 75 | 10175E5923604822004F48C4 /* Info.plist */, 76 | ); 77 | path = CSV; 78 | sourceTree = ""; 79 | }; 80 | 106FE9E51EE14F340061918B = { 81 | isa = PBXGroup; 82 | children = ( 83 | 106FE9F01EE14F340061918B /* swift-csv */, 84 | 106FEA091EE14F630061918B /* Tests */, 85 | 10175E5723604822004F48C4 /* CSV */, 86 | 106FE9EF1EE14F340061918B /* Products */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | 106FE9EF1EE14F340061918B /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 106FE9EE1EE14F340061918B /* swift-csv */, 94 | 106FEA081EE14F630061918B /* Tests.xctest */, 95 | 10175E5623604822004F48C4 /* CSV.framework */, 96 | ); 97 | name = Products; 98 | sourceTree = ""; 99 | }; 100 | 106FE9F01EE14F340061918B /* swift-csv */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 106FEA141EE14F970061918B /* CSV.swift */, 104 | 106FE9F11EE14F340061918B /* main.swift */, 105 | ); 106 | path = "swift-csv"; 107 | sourceTree = ""; 108 | }; 109 | 106FEA091EE14F630061918B /* Tests */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 106FEA101EE14F900061918B /* CSVParserTest.swift */, 113 | 106FEA111EE14F900061918B /* CSVWriterTest.swift */, 114 | 364316621EE1C34800C879E6 /* Reading Test Documents */, 115 | 106FEA0C1EE14F630061918B /* Info.plist */, 116 | ); 117 | path = Tests; 118 | sourceTree = ""; 119 | }; 120 | /* End PBXGroup section */ 121 | 122 | /* Begin PBXHeadersBuildPhase section */ 123 | 10175E5123604822004F48C4 /* Headers */ = { 124 | isa = PBXHeadersBuildPhase; 125 | buildActionMask = 2147483647; 126 | files = ( 127 | 10175E5A23604822004F48C4 /* CSV.h in Headers */, 128 | ); 129 | runOnlyForDeploymentPostprocessing = 0; 130 | }; 131 | /* End PBXHeadersBuildPhase section */ 132 | 133 | /* Begin PBXNativeTarget section */ 134 | 10175E5523604822004F48C4 /* CSV */ = { 135 | isa = PBXNativeTarget; 136 | buildConfigurationList = 10175E5B23604822004F48C4 /* Build configuration list for PBXNativeTarget "CSV" */; 137 | buildPhases = ( 138 | 10175E5123604822004F48C4 /* Headers */, 139 | 10175E5223604822004F48C4 /* Sources */, 140 | 10175E5323604822004F48C4 /* Frameworks */, 141 | 10175E5423604822004F48C4 /* Resources */, 142 | ); 143 | buildRules = ( 144 | ); 145 | dependencies = ( 146 | ); 147 | name = CSV; 148 | productName = CSV; 149 | productReference = 10175E5623604822004F48C4 /* CSV.framework */; 150 | productType = "com.apple.product-type.framework"; 151 | }; 152 | 106FE9ED1EE14F340061918B /* swift-csv */ = { 153 | isa = PBXNativeTarget; 154 | buildConfigurationList = 106FE9F51EE14F340061918B /* Build configuration list for PBXNativeTarget "swift-csv" */; 155 | buildPhases = ( 156 | 106FE9EA1EE14F340061918B /* Sources */, 157 | 106FE9EB1EE14F340061918B /* Frameworks */, 158 | 106FE9EC1EE14F340061918B /* CopyFiles */, 159 | ); 160 | buildRules = ( 161 | ); 162 | dependencies = ( 163 | ); 164 | name = "swift-csv"; 165 | productName = "swift-csv"; 166 | productReference = 106FE9EE1EE14F340061918B /* swift-csv */; 167 | productType = "com.apple.product-type.tool"; 168 | }; 169 | 106FEA071EE14F630061918B /* Tests */ = { 170 | isa = PBXNativeTarget; 171 | buildConfigurationList = 106FEA0D1EE14F630061918B /* Build configuration list for PBXNativeTarget "Tests" */; 172 | buildPhases = ( 173 | 106FEA041EE14F630061918B /* Sources */, 174 | 106FEA051EE14F630061918B /* Frameworks */, 175 | 106FEA061EE14F630061918B /* Resources */, 176 | ); 177 | buildRules = ( 178 | ); 179 | dependencies = ( 180 | ); 181 | name = Tests; 182 | productName = Tests; 183 | productReference = 106FEA081EE14F630061918B /* Tests.xctest */; 184 | productType = "com.apple.product-type.bundle.unit-test"; 185 | }; 186 | /* End PBXNativeTarget section */ 187 | 188 | /* Begin PBXProject section */ 189 | 106FE9E61EE14F340061918B /* Project object */ = { 190 | isa = PBXProject; 191 | attributes = { 192 | LastSwiftUpdateCheck = 0830; 193 | LastUpgradeCheck = 1120; 194 | ORGANIZATIONNAME = "Matthias Hochgatterer"; 195 | TargetAttributes = { 196 | 10175E5523604822004F48C4 = { 197 | CreatedOnToolsVersion = 11.2; 198 | DevelopmentTeam = 3UMKW67674; 199 | ProvisioningStyle = Automatic; 200 | }; 201 | 106FE9ED1EE14F340061918B = { 202 | CreatedOnToolsVersion = 8.3.2; 203 | DevelopmentTeam = 3UMKW67674; 204 | LastSwiftMigration = 1020; 205 | ProvisioningStyle = Automatic; 206 | }; 207 | 106FEA071EE14F630061918B = { 208 | CreatedOnToolsVersion = 8.3.2; 209 | DevelopmentTeam = 3UMKW67674; 210 | LastSwiftMigration = 1020; 211 | ProvisioningStyle = Automatic; 212 | }; 213 | }; 214 | }; 215 | buildConfigurationList = 106FE9E91EE14F340061918B /* Build configuration list for PBXProject "swift-csv" */; 216 | compatibilityVersion = "Xcode 3.2"; 217 | developmentRegion = en; 218 | hasScannedForEncodings = 0; 219 | knownRegions = ( 220 | en, 221 | Base, 222 | ); 223 | mainGroup = 106FE9E51EE14F340061918B; 224 | productRefGroup = 106FE9EF1EE14F340061918B /* Products */; 225 | projectDirPath = ""; 226 | projectRoot = ""; 227 | targets = ( 228 | 106FE9ED1EE14F340061918B /* swift-csv */, 229 | 106FEA071EE14F630061918B /* Tests */, 230 | 10175E5523604822004F48C4 /* CSV */, 231 | ); 232 | }; 233 | /* End PBXProject section */ 234 | 235 | /* Begin PBXResourcesBuildPhase section */ 236 | 10175E5423604822004F48C4 /* Resources */ = { 237 | isa = PBXResourcesBuildPhase; 238 | buildActionMask = 2147483647; 239 | files = ( 240 | ); 241 | runOnlyForDeploymentPostprocessing = 0; 242 | }; 243 | 106FEA061EE14F630061918B /* Resources */ = { 244 | isa = PBXResourcesBuildPhase; 245 | buildActionMask = 2147483647; 246 | files = ( 247 | 364316631EE1C34800C879E6 /* Reading Test Documents in Resources */, 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | /* End PBXResourcesBuildPhase section */ 252 | 253 | /* Begin PBXSourcesBuildPhase section */ 254 | 10175E5223604822004F48C4 /* Sources */ = { 255 | isa = PBXSourcesBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | 10175E5E2360482A004F48C4 /* CSV.swift in Sources */, 259 | ); 260 | runOnlyForDeploymentPostprocessing = 0; 261 | }; 262 | 106FE9EA1EE14F340061918B /* Sources */ = { 263 | isa = PBXSourcesBuildPhase; 264 | buildActionMask = 2147483647; 265 | files = ( 266 | 106FEA151EE14F970061918B /* CSV.swift in Sources */, 267 | 106FE9F21EE14F340061918B /* main.swift in Sources */, 268 | ); 269 | runOnlyForDeploymentPostprocessing = 0; 270 | }; 271 | 106FEA041EE14F630061918B /* Sources */ = { 272 | isa = PBXSourcesBuildPhase; 273 | buildActionMask = 2147483647; 274 | files = ( 275 | 106FEA161EE14F9B0061918B /* CSV.swift in Sources */, 276 | 106FEA131EE14F900061918B /* CSVWriterTest.swift in Sources */, 277 | 106FEA121EE14F900061918B /* CSVParserTest.swift in Sources */, 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | /* End PBXSourcesBuildPhase section */ 282 | 283 | /* Begin XCBuildConfiguration section */ 284 | 10175E5C23604822004F48C4 /* Debug */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 288 | CLANG_ENABLE_OBJC_WEAK = YES; 289 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 290 | CODE_SIGN_STYLE = Automatic; 291 | CURRENT_PROJECT_VERSION = 1; 292 | DEFINES_MODULE = YES; 293 | DEVELOPMENT_TEAM = 3UMKW67674; 294 | DYLIB_COMPATIBILITY_VERSION = 1; 295 | DYLIB_CURRENT_VERSION = 1; 296 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 297 | GCC_C_LANGUAGE_STANDARD = gnu11; 298 | INFOPLIST_FILE = CSV/Info.plist; 299 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 300 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 301 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 302 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 303 | MTL_FAST_MATH = YES; 304 | PRODUCT_BUNDLE_IDENTIFIER = at.mah.CSV; 305 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 306 | SDKROOT = iphoneos; 307 | SKIP_INSTALL = YES; 308 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 309 | SWIFT_VERSION = 5.0; 310 | TARGETED_DEVICE_FAMILY = "1,2"; 311 | VERSIONING_SYSTEM = "apple-generic"; 312 | VERSION_INFO_PREFIX = ""; 313 | }; 314 | name = Debug; 315 | }; 316 | 10175E5D23604822004F48C4 /* Release */ = { 317 | isa = XCBuildConfiguration; 318 | buildSettings = { 319 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 320 | CLANG_ENABLE_OBJC_WEAK = YES; 321 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 322 | CODE_SIGN_STYLE = Automatic; 323 | CURRENT_PROJECT_VERSION = 1; 324 | DEFINES_MODULE = YES; 325 | DEVELOPMENT_TEAM = 3UMKW67674; 326 | DYLIB_COMPATIBILITY_VERSION = 1; 327 | DYLIB_CURRENT_VERSION = 1; 328 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 329 | GCC_C_LANGUAGE_STANDARD = gnu11; 330 | INFOPLIST_FILE = CSV/Info.plist; 331 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 332 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 333 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 334 | MTL_FAST_MATH = YES; 335 | PRODUCT_BUNDLE_IDENTIFIER = at.mah.CSV; 336 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 337 | SDKROOT = iphoneos; 338 | SKIP_INSTALL = YES; 339 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 340 | SWIFT_VERSION = 5.0; 341 | TARGETED_DEVICE_FAMILY = "1,2"; 342 | VALIDATE_PRODUCT = YES; 343 | VERSIONING_SYSTEM = "apple-generic"; 344 | VERSION_INFO_PREFIX = ""; 345 | }; 346 | name = Release; 347 | }; 348 | 106FE9F31EE14F340061918B /* Debug */ = { 349 | isa = XCBuildConfiguration; 350 | buildSettings = { 351 | ALWAYS_SEARCH_USER_PATHS = NO; 352 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 353 | CLANG_ANALYZER_NONNULL = YES; 354 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 355 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 356 | CLANG_CXX_LIBRARY = "libc++"; 357 | CLANG_ENABLE_MODULES = YES; 358 | CLANG_ENABLE_OBJC_ARC = YES; 359 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 360 | CLANG_WARN_BOOL_CONVERSION = YES; 361 | CLANG_WARN_COMMA = YES; 362 | CLANG_WARN_CONSTANT_CONVERSION = YES; 363 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 364 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 365 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 366 | CLANG_WARN_EMPTY_BODY = YES; 367 | CLANG_WARN_ENUM_CONVERSION = YES; 368 | CLANG_WARN_INFINITE_RECURSION = YES; 369 | CLANG_WARN_INT_CONVERSION = YES; 370 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 371 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 372 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 373 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 374 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 375 | CLANG_WARN_STRICT_PROTOTYPES = YES; 376 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 377 | CLANG_WARN_UNREACHABLE_CODE = YES; 378 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 379 | CODE_SIGN_IDENTITY = "-"; 380 | COPY_PHASE_STRIP = NO; 381 | DEBUG_INFORMATION_FORMAT = dwarf; 382 | ENABLE_STRICT_OBJC_MSGSEND = YES; 383 | ENABLE_TESTABILITY = YES; 384 | GCC_C_LANGUAGE_STANDARD = gnu99; 385 | GCC_DYNAMIC_NO_PIC = NO; 386 | GCC_NO_COMMON_BLOCKS = YES; 387 | GCC_OPTIMIZATION_LEVEL = 0; 388 | GCC_PREPROCESSOR_DEFINITIONS = ( 389 | "DEBUG=1", 390 | "$(inherited)", 391 | ); 392 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 393 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 394 | GCC_WARN_UNDECLARED_SELECTOR = YES; 395 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 396 | GCC_WARN_UNUSED_FUNCTION = YES; 397 | GCC_WARN_UNUSED_VARIABLE = YES; 398 | MACOSX_DEPLOYMENT_TARGET = 10.12; 399 | MTL_ENABLE_DEBUG_INFO = YES; 400 | ONLY_ACTIVE_ARCH = YES; 401 | SDKROOT = macosx; 402 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 403 | }; 404 | name = Debug; 405 | }; 406 | 106FE9F41EE14F340061918B /* Release */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | ALWAYS_SEARCH_USER_PATHS = NO; 410 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 411 | CLANG_ANALYZER_NONNULL = YES; 412 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 413 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 414 | CLANG_CXX_LIBRARY = "libc++"; 415 | CLANG_ENABLE_MODULES = YES; 416 | CLANG_ENABLE_OBJC_ARC = YES; 417 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 418 | CLANG_WARN_BOOL_CONVERSION = YES; 419 | CLANG_WARN_COMMA = YES; 420 | CLANG_WARN_CONSTANT_CONVERSION = YES; 421 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 422 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 423 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 424 | CLANG_WARN_EMPTY_BODY = YES; 425 | CLANG_WARN_ENUM_CONVERSION = YES; 426 | CLANG_WARN_INFINITE_RECURSION = YES; 427 | CLANG_WARN_INT_CONVERSION = YES; 428 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 429 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 430 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 431 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 432 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 433 | CLANG_WARN_STRICT_PROTOTYPES = YES; 434 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 435 | CLANG_WARN_UNREACHABLE_CODE = YES; 436 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 437 | CODE_SIGN_IDENTITY = "-"; 438 | COPY_PHASE_STRIP = NO; 439 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 440 | ENABLE_NS_ASSERTIONS = NO; 441 | ENABLE_STRICT_OBJC_MSGSEND = YES; 442 | GCC_C_LANGUAGE_STANDARD = gnu99; 443 | GCC_NO_COMMON_BLOCKS = YES; 444 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 445 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 446 | GCC_WARN_UNDECLARED_SELECTOR = YES; 447 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 448 | GCC_WARN_UNUSED_FUNCTION = YES; 449 | GCC_WARN_UNUSED_VARIABLE = YES; 450 | MACOSX_DEPLOYMENT_TARGET = 10.12; 451 | MTL_ENABLE_DEBUG_INFO = NO; 452 | SDKROOT = macosx; 453 | SWIFT_COMPILATION_MODE = wholemodule; 454 | }; 455 | name = Release; 456 | }; 457 | 106FE9F61EE14F340061918B /* Debug */ = { 458 | isa = XCBuildConfiguration; 459 | buildSettings = { 460 | CODE_SIGN_IDENTITY = "-"; 461 | DEVELOPMENT_TEAM = 3UMKW67674; 462 | PRODUCT_NAME = "$(TARGET_NAME)"; 463 | SWIFT_VERSION = 5.0; 464 | }; 465 | name = Debug; 466 | }; 467 | 106FE9F71EE14F340061918B /* Release */ = { 468 | isa = XCBuildConfiguration; 469 | buildSettings = { 470 | CODE_SIGN_IDENTITY = "-"; 471 | DEVELOPMENT_TEAM = 3UMKW67674; 472 | PRODUCT_NAME = "$(TARGET_NAME)"; 473 | SWIFT_VERSION = 5.0; 474 | }; 475 | name = Release; 476 | }; 477 | 106FEA0E1EE14F630061918B /* Debug */ = { 478 | isa = XCBuildConfiguration; 479 | buildSettings = { 480 | COMBINE_HIDPI_IMAGES = YES; 481 | DEVELOPMENT_TEAM = 3UMKW67674; 482 | INFOPLIST_FILE = Tests/Info.plist; 483 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 484 | PRODUCT_BUNDLE_IDENTIFIER = at.mah.Tests; 485 | PRODUCT_NAME = "$(TARGET_NAME)"; 486 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 487 | SWIFT_VERSION = 5.0; 488 | }; 489 | name = Debug; 490 | }; 491 | 106FEA0F1EE14F630061918B /* Release */ = { 492 | isa = XCBuildConfiguration; 493 | buildSettings = { 494 | COMBINE_HIDPI_IMAGES = YES; 495 | DEVELOPMENT_TEAM = 3UMKW67674; 496 | INFOPLIST_FILE = Tests/Info.plist; 497 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 498 | PRODUCT_BUNDLE_IDENTIFIER = at.mah.Tests; 499 | PRODUCT_NAME = "$(TARGET_NAME)"; 500 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 501 | SWIFT_VERSION = 5.0; 502 | }; 503 | name = Release; 504 | }; 505 | /* End XCBuildConfiguration section */ 506 | 507 | /* Begin XCConfigurationList section */ 508 | 10175E5B23604822004F48C4 /* Build configuration list for PBXNativeTarget "CSV" */ = { 509 | isa = XCConfigurationList; 510 | buildConfigurations = ( 511 | 10175E5C23604822004F48C4 /* Debug */, 512 | 10175E5D23604822004F48C4 /* Release */, 513 | ); 514 | defaultConfigurationIsVisible = 0; 515 | defaultConfigurationName = Release; 516 | }; 517 | 106FE9E91EE14F340061918B /* Build configuration list for PBXProject "swift-csv" */ = { 518 | isa = XCConfigurationList; 519 | buildConfigurations = ( 520 | 106FE9F31EE14F340061918B /* Debug */, 521 | 106FE9F41EE14F340061918B /* Release */, 522 | ); 523 | defaultConfigurationIsVisible = 0; 524 | defaultConfigurationName = Release; 525 | }; 526 | 106FE9F51EE14F340061918B /* Build configuration list for PBXNativeTarget "swift-csv" */ = { 527 | isa = XCConfigurationList; 528 | buildConfigurations = ( 529 | 106FE9F61EE14F340061918B /* Debug */, 530 | 106FE9F71EE14F340061918B /* Release */, 531 | ); 532 | defaultConfigurationIsVisible = 0; 533 | defaultConfigurationName = Release; 534 | }; 535 | 106FEA0D1EE14F630061918B /* Build configuration list for PBXNativeTarget "Tests" */ = { 536 | isa = XCConfigurationList; 537 | buildConfigurations = ( 538 | 106FEA0E1EE14F630061918B /* Debug */, 539 | 106FEA0F1EE14F630061918B /* Release */, 540 | ); 541 | defaultConfigurationIsVisible = 0; 542 | defaultConfigurationName = Release; 543 | }; 544 | /* End XCConfigurationList section */ 545 | }; 546 | rootObject = 106FE9E61EE14F340061918B /* Project object */; 547 | } 548 | -------------------------------------------------------------------------------- /swift-csv.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /swift-csv.xcodeproj/xcshareddata/xcschemes/CSV.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 44 | 50 | 51 | 57 | 58 | 59 | 60 | 62 | 63 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /swift-csv.xcodeproj/xcuserdata/mah.xcuserdatad/xcschemes/swift-csv.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /swift-csv.xcodeproj/xcuserdata/mah.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | CSV.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 3 11 | 12 | Tests.xcscheme 13 | 14 | orderHint 15 | 2 16 | 17 | swift-csv-tests.xcscheme 18 | 19 | orderHint 20 | 1 21 | 22 | swift-csv.xcscheme 23 | 24 | orderHint 25 | 0 26 | 27 | 28 | SuppressBuildableAutocreation 29 | 30 | 10175E5523604822004F48C4 31 | 32 | primary 33 | 34 | 35 | 106FE9ED1EE14F340061918B 36 | 37 | primary 38 | 39 | 40 | 106FE9FB1EE14F500061918B 41 | 42 | primary 43 | 44 | 45 | 106FEA071EE14F630061918B 46 | 47 | primary 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /swift-csv/CSV.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CSV.swift 3 | // swift-csv 4 | // 5 | // Created by Matthias Hochgatterer on 02/06/2017. 6 | // Copyright © 2017 Matthias Hochgatterer. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ParserDelegate: class { 12 | 13 | /// Called when the parser begins parsing. 14 | func parserDidBeginDocument(_ parser: CSV.Parser) 15 | 16 | /// Called when the parser finished parsing without errors. 17 | func parserDidEndDocument(_ parser: CSV.Parser) 18 | 19 | /// Called when the parser begins parsing a line. 20 | func parser(_ parser: CSV.Parser, didBeginLineAt index: UInt) 21 | 22 | /// Called when the parser finished parsing a line. 23 | func parser(_ parser: CSV.Parser, didEndLineAt index: UInt) 24 | 25 | /// Called for every field in a line. 26 | func parser(_ parser: CSV.Parser, didReadFieldAt index: UInt, value: String) 27 | } 28 | 29 | extension String.Encoding { 30 | /// Unicode text data may start with a [byte order mark](https://en.wikipedia.org/wiki/Byte_order_mark) to specify the encoding and endianess. 31 | struct BOM { 32 | let encoding: String.Encoding 33 | init?(bom0: UInt8, bom1: UInt8, bom2: UInt8, bom3: UInt8) { 34 | switch (bom0, bom1, bom2, bom3) { 35 | case (0xEF, 0xBB, 0xBF, _): 36 | self.encoding = .utf8 37 | case (0xFE, 0xFF, _, _): 38 | self.encoding = .utf16BigEndian 39 | case (0xFF, 0xFE, _, _): 40 | self.encoding = .utf16LittleEndian 41 | case (0x00, 0x00, 0xFE, 0xFF): 42 | self.encoding = .utf32BigEndian 43 | case (0xFF, 0xFE, 0x00, 0x00): 44 | self.encoding = .utf32LittleEndian 45 | default: 46 | return nil 47 | } 48 | } 49 | 50 | var length: Int { 51 | switch self.encoding { 52 | case String.Encoding.utf8: 53 | return 3 54 | case String.Encoding.utf16BigEndian, String.Encoding.utf16LittleEndian: 55 | return 2 56 | case String.Encoding.utf32BigEndian, String.Encoding.utf32LittleEndian: 57 | return 4 58 | default: 59 | return 0 60 | } 61 | } 62 | } 63 | } 64 | 65 | public struct CSVError: Error { 66 | public let description: String 67 | } 68 | 69 | public struct CSV { 70 | 71 | static let CarriageReturn: UnicodeScalar = "\r" 72 | static let LineFeed: UnicodeScalar = "\n" 73 | static let DoubleQuote: UnicodeScalar = "\"" 74 | static let Nul: UnicodeScalar = UnicodeScalar(0) 75 | 76 | /// Writes data in CSV format into an output stream. 77 | /// The writer uses the line feed "\n" character for line breaks. (Even though [RFC 4180](https://tools.ietf.org/html/rfc4180) specifies CRLF as line break characters.) 78 | public class Writer { 79 | 80 | let outputStream: OutputStream 81 | let configuration: CSV.Configuration 82 | 83 | internal let illegalCharacterSet: CharacterSet 84 | internal var maxNumberOfWrittenFields: Int? 85 | internal var numberOfWrittenLines: Int = 0 86 | 87 | public init(outputStream: OutputStream, configuration: CSV.Configuration) { 88 | 89 | if outputStream.streamStatus == .notOpen { 90 | outputStream.open() 91 | } 92 | 93 | self.outputStream = outputStream 94 | self.configuration = configuration 95 | self.illegalCharacterSet = CharacterSet(charactersIn: "\(DoubleQuote)\(configuration.delimiter)\(CarriageReturn)\(LineFeed)") 96 | } 97 | 98 | /// Writes fields as a line to the output stream. 99 | public func writeLine(of fields: [String]) throws { 100 | if let count = self.maxNumberOfWrittenFields { 101 | if count != fields.count { 102 | throw CSVError(description: "Invalid number of fields") 103 | } 104 | } else { 105 | maxNumberOfWrittenFields = fields.count 106 | } 107 | 108 | if numberOfWrittenLines > 0 { 109 | self.writeNewLineCharacter() 110 | } 111 | 112 | let escapedValues = fields.map({ self.escapedValue(for: $0) }) 113 | let string = escapedValues.joined(separator: String(configuration.delimiter)) 114 | self.writeString(string) 115 | 116 | numberOfWrittenLines += 1 117 | } 118 | 119 | internal func writeNewLineCharacter() { 120 | self.writeString(String(LineFeed)) 121 | } 122 | 123 | internal func escapedValue(for field: String) -> String { 124 | if let _ = field.rangeOfCharacter(from: illegalCharacterSet) { 125 | // A double quote must be preceded by another double quote 126 | let value = field.replacingOccurrences(of: String(DoubleQuote), with: "\"\"") 127 | // Quote fields containing illegal characters 128 | return "\"\(value)\"" 129 | } 130 | 131 | return field 132 | } 133 | 134 | internal func writeString(_ string: String) { 135 | if let data = string.data(using: configuration.encoding) { 136 | data.withUnsafeBytes({ 137 | (pointer: UnsafeRawBufferPointer) -> Void in 138 | if let bytes = pointer.bindMemory(to: UInt8.self).baseAddress { 139 | self.outputStream.write(bytes, maxLength: pointer.count) 140 | } 141 | }) 142 | } 143 | } 144 | } 145 | 146 | public class Parser { 147 | 148 | public weak var delegate: ParserDelegate? 149 | public let configuration: CSV.Configuration 150 | public var trimsWhitespaces: Bool = false 151 | 152 | // Reference to the file stream 153 | private let inputStream: InputStream 154 | 155 | // The buffer for field values 156 | private var fieldBuffer = [UInt8]() 157 | 158 | // The current row index 159 | private var row: UInt = 0 160 | 161 | // The current column index 162 | private var column: UInt = 0 163 | 164 | // Flag to know if the parser was cancelled. 165 | private var cancelled: Bool = false 166 | 167 | private enum State { 168 | case beginningOfDocument 169 | case endOfDocument 170 | case beginningOfLine 171 | case endOfLine 172 | case inField 173 | case inQuotedField 174 | case maybeEndOfQuotedField 175 | case endOfField 176 | } 177 | 178 | // The current parser state 179 | private var state: State = .beginningOfDocument { 180 | didSet { 181 | if oldValue == .beginningOfDocument { 182 | delegate?.parserDidBeginDocument(self) 183 | } 184 | 185 | switch (state) { 186 | case .endOfDocument: 187 | delegate?.parserDidEndDocument(self) 188 | case .beginningOfLine: 189 | delegate?.parser(self, didBeginLineAt: row) 190 | case .endOfLine: 191 | delegate?.parser(self, didEndLineAt: row) 192 | column = 0 193 | row += 1 194 | case .endOfField: 195 | let data = Data(fieldBuffer) 196 | let value: String 197 | if let string = String(data: data, encoding: configuration.encoding) { // Try to decode using the specified encoding 198 | value = string 199 | } else { 200 | value = String(cString: fieldBuffer + [0]) // cString requires '\0' at the end 201 | } 202 | fieldBuffer.removeAll() 203 | 204 | if !value.isEmpty && self.trimsWhitespaces { 205 | let trimmed = value.trimmingCharacters(in: CharacterSet.whitespaces) 206 | delegate?.parser(self, didReadFieldAt: column, value: trimmed) 207 | } else { 208 | delegate?.parser(self, didReadFieldAt: column, value: value) 209 | } 210 | 211 | column += 1 212 | default: 213 | break 214 | } 215 | } 216 | } 217 | 218 | /// Initializes the parser with an url. 219 | /// 220 | /// - Paramter url: An url referencing a CSV file. 221 | public convenience init?(url: URL, configuration: CSV.Configuration) { 222 | guard let inputStream = InputStream(url: url) else { 223 | return nil 224 | } 225 | 226 | self.init(inputStream: inputStream, configuration: configuration) 227 | } 228 | 229 | /// Initializes the parser with a string. 230 | /// 231 | /// - Paramter string: A CSV string. 232 | public convenience init(string: String, configuration: CSV.Configuration) { 233 | self.init(data: string.data(using: .utf8)!, configuration: configuration) 234 | } 235 | 236 | /// Initializes the parser with data. 237 | /// 238 | /// - Paramter data: Data represeting CSV content. 239 | public convenience init(data: Data, configuration: CSV.Configuration) { 240 | self.init(inputStream: InputStream(data: data), configuration: configuration) 241 | } 242 | 243 | /// Initializes the parser with an input stream. 244 | /// 245 | /// - Paramter inputStream: An input stream of CSV data. 246 | public init(inputStream: InputStream, configuration: CSV.Configuration = CSV.Configuration(delimiter: ",")) { 247 | self.inputStream = inputStream 248 | self.configuration = configuration 249 | } 250 | 251 | /// Cancels the parser. 252 | public func cancel() { 253 | self.cancelled = true 254 | } 255 | 256 | 257 | /// Starts parsing the CSV data. Calling this method does nothing if the parser already finished parsing the data. 258 | /// 259 | /// - Throws: An error if the data doesn't conform to [RFC 4180](https://tools.ietf.org/html/rfc4180). 260 | public func parse() throws { 261 | 262 | guard self.state != .endOfDocument && !cancelled else { 263 | return 264 | } 265 | 266 | let reader = BufferedByteReader(inputStream: inputStream) 267 | 268 | // Consume bom if available 269 | if let bom0 = reader.peek(at: 0), let bom1 = reader.peek(at: 1), let bom2 = reader.peek(at: 2), let bom3 = reader.peek(at: 3) { 270 | if let bom = String.Encoding.BOM(bom0: bom0, bom1: bom1, bom2: bom2, bom3: bom3) { 271 | for _ in 0.. 0 { 372 | // Append to any existing character 373 | fieldBuffer.append(char) 374 | } 375 | default: 376 | break 377 | } 378 | default: // Any other characters 379 | switch state { 380 | case .beginningOfLine: 381 | fieldBuffer.append(char) 382 | state = .inField 383 | case .endOfField: 384 | fieldBuffer.append(char) 385 | state = .inField 386 | case .maybeEndOfQuotedField: 387 | // Skip values outside of quoted fields 388 | break 389 | case .inField: 390 | fieldBuffer.append(char) 391 | case .inQuotedField: 392 | fieldBuffer.append(char) 393 | default: 394 | assertionFailure("Invalid state") 395 | } 396 | } 397 | } 398 | 399 | if cancelled { 400 | return 401 | } 402 | 403 | // Transition to the correct state at the end of the document 404 | switch state { 405 | case .beginningOfDocument: 406 | // There was no data at all 407 | break 408 | case .endOfDocument: 409 | assertionFailure("Invalid state") 410 | case .beginningOfLine: 411 | break 412 | case .endOfLine: 413 | break 414 | case .endOfField: 415 | // Rows must not end with the delimieter 416 | // Therefore we there must be a new field before the end 417 | state = .inField 418 | state = .endOfField 419 | state = .endOfLine 420 | case .inField: 421 | state = .endOfField 422 | state = .endOfLine 423 | case .inQuotedField: 424 | throw CSVError(description: "Unexpected end of quoted field") 425 | case .maybeEndOfQuotedField: 426 | state = .endOfField 427 | state = .endOfLine 428 | } 429 | 430 | // Now we are at the end 431 | state = .endOfDocument 432 | } 433 | } 434 | } 435 | 436 | /// A configuration specifies the delimiter and encoding for parsing CSV data. 437 | public struct Configuration { 438 | 439 | public let delimiter: UnicodeScalar 440 | public let encoding: String.Encoding 441 | 442 | /// Returns a configuration by detecting the delimeter and text encoding from a file at an url. 443 | public static func detectConfigurationForContentsOfURL(_ url: URL) -> Configuration? { 444 | guard let stream = InputStream(url: url) else { 445 | return nil 446 | } 447 | 448 | return self.detectConfigurationForInputStream(stream) 449 | } 450 | 451 | /// Returns a configuration by detecting the delimeter and text encoding from the CSV input stream. 452 | public static func detectConfigurationForInputStream(_ stream: InputStream) -> Configuration? { 453 | if stream.streamStatus == .notOpen { 454 | stream.open() 455 | } 456 | 457 | let maxLength = 400 458 | var buffer = Array(repeating: 0, count: maxLength) 459 | let length = stream.read(&buffer, maxLength: buffer.count) 460 | if let error = stream.streamError { 461 | print(error) 462 | return nil 463 | } 464 | 465 | var encoding: String.Encoding = .utf8 466 | 467 | if length > 4, let bom = String.Encoding.BOM(bom0: buffer[0], bom1: buffer[1], bom2: buffer[2], bom3: buffer[3]) { 468 | encoding = bom.encoding 469 | buffer.removeFirst(bom.length) 470 | } 471 | 472 | let string: String 473 | if let decoded = String(bytes: buffer, encoding: encoding) { 474 | string = decoded 475 | } else if let macOSRoman = String(bytes: buffer, encoding: .macOSRoman) { 476 | string = macOSRoman 477 | encoding = .macOSRoman 478 | } else { 479 | return nil 480 | } 481 | 482 | let scanner = Scanner(string: string) 483 | 484 | var firstLine: NSString? = nil 485 | scanner.scanUpToCharacters(from: CharacterSet.newlines, into: &firstLine) 486 | 487 | guard let header = firstLine else { 488 | return nil 489 | } 490 | 491 | return self.detectConfigurationForString(header as String, encoding: encoding) 492 | } 493 | 494 | /// Returns a configuration by detecting the delimeter and text encoding from a CSV string. 495 | public static func detectConfigurationForString(_ string: String, encoding: String.Encoding) -> Configuration { 496 | struct Delimiter { 497 | let scalar: UnicodeScalar 498 | let weight: Int 499 | } 500 | 501 | let delimiters = [",", ";", "\t"].map({ Delimiter(scalar: UnicodeScalar($0)!, weight: string.components(separatedBy: $0).count) }) 502 | 503 | let winner = delimiters.sorted(by: { 504 | lhs, rhs in 505 | return lhs.weight > rhs.weight 506 | }).first! 507 | 508 | return Configuration(delimiter: winner.scalar, encoding: encoding) 509 | } 510 | 511 | /// Initializes a configuration with a delimiter and text encoding. 512 | public init(delimiter: UnicodeScalar, encoding: String.Encoding = .utf8) { 513 | self.delimiter = delimiter 514 | self.encoding = encoding 515 | } 516 | } 517 | } 518 | 519 | internal class BufferedByteReader { 520 | let inputStream: InputStream 521 | var isAtEnd = false 522 | var buffer = [UInt8]() 523 | var bufferIndex = 0 524 | 525 | init(inputStream: InputStream) { 526 | if inputStream.streamStatus == .notOpen { 527 | inputStream.open() 528 | } 529 | 530 | self.inputStream = inputStream 531 | } 532 | 533 | /// - returns: The next character and removes it from the stream after it has been returned, or nil if the stream is at the end. 534 | func pop() -> UInt8? { 535 | guard let byte = self.peek() else { 536 | isAtEnd = true 537 | return nil 538 | } 539 | bufferIndex += 1 540 | return byte 541 | } 542 | 543 | /// - Returns: The character at `index`, or nil if the stream is at the end. 544 | func peek(at index: Int = 0) -> UInt8? { 545 | let peekIndex = bufferIndex + index 546 | guard peekIndex < buffer.count else { 547 | guard read(100) > 0 else { 548 | // end of file or error 549 | return nil 550 | } 551 | return self.peek(at:index) 552 | } 553 | return buffer[peekIndex] 554 | } 555 | 556 | // MARK: - Private 557 | 558 | private func read(_ amount: Int) -> Int { 559 | if bufferIndex > 0 { 560 | buffer.removeFirst(bufferIndex) 561 | bufferIndex = 0 562 | } 563 | var temp = [UInt8](repeating: 0, count: amount) 564 | let length = inputStream.read(&temp, maxLength: temp.count) 565 | if length > 0 { 566 | buffer.append(contentsOf: temp[0..