From f28d98f7086abd7d9ee2aadb42e49aaca1fd03ae Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 2 May 2026 11:10:39 -0700 Subject: [PATCH] clear up the attributions output --- src/commands/compare.zig | 69 ++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/commands/compare.zig b/src/commands/compare.zig index 639e417..6628065 100644 --- a/src/commands/compare.zig +++ b/src/commands/compare.zig @@ -499,35 +499,46 @@ fn renderTotalsLine(out: *std.Io.Writer, color: bool, t: view.TotalsRow) !void { try cli.printIntent(out, color, c.style, " {s} {s}\n", .{ c.delta, c.pct }); } -/// Render the contributions-vs-gains attribution line directly beneath -/// the Liquid totals. Matches the email format: +/// Render the investment-gains vs. cash-contributions breakdown of +/// the liquid delta, stacked as two labeled rows directly beneath +/// the Liquid line: /// -/// Attribution: +$30,148.02 delta = +$22,636.00 contributions + +$7,512.02 gains +/// Investment gains: +$85,062.72 +/// Cash contributions: +$7,866.22 /// -/// All three amounts are signed: negative contributions (net -/// withdrawal) and negative gains (market loss) both print with a -/// leading `-`. Indent of the label column aligns with "Liquid:". +/// Both amounts are signed. Negative gains (market loss) and +/// negative contributions (net withdrawal) print with a leading `-` +/// in the negative color. Labels use the muted color to stay out of +/// the way of the eye scan for the numbers. +/// +/// Labels are padded to a fixed width so the `$` column aligns +/// regardless of label length. Indent of 2 spaces matches the +/// gainer/loser summary beneath the per-symbol table. +/// +/// Math identity: `delta = gains + contributions`, so +/// `gains = delta - contributions`. The caller derives `gains` from +/// the liquid delta before this is called; we just render. fn renderAttributionLine(out: *std.Io.Writer, color: bool, delta: f64, attribution: view.Attribution) !void { - var delta_buf: [32]u8 = undefined; - var contrib_buf: [32]u8 = undefined; + _ = delta; // delta is already shown on the Liquid row above; we + // don't repeat it here. Kept in the signature so the + // callsite stays readable (`renderAttributionLine(out, + // color, cv.liquid.delta, a)`) and future revisions + // that want to restate the Δ have it in scope. + var gains_buf: [32]u8 = undefined; - const delta_str = view_hist.fmtSignedMoneyBuf(&delta_buf, delta); - const contrib_str = view_hist.fmtSignedMoneyBuf(&contrib_buf, attribution.contributions); + var contrib_buf: [32]u8 = undefined; const gains_str = view_hist.fmtSignedMoneyBuf(&gains_buf, attribution.gains); + const contrib_str = view_hist.fmtSignedMoneyBuf(&contrib_buf, attribution.contributions); - // Sign-aware joiner between `contributions` and `gains`: - // gains >= 0 → " + " (explicit addition). - // gains < 0 → " " (the leading "-" on gains_str carries the sign; - // avoids visual clutter of "+$X + -$Y"). - const joiner: []const u8 = if (attribution.gains >= 0) " + " else " "; - - try cli.printFg(out, color, cli.CLR_MUTED, "Attribution: ", .{}); - try cli.printGainLoss(out, color, delta, "{s}", .{delta_str}); - try cli.printFg(out, color, cli.CLR_MUTED, " delta = ", .{}); - try cli.printGainLoss(out, color, attribution.contributions, "{s}", .{contrib_str}); - try cli.printFg(out, color, cli.CLR_MUTED, " contributions{s}", .{joiner}); - try cli.printGainLoss(out, color, attribution.gains, "{s}", .{gains_str}); - try cli.printFg(out, color, cli.CLR_MUTED, " gains\n", .{}); + // 19-char label column aligns the amount columns. "Investment + // gains:" is 17 chars → 2 trailing pad; "Cash contributions:" is + // 19 chars → 0 trailing pad. The 2-space gutter that follows + // keeps the amounts clearly separated from the labels even on + // narrow terminals. + try cli.printFg(out, color, cli.CLR_MUTED, " {s:<19} ", .{"Investment gains:"}); + try cli.printGainLoss(out, color, attribution.gains, "{s}\n", .{gains_str}); + try cli.printFg(out, color, cli.CLR_MUTED, " {s:<19} ", .{"Cash contributions:"}); + try cli.printGainLoss(out, color, attribution.contributions, "{s}\n", .{contrib_str}); } fn renderSymbolRow(out: *std.Io.Writer, color: bool, s: view.SymbolChange) !void { @@ -738,12 +749,13 @@ test "renderCompare: attribution line when attribution is set" { try renderCompare(&stream, false, cv); const out = stream.buffered(); - try testing.expect(std.mem.indexOf(u8, out, "Attribution:") != null); - try testing.expect(std.mem.indexOf(u8, out, "+$30,148.02") != null); + try testing.expect(std.mem.indexOf(u8, out, "Investment gains:") != null); + try testing.expect(std.mem.indexOf(u8, out, "Cash contributions:") != null); + try testing.expect(std.mem.indexOf(u8, out, "+$30,148.02") != null); // on the Liquid row above try testing.expect(std.mem.indexOf(u8, out, "+$22,636.00") != null); try testing.expect(std.mem.indexOf(u8, out, "+$7,512.02") != null); - try testing.expect(std.mem.indexOf(u8, out, "contributions") != null); - try testing.expect(std.mem.indexOf(u8, out, "gains") != null); + // The old `Attribution:` prefix is gone. + try testing.expect(std.mem.indexOf(u8, out, "Attribution:") == null); } test "renderCompare: no attribution line when attribution is null" { @@ -765,7 +777,8 @@ test "renderCompare: no attribution line when attribution is null" { try renderCompare(&stream, false, cv); const out = stream.buffered(); - try testing.expect(std.mem.indexOf(u8, out, "Attribution:") == null); + try testing.expect(std.mem.indexOf(u8, out, "Investment gains:") == null); + try testing.expect(std.mem.indexOf(u8, out, "Cash contributions:") == null); } test "renderCompare: attribution handles negative gains" {