Package periods turns a cumulative double-entry book into a period report: per-account movement over a date window, the period income statement, and the closing entry that sweeps nominal accounts into equity.
p/journal is cumulative by construction and says so. Its Close is a backdating seal — it refuses entries dated at or before a cutoff and does not sweep anything — so revenue and expense accumulate from the book's first posting forever, and p/statements names its three figures CumulativeRevenue, CumulativeExpenses and CumulativeNet for that reason. Its package doc states the gap in one sentence: "A period income statement needs entry-level data and is out of scope." This package is that entry-level consumer. It reads no chain, holds no state, and writes nothing.
Example
1w := Window{From: q1Start, Until: q2Start}
2rep, err := Derive(chart, entries, w)
3// rep.Activity, rep.PeriodNet, rep.CrossWindowReversals
4post, err := rep.ClosingEntry("3100") // a PROPOSAL, not a write
Why the chart is a parameter
journal.Posting is {Account string, Side, Amount int64}: it carries the account's CODE and never its Kind. So revenue cannot be told from expense from postings alone, and no income statement is derivable without the chart. That single fact fixes this package's signature. Only Code, Name and Kind are read; Debits and Credits are the book's CUMULATIVE columns and are deliberately neither read nor validated here, because period movement comes from the entries.
The window is closed-open
Window is [From, Until): From is included, Until is not. Consecutive periods therefore TILE — Window.Tiles is the predicate — with no overlap and no gap, which is what makes a sequence of period reports add up to the cumulative figures. A closed-closed window double-counts exactly one timestamp per boundary, and that error is invisible in any single report.
This package imposes an ordering precondition on one input and refuses one on the other, and the rule separating them is what journal GUARANTEES:
- chart must be strictly ascending by Code (ErrChartNotOrdered). journal.Book.IterateAccounts is "ascending code order", so every producer meets it. It buys duplicate-code detection, chart-order output with no sort, and a binary-search code lookup instead of a map.
- entries must be strictly ascending by ID (ErrEntriesNotOrdered). journal.Book.IterateEntries and journal.Book.Entries both guarantee it. It buys duplicate-ID detection, the binary search that resolves ReversalOf, and ID >= 1 for free, 0 being ReversalOf's sentinel.
- entries must NOT be required in ascending At order, and this package must never require it. At is caller-supplied and backdating is legal until Close seals it, so journal guarantees nothing about it. Such a precondition would be one no producer in this portfolio can meet — a gate that can never pass, in an API signature rather than in a checker.
The reversal census has three buckets, and the third is the point
A reversal dated inside the window whose original is dated outside it changes what the period means: the window carries a correction to activity it never contained. The census reports three things and never two:
- [Report.Reversals] — in-window reversal, original also in-window: ordinary, classified.
- [Report.CrossWindowReversals] — in-window reversal, original dated outside: classified, and material.
- [Report.UnresolvedReversals] — in-window reversal whose original is ABSENT from the entries slice, so the classification COULD NOT BE MADE.
Collapsing the third into either of the first two would read "could not evaluate" as assent. It is a census, not a verdict — the same posture as statements.Statement.Abnormal — and it is a function of what the caller passed, not of the book: pass a window-filtered slice and every reversal reaching back before it is unresolved by construction.
What this package does not do
- **It never writes.** Report.ClosingEntry returns postings for the caller to Post. It sweeps PERIOD MOVEMENT, not cumulative balance, so running it twice over one window and posting both results double-sweeps — and this package never sees the Book, so it cannot detect that. The caller owns idempotence.
- **No paging, no Merge.** MaxEntries is the hard bound. Merging reports across pages would make UnresolvedReversals an artifact of the paging rather than of the data; fixing that needs a two-pass design over the whole entry set and is a different package.
- **No cash-flow statement.** The indirect method needs a cash-vs-accrual classification journal.Posting does not carry. [Report.Activity] is the balance-sheet delta half it would need.
- **No audit.** Every figure is derived from what the caller supplied. A chart from one book and entries from another produce a plausible report and nothing here detects it.
p/journal: /p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/journal p/statements: /p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/statements