const std = @import("std"); const zfin = @import("../root.zig"); const cli = @import("common.zig"); const fmt = cli.fmt; pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const u8, color: bool, out: *std.Io.Writer) !void { const result = svc.getEarnings(symbol) catch |err| switch (err) { zfin.DataError.NoApiKey => { try cli.stderrPrint("Error: FINNHUB_API_KEY not set. Get a free key at https://finnhub.io\n"); return; }, else => { try cli.stderrPrint("Error fetching earnings data.\n"); return; }, }; defer allocator.free(result.data); if (result.source == .cached) try cli.stderrPrint("(using cached earnings data)\n"); try display(result.data, symbol, color, out); } pub fn display(events: []const zfin.EarningsEvent, symbol: []const u8, color: bool, out: *std.Io.Writer) !void { try cli.setBold(out, color); try out.print("\nEarnings History for {s}\n", .{symbol}); try cli.reset(out, color); try out.print("========================================\n", .{}); if (events.len == 0) { try cli.setFg(out, color, cli.CLR_MUTED); try out.print(" No earnings data found.\n\n", .{}); try cli.reset(out, color); return; } try cli.setFg(out, color, cli.CLR_MUTED); try out.print("{s:>12} {s:>4} {s:>12} {s:>12} {s:>12} {s:>10} {s:>5}\n", .{ "Date", "Q", "EPS Est", "EPS Act", "Surprise", "Surprise %", "When", }); try out.print("{s:->12} {s:->4} {s:->12} {s:->12} {s:->12} {s:->10} {s:->5}\n", .{ "", "", "", "", "", "", "", }); try cli.reset(out, color); for (events) |e| { var db: [10]u8 = undefined; const is_future = e.isFuture(); const surprise_positive = if (e.surpriseAmount()) |s| s >= 0 else true; if (is_future) { try cli.setFg(out, color, cli.CLR_MUTED); } else if (surprise_positive) { try cli.setFg(out, color, cli.CLR_POSITIVE); } else { try cli.setFg(out, color, cli.CLR_NEGATIVE); } try out.print("{s:>12}", .{e.date.format(&db)}); if (e.quarter) |q| try out.print(" Q{d}", .{q}) else try out.print(" {s:>4}", .{"--"}); if (e.estimate) |est| try out.print(" {s:>12}", .{fmtEps(est)}) else try out.print(" {s:>12}", .{"--"}); if (e.actual) |act| try out.print(" {s:>12}", .{fmtEps(act)}) else try out.print(" {s:>12}", .{"--"}); if (e.surpriseAmount()) |s| { var surp_buf: [12]u8 = undefined; const surp_str = if (s >= 0) std.fmt.bufPrint(&surp_buf, "+${d:.4}", .{s}) catch "?" else std.fmt.bufPrint(&surp_buf, "-${d:.4}", .{-s}) catch "?"; try out.print(" {s:>12}", .{surp_str}); } else { try out.print(" {s:>12}", .{"--"}); } if (e.surprisePct()) |sp| { var pct_buf: [12]u8 = undefined; const pct_str = if (sp >= 0) std.fmt.bufPrint(&pct_buf, "+{d:.1}%", .{sp}) catch "?" else std.fmt.bufPrint(&pct_buf, "{d:.1}%", .{sp}) catch "?"; try out.print(" {s:>10}", .{pct_str}); } else { try out.print(" {s:>10}", .{"--"}); } try out.print(" {s:>5}", .{@tagName(e.report_time)}); try cli.reset(out, color); try out.print("\n", .{}); } try out.print("\n{d} earnings event(s)\n\n", .{events.len}); } pub fn fmtEps(val: f64) [12]u8 { var buf: [12]u8 = .{' '} ** 12; _ = std.fmt.bufPrint(&buf, "${d:.2}", .{val}) catch {}; return buf; } // ── Tests ──────────────────────────────────────────────────── test "fmtEps formats positive value" { const result = fmtEps(1.25); const trimmed = std.mem.trimRight(u8, &result, &.{' '}); try std.testing.expectEqualStrings("$1.25", trimmed); } test "fmtEps formats negative value" { const result = fmtEps(-0.50); const trimmed = std.mem.trimRight(u8, &result, &.{' '}); try std.testing.expect(std.mem.indexOf(u8, trimmed, "0.5") != null); } test "fmtEps formats zero" { const result = fmtEps(0.0); const trimmed = std.mem.trimRight(u8, &result, &.{' '}); try std.testing.expectEqualStrings("$0.00", trimmed); } test "display shows earnings with beat" { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const events = [_]zfin.EarningsEvent{ .{ .symbol = "AAPL", .date = .{ .days = 19000 }, .quarter = 4, .estimate = 1.50, .actual = 1.65, .surprise = 0.15, .surprise_percent = 10.0, .report_time = .amc }, }; try display(&events, "AAPL", false, &w); const out = w.buffered(); try std.testing.expect(std.mem.indexOf(u8, out, "AAPL") != null); try std.testing.expect(std.mem.indexOf(u8, out, "Q4") != null); try std.testing.expect(std.mem.indexOf(u8, out, "$1.50") != null); try std.testing.expect(std.mem.indexOf(u8, out, "$1.65") != null); try std.testing.expect(std.mem.indexOf(u8, out, "1 earnings event(s)") != null); } test "display shows empty message" { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const events = [_]zfin.EarningsEvent{}; try display(&events, "XYZ", false, &w); const out = w.buffered(); try std.testing.expect(std.mem.indexOf(u8, out, "No earnings data found") != null); } test "display no ANSI without color" { var buf: [4096]u8 = undefined; var w: std.Io.Writer = .fixed(&buf); const events = [_]zfin.EarningsEvent{ .{ .symbol = "MSFT", .date = .{ .days = 19000 }, .report_time = .bmo }, }; try display(&events, "MSFT", false, &w); const out = w.buffered(); try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[") == null); }