Skip to content

Commit 47a8055

Browse files
committed
Merge branch 'deploy/1.5.0' into productive
2 parents fb6ee1c + 14787d0 commit 47a8055

File tree

12 files changed

+177
-72
lines changed

12 files changed

+177
-72
lines changed

.swift-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.0.1
1+
3.0

.swiftlint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ excluded:
1414
- Carthage
1515
- Sources/Constants
1616

17-
line_length: 200
17+
line_length: 180

CSVImporter.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Pod::Spec.new do |s|
22

33
s.name = "CSVImporter"
4-
s.version = "1.4.0"
4+
s.version = "1.5.0"
55
s.summary = "Import CSV files line by line with ease."
66

77
s.description = <<-DESC

CSVImporter.xcodeproj/project.pbxproj

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@
233233
A1EC02D91E0431C00021718E /* Cartfile.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile.resolved; sourceTree = "<group>"; };
234234
A1EC02DA1E0431F20021718E /* TextFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TextFile.swift; path = Sources/Code/TextFile.swift; sourceTree = SOURCE_ROOT; };
235235
A1F5AEE51E05599F003D6949 /* UTF16_Example.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = UTF16_Example.csv; sourceTree = "<group>"; };
236+
A1F5AEE91E056FBA003D6949 /* .swift-version */ = {isa = PBXFileReference; lastKnownFileType = text; path = ".swift-version"; sourceTree = "<group>"; };
236237
/* End PBXFileReference section */
237238

238239
/* Begin PBXFrameworksBuildPhase section */
@@ -486,6 +487,7 @@
486487
A1EC02D91E0431C00021718E /* Cartfile.resolved */,
487488
A110355E1D666CFD00214547 /* CSVImporter.podspec */,
488489
828348671CA6E1B000DC4C26 /* .swiftlint.yml */,
490+
A1F5AEE91E056FBA003D6949 /* .swift-version */,
489491
);
490492
name = "Root Files";
491493
sourceTree = "<group>";
@@ -660,30 +662,30 @@
660662
TargetAttributes = {
661663
82239F461C4AF70500627674 = {
662664
CreatedOnToolsVersion = 7.2;
663-
LastSwiftMigration = 0800;
665+
LastSwiftMigration = 0820;
664666
};
665667
82239F501C4AF70500627674 = {
666668
CreatedOnToolsVersion = 7.2;
667669
DevelopmentTeam = 767S6EFMJ8;
668-
LastSwiftMigration = 0800;
670+
LastSwiftMigration = 0820;
669671
};
670672
82239F761C4AFAFF00627674 = {
671673
CreatedOnToolsVersion = 7.2;
672-
LastSwiftMigration = 0800;
674+
LastSwiftMigration = 0820;
673675
};
674676
82239F7F1C4AFAFF00627674 = {
675677
CreatedOnToolsVersion = 7.2;
676678
DevelopmentTeam = 767S6EFMJ8;
677-
LastSwiftMigration = 0800;
679+
LastSwiftMigration = 0820;
678680
};
679681
82239F921C4AFB1000627674 = {
680682
CreatedOnToolsVersion = 7.2;
681-
LastSwiftMigration = 0800;
683+
LastSwiftMigration = 0820;
682684
};
683685
82239F9B1C4AFB1000627674 = {
684686
CreatedOnToolsVersion = 7.2;
685687
DevelopmentTeam = 767S6EFMJ8;
686-
LastSwiftMigration = 0800;
688+
LastSwiftMigration = 0820;
687689
ProvisioningStyle = Automatic;
688690
};
689691
};

Cartfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
# Handy Swift features that didn't make it into the Swift standard library.
2-
github "Flinesoft/HandySwift" ~> 1.3
2+
github "Flinesoft/HandySwift" ~> 2.0

Cartfile.resolved

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
github "Flinesoft/HandySwift" "1.3.2"
2-
github "Quick/Nimble" "v5.1.1"
1+
github "Flinesoft/HandySwift" "2.0.0"
2+
github "Quick/Nimble" "v6.0.1"
33
github "Quick/Quick" "v1.0.0"

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
alt="codebeat badge">
1313
</a>
1414
<a href="https://github.com/Flinesoft/CSVImporter/releases">
15-
<img src="https://img.shields.io/badge/Version-1.4.0-blue.svg"
16-
alt="Version: 1.4.0">
15+
<img src="https://img.shields.io/badge/Version-1.5.0-blue.svg"
16+
alt="Version: 1.5.0">
1717
</a>
1818
<img src="https://img.shields.io/badge/Swift-3-FFAC45.svg"
1919
alt="Swift: 3">
@@ -47,7 +47,7 @@ Import CSV files line by line with ease.
4747
## Installation
4848

4949
Currently the recommended way of installing this library is via [Carthage](https://github.com/Carthage/Carthage).
50-
[Cocoapods](https://github.com/CocoaPods/CocoaPods) is supported too, if you really don't like Carthage. ;)
50+
[Cocoapods](https://github.com/CocoaPods/CocoaPods) is supported too.
5151

5252
You can of course also just include this framework manually into your project by downloading it or by using git submodules.
5353

@@ -59,7 +59,7 @@ Simply add this line to your Cartfile:
5959
github "Flinesoft/CSVImporter" ~> 1.4
6060
```
6161

62-
And run `carthage update`. Then drag & drop the HandySwift.framework in the Carthage/build folder to your project. Also do the same with the dependent frameworks `Filekit` and `HandySwift`. Now you can `import CSVImporter` in each class you want to use its features. Refer to the [Carthage README](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) for detailed / updated instructions.
62+
And run `carthage update`. Then drag & drop the HandySwift.framework in the Carthage/build folder to your project. Also do the same with the dependent framework `HandySwift`. Now you can `import CSVImporter` in each class you want to use its features. Refer to the [Carthage README](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application) for detailed / updated instructions.
6363

6464
### CocoaPods
6565

@@ -81,7 +81,7 @@ Refer to [CocoaPods.org](https://cocoapods.org) for detailed / updates instructi
8181

8282
## Usage
8383

84-
Please have a look at the UsageExamples.playground for a complete list of features provided.
84+
Please have a look at the UsageExamples.playground and the Tests/Code/CSVImporterSpec.swift files for a complete list of features provided.
8585
Open the Playground from within the `.xcworkspace` in order for it to work.
8686

8787

Sources/Code/CSVImporter.swift

Lines changed: 118 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,8 @@ private let chunkSize = 4096
2323
public 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+
}

Sources/Supporting Files/Info.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
<key>CFBundlePackageType</key>
1616
<string>FMWK</string>
1717
<key>CFBundleShortVersionString</key>
18-
<string>1.4.0</string>
18+
<string>1.5.0</string>
1919
<key>CFBundleSignature</key>
2020
<string>????</string>
2121
<key>CFBundleVersion</key>

Tests/Code/CSVImporterSpec.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
// Copyright © 2016 Flinesoft. All rights reserved.
77
//
88

9+
// swiftlint:disable file_length
10+
911
import XCTest
1012

1113
import Quick
@@ -31,7 +33,7 @@ class CSVImporterSpec: QuickSpec { // swiftlint:disable:this type_body_length
3133
print("Did finish import, first array: \(importedRecords.first)")
3234
}
3335

34-
expect(didFail).toEventually(beTrue())
36+
expect(didFail).toEventually(beTrue(), timeout: 5)
3537
}
3638

3739
it("imports data from CSV file without headers") {
@@ -112,6 +114,31 @@ class CSVImporterSpec: QuickSpec { // swiftlint:disable:this type_body_length
112114
expect(recordValues!.first!).toEventually(equal(self.validTeamsFirstRecord()))
113115
}
114116

117+
it("imports data from CSV file content string with headers") {
118+
let path = Bundle(for: CSVImporterSpec.self).path(forResource: "Teams", ofType: "csv")
119+
let contentString = try! String(contentsOfFile: path!) // swiftlint:disable:this force_try
120+
121+
var recordValues: [[String: String]]?
122+
123+
let importer = CSVImporter<[String: String]>(contentString: contentString)
124+
125+
importer.startImportingRecords(structure: { (headerValues) -> Void in
126+
print(headerValues)
127+
}, recordMapper: { (recordValues) -> [String : String] in
128+
return recordValues
129+
}).onFail {
130+
print("Did fail")
131+
}.onProgress { importedDataLinesCount in
132+
print("Progress: \(importedDataLinesCount)")
133+
}.onFinish { importedRecords in
134+
print("Did finish import, first array: \(importedRecords.first)")
135+
recordValues = importedRecords
136+
}
137+
138+
expect(recordValues).toEventuallyNot(beNil(), timeout: 10)
139+
expect(recordValues!.first!).toEventually(equal(self.validTeamsFirstRecord()))
140+
}
141+
115142
it("imports data from CSV file with headers Specifying lineEnding") {
116143
let path = self.pathForResourceFile("Teams.csv")
117144
var recordValues: [[String: String]]?

0 commit comments

Comments
 (0)