Skip to content

Commit

Permalink
Merge branch 'deploy/1.5.0' into productive
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeehut committed Feb 6, 2017
2 parents fb6ee1c + 14787d0 commit 47a8055
Show file tree
Hide file tree
Showing 12 changed files with 177 additions and 72 deletions.
2 changes: 1 addition & 1 deletion .swift-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.0.1
3.0
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ excluded:
- Carthage
- Sources/Constants

line_length: 200
line_length: 180
2 changes: 1 addition & 1 deletion CSVImporter.podspec
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Pod::Spec.new do |s|

s.name = "CSVImporter"
s.version = "1.4.0"
s.version = "1.5.0"
s.summary = "Import CSV files line by line with ease."

s.description = <<-DESC
Expand Down
14 changes: 8 additions & 6 deletions CSVImporter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@
A1EC02D91E0431C00021718E /* Cartfile.resolved */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile.resolved; sourceTree = "<group>"; };
A1EC02DA1E0431F20021718E /* TextFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TextFile.swift; path = Sources/Code/TextFile.swift; sourceTree = SOURCE_ROOT; };
A1F5AEE51E05599F003D6949 /* UTF16_Example.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = UTF16_Example.csv; sourceTree = "<group>"; };
A1F5AEE91E056FBA003D6949 /* .swift-version */ = {isa = PBXFileReference; lastKnownFileType = text; path = ".swift-version"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -486,6 +487,7 @@
A1EC02D91E0431C00021718E /* Cartfile.resolved */,
A110355E1D666CFD00214547 /* CSVImporter.podspec */,
828348671CA6E1B000DC4C26 /* .swiftlint.yml */,
A1F5AEE91E056FBA003D6949 /* .swift-version */,
);
name = "Root Files";
sourceTree = "<group>";
Expand Down Expand Up @@ -660,30 +662,30 @@
TargetAttributes = {
82239F461C4AF70500627674 = {
CreatedOnToolsVersion = 7.2;
LastSwiftMigration = 0800;
LastSwiftMigration = 0820;
};
82239F501C4AF70500627674 = {
CreatedOnToolsVersion = 7.2;
DevelopmentTeam = 767S6EFMJ8;
LastSwiftMigration = 0800;
LastSwiftMigration = 0820;
};
82239F761C4AFAFF00627674 = {
CreatedOnToolsVersion = 7.2;
LastSwiftMigration = 0800;
LastSwiftMigration = 0820;
};
82239F7F1C4AFAFF00627674 = {
CreatedOnToolsVersion = 7.2;
DevelopmentTeam = 767S6EFMJ8;
LastSwiftMigration = 0800;
LastSwiftMigration = 0820;
};
82239F921C4AFB1000627674 = {
CreatedOnToolsVersion = 7.2;
LastSwiftMigration = 0800;
LastSwiftMigration = 0820;
};
82239F9B1C4AFB1000627674 = {
CreatedOnToolsVersion = 7.2;
DevelopmentTeam = 767S6EFMJ8;
LastSwiftMigration = 0800;
LastSwiftMigration = 0820;
ProvisioningStyle = Automatic;
};
};
Expand Down
2 changes: 1 addition & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Handy Swift features that didn't make it into the Swift standard library.
github "Flinesoft/HandySwift" ~> 1.3
github "Flinesoft/HandySwift" ~> 2.0
4 changes: 2 additions & 2 deletions Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
github "Flinesoft/HandySwift" "1.3.2"
github "Quick/Nimble" "v5.1.1"
github "Flinesoft/HandySwift" "2.0.0"
github "Quick/Nimble" "v6.0.1"
github "Quick/Quick" "v1.0.0"
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
alt="codebeat badge">
</a>
<a href="https://github.com/Flinesoft/CSVImporter/releases">
<img src="https://img.shields.io/badge/Version-1.4.0-blue.svg"
alt="Version: 1.4.0">
<img src="https://img.shields.io/badge/Version-1.5.0-blue.svg"
alt="Version: 1.5.0">
</a>
<img src="https://img.shields.io/badge/Swift-3-FFAC45.svg"
alt="Swift: 3">
Expand Down Expand Up @@ -47,7 +47,7 @@ Import CSV files line by line with ease.
## Installation

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

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

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

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.
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.

### CocoaPods

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

## Usage

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


