Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CSV export feature #91

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions BuildTimeAnalyzer.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
2AE775121D225D5D00D1A744 /* DerivedDataManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AE775111D225D5D00D1A744 /* DerivedDataManager.swift */; };
2AF821441D21D6B900D65186 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AF821431D21D6B900D65186 /* AppDelegate.swift */; };
2AF821461D21D6B900D65186 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2AF821451D21D6B900D65186 /* Assets.xcassets */; };
5603EB6221EF93E90013D77B /* CSVExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5603EB6121EF93E90013D77B /* CSVExporter.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -67,6 +68,7 @@
2AF8214A1D21D6B900D65186 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
2AF8214F1D21D6B900D65186 /* BuildTimeAnalyzerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BuildTimeAnalyzerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
2AF821551D21D6B900D65186 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
5603EB6121EF93E90013D77B /* CSVExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CSVExporter.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -160,6 +162,7 @@
2A3164C11D21D73F00064045 /* LogProcessor.swift */,
2A9807DE1D7C76FD00B9232C /* ProjectSelection.swift */,
2ACBFD0B1D8835E60009567E /* UserSettings.swift */,
5603EB6121EF93E90013D77B /* CSVExporter.swift */,
);
name = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -314,6 +317,7 @@
2A3164C91D21D73F00064045 /* LogProcessor.swift in Sources */,
2839B8691FD2896F004C075C /* ViewControllerDataSource.swift in Sources */,
2A5404011D86D01700DBD44C /* BuildManager.swift in Sources */,
5603EB6221EF93E90013D77B /* CSVExporter.swift in Sources */,
2A5404051D86F3C700DBD44C /* File.swift in Sources */,
2ABFB6CE1D81F2DE00D060BF /* NSAlert+Extensions.swift in Sources */,
2A5404031D86DE0C00DBD44C /* XcodeDatabase.swift in Sources */,
Expand Down
2 changes: 2 additions & 0 deletions BuildTimeAnalyzer/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet weak var projectSelectionMenuItem: NSMenuItem!
@IBOutlet weak var buildTimesMenuItem: NSMenuItem!
@IBOutlet weak var alwaysInFrontMenuItem: NSMenuItem!

@objc var canExport: Bool = false

var viewController: ViewController? {
return NSApplication.shared.mainWindow?.contentViewController as? ViewController
Expand Down
77 changes: 77 additions & 0 deletions BuildTimeAnalyzer/CSVExporter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//
// CSVExporter.swift
// BuildTimeAnalyzer
//
// Created by Bruno Resende on 16.01.19.
// Copyright © 2019 Cane Media Ltd. All rights reserved.
//

import Foundation

struct CSVExporter {

static var filenameDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd-HHmmss"
return formatter
}()

func filename(with prefix: String) -> String {
return "\(prefix)_\(CSVExporter.filenameDateFormatter.string(from: Date())).csv"
}

func export<T>(elements: [T], to url: URL) throws where T: CSVExportable {

guard let data = elements.joinedAsCSVString(delimiter: .doubleQuote).data(using: .utf8) else {
throw ExportErrors.couldNotParseStringAsUTF8
}

do {
try data.write(to: url, options: .atomic)
} catch {
throw ExportErrors.fileIO(error)
}
}

enum ExportErrors: Error {
case couldNotParseStringAsUTF8
case fileIO(Error)
}
}

enum CSVDelimiter: String {
case singleQuote = "'"
case doubleQuote = "\""
case none = ""
}

protocol CSVExportable {

static var csvHeaderLine: String { get }

var csvLine: String { get }
}

extension Array where Element: CSVExportable {

func joinedAsCSVString(delimiter: CSVDelimiter) -> String {

return ([Element.csvHeaderLine] + self.map({ $0.csvLine })).joined(separator: "\n")
}
}

extension Array where Element == String {

func joinedAsCSVLine(delimiter: CSVDelimiter) -> String {

let formatter: (String) -> String

switch delimiter {
case .singleQuote: formatter = { $0.replacingOccurrences(of: "'", with: "\\'") }
case .doubleQuote: formatter = { $0.replacingOccurrences(of: "\"", with: "\\\"") }
case .none: formatter = { $0 }
}

return self.map({ "\(delimiter.rawValue)\(formatter($0))\(delimiter.rawValue)" }).joined(separator: ",")
}
}
10 changes: 10 additions & 0 deletions BuildTimeAnalyzer/CompileMeasure.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,13 @@ import Foundation
}
}
}

