Skip to content

Commit

Permalink
Tightening up use of Time Zone (#5)
Browse files Browse the repository at this point in the history
Relying on abbreviations was overly-fragile.
  • Loading branch information
reedes authored Oct 24, 2021
1 parent 016f532 commit 361e75e
Show file tree
Hide file tree
Showing 21 changed files with 181 additions and 115 deletions.
9 changes: 5 additions & 4 deletions CLI/main+transform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,21 +37,22 @@ extension Finporter {
var importer: String?
@Option(help: "the target schema (e.g. \"openalloc/history\")")
var outputSchema: String?
@Option(help: "default time of day, in 24 hour format, for naked dates (e.g. \"13:00\")")
@Option(help: "default time of day, in 24 hour format, for parsing naked dates (e.g. \"13:00\")")
var defTimeOfDay: String?
@Option(help: "default time zone, for naked dates (e.g. \"EST\" or \"-05:00\")")
var defTimeZone: String?
@Option(help: "geopolitical time zone identifier, for parsing naked dates (e.g. \"America/New_York\")")
var timeZoneID: String?
func run() {
do {
let outputSchema_ = outputSchema != nil ? AllocSchema(rawValue: outputSchema!) : nil

var rejectedRows: [AllocRowed.RawRow] = []
let timeZone = TimeZone(identifier: timeZoneID ?? "") ?? TimeZone.current
let str = try handleTransform(inputFilePath: inputFilePath,
rejectedRows: &rejectedRows,
finPorterID: importer,
outputSchema: outputSchema_,
defTimeOfDay: defTimeOfDay,
defTimeZone: defTimeZone)
timeZone: timeZone)
print(str)
} catch let CSVParseError.generic(message) {
fputs("CSV generic: \(message)", stderr)
Expand Down
16 changes: 8 additions & 8 deletions Sources/Core/FINporter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ open class FINporter: Identifiable, Hashable {
}

open func decode<T: AllocRowed>(_ type: T.Type,
_: Data,
rejectedRows _: inout [T.RawRow],
inputFormat _: AllocFormat? = nil,
outputSchema _: AllocSchema? = nil,
url _: URL? = nil,
defTimeOfDay _: String? = nil,
defTimeZone _: String? = nil,
timestamp _: Date? = nil) throws -> [T.DecodedRow] {
_: Data,
rejectedRows _: inout [T.RawRow],
inputFormat _: AllocFormat? = nil,
outputSchema _: AllocSchema? = nil,
url _: URL? = nil,
defTimeOfDay _: String? = nil,
timeZone _: TimeZone = TimeZone.current,
timestamp _: Date? = nil) throws -> [T.DecodedRow] {
throw FINporterError.notImplementedError
}

Expand Down
20 changes: 10 additions & 10 deletions Sources/Handlers/TransformHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,27 @@ public func handleTransform(inputFilePath: String,
finPorterID: String? = nil,
outputSchema: AllocSchema? = nil,
defTimeOfDay: String? = nil,
defTimeZone: String? = nil) throws -> String {
timeZone: TimeZone) throws -> String {
let fileURL = URL(fileURLWithPath: inputFilePath)
let data = try Data(contentsOf: fileURL)

let pair = try getPair(data: data, finPorterID: finPorterID, outputSchema: outputSchema)

switch pair.schema {
case .allocAccount:
return try decodeAndExport(MAccount.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, defTimeZone)
return try decodeAndExport(MAccount.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, timeZone)
case .allocAllocation:
return try decodeAndExport(MAllocation.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, defTimeZone)
return try decodeAndExport(MAllocation.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, timeZone)
case .allocAsset:
return try decodeAndExport(MAsset.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, defTimeZone)
return try decodeAndExport(MAsset.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, timeZone)
case .allocHolding:
return try decodeAndExport(MHolding.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, defTimeZone)
return try decodeAndExport(MHolding.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, timeZone)
case .allocSecurity:
return try decodeAndExport(MSecurity.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, defTimeZone)
return try decodeAndExport(MSecurity.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, timeZone)
case .allocStrategy:
return try decodeAndExport(MStrategy.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, defTimeZone)
return try decodeAndExport(MStrategy.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, timeZone)
case .allocTransaction:
return try decodeAndExport(MTransaction.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, defTimeZone)
return try decodeAndExport(MTransaction.self, pair.finPorter, data, &rejectedRows, pair.schema, fileURL, defTimeOfDay, timeZone)
default:
throw FINporterError.notImplementedError
}
Expand Down Expand Up @@ -114,14 +114,14 @@ internal func decodeAndExport<T: AllocBase & AllocRowed & AllocAttributable & Co
_ outputSchema: AllocSchema,
_ url: URL,
_ defTimeOfDay: String? = nil,
_ defTimeZone: String?) throws -> String {
_ timeZone: TimeZone) throws -> String {
let finRows: [T.DecodedRow] = try finPorter.decode(T.self,
data,
rejectedRows: &rejectedRows,
outputSchema: outputSchema,
url: url,
defTimeOfDay: defTimeOfDay,
defTimeZone: defTimeZone)
timeZone: timeZone)
let items: [T] = try finRows.map { try T(from: $0) }
let data = try finPorter.export(elements: items, format: .CSV)
return FINporter.normalizeDecode(data) ?? ""
Expand Down
26 changes: 10 additions & 16 deletions Sources/Helpers/ChuckDateFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,32 +27,26 @@ let chuckDateFormatter: DateFormatter = {
return df
}()

private let chuckDateFormatterGeneric: DateFormatter = {
let df = DateFormatter()
// HH: Hour [00-23]
// mm: minute [00-59] (2 for zero padding)
// Z: Use one to three letters for RFC 822, four letters for GMT format.
df.dateFormat = "MM/dd/yyyy HH:mm zzz"
return df
}()

/// Parse a 'naked' MM/dd/yyyy date into a fully resolved date.
/// Assume noon ET for any Chuck date.
/// Assume noon of current time zone for any Chuck date.
/// If "08/16/2021 as of 08/15/2021" just parse the first date and ignore the second.
func parseChuckMMDDYYYY(_ rawDateStr: String?,
defTimeOfDay: String? = nil,
defTimeZone: String? = nil) -> Date? {
timeZone: TimeZone) -> Date? {
let pattern = #"^(\d\d/\d\d/\d\d\d\d)( as of.+)?"#

let timeOfDay: String = defTimeOfDay ?? "12:00"
let timeZone: String = defTimeZone ?? "EST" // "-05:00"
guard let _rawDateStr = rawDateStr,
let captureGroups = _rawDateStr.captureGroups(for: pattern),
let foundDateStr = captureGroups.first,
timeOfDay.count == 5,
timeZone.count > 0
timeOfDay.count == 5
else { return nil }
let dateStr = "\(foundDateStr) \(timeOfDay) \(timeZone)"
let result = chuckDateFormatterGeneric.date(from: dateStr)

let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy HH:mm"
df.timeZone = timeZone

let dateStr = "\(foundDateStr) \(timeOfDay)"
let result = df.date(from: dateStr)
return result
}
26 changes: 10 additions & 16 deletions Sources/Helpers/FidoDateFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,22 @@ let fidoDateFormatter: DateFormatter = {
return df
}()

private let fidoDateFormatterGeneric: DateFormatter = {
let df = DateFormatter()
// HH: Hour [00-23]
// mm: minute [00-59] (2 for zero padding)
// Z: Use one to three letters for RFC 822, four letters for GMT format.
df.dateFormat = "MM/dd/yyyy HH:mm zzz"
return df
}()

/// parse a 'naked' MM/dd/yyyy date into a fully resolved date
/// assume noon ET for any Fido date
/// assume noon of current time zone for any Fido date
func parseFidoMMDDYYYY(_ mmddyyyy: String?,
defTimeOfDay: String? = nil,
defTimeZone: String? = nil) -> Date? {
timeZone: TimeZone) -> Date? {
let timeOfDay: String = defTimeOfDay ?? "12:00"
let timeZone: String = defTimeZone ?? "EST" // "-05:00"
guard let _mmddyyyy = mmddyyyy,
timeOfDay.count == 5,
timeZone.count > 0
timeOfDay.count == 5
else { return nil }
let dateStr = "\(_mmddyyyy) \(timeOfDay) \(timeZone)"
let result = fidoDateFormatterGeneric.date(from: dateStr)

let df = DateFormatter()
df.dateFormat = "MM/dd/yyyy HH:mm"
df.timeZone = timeZone

let dateStr = "\(_mmddyyyy) \(timeOfDay)"
let result = df.date(from: dateStr)
return result
}

2 changes: 1 addition & 1 deletion Sources/Importers/AllocSmart.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ class AllocSmart: FINporter {
outputSchema _: AllocSchema? = nil,
url _: URL? = nil,
defTimeOfDay _: String? = nil,
defTimeZone _: String? = nil,
timeZone _: TimeZone = TimeZone.current,
timestamp _: Date? = nil) throws -> [T.DecodedRow] {
guard var str = FINporter.normalizeDecode(data) else {
throw FINporterError.decodingError("unable to parse data")
Expand Down
8 changes: 4 additions & 4 deletions Sources/Importers/ChuckHistory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class ChuckHistory: FINporter {
outputSchema: AllocSchema? = nil,
url: URL? = nil,
defTimeOfDay: String? = nil,
defTimeZone: String? = nil,
timeZone: TimeZone = TimeZone.current,
timestamp: Date? = nil) throws -> [T.DecodedRow] {
guard var str = FINporter.normalizeDecode(data) else {
throw FINporterError.decodingError("unable to parse data")
Expand Down Expand Up @@ -97,7 +97,7 @@ class ChuckHistory: FINporter {
let nuItems = try decodeDelimitedRows(delimitedRows: delimitedRows,
accountID: _accountID,
defTimeOfDay: defTimeOfDay,
defTimeZone: defTimeZone,
timeZone: timeZone,
rejectedRows: &rejectedRows)
items.append(contentsOf: nuItems)
}
Expand All @@ -111,14 +111,14 @@ class ChuckHistory: FINporter {
internal func decodeDelimitedRows(delimitedRows: [AllocRowed.RawRow],
accountID: String,
defTimeOfDay: String? = nil,
defTimeZone: String? = nil,
timeZone: TimeZone = TimeZone.current,
rejectedRows: inout [AllocRowed.RawRow]) throws -> [AllocRowed.DecodedRow] {

delimitedRows.reduce(into: []) { decodedRows, delimitedRow in

guard let rawAction = MTransaction.parseString(delimitedRow["Action"]),
let rawDate = delimitedRow["Date"],
let transactedAt = parseChuckMMDDYYYY(rawDate, defTimeOfDay: defTimeOfDay, defTimeZone: defTimeZone),
let transactedAt = parseChuckMMDDYYYY(rawDate, defTimeOfDay: defTimeOfDay, timeZone: timeZone),
let amount = MTransaction.parseDouble(delimitedRow["Amount"])
else {
rejectedRows.append(delimitedRow)
Expand Down
2 changes: 1 addition & 1 deletion Sources/Importers/ChuckPositions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class ChuckPositions: FINporter {
outputSchema: AllocSchema? = nil,
url: URL? = nil,
defTimeOfDay _: String? = nil,
defTimeZone _: String? = nil,
timeZone _: TimeZone = TimeZone.current,
timestamp: Date? = nil) throws -> [T.DecodedRow] {
guard var str = FINporter.normalizeDecode(data) else {
throw FINporterError.decodingError("unable to parse data")
Expand Down
8 changes: 4 additions & 4 deletions Sources/Importers/ChuckSales.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ class ChuckSales: FINporter {
outputSchema: AllocSchema? = nil,
url: URL? = nil,
defTimeOfDay: String? = nil,
defTimeZone: String? = nil,
timeZone: TimeZone = TimeZone.current,
timestamp: Date? = nil) throws -> [T.DecodedRow] {
guard var str = FINporter.normalizeDecode(data) else {
throw FINporterError.decodingError("unable to parse data")
Expand Down Expand Up @@ -97,7 +97,7 @@ class ChuckSales: FINporter {
let nuItems = try decodeDelimitedRows(delimitedRows: delimitedRows,
accountID: _accountID,
defTimeOfDay: defTimeOfDay,
defTimeZone: defTimeZone,
timeZone: timeZone,
rejectedRows: &rejectedRows)
items.append(contentsOf: nuItems)
}
Expand All @@ -111,7 +111,7 @@ class ChuckSales: FINporter {
internal func decodeDelimitedRows(delimitedRows: [AllocRowed.RawRow],
accountID: String,
defTimeOfDay: String? = nil,
defTimeZone: String? = nil,
timeZone: TimeZone = TimeZone.current,
rejectedRows: inout [AllocRowed.RawRow]) throws -> [AllocRowed.DecodedRow] {

delimitedRows.reduce(into: []) { decodedRows, delimitedRow in
Expand All @@ -121,7 +121,7 @@ class ChuckSales: FINporter {
let shareCount = MTransaction.parseDouble(delimitedRow["Quantity"]),
let proceeds = MTransaction.parseDouble(delimitedRow["Proceeds"]),
let dateSold = delimitedRow["Closed Date"],
let transactedAt = parseChuckMMDDYYYY(dateSold, defTimeOfDay: defTimeOfDay, defTimeZone: defTimeZone)
let transactedAt = parseChuckMMDDYYYY(dateSold, defTimeOfDay: defTimeOfDay, timeZone: timeZone)
else {
rejectedRows.append(delimitedRow)
return
Expand Down
10 changes: 5 additions & 5 deletions Sources/Importers/FidoHistory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class FidoHistory: FINporter {
outputSchema _: AllocSchema? = nil,
url _: URL? = nil,
defTimeOfDay: String? = nil,
defTimeZone: String? = nil,
timeZone: TimeZone = TimeZone.current,
timestamp _: Date? = nil) throws -> [T.DecodedRow] {
guard let str = FINporter.normalizeDecode(data) else {
throw FINporterError.decodingError("unable to parse data")
Expand All @@ -74,7 +74,7 @@ class FidoHistory: FINporter {
let delimitedRows = try CSV(string: String(csvStr)).namedRows
let nuItems = decodeDelimitedRows(delimitedRows: delimitedRows,
defTimeOfDay: defTimeOfDay,
defTimeZone: defTimeZone,
timeZone: timeZone,
rejectedRows: &rejectedRows)
items.append(contentsOf: nuItems)
}
Expand All @@ -84,7 +84,7 @@ class FidoHistory: FINporter {

internal func decodeDelimitedRows(delimitedRows: [AllocRowed.RawRow],
defTimeOfDay: String? = nil,
defTimeZone: String? = nil,
timeZone: TimeZone = TimeZone.current,
rejectedRows: inout [AllocRowed.RawRow]) -> [AllocRowed.DecodedRow] {

//let trimFromTicker = CharacterSet(charactersIn: "*")
Expand All @@ -93,7 +93,7 @@ class FidoHistory: FINporter {
// required values
guard let rawAction = MTransaction.parseString(delimitedRow["Action"]),
let rawDate = delimitedRow["Run Date"],
let transactedAt = parseFidoMMDDYYYY(rawDate, defTimeOfDay: defTimeOfDay, defTimeZone: defTimeZone),
let transactedAt = parseFidoMMDDYYYY(rawDate, defTimeOfDay: defTimeOfDay, timeZone: timeZone),
let accountNameNumber = MTransaction.parseString(delimitedRow["Account"]),
let amount = MTransaction.parseDouble(delimitedRow["Amount ($)"]),
let accountID = accountNameNumber.split(separator: " ").last,
Expand Down Expand Up @@ -206,7 +206,7 @@ class FidoHistory: FINporter {
// let shareCount = MTransaction.parseDouble(delimitedRow["Quantity"]),
// let sharePrice = MTransaction.parseDouble(delimitedRow["Price ($)"]),
// let runDate = delimitedRow["Run Date"],
// let transactedAt = parseFidoMMDDYYYY(runDate, defTimeOfDay: defTimeOfDay, defTimeZone: defTimeZone)
// let transactedAt = parseFidoMMDDYYYY(runDate, defTimeOfDay: defTimeOfDay, timeZone: timeZone)

// unfortunately, no realized gain/loss info available in this export
// see the fido_sales report for that
Expand Down
2 changes: 1 addition & 1 deletion Sources/Importers/FidoPositions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class FidoPositions: FINporter {
outputSchema: AllocSchema? = nil,
url: URL? = nil,
defTimeOfDay _: String? = nil,
defTimeZone _: String? = nil,
timeZone _: TimeZone = TimeZone.current,
timestamp: Date? = nil) throws -> [T.DecodedRow] {
guard let str = FINporter.normalizeDecode(data) else {
throw FINporterError.decodingError("unable to parse data")
Expand Down
Loading

0 comments on commit 361e75e

Please sign in to comment.