Expand Down
166 changes: 118 additions & 48 deletions Sources/Code/CSVImporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,8 @@ private let chunkSize = 4096
public class CSVImporter<T> {
// MARK: - Stored Instance Properties

let csvFile: TextFile
let source: Source
let delimiter: String
var lineEnding: LineEnding
let encoding: String.Encoding

var lastProgressReport: Date?

Expand All @@ -44,34 +42,57 @@ public class CSVImporter<T> {

// MARK: - Initializers

/// Creates a `CSVImporter` object with required configuration options.
///
/// - Parameters:
/// - path: The path to the CSV file to import.
/// - delimiter: The delimiter used within the CSV file for separating fields. Defaults to ",".
/// - lineEnding: The lineEnding of the file. If not specified will be determined automatically.
public init(path: String, delimiter: String = ",", lineEnding: LineEnding = .unknown, encoding: String.Encoding = .utf8) {
self.csvFile = TextFile(path: path, encoding: encoding)
/// Internal initializer to prevent duplicate code.
private init(source: Source, delimiter: String) {
self.source = source
self.delimiter = delimiter
self.lineEnding = lineEnding
self.encoding = encoding

delimiterQuoteDelimiter = "\(delimiter)\"\"\(delimiter)"
delimiterDelimiter = delimiter+delimiter
quoteDelimiter = "\"\"\(delimiter)"
delimiterQuote = "\(delimiter)\"\""
}


/// Creates a `CSVImporter` object with required configuration options.
///
/// - Parameters:
/// - path: The path to the CSV file to import.
/// - delimiter: The delimiter used within the CSV file for separating fields. Defaults to ",".
/// - lineEnding: The lineEnding used in the file. If not specified will be determined automatically.
/// - encoding: The encoding the file is read with. Defaults to `.utf8`.
public convenience init(path: String, delimiter: String = ",", lineEnding: LineEnding = .unknown, encoding: String.Encoding = .utf8) {
let textFile = TextFile(path: path, encoding: encoding)
let fileSource = FileSource(textFile: textFile, encoding: encoding, lineEnding: lineEnding)
self.init(source: fileSource, delimiter: delimiter)
}

/// Creates a `CSVImporter` object with required configuration options.
///
/// - Parameters:
/// - url: File URL for the CSV file to import.
/// - delimiter: The delimiter used within the CSV file for separating fields. Defaults to ",".
/// - lineEnding: The lineEnding used in the file. If not specified will be determined automatically.
/// - encoding: The encoding the file is read with. Defaults to `.utf8`.
public convenience init?(url: URL, delimiter: String = ",", lineEnding: LineEnding = .unknown, encoding: String.Encoding = .utf8) {
guard url.isFileURL else { return nil }
self.init(path: url.path, delimiter: delimiter, lineEnding: lineEnding, encoding: encoding)
}

/// Creates a `CSVImporter` object with required configuration options.
///
/// NOTE: This initializer doesn't save any memory as the given String is already loaded into memory.
/// Don't use this if you are working with a large file which you could refer to with a path also.
///
/// - Parameters:
/// - contentString: The string which contains the content of a CSV file.
/// - delimiter: The delimiter used within the CSV file for separating fields. Defaults to ",".
/// - lineEnding: The lineEnding used in the file. If not specified will be determined automatically.
public convenience init(contentString: String, delimiter: String = ",", lineEnding: LineEnding = .unknown) {
let stringSource = StringSource(contentString: contentString, lineEnding: lineEnding)
self.init(source: stringSource, delimiter: delimiter)
}

// MARK: - Instance Methods

/// Starts importing the records within the CSV file line by line.
Expand Down Expand Up @@ -145,51 +166,28 @@ public class CSVImporter<T> {
/// - valuesInLine: The values found within a line.
/// - Returns: `true` on finish or `false` if can't read file.
func importLines(_ closure: (_ valuesInLine: [String]) -> Void) -> Bool {
if lineEnding == .unknown {
lineEnding = lineEndingForFile()
}
guard let csvStreamReader = self.csvFile.streamReader(lineEnding: lineEnding, chunkSize: chunkSize) else { return false }
var anyLine = false

for line in csvStreamReader {
source.forEach { line in
anyLine = true
autoreleasepool {
let valuesInLine = readValuesInLine(line)
closure(valuesInLine)
}
}

return true
}

/// Determines the line ending for the CSV file
///
/// - Returns: the lineEnding for the CSV file or default of NL.
fileprivate func lineEndingForFile() -> LineEnding {
var lineEnding: LineEnding = .nl
if let fileHandle = self.csvFile.handleForReading {
if let data = (fileHandle.readData(ofLength: chunkSize) as NSData).mutableCopy() as? NSMutableData {
if let contents = NSString(bytesNoCopy: data.mutableBytes, length: data.length, encoding: encoding.rawValue, freeWhenDone: false) {
if contents.contains(LineEnding.crlf.rawValue) {
lineEnding = .crlf
} else if contents.contains(LineEnding.nl.rawValue) {
lineEnding = .nl
} else if contents.contains(LineEnding.cr.rawValue) {
lineEnding = .cr
}
}
}
}
return lineEnding
return anyLine
}

// Various private constants used for reading lines
fileprivate let startPartRegex = try! NSRegularExpression(pattern: "\\A\"[^\"]*\\z", options: .caseInsensitive) // swiftlint:disable:this force_try
fileprivate let middlePartRegex = try! NSRegularExpression(pattern: "\\A[^\"]*\\z", options: .caseInsensitive) // swiftlint:disable:this force_try
fileprivate let endPartRegex = try! NSRegularExpression(pattern: "\\A[^\"]*\"\\z", options: .caseInsensitive) // swiftlint:disable:this force_try
fileprivate let substitute = "\u{001a}"
fileprivate let delimiterQuoteDelimiter: String
fileprivate let delimiterDelimiter: String
fileprivate let quoteDelimiter: String
fileprivate let delimiterQuote: String
private let startPartRegex = try! NSRegularExpression(pattern: "\\A\"[^\"]*\\z", options: .caseInsensitive) // swiftlint:disable:this force_try
private let middlePartRegex = try! NSRegularExpression(pattern: "\\A[^\"]*\\z", options: .caseInsensitive) // swiftlint:disable:this force_try
private let endPartRegex = try! NSRegularExpression(pattern: "\\A[^\"]*\"\\z", options: .caseInsensitive) // swiftlint:disable:this force_try
private let substitute = "\u{001a}"
private let delimiterQuoteDelimiter: String
private let delimiterDelimiter: String
private let quoteDelimiter: String
private let delimiterQuote: String

/// Reads the line and returns the fields found. Handles double quotes according to RFC 4180.
///
Expand Down Expand Up @@ -308,3 +306,75 @@ extension String {
return NSRange(location: 0, length: self.utf16.count)
}
}


// MARK: - Sub Types

protocol Source {
func forEach(_ closure: (String) -> Void)
}

class FileSource: Source {
private let textFile: TextFile
private let encoding: String.Encoding
private var lineEnding: LineEnding

init(textFile: TextFile, encoding: String.Encoding, lineEnding: LineEnding) {
self.textFile = textFile
self.encoding = encoding
self.lineEnding = lineEnding
}

func forEach(_ closure: (String) -> Void) {
if lineEnding == .unknown {
lineEnding = lineEndingForFile()
}
guard let csvStreamReader = textFile.streamReader(lineEnding: lineEnding, chunkSize: chunkSize) else { return }
csvStreamReader.forEach(closure)
}

/// Determines the line ending for the CSV file
///
/// - Returns: the lineEnding for the CSV file or default of NL.
private func lineEndingForFile() -> LineEnding {
var lineEnding: LineEnding = .nl
if let fileHandle = textFile.handleForReading {
if let data = (fileHandle.readData(ofLength: chunkSize) as NSData).mutableCopy() as? NSMutableData {
if let contents = NSString(bytesNoCopy: data.mutableBytes, length: data.length, encoding: encoding.rawValue, freeWhenDone: false) {
if contents.contains(LineEnding.crlf.rawValue) {
lineEnding = .crlf
} else if contents.contains(LineEnding.nl.rawValue) {
lineEnding = .nl
} else if contents.contains(LineEnding.cr.rawValue) {
lineEnding = .cr
}
}
}
}
return lineEnding
}
}
class StringSource: Source {
private let lines: [String]

init(contentString: String, lineEnding: LineEnding) {
let correctedLineEnding: LineEnding = {
if lineEnding == .unknown {
if contentString.contains(LineEnding.crlf.rawValue) {
return .crlf
} else if contentString.contains(LineEnding.nl.rawValue) {
return .nl
} else if contentString.contains(LineEnding.cr.rawValue) {
return .cr
}
}
return lineEnding
}()

lines = contentString.components(separatedBy: correctedLineEnding.rawValue)
}

func forEach(_ closure: (String) -> Void) {
lines.forEach(closure)
}
}
2 changes: 1 addition & 1 deletion Sources/Supporting Files/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.4.0</string>
<string>1.5.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
29 changes: 28 additions & 1 deletion Tests/Code/CSVImporterSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
// Copyright © 2016 Flinesoft. All rights reserved.
//

// swiftlint:disable file_length

import XCTest

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

expect(didFail).toEventually(beTrue())
expect(didFail).toEventually(beTrue(), timeout: 5)
}

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

it("imports data from CSV file content string with headers") {
let path = Bundle(for: CSVImporterSpec.self).path(forResource: "Teams", ofType: "csv")
let contentString = try! String(contentsOfFile: path!) // swiftlint:disable:this force_try

var recordValues: [[String: String]]?

let importer = CSVImporter<[String: String]>(contentString: contentString)

importer.startImportingRecords(structure: { (headerValues) -> Void in
print(headerValues)
}, recordMapper: { (recordValues) -> [String : String] in
return recordValues
}).onFail {
print("Did fail")
}.onProgress { importedDataLinesCount in
print("Progress: \(importedDataLinesCount)")
}.onFinish { importedRecords in
print("Did finish import, first array: \(importedRecords.first)")
recordValues = importedRecords
}

expect(recordValues).toEventuallyNot(beNil(), timeout: 10)
expect(recordValues!.first!).toEventually(equal(self.validTeamsFirstRecord()))
}

it("imports data from CSV file with headers Specifying lineEnding") {
let path = self.pathForResourceFile("Teams.csv")
var recordValues: [[String: String]]?
Expand Down
Loading

0 comments on commit 47a8055

Please sign in to comment.