diff --git a/src/commands/snapshot.zig b/src/commands/snapshot.zig index ce879e1..e49510a 100644 --- a/src/commands/snapshot.zig +++ b/src/commands/snapshot.zig @@ -22,6 +22,17 @@ const atomic = @import("../atomic.zig"); const version = @import("../version.zig"); const portfolio_mod = @import("../models/portfolio.zig"); const Date = @import("../models/date.zig").Date; +const model = @import("../models/snapshot.zig"); + +// Re-export record types so callers that reach `commands/snapshot.zig` +// (tests, mostly) still see the familiar names. New code should prefer +// `@import("models/snapshot.zig")` directly. +pub const MetaRow = model.MetaRow; +pub const TotalRow = model.TotalRow; +pub const TaxTypeRow = model.TaxTypeRow; +pub const AccountRow = model.AccountRow; +pub const LotRow = model.LotRow; +pub const Snapshot = model.Snapshot; pub const SnapshotError = error{ PortfolioEmpty, @@ -356,80 +367,10 @@ pub fn quoteDateRange(infos: []const QuoteInfo) ?struct { min: Date, max: Date } // ── Snapshot records ───────────────────────────────────────── // -// Each record kind below is a plain struct suitable for `srf.fmtFrom`. -// Field order in the struct declaration IS the on-disk order — srf's -// `Record.from` iterates `inline for (info.fields)`. The leading `kind` -// field is the discriminator readers demux on. -// -// IMPORTANT: `kind` does NOT have a default value. SRF elides fields -// whose value matches the declared default (see setField in srf.zig), -// so a `kind: []const u8 = "meta"` would vanish from the output. Each -// construction site supplies the tag explicitly. -// -// Optional fields default to `null` so they're elided on null values — -// that's the behavior we want for `price`, `quote_date`, etc. - -pub const MetaRow = struct { - kind: []const u8, - snapshot_version: u32, - as_of_date: Date, - captured_at: i64, - zfin_version: []const u8, - quote_date_min: ?Date = null, - quote_date_max: ?Date = null, - stale_count: usize, -}; - -pub const TotalRow = struct { - kind: []const u8, - scope: []const u8, - value: f64, -}; - -pub const TaxTypeRow = struct { - kind: []const u8, - label: []const u8, - value: f64, -}; - -pub const AccountRow = struct { - kind: []const u8, - name: []const u8, - value: f64, -}; - -pub const LotRow = struct { - kind: []const u8, - symbol: []const u8, - lot_symbol: []const u8, - account: []const u8, - security_type: []const u8, - shares: f64, - open_price: f64, - cost_basis: f64, - value: f64, - /// Null for non-stock lots (cash/CD/illiquid have no per-share price). - price: ?f64 = null, - /// Null for non-stock lots. - quote_date: ?Date = null, - /// Emitted only when true (default is false, which srf skips). - quote_stale: bool = false, -}; - -pub const Snapshot = struct { - meta: MetaRow, - totals: []TotalRow, - tax_types: []TaxTypeRow, - accounts: []AccountRow, - lots: []LotRow, - - pub fn deinit(self: *Snapshot, allocator: std.mem.Allocator) void { - allocator.free(self.totals); - allocator.free(self.tax_types); - allocator.free(self.accounts); - allocator.free(self.lots); - } -}; +// Record structs live in `src/models/snapshot.zig` — see the re-exports +// near the top of this file. The types are separated from this command +// module so analytics code (`src/analytics/timeline.zig`) can reference +// them without depending on a `commands/` module. /// Build the full snapshot in memory. Does not touch disk. fn buildSnapshot( diff --git a/src/models/snapshot.zig b/src/models/snapshot.zig new file mode 100644 index 0000000..f313ec1 --- /dev/null +++ b/src/models/snapshot.zig @@ -0,0 +1,89 @@ +//! Snapshot record types — the wire format for `history/-portfolio.srf`. +//! +//! Each record kind below is a plain struct suitable for `srf.fmtFrom` +//! on the write side and `srf.parse` → `Record.to(T)` on the read side. +//! Field order in the struct declaration IS the on-disk order: srf's +//! `Record.from` iterates `inline for (info.fields)`. The leading `kind` +//! field is the discriminator readers use to demux heterogeneous records. +//! +//! Lives in `src/models/` because these types describe the data format +//! itself — they're consumed by the snapshot writer command +//! (`src/commands/snapshot.zig`), the history reader (`src/history.zig`), +//! and the timeline analytics (`src/analytics/timeline.zig`). Putting +//! them in `commands/` would force analytics to depend on a command +//! module, which would be backwards. +//! +//! IMPORTANT: `kind` does NOT have a default value. SRF elides fields +//! whose value matches the declared default (see `setField` in +//! `srf.zig`), so `kind: []const u8 = "meta"` would vanish from the +//! output. Each construction site supplies the tag explicitly. +//! +//! Optional fields default to `null` so they're elided on null values — +//! that's the behavior we want for `price`, `quote_date`, etc. + +const std = @import("std"); +const Date = @import("date.zig").Date; + +pub const MetaRow = struct { + kind: []const u8, + snapshot_version: u32, + as_of_date: Date, + captured_at: i64, + zfin_version: []const u8, + quote_date_min: ?Date = null, + quote_date_max: ?Date = null, + stale_count: usize, +}; + +pub const TotalRow = struct { + kind: []const u8, + scope: []const u8, + value: f64, +}; + +pub const TaxTypeRow = struct { + kind: []const u8, + label: []const u8, + value: f64, +}; + +pub const AccountRow = struct { + kind: []const u8, + name: []const u8, + value: f64, +}; + +pub const LotRow = struct { + kind: []const u8, + symbol: []const u8, + lot_symbol: []const u8, + account: []const u8, + security_type: []const u8, + shares: f64, + open_price: f64, + cost_basis: f64, + value: f64, + /// Null for non-stock lots (cash/CD/illiquid have no per-share price). + price: ?f64 = null, + /// Null for non-stock lots. + quote_date: ?Date = null, + /// Emitted only when true (default is false, which srf skips). + quote_stale: bool = false, +}; + +/// In-memory portfolio snapshot. `meta` is a single record; the four +/// slices are per-section record collections. +pub const Snapshot = struct { + meta: MetaRow, + totals: []TotalRow, + tax_types: []TaxTypeRow, + accounts: []AccountRow, + lots: []LotRow, + + pub fn deinit(self: *Snapshot, allocator: std.mem.Allocator) void { + allocator.free(self.totals); + allocator.free(self.tax_types); + allocator.free(self.accounts); + allocator.free(self.lots); + } +};