extension CompileMeasure: CSVExportable {

static var csvHeaderLine: String = ["time", "file", "references", "code"].joinedAsCSVLine(delimiter: .doubleQuote)

var csvLine: String
{
return [timeString, fileInfo, "\(references)", code].joinedAsCSVLine(delimiter: .doubleQuote)
}
}
15 changes: 11 additions & 4 deletions BuildTimeAnalyzer/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14313.18"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14460.31"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
Expand Down Expand Up @@ -64,6 +64,13 @@
<action selector="performClose:" target="Ady-hI-5gd" id="HmO-Ls-i7Q"/>
</connections>
</menuItem>
<menuItem isSeparatorItem="YES" id="Kk2-Jz-LKd"/>
<menuItem title="Export data as CSV…" keyEquivalent="e" id="3xt-bK-nQa">
<connections>
<action selector="exportAsCSVClicked:" target="Ady-hI-5gd" id="kbQ-xo-OtY"/>
<binding destination="Voe-Tx-rLC" name="enabled" keyPath="self.canExport" id="nAf-82-Fb2"/>
</connections>
</menuItem>
</items>
</menu>
</menuItem>
Expand Down Expand Up @@ -200,7 +207,7 @@
</customObject>
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="75" y="0.0"/>
<point key="canvasLocation" x="-33" y="-217"/>
</scene>
<!--Window Controller-->
<scene sceneID="R2V-B0-nI4">
Expand Down Expand Up @@ -765,7 +772,7 @@ Gw
</customObject>
<customObject id="f4z-Qu-43g" customClass="BuildManager" customModule="BuildTimeAnalyzer" customModuleProvider="target"/>
</objects>
<point key="canvasLocation" x="257.5" y="785.5"/>
<point key="canvasLocation" x="258" y="933"/>
</scene>
</scenes>
<resources>
Expand Down
39 changes: 39 additions & 0 deletions BuildTimeAnalyzer/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ class ViewController: NSViewController {

private var currentKey: String?
private var nextDatabase: XcodeDatabase?

private(set) var lastProcessedDatabaseSchemeName: String? = nil
{
didSet
{
(NSApp.delegate as? AppDelegate)?.canExport = lastProcessedDatabaseSchemeName != nil
}
}

private var processor = LogProcessor()

Expand Down Expand Up @@ -161,6 +169,36 @@ class ViewController: NSViewController {
showInstructions(true)
projectSelection.listFolders()
}

@IBAction func exportAsCSVClicked(_ sender: Any?) {
guard let keyWindow = NSApp.keyWindow, let scheme = lastProcessedDatabaseSchemeName else {
return
}

let exporter = CSVExporter()

let savePanel = NSSavePanel()
savePanel.title = "Exporting data as CSV…"
savePanel.message = "Pick location for CSV file to be exported:"
savePanel.prompt = "Export"
savePanel.allowedFileTypes = ["csv"]
savePanel.nameFieldStringValue = exporter.filename(with: scheme)

savePanel.beginSheetModal(for: keyWindow) { [dataSource] (response) in
guard response == NSApplication.ModalResponse.OK, let fileUrl = savePanel.url else {
return
}

do
{
try dataSource.exportProcessedData(using: exporter, to: fileUrl)
}
catch
{
NSAlert(error: error).runModal()
}
}
}

override func controlTextDidChange(_ obj: Notification) {
if let field = obj.object as? NSSearchField, field == searchField {
Expand Down Expand Up @@ -203,6 +241,7 @@ class ViewController: NSViewController {

processingState = .processing
currentKey = database.key
lastProcessedDatabaseSchemeName = database.schemeName

updateTotalLabel(with: database.buildTime)

Expand Down
4 changes: 4 additions & 0 deletions BuildTimeAnalyzer/ViewControllerDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ class ViewControllerDataSource {
return processedData[index]
}

func exportProcessedData(using exporter: CSVExporter, to url: URL) throws {
try exporter.export(elements: processedData, to: url)
}

// MARK: - Private methods

private func processData() {
Expand Down