Merge pull request #24 from yaslab/writer

Add `CSVWriter` class
This commit is contained in:
Yasuhiro Hatta 2017-05-28 18:26:56 +09:00 committed by GitHub
commit 03c2d8bce3
14 changed files with 477 additions and 10 deletions

View File

@ -1 +1 @@
3.0
3.1

View File

@ -1,5 +1,5 @@
language: objective-c
osx_image: xcode8
osx_image: xcode8.3
env:
- LC_CTYPE=en_US.UTF-8
git:

View File

@ -15,6 +15,13 @@
0E47EEC21DBCDB1800EBF783 /* CSV+iterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E47EEC01DBCDB1800EBF783 /* CSV+iterator.swift */; };
0E47EEC31DBCDB1800EBF783 /* CSV+iterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E47EEC01DBCDB1800EBF783 /* CSV+iterator.swift */; };
0E47EEC41DBCDB1800EBF783 /* CSV+iterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E47EEC01DBCDB1800EBF783 /* CSV+iterator.swift */; };
0E54021B1ED9DDF40019C3ED /* CSVWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E54021A1ED9DDF40019C3ED /* CSVWriterTests.swift */; };
0E54021C1ED9DDF40019C3ED /* CSVWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E54021A1ED9DDF40019C3ED /* CSVWriterTests.swift */; };
0E54021D1ED9DDF40019C3ED /* CSVWriterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E54021A1ED9DDF40019C3ED /* CSVWriterTests.swift */; };
0E5402221EDA82220019C3ED /* CSVWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E54021E1EDA81E80019C3ED /* CSVWriter.swift */; };
0E5402231EDA82220019C3ED /* CSVWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E54021E1EDA81E80019C3ED /* CSVWriter.swift */; };
0E5402241EDA82220019C3ED /* CSVWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E54021E1EDA81E80019C3ED /* CSVWriter.swift */; };
0E5402251EDA82230019C3ED /* CSVWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E54021E1EDA81E80019C3ED /* CSVWriter.swift */; };
0E7E8C8C1D0BC7BB0057A1C1 /* CSV.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E7E8C811D0BC7BB0057A1C1 /* CSV.framework */; };
0E7E8CA11D0BC7F10057A1C1 /* CSV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E8C9D1D0BC7F10057A1C1 /* CSV.swift */; };
0E7E8CA21D0BC7F10057A1C1 /* CSVError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E7E8C9E1D0BC7F10057A1C1 /* CSVError.swift */; };
@ -90,6 +97,8 @@
/* Begin PBXFileReference section */
0E0F160D1D197DB800C92580 /* Endian.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Endian.swift; sourceTree = "<group>"; };
0E47EEC01DBCDB1800EBF783 /* CSV+iterator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CSV+iterator.swift"; sourceTree = "<group>"; };
0E54021A1ED9DDF40019C3ED /* CSVWriterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSVWriterTests.swift; sourceTree = "<group>"; };
0E54021E1EDA81E80019C3ED /* CSVWriter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSVWriter.swift; sourceTree = "<group>"; };
0E7E8C811D0BC7BB0057A1C1 /* CSV.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CSV.framework; sourceTree = BUILT_PRODUCTS_DIR; };
0E7E8C8B1D0BC7BB0057A1C1 /* CSVTests-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "CSVTests-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
0E7E8C9D1D0BC7F10057A1C1 /* CSV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CSV.swift; sourceTree = "<group>"; };
@ -203,6 +212,7 @@
0EDF8EAE1DDB6D620068056A /* CSVConfiguration.swift */,
0E7E8C9E1D0BC7F10057A1C1 /* CSVError.swift */,
0E7E8C9F1D0BC7F10057A1C1 /* CSVVersion.h */,
0E54021E1EDA81E80019C3ED /* CSVWriter.swift */,
0E0F160D1D197DB800C92580 /* Endian.swift */,
0E7E8CAC1D0BC8610057A1C1 /* Info.plist */,
0EA2AB801D183BA9003EC967 /* UnicodeIterator.swift */,
@ -223,6 +233,7 @@
isa = PBXGroup;
children = (
0EDF8ECD1DDB73370068056A /* CSVTests.swift */,
0E54021A1ED9DDF40019C3ED /* CSVWriterTests.swift */,
0EDF8ECE1DDB73370068056A /* LineBreakTests.swift */,
0EDF8ECF1DDB73370068056A /* ReadmeTests.swift */,
0EDF8ED01DDB73370068056A /* TrimFieldsTests.swift */,
@ -536,6 +547,7 @@
0E9317D51D0DB2F200AC20A0 /* CSV+init.swift in Sources */,
0EA2AB821D183BA9003EC967 /* UnicodeIterator.swift in Sources */,
0E47EEC21DBCDB1800EBF783 /* CSV+iterator.swift in Sources */,
0E5402241EDA82220019C3ED /* CSVWriter.swift in Sources */,
0E7E8CA11D0BC7F10057A1C1 /* CSV.swift in Sources */,
0E0F160F1D197DB800C92580 /* Endian.swift in Sources */,
0EDF8EB01DDB6DAC0068056A /* CSVConfiguration.swift in Sources */,
@ -553,6 +565,7 @@
0EDF8EDF1DDB73520068056A /* TrimFieldsTests.swift in Sources */,
0EDF8EE01DDB73520068056A /* UnicodeTests.swift in Sources */,
0EDF8EDD1DDB73520068056A /* LineBreakTests.swift in Sources */,
0E54021C1ED9DDF40019C3ED /* CSVWriterTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -563,6 +576,7 @@
0E9317D71D0DB2F200AC20A0 /* CSV+init.swift in Sources */,
0EA2AB841D183BA9003EC967 /* UnicodeIterator.swift in Sources */,
0E47EEC41DBCDB1800EBF783 /* CSV+iterator.swift in Sources */,
0E5402221EDA82220019C3ED /* CSVWriter.swift in Sources */,
0E7E8CBE1D0BC9D70057A1C1 /* CSV.swift in Sources */,
0E0F16111D197DB800C92580 /* Endian.swift in Sources */,
0EDF8EB21DDB6DAD0068056A /* CSVConfiguration.swift in Sources */,
@ -578,6 +592,7 @@
0E9317D41D0DB2F200AC20A0 /* CSV+init.swift in Sources */,
0EA2AB811D183BA9003EC967 /* UnicodeIterator.swift in Sources */,
0E47EEC11DBCDB1800EBF783 /* CSV+iterator.swift in Sources */,
0E5402251EDA82230019C3ED /* CSVWriter.swift in Sources */,
0E7E8CE01D0BCA8E0057A1C1 /* CSV.swift in Sources */,
0E0F160E1D197DB800C92580 /* Endian.swift in Sources */,
0EDF8EAF1DDB6D620068056A /* CSVConfiguration.swift in Sources */,
@ -595,6 +610,7 @@
0EDF8EDA1DDB73520068056A /* TrimFieldsTests.swift in Sources */,
0EDF8EDB1DDB73520068056A /* UnicodeTests.swift in Sources */,
0EDF8ED81DDB73520068056A /* LineBreakTests.swift in Sources */,
0E54021B1ED9DDF40019C3ED /* CSVWriterTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -605,6 +621,7 @@
0E9317D61D0DB2F200AC20A0 /* CSV+init.swift in Sources */,
0EA2AB831D183BA9003EC967 /* UnicodeIterator.swift in Sources */,
0E47EEC31DBCDB1800EBF783 /* CSV+iterator.swift in Sources */,
0E5402231EDA82220019C3ED /* CSVWriter.swift in Sources */,
0E7E8D001D0BCDCF0057A1C1 /* CSV.swift in Sources */,
0E0F16101D197DB800C92580 /* Endian.swift in Sources */,
0EDF8EB11DDB6DAC0068056A /* CSVConfiguration.swift in Sources */,
@ -622,6 +639,7 @@
0EDF8EE41DDB73530068056A /* TrimFieldsTests.swift in Sources */,
0EDF8EE51DDB73530068056A /* UnicodeTests.swift in Sources */,
0EDF8EE21DDB73530068056A /* LineBreakTests.swift in Sources */,
0E54021D1ED9DDF40019C3ED /* CSVWriterTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

6
Sources/BinaryReader.swift Executable file → Normal file
View File

@ -62,7 +62,7 @@ internal class BinaryReader {
stream.open()
}
if stream.streamStatus != .open {
throw CSVError.cannotOpenFile
throw CSVError.cannotOpenStream
}
let readCount = stream.read(tempBuffer, maxLength: tempBufferSize)
@ -93,7 +93,7 @@ internal class BinaryReader {
private func readStream(_ buffer: UnsafeMutablePointer<UInt8>, maxLength: Int) throws -> Int {
if stream.streamStatus != .open {
throw CSVError.cannotReadFile
throw CSVError.cannotReadStream
}
var i = 0
@ -115,7 +115,7 @@ internal class BinaryReader {
throw CSVError.streamErrorHasOccurred(error: stream.streamError!)
}
if length != bufferSize {
throw CSVError.cannotReadFile
throw CSVError.cannotReadStream
}
return buffer[0]
}

0
Sources/CSV+init.swift Executable file → Normal file
View File

9
Sources/CSV.swift Executable file → Normal file
View File

@ -8,9 +8,12 @@
import Foundation
private let LF: UnicodeScalar = "\n"
private let CR: UnicodeScalar = "\r"
private let DQUOTE: UnicodeScalar = "\""
internal let LF: UnicodeScalar = "\n"
internal let CR: UnicodeScalar = "\r"
internal let DQUOTE: UnicodeScalar = "\""
internal let DQUOTE_STR: String = "\""
internal let DQUOTE2_STR: String = "\"\""
/// No overview available.
public class CSV {

View File

@ -10,9 +10,11 @@
public enum CSVError: Error {
/// No overview available.
case cannotOpenFile
case cannotOpenStream
/// No overview available.
case cannotReadFile
case cannotReadStream
/// No overview available.
case cannotWriteStream
/// No overview available.
case streamErrorHasOccurred(error: Error)
/// No overview available.

0
Sources/CSVVersion.h Executable file → Normal file
View File

173
Sources/CSVWriter.swift Normal file
View File

@ -0,0 +1,173 @@
//
// CSVWriter.swift
// CSV
//
// Created by Yasuhiro Hatta on 2017/05/28.
// Copyright © 2017 yaslab. All rights reserved.
//
import Foundation
public class CSVWriter {
public struct Configuration {
public var delimiter: String
public var newline: String
public init(delimiter: String = String(defaultDelimiter), newline: String = String(LF)) {
self.delimiter = delimiter
self.newline = newline
}
}
public let stream: OutputStream
public let configuration: Configuration
fileprivate let writeScalar: ((UnicodeScalar) throws -> Void)
fileprivate var isFirstRecord: Bool = true
fileprivate var isFirstField: Bool = true
fileprivate init(
stream: OutputStream,
configuration: Configuration,
writeScalar: @escaping ((UnicodeScalar) throws -> Void)) throws {
self.stream = stream
self.configuration = configuration
self.writeScalar = writeScalar
if stream.streamStatus == .notOpen {
stream.open()
}
if stream.streamStatus != .open {
throw CSVError.cannotOpenStream
}
}
}
extension CSVWriter {
public convenience init(
stream: OutputStream,
configuration: Configuration = Configuration()) throws {
try self.init(stream: stream, codecType: UTF8.self, configuration: configuration)
}
public convenience init<T: UnicodeCodec>(
stream: OutputStream,
codecType: T.Type,
configuration: Configuration = Configuration()
) throws where T.CodeUnit == UInt8 {
try self.init(stream: stream, configuration: configuration) { (scalar: UnicodeScalar) throws in
var error: CSVError? = nil
codecType.encode(scalar) { (code: UInt8) in
var code = code
let count = stream.write(&code, maxLength: 1)
if count != 1 {
error = CSVError.cannotWriteStream
}
}
if let error = error {
throw error
}
}
}
public convenience init<T: UnicodeCodec>(
stream: OutputStream,
codecType: T.Type,
endian: Endian = .big,
configuration: Configuration = Configuration()
) throws where T.CodeUnit == UInt16 {
try self.init(stream: stream, configuration: configuration) { (scalar: UnicodeScalar) throws in
var error: CSVError? = nil
codecType.encode(scalar) { (code: UInt16) in
var code = (endian == .big) ? code.bigEndian : code.littleEndian
withUnsafeBytes(of: &code) { (buffer) -> Void in
let count = stream.write(buffer.baseAddress!.assumingMemoryBound(to: UInt8.self), maxLength: buffer.count)
if count != buffer.count {
error = CSVError.cannotWriteStream
}
}
}
if let error = error {
throw error
}
}
}
public convenience init<T: UnicodeCodec>(
stream: OutputStream,
codecType: T.Type,
endian: Endian = .big,
configuration: Configuration = Configuration()
) throws where T.CodeUnit == UInt32 {
try self.init(stream: stream, configuration: configuration) { (scalar: UnicodeScalar) throws in
var error: CSVError? = nil
codecType.encode(scalar) { (code: UInt32) in
var code = (endian == .big) ? code.bigEndian : code.littleEndian
withUnsafeBytes(of: &code) { (buffer) -> Void in
let count = stream.write(buffer.baseAddress!.assumingMemoryBound(to: UInt8.self), maxLength: buffer.count)
if count != buffer.count {
error = CSVError.cannotWriteStream
}
}
}
if let error = error {
throw error
}
}
}
}
extension CSVWriter {
public func beginNewRecord() {
isFirstField = true
}
public func write(field value: String, quoted: Bool = false) throws {
if isFirstRecord {
isFirstRecord = false
} else {
if isFirstField {
try configuration.newline.unicodeScalars.forEach(writeScalar)
}
}
if isFirstField {
isFirstField = false
} else {
try configuration.delimiter.unicodeScalars.forEach(writeScalar)
}
var value = value
if quoted {
value = value.replacingOccurrences(of: DQUOTE_STR, with: DQUOTE2_STR)
try writeScalar(DQUOTE)
}
try value.unicodeScalars.forEach(writeScalar)
if quoted {
try writeScalar(DQUOTE)
}
}
public func write(record values: [String], quotedAtIndex: ((Int) -> Bool) = { _ in false }) throws {
beginNewRecord()
for (i, value) in values.enumerated() {
try write(field: value, quoted: quotedAtIndex(i))
}
}
}

0
Sources/UnicodeIterator.swift Executable file → Normal file
View File

0
Tests/CSVTests/CSVTests.swift Executable file → Normal file
View File

View File

@ -0,0 +1,270 @@
//
// CSVWriterTests.swift
// CSV
//
// Created by Yasuhiro Hatta on 2017/05/28.
// Copyright © 2017 yaslab. All rights reserved.
//
import Foundation
import XCTest
import CSV
extension OutputStream {
var data: Data? {
guard let nsData = property(forKey: .dataWrittenToMemoryStreamKey) as? NSData else {
return nil
}
return Data(referencing: nsData)
}
}
class CSVWriterTests: XCTestCase {
static let allTests = [
("testSingleFieldSingleRecord", testSingleFieldSingleRecord),
("testSingleFieldMultipleRecord", testSingleFieldMultipleRecord),
("testMultipleFieldSingleRecord", testMultipleFieldSingleRecord),
("testMultipleFieldMultipleRecord", testMultipleFieldMultipleRecord),
("testQuoted", testQuoted),
("testQuotedNewline", testQuotedNewline),
("testEscapeQuote", testEscapeQuote),
("testDelimiter", testDelimiter),
("testNewline", testNewline),
("testUTF16BE", testUTF16BE),
("testUTF16LE", testUTF16LE),
("testUTF32BE", testUTF32BE),
("testUTF32LE", testUTF32LE)
]
let str = "TEST-test-1234-😄😆👨‍👩‍👧‍👦"
/// xxxx
func testSingleFieldSingleRecord() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream)
csv.beginNewRecord()
try! csv.write(field: str)
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf8)!
XCTAssertEqual(csvStr, str)
}
/// xxxx
/// xxxx
func testSingleFieldMultipleRecord() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream)
csv.beginNewRecord()
try! csv.write(field: str + "-1")
csv.beginNewRecord()
try! csv.write(field: str + "-2")
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf8)!
XCTAssertEqual(csvStr, "\(str)-1\n\(str)-2")
}
/// xxxx,xxxx
func testMultipleFieldSingleRecord() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream)
csv.beginNewRecord()
try! csv.write(field: str + "-1")
try! csv.write(field: str + "-2")
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf8)!
XCTAssertEqual(csvStr, "\(str)-1,\(str)-2")
}
/// xxxx,xxxx
/// xxxx,xxxx
func testMultipleFieldMultipleRecord() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream)
csv.beginNewRecord()
try! csv.write(field: str + "-1-1")
try! csv.write(field: str + "-1-2")
csv.beginNewRecord()
try! csv.write(field: str + "-2-1")
try! csv.write(field: str + "-2-2")
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf8)!
XCTAssertEqual(csvStr, "\(str)-1-1,\(str)-1-2\n\(str)-2-1,\(str)-2-2")
}
/// "xxxx",xxxx
func testQuoted() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream)
csv.beginNewRecord()
try! csv.write(field: str + "-1", quoted: true)
try! csv.write(field: str + "-2") // quoted: false
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf8)!
XCTAssertEqual(csvStr, "\"\(str)-1\",\(str)-2")
}
/// xxxx,"xx\nxx"
func testQuotedNewline() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream)
csv.beginNewRecord()
try! csv.write(field: str + "-1") // quoted: false
try! csv.write(field: str + "-\n-2", quoted: true)
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf8)!
XCTAssertEqual(csvStr, "\(str)-1,\"\(str)-\n-2\"")
}
/// xxxx,"xx""xx"
func testEscapeQuote() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream)
csv.beginNewRecord()
try! csv.write(field: str + "-1") // quoted: false
try! csv.write(field: str + "-\"-2", quoted: true)
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf8)!
XCTAssertEqual(csvStr, "\(str)-1,\"\(str)-\"\"-2\"")
}
/// Test delimiter: "\t"
func testDelimiter() {
let stream = OutputStream(toMemory: ())
stream.open()
let config = CSVWriter.Configuration(delimiter: "\t")
let csv = try! CSVWriter.init(stream: stream, configuration: config)
csv.beginNewRecord()
try! csv.write(field: str + "-1")
try! csv.write(field: str + "-2")
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf8)!
XCTAssertEqual(csvStr, "\(str)-1\t\(str)-2")
}
/// Test newline: "\r\n"
func testNewline() {
let stream = OutputStream(toMemory: ())
stream.open()
let config = CSVWriter.Configuration(newline: "\r\n")
let csv = try! CSVWriter.init(stream: stream, configuration: config)
csv.beginNewRecord()
try! csv.write(field: str + "-1")
csv.beginNewRecord()
try! csv.write(field: str + "-2")
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf8)!
XCTAssertEqual(csvStr, "\(str)-1\r\n\(str)-2")
}
/// UTF16 Big Endian
func testUTF16BE() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream, codecType: UTF16.self, endian: .big)
csv.beginNewRecord()
try! csv.write(field: str)
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf16BigEndian)!
XCTAssertEqual(csvStr, str)
}
/// UTF16 Little Endian
func testUTF16LE() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream, codecType: UTF16.self, endian: .little)
csv.beginNewRecord()
try! csv.write(field: str)
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf16LittleEndian)!
XCTAssertEqual(csvStr, str)
}
/// UTF32 Big Endian
func testUTF32BE() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream, codecType: UTF32.self, endian: .big)
csv.beginNewRecord()
try! csv.write(field: str)
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf32BigEndian)!
XCTAssertEqual(csvStr, str)
}
/// UTF32 Little Endian
func testUTF32LE() {
let stream = OutputStream(toMemory: ())
stream.open()
let csv = try! CSVWriter(stream: stream, codecType: UTF32.self, endian: .little)
csv.beginNewRecord()
try! csv.write(field: str)
stream.close()
let data = stream.data!
let csvStr = String(data: data, encoding: .utf32LittleEndian)!
XCTAssertEqual(csvStr, str)
}
}

0
Tests/CSVTests/ReadmeTests.swift Executable file → Normal file
View File

View File

@ -11,6 +11,7 @@ import XCTest
XCTMain([
testCase(CSVTests.allTests),
testCase(CSVWriterTests.allTests),
testCase(LineBreakTests.allTests),
testCase(ReadmeTests.allTests),
testCase(TrimFieldsTests.allTests),