fidelity detection fix (part 1 - see below)
In early July fidelity changed the casing on the CSV headers. This broke the file detection (Schwab/Fidelity) because zfin was doing a case sensitive match on these headers. This quick fix addresses this problem, and a wider fix to make detection less fragile will follow shortly.
This commit is contained in:
parent
4d185f9502
commit
6e48954190
2 changed files with 60 additions and 5 deletions
|
|
@ -81,11 +81,14 @@ pub fn parseCsv(allocator: std.mem.Allocator, data: []const u8) ![]BrokeragePosi
|
|||
|
||||
var lines = std.mem.splitScalar(u8, content, '\n');
|
||||
|
||||
// Validate header row
|
||||
// Validate header row. Case-insensitive: Fidelity switched the export
|
||||
// header from Title Case to sentence case ("Account number,Account
|
||||
// name,..."). The column order is unchanged, so the positional parsing
|
||||
// below still holds; only this guard needed to loosen.
|
||||
const header_line = lines.next() orelse return error.EmptyFile;
|
||||
const header_trimmed = std.mem.trimEnd(u8, header_line, &.{ '\r', ' ' });
|
||||
if (header_trimmed.len == 0) return error.EmptyFile;
|
||||
if (!std.mem.startsWith(u8, header_trimmed, "Account Number")) {
|
||||
if (!std.ascii.startsWithIgnoreCase(header_trimmed, "Account Number")) {
|
||||
return error.UnexpectedHeader;
|
||||
}
|
||||
|
||||
|
|
@ -247,3 +250,36 @@ test "parseCsv cash account type is not cash position" {
|
|||
try std.testing.expect(!positions[0].is_cash);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 190), positions[0].quantity.?, 0.01);
|
||||
}
|
||||
|
||||
test "parseCsv accepts Fidelity's lowercase (sentence-case) header" {
|
||||
// Regression: Fidelity switched the export header from Title Case to
|
||||
// sentence case ("Account number,Account name,...,Last price,...,Cost
|
||||
// basis total,Average cost basis,Type"). Column order is unchanged, so
|
||||
// positional parsing must still work. The trailing comma after the Type
|
||||
// value and the blank-line-separated legal footer mirror the real export.
|
||||
const csv =
|
||||
"Account number,Account name,Symbol,Description,Quantity,Last price,Last price change,Current value,Today's gain/loss dollar,Today's gain/loss percent,Total gain/loss dollar,Total gain/loss percent,Percent of account,Cost basis total,Average cost basis,Type\n" ++
|
||||
"Z123,Individual - TOD,FZFXX**,HELD IN MONEY MARKET,,,,$24.03,,,,,0.12%,,,Cash,\n" ++
|
||||
"Z123,Individual - TOD,AAPL,APPLE INC,100,$150.00,-$2.00,$15000.00,-$200.00,-1.3%,+$5000.00,+50%,99.88%,$10000.00,$100.00,Margin,\n" ++
|
||||
"\n" ++
|
||||
"\"Brokerage services are provided by Fidelity Brokerage Services LLC (FBS)...\"\n";
|
||||
|
||||
const allocator = std.testing.allocator;
|
||||
const positions = try parseCsv(allocator, csv);
|
||||
defer allocator.free(positions);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 2), positions.len);
|
||||
|
||||
// Money-market row -> cash, no quantity.
|
||||
try std.testing.expectEqualStrings("FZFXX", positions[0].symbol);
|
||||
try std.testing.expect(positions[0].is_cash);
|
||||
try std.testing.expect(positions[0].quantity == null);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 24.03), positions[0].current_value.?, 0.01);
|
||||
|
||||
// Stock row -> positional columns still resolve correctly.
|
||||
try std.testing.expectEqualStrings("AAPL", positions[1].symbol);
|
||||
try std.testing.expect(!positions[1].is_cash);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 100), positions[1].quantity.?, 0.01);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 15000.00), positions[1].current_value.?, 0.01);
|
||||
try std.testing.expectApproxEqAbs(@as(f64, 10000.00), positions[1].cost_basis.?, 0.01);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,9 +59,14 @@ fn detectBrokerFileKind(data: []const u8) ?BrokerFileKind {
|
|||
else
|
||||
data;
|
||||
|
||||
// Fidelity CSV: first line starts with "Account Number" or "Account Name"
|
||||
if (std.mem.startsWith(u8, content, "Account Number") or
|
||||
std.mem.startsWith(u8, content, "Account Name"))
|
||||
// Fidelity CSV: first line starts with "Account Number"/"Account Name".
|
||||
// Case-insensitive: Fidelity switched the export header from Title Case
|
||||
// to sentence case ("Account number,Account name,...") and could change
|
||||
// it again. Matching this loosely also keeps the file from falling
|
||||
// through to the broad schwab-summary heuristic below, which would
|
||||
// otherwise misclassify it via the "Brokerage services..." legal footer.
|
||||
if (std.ascii.startsWithIgnoreCase(content, "Account Number") or
|
||||
std.ascii.startsWithIgnoreCase(content, "Account Name"))
|
||||
return .fidelity_csv;
|
||||
|
||||
// Schwab per-account CSV: starts with a quoted title line like "Positions for ..."
|
||||
|
|
@ -1077,6 +1082,20 @@ test "detectBrokerFileKind: fidelity csv with BOM" {
|
|||
try std.testing.expectEqual(BrokerFileKind.fidelity_csv, detectBrokerFileKind(fidelity_bom).?);
|
||||
}
|
||||
|
||||
test "detectBrokerFileKind: fidelity csv with lowercase header is not misclassified as schwab" {
|
||||
// Regression: Fidelity switched the header from Title Case to sentence
|
||||
// case ("Account number,..."). The old case-sensitive check failed and
|
||||
// the file fell through to the schwab-summary heuristic, which
|
||||
// false-matched on the "Brokerage services are provided by..." legal
|
||||
// footer plus the "$" amounts. Must classify as fidelity_csv.
|
||||
const data =
|
||||
"Account number,Account name,Symbol,Description,Quantity,Last price,Last price change,Current value,Today's gain/loss dollar,Today's gain/loss percent,Total gain/loss dollar,Total gain/loss percent,Percent of account,Cost basis total,Average cost basis,Type\n" ++
|
||||
"Z123,Individual - TOD,AAPL,APPLE INC,100,$150.00,-$2.00,$15000.00,,,,,99.88%,$10000.00,$100.00,Margin,\n" ++
|
||||
"\n" ++
|
||||
"\"Brokerage services are provided by Fidelity Brokerage Services LLC (FBS)...\" $\n";
|
||||
try std.testing.expectEqual(BrokerFileKind.fidelity_csv, detectBrokerFileKind(data).?);
|
||||
}
|
||||
|
||||
test "detectBrokerFileKind: schwab csv" {
|
||||
const schwab_header = "\"Positions for account Roth IRA ...1234 as of\"";
|
||||
try std.testing.expectEqual(BrokerFileKind.schwab_csv, detectBrokerFileKind(schwab_header).?);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue