clean up earnings

This commit is contained in:
Emil Lerch 2026-03-20 08:12:53 -07:00
parent 8ae9089975
commit 2bcb84dafa
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 22 additions and 50 deletions

View file

@ -16,6 +16,15 @@ pub fn run(allocator: std.mem.Allocator, svc: *zfin.DataService, symbol: []const
};
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);
@ -64,32 +73,8 @@ pub fn display(events: []const zfin.EarningsEvent, symbol: []const u8, color: bo
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);

View file

@ -1117,29 +1117,7 @@ pub const App = struct {
}
fn loadEarningsData(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");
earnings_tab.loadData(self);
}
fn loadOptionsData(self: *App) void {

View file

@ -27,6 +27,15 @@ pub fn loadData(self: *App) void {
self.earnings_data = result.data;
self.earnings_timestamp = result.timestamp;
// 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.data.len == 0) {
self.earnings_disabled = true;
self.setStatus("No earnings data available (ETF/index?)");
@ -81,15 +90,15 @@ pub fn renderEarningsLines(
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 %",
try lines.append(arena, .{ .text = try std.fmt.allocPrint(arena, " {s:>12} {s:>4} {s:>12} {s:>12} {s:>12} {s:>10} {s:>5}", .{
"Date", "Q", "EPS Est", "EPS Act", "Surprise", "Surprise %", "When",
}), .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 text = try std.fmt.allocPrint(arena, " {s} {s:>5}", .{ row.text, @tagName(e.report_time) });
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 });