@@ -23,10 +23,8 @@ private let chunkSize = 4096
2323public class CSVImporter < T> {
2424 // MARK: - Stored Instance Properties
2525
26- let csvFile : TextFile
26+ let source : Source
2727 let delimiter : String
28- var lineEnding : LineEnding
29- let encoding : String . Encoding
3028
3129 var lastProgressReport : Date ?
3230
@@ -44,34 +42,57 @@ public class CSVImporter<T> {
4442
4543 // MARK: - Initializers
4644
47- /// Creates a `CSVImporter` object with required configuration options.
48- ///
49- /// - Parameters:
50- /// - path: The path to the CSV file to import.
51- /// - delimiter: The delimiter used within the CSV file for separating fields. Defaults to ",".
52- /// - lineEnding: The lineEnding of the file. If not specified will be determined automatically.
53- public init ( path: String , delimiter: String = " , " , lineEnding: LineEnding = . unknown, encoding: String . Encoding = . utf8) {
54- self . csvFile = TextFile ( path: path, encoding: encoding)
45+ /// Internal initializer to prevent duplicate code.
46+ private init ( source: Source , delimiter: String ) {
47+ self . source = source
5548 self . delimiter = delimiter
56- self . lineEnding = lineEnding
57- self . encoding = encoding
5849
5950 delimiterQuoteDelimiter = " \( delimiter) \" \" \( delimiter) "
6051 delimiterDelimiter = delimiter+ delimiter
6152 quoteDelimiter = " \" \" \( delimiter) "
6253 delimiterQuote = " \( delimiter) \" \" "
6354 }
6455
56+
57+ /// Creates a `CSVImporter` object with required configuration options.
58+ ///
59+ /// - Parameters:
60+ /// - path: The path to the CSV file to import.
61+ /// - delimiter: The delimiter used within the CSV file for separating fields. Defaults to ",".
62+ /// - lineEnding: The lineEnding used in the file. If not specified will be determined automatically.
63+ /// - encoding: The encoding the file is read with. Defaults to `.utf8`.
64+ public convenience init ( path: String , delimiter: String = " , " , lineEnding: LineEnding = . unknown, encoding: String . Encoding = . utf8) {
65+ let textFile = TextFile ( path: path, encoding: encoding)
66+ let fileSource = FileSource ( textFile: textFile, encoding: encoding, lineEnding: lineEnding)
67+ self . init ( source: fileSource, delimiter: delimiter)
68+ }
69+
6570 /// Creates a `CSVImporter` object with required configuration options.
6671 ///
6772 /// - Parameters:
6873 /// - url: File URL for the CSV file to import.
6974 /// - delimiter: The delimiter used within the CSV file for separating fields. Defaults to ",".
75+ /// - lineEnding: The lineEnding used in the file. If not specified will be determined automatically.
76+ /// - encoding: The encoding the file is read with. Defaults to `.utf8`.
7077 public convenience init ? ( url: URL , delimiter: String = " , " , lineEnding: LineEnding = . unknown, encoding: String . Encoding = . utf8) {
7178 guard url. isFileURL else { return nil }
7279 self . init ( path: url. path, delimiter: delimiter, lineEnding: lineEnding, encoding: encoding)
7380 }
7481
82+ /// Creates a `CSVImporter` object with required configuration options.
83+ ///
84+ /// NOTE: This initializer doesn't save any memory as the given String is already loaded into memory.
85+ /// Don't use this if you are working with a large file which you could refer to with a path also.
86+ ///
87+ /// - Parameters:
88+ /// - contentString: The string which contains the content of a CSV file.
89+ /// - delimiter: The delimiter used within the CSV file for separating fields. Defaults to ",".
90+ /// - lineEnding: The lineEnding used in the file. If not specified will be determined automatically.
91+ public convenience init ( contentString: String , delimiter: String = " , " , lineEnding: LineEnding = . unknown) {
92+ let stringSource = StringSource ( contentString: contentString, lineEnding: lineEnding)
93+ self . init ( source: stringSource, delimiter: delimiter)
94+ }
95+
7596 // MARK: - Instance Methods
7697
7798 /// Starts importing the records within the CSV file line by line.
@@ -145,51 +166,28 @@ public class CSVImporter<T> {
145166 /// - valuesInLine: The values found within a line.
146167 /// - Returns: `true` on finish or `false` if can't read file.
147168 func importLines( _ closure: ( _ valuesInLine: [ String ] ) -> Void ) -> Bool {
148- if lineEnding == . unknown {
149- lineEnding = lineEndingForFile ( )
150- }
151- guard let csvStreamReader = self . csvFile. streamReader ( lineEnding: lineEnding, chunkSize: chunkSize) else { return false }
169+ var anyLine = false
152170
153- for line in csvStreamReader {
171+ source. forEach { line in
172+ anyLine = true
154173 autoreleasepool {
155174 let valuesInLine = readValuesInLine ( line)
156175 closure ( valuesInLine)
157176 }
158177 }
159178
160- return true
161- }
162-
163- /// Determines the line ending for the CSV file
164- ///
165- /// - Returns: the lineEnding for the CSV file or default of NL.
166- fileprivate func lineEndingForFile( ) -> LineEnding {
167- var lineEnding : LineEnding = . nl
168- if let fileHandle = self . csvFile. handleForReading {
169- if let data = ( fileHandle. readData ( ofLength: chunkSize) as NSData ) . mutableCopy ( ) as? NSMutableData {
170- if let contents = NSString ( bytesNoCopy: data. mutableBytes, length: data. length, encoding: encoding. rawValue, freeWhenDone: false ) {
171- if contents. contains ( LineEnding . crlf. rawValue) {
172- lineEnding = . crlf
173- } else if contents. contains ( LineEnding . nl. rawValue) {
174- lineEnding = . nl
175- } else if contents. contains ( LineEnding . cr. rawValue) {
176- lineEnding = . cr
177- }
178- }
179- }
180- }
181- return lineEnding
179+ return anyLine
182180 }
183181
184182 // Various private constants used for reading lines
185- fileprivate let startPartRegex = try ! NSRegularExpression ( pattern: " \\ A \" [^ \" ]* \\ z " , options: . caseInsensitive) // swiftlint:disable:this force_try
186- fileprivate let middlePartRegex = try ! NSRegularExpression ( pattern: " \\ A[^ \" ]* \\ z " , options: . caseInsensitive) // swiftlint:disable:this force_try
187- fileprivate let endPartRegex = try ! NSRegularExpression ( pattern: " \\ A[^ \" ]* \" \\ z " , options: . caseInsensitive) // swiftlint:disable:this force_try
188- fileprivate let substitute = " \u{001a} "
189- fileprivate let delimiterQuoteDelimiter : String
190- fileprivate let delimiterDelimiter : String
191- fileprivate let quoteDelimiter : String
192- fileprivate let delimiterQuote : String
183+ private let startPartRegex = try ! NSRegularExpression ( pattern: " \\ A \" [^ \" ]* \\ z " , options: . caseInsensitive) // swiftlint:disable:this force_try
184+ private let middlePartRegex = try ! NSRegularExpression ( pattern: " \\ A[^ \" ]* \\ z " , options: . caseInsensitive) // swiftlint:disable:this force_try
185+ private let endPartRegex = try ! NSRegularExpression ( pattern: " \\ A[^ \" ]* \" \\ z " , options: . caseInsensitive) // swiftlint:disable:this force_try
186+ private let substitute = " \u{001a} "
187+ private let delimiterQuoteDelimiter : String
188+ private let delimiterDelimiter : String
189+ private let quoteDelimiter : String
190+ private let delimiterQuote : String
193191
194192 /// Reads the line and returns the fields found. Handles double quotes according to RFC 4180.
195193 ///
@@ -308,3 +306,75 @@ extension String {
308306 return NSRange ( location: 0 , length: self . utf16. count)
309307 }
310308}
309+
310+
311+ // MARK: - Sub Types
312+
313+ protocol Source {
314+ func forEach( _ closure: ( String ) -> Void )
315+ }
316+
317+ class FileSource : Source {
318+ private let textFile : TextFile
319+ private let encoding : String . Encoding
320+ private var lineEnding : LineEnding
321+
322+ init ( textFile: TextFile , encoding: String . Encoding , lineEnding: LineEnding ) {
323+ self . textFile = textFile
324+ self . encoding = encoding
325+ self . lineEnding = lineEnding
326+ }
327+
328+ func forEach( _ closure: ( String ) -> Void ) {
329+ if lineEnding == . unknown {
330+ lineEnding = lineEndingForFile ( )
331+ }
332+ guard let csvStreamReader = textFile. streamReader ( lineEnding: lineEnding, chunkSize: chunkSize) else { return }
333+ csvStreamReader. forEach ( closure)
334+ }
335+
336+ /// Determines the line ending for the CSV file
337+ ///
338+ /// - Returns: the lineEnding for the CSV file or default of NL.
339+ private func lineEndingForFile( ) -> LineEnding {
340+ var lineEnding : LineEnding = . nl
341+ if let fileHandle = textFile. handleForReading {
342+ if let data = ( fileHandle. readData ( ofLength: chunkSize) as NSData ) . mutableCopy ( ) as? NSMutableData {
343+ if let contents = NSString ( bytesNoCopy: data. mutableBytes, length: data. length, encoding: encoding. rawValue, freeWhenDone: false ) {
344+ if contents. contains ( LineEnding . crlf. rawValue) {
345+ lineEnding = . crlf
346+ } else if contents. contains ( LineEnding . nl. rawValue) {
347+ lineEnding = . nl
348+ } else if contents. contains ( LineEnding . cr. rawValue) {
349+ lineEnding = . cr
350+ }
351+ }
352+ }
353+ }
354+ return lineEnding
355+ }
356+ }
357+ class StringSource : Source {
358+ private let lines : [ String ]
359+
360+ init ( contentString: String , lineEnding: LineEnding ) {
361+ let correctedLineEnding : LineEnding = {
362+ if lineEnding == . unknown {
363+ if contentString. contains ( LineEnding . crlf. rawValue) {
364+ return . crlf
365+ } else if contentString. contains ( LineEnding . nl. rawValue) {
366+ return . nl
367+ } else if contentString. contains ( LineEnding . cr. rawValue) {
368+ return . cr
369+ }
370+ }
371+ return lineEnding
372+ } ( )
373+
374+ lines = contentString. components ( separatedBy: correctedLineEnding. rawValue)
375+ }
376+
377+ func forEach( _ closure: ( String ) -> Void ) {
378+ lines. forEach ( closure)
379+ }
380+ }
0 commit comments