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); // Sort chronologically (oldest first) — providers may return in any order if (result.data.len > 1) { std.mem.sort(zfin.EarningsEvent, result.data, {}, struct { fn f(_: void, a: zfin.EarningsEvent, b: zfin.EarningsEvent) bool { return a.date.days < b.date.days; } }.f); } 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 row_buf: [128]u8 = undefined; const row = fmt.fmtEarningsRow(&row_buf, e); if (row.is_future) { try cli.setFg(out, color, cli.CLR_MUTED); } else if (row.is_positive) { try cli.setFg(out, color, cli.CLR_POSITIVE); } else { try cli.setFg(out, color, cli.CLR_NEGATIVE); } try out.print("{s}", .{row.text}); 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}); } // ── Tests ──────────────────────────────────────────────────── 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); }