161 lines
6.7 KiB
Zig
161 lines
6.7 KiB
Zig
const std = @import("std");
|
|
const vaxis = @import("vaxis");
|
|
const zfin = @import("../root.zig");
|
|
const fmt = @import("../format.zig");
|
|
const theme_mod = @import("theme.zig");
|
|
const tui = @import("../tui.zig");
|
|
const App = tui.App;
|
|
const StyledLine = tui.StyledLine;
|
|
|
|
// ── Data loading ──────────────────────────────────────────────
|
|
|
|
pub fn loadData(self: *App) void {
|
|
self.earnings_loaded = true;
|
|
self.freeEarnings();
|
|
|
|
const result = self.svc.getEarnings(self.symbol) catch |err| {
|
|
switch (err) {
|
|
zfin.DataError.NoApiKey => self.setStatus("No API key. Set FINNHUB_API_KEY"),
|
|
zfin.DataError.FetchFailed => {
|
|
self.earnings_disabled = true;
|
|
self.setStatus("No earnings data (ETF/index?)");
|
|
},
|
|
else => self.setStatus("Error loading earnings"),
|
|
}
|
|
return;
|
|
};
|
|
self.earnings_data = result.data;
|
|
self.earnings_timestamp = result.timestamp;
|
|
|
|
if (result.data.len == 0) {
|
|
self.earnings_disabled = true;
|
|
self.setStatus("No earnings data available (ETF/index?)");
|
|
return;
|
|
}
|
|
self.setStatus(if (result.source == .cached) "r/F5 to refresh" else "Fetched | r/F5 to refresh");
|
|
}
|
|
|
|
// ── Rendering ─────────────────────────────────────────────────
|
|
|
|
pub fn buildStyledLines(self: *App, arena: std.mem.Allocator) ![]const StyledLine {
|
|
return renderEarningsLines(arena, self.theme, self.symbol, self.earnings_disabled, self.earnings_data, self.earnings_timestamp);
|
|
}
|
|
|
|
/// Render earnings tab content. Pure function — no App dependency.
|
|
pub fn renderEarningsLines(
|
|
arena: std.mem.Allocator,
|
|
th: theme_mod.Theme,
|
|
symbol: []const u8,
|
|
earnings_disabled: bool,
|
|
earnings_data: ?[]const zfin.EarningsEvent,
|
|
earnings_timestamp: i64,
|
|
) ![]const StyledLine {
|
|
var lines: std.ArrayList(StyledLine) = .empty;
|
|
|
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
|
|
|
if (symbol.len == 0) {
|
|
try lines.append(arena, .{ .text = " No symbol selected. Press / to enter a symbol.", .style = th.mutedStyle() });
|
|
return lines.toOwnedSlice(arena);
|
|
}
|
|
if (earnings_disabled) {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Earnings not available for {s} (ETF/index)", .{symbol}), .style = th.mutedStyle() });
|
|
return lines.toOwnedSlice(arena);
|
|
}
|
|
|
|
var earn_ago_buf: [16]u8 = undefined;
|
|
const earn_ago = fmt.fmtTimeAgo(&earn_ago_buf, earnings_timestamp);
|
|
if (earn_ago.len > 0) {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Earnings: {s} (data {s})", .{ symbol, earn_ago }), .style = th.headerStyle() });
|
|
} else {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " Earnings: {s}", .{symbol}), .style = th.headerStyle() });
|
|
}
|
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
|
|
|
const ev = earnings_data orelse {
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " No data. Run: zfin earnings {s}", .{symbol}), .style = th.mutedStyle() });
|
|
return lines.toOwnedSlice(arena);
|
|
};
|
|
if (ev.len == 0) {
|
|
try lines.append(arena, .{ .text = " No earnings events found.", .style = th.mutedStyle() });
|
|
return lines.toOwnedSlice(arena);
|
|
}
|
|
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {s:>12} {s:>4} {s:>12} {s:>12} {s:>12} {s:>10}", .{
|
|
"Date", "Q", "EPS Est", "EPS Act", "Surprise", "Surprise %",
|
|
}), .style = th.mutedStyle() });
|
|
|
|
for (ev) |e| {
|
|
var row_buf: [128]u8 = undefined;
|
|
const row = fmt.fmtEarningsRow(&row_buf, e);
|
|
|
|
const text = try std.fmt.allocPrint(arena, " {s}", .{row.text});
|
|
const row_style = if (row.is_future) th.mutedStyle() else if (row.is_positive) th.positiveStyle() else th.negativeStyle();
|
|
|
|
try lines.append(arena, .{ .text = text, .style = row_style });
|
|
}
|
|
|
|
try lines.append(arena, .{ .text = "", .style = th.contentStyle() });
|
|
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {d} earnings event(s)", .{ev.len}), .style = th.mutedStyle() });
|
|
|
|
return lines.toOwnedSlice(arena);
|
|
}
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────────────
|
|
|
|
const testing = std.testing;
|
|
|
|
test "renderEarningsLines with earnings data" {
|
|
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
|
|
defer arena_state.deinit();
|
|
const arena = arena_state.allocator();
|
|
const th = theme_mod.default_theme;
|
|
|
|
const events = [_]zfin.EarningsEvent{.{
|
|
.symbol = "AAPL",
|
|
.date = try zfin.Date.parse("2025-01-15"),
|
|
.quarter = 4,
|
|
.estimate = 1.50,
|
|
.actual = 1.65,
|
|
}};
|
|
const lines = try renderEarningsLines(arena, th, "AAPL", false, &events, 0);
|
|
// blank + header + blank + col_header + data_row + blank + count = 7
|
|
try testing.expectEqual(@as(usize, 7), lines.len);
|
|
try testing.expect(std.mem.indexOf(u8, lines[1].text, "AAPL") != null);
|
|
try testing.expect(std.mem.indexOf(u8, lines[3].text, "EPS Est") != null);
|
|
// Data row should contain the date
|
|
try testing.expect(std.mem.indexOf(u8, lines[4].text, "2025-01-15") != null);
|
|
}
|
|
|
|
test "renderEarningsLines no symbol" {
|
|
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
|
|
defer arena_state.deinit();
|
|
const arena = arena_state.allocator();
|
|
const th = theme_mod.default_theme;
|
|
|
|
const lines = try renderEarningsLines(arena, th, "", false, null, 0);
|
|
try testing.expectEqual(@as(usize, 2), lines.len);
|
|
try testing.expect(std.mem.indexOf(u8, lines[1].text, "No symbol") != null);
|
|
}
|
|
|
|
test "renderEarningsLines disabled" {
|
|
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
|
|
defer arena_state.deinit();
|
|
const arena = arena_state.allocator();
|
|
const th = theme_mod.default_theme;
|
|
|
|
const lines = try renderEarningsLines(arena, th, "VTI", true, null, 0);
|
|
try testing.expectEqual(@as(usize, 2), lines.len);
|
|
try testing.expect(std.mem.indexOf(u8, lines[1].text, "ETF/index") != null);
|
|
}
|
|
|
|
test "renderEarningsLines no data" {
|
|
var arena_state = std.heap.ArenaAllocator.init(testing.allocator);
|
|
defer arena_state.deinit();
|
|
const arena = arena_state.allocator();
|
|
const th = theme_mod.default_theme;
|
|
|
|
const lines = try renderEarningsLines(arena, th, "AAPL", false, null, 0);
|
|
try testing.expectEqual(@as(usize, 4), lines.len);
|
|
try testing.expect(std.mem.indexOf(u8, lines[3].text, "No data") != null);
|
|
}
|