From Warm-Up to the Hard Climb: Contributing ACCRINTM and ACCRINT to a Rust Spreadsheet Engine
The warm-up: shipping ACCRINTM
Last spring I set out to add two functions to IronCalc, an open-source spreadsheet engine written in Rust. Both compute accrued interest on bonds. One took a couple of focused sessions. The other turned into a multi-week investigation that ended in a public discussion with the maintainer about which of two Microsoft documents is “correct.” This is the story of both, and of the working method that let the hard one span weeks without losing the thread.
I started with the easy one on purpose. ACCRINTM computes the accrued interest on a security that pays interest only at maturity, one period, one division. Its harder sibling ACCRINT handles securities that pay periodically, which means walking a schedule of coupon periods. I wanted ACCRINTM first not because the project needed it first, but because I needed a warm-up, a small, well-bounded change that would teach me the codebase’s conventions before I attempted anything genuinely hard.
That instinct paid off immediately, because the warm-up was where I tripped. The maintainer had left a scaffold branch, and my first move was to cherry-pick from it. It fought back. Rather than force the merge, I stopped and asked a different question: what is the canonical way this project adds a function? An hour of reading the existing financial functions answered it. There is a precise, repeatable pattern: a new variant in the Function enum, a lexer mapping, a name-lookup arm, a dispatch arm, an entry in a fixed-size iteration array, an argument signature for the static analyzer, the function name in five locales, and a regenerated localization binary. Miss one and the build breaks in a way that is obvious. Miss the array-size constant and it compiles clean but is quietly wrong.
Writing that pattern down, once, turned out to be the most valuable thing I did in the warm-up. I would lean on it every subsequent session, and it is the reason the harder function went smoothly later even when I rebuilt it from scratch twice.
ACCRINTM itself was small: delegate the day-count to the engine’s existing YEARFRAC, validate the inputs, return #NUM! on the documented error conditions. Thirteen unit tests, an Excel-authored round-trip fixture, and it merged. The interesting part was not the code. It was that, by the time it merged, I had a written map of how this codebase wants to be extended, and a habit of stopping to find the canonical path instead of forcing the first one.
Why ACCRINT was the hard one
On the surface, ACCRINT looks like ACCRINTM with three more arguments. That framing is a trap, and falling for it early would have cost me.
ACCRINTM has a single accrual span: issue date to maturity, one day-count, one division. ACCRINT models a bond that pays interest on a schedule, so its accrual is a sum over coupon periods. The full signature is ACCRINT(issue, first_interest, settlement, rate, par, frequency, [basis], [calc_method]), and almost every added argument opens a question that has no obvious answer from the function’s one-line documentation.
frequency is 1, 2, or 4 payments a year, so the accrual interval has to be sliced into quarterly, semi-annual, or annual periods. But the period boundaries are not given to you. They have to be derived by walking backward from first_interest in steps of 12/frequency months, and that walk has to snap to the end of the month: step back three months from August 31 and you land on May 31, not May 28, and from there to February 29 in a leap year. Get the snapping wrong and every downstream day-count is off.
basis is the day-count convention, five of them (30/360 in two flavors, Actual/360, Actual/365, Actual/Actual), and it changes both the numerator (accrued days in a period) and the denominator (the period’s “normal length”). The Actual/Actual case in particular makes the denominator depend on the specific period rather than a fixed constant.
calc_method is a boolean that almost nobody thinks about: it selects whether accrual starts at the issue date or at the first interest date. The two disagree only in an unusual configuration, which is exactly the kind of edge a naive implementation gets wrong silently.
And then there is the case that ate the most time, the odd-long first coupon. When the first coupon period is longer than a regular one, the front stub of accrual is not measured against its own length, it is normalized against the length of a regular coupon. That single rule, which is not stated plainly anywhere in the user-facing documentation, is the difference between matching the reference and being off by a few hundredths on exactly the inputs a reviewer is most likely to try.
So ACCRINT is not “ACCRINTM with more arguments.” It is a small scheduling engine with four interacting dimensions, sitting on top of a day-count layer, specified by documentation that tells you the argument names and almost nothing about the math. The warm-up taught me the codebase. The hard function would force me to go find the actual specification, because the obvious source did not contain it.
Research before code: finding the real spec
My first instinct was to read Microsoft’s documentation for ACCRINT and implement what it said. The problem: it does not say much. The support page lists the arguments and the error conditions, and that is essentially it. No formula. No worked example. For a function with a four-dimensional accrual model, “here are the argument names” is not a specification.
So the real work of this project started before I wrote a line of the algorithm: finding the spec that actually exists. It turned out to live in two places, neither of them the obvious one.
The first is Standard Securities Calculation Methods, the fixed-income formula book the U.S. securities industry has treated as canonical since the 1990s. This is where accrued-interest math is actually defined, with the precise vocabulary the spreadsheet docs gloss over: the quasi-coupon period, the number of days in the current period, the first coupon date after settlement. The second is a sibling Microsoft document I almost missed, the DAX version of ACCRINT (the function as implemented in Power BI). Unlike the spreadsheet support page, the DAX page publishes the formula explicitly and includes two worked examples, including the subtle case where the calc_method boolean changes the answer. The function end-users are pointed at is documented thinly; the same vendor’s database-engine documentation of the same function is rigorous. That gap is the whole story of why this was hard.
Once I had a formula, the most useful thing I did was not to trust any single source but to map where the implementations disagree. I read how LibreOffice and OpenOffice implement ACCRINT (both quietly collapse it to the single-period case, ignoring the first-interest date entirely, the same shortcut a naive reading invites). I read how Google Sheets renames the argument and drops calc_method altogether. Each divergence is a signpost: where implementations differ is exactly where the specification is ambiguous, and seeing five readings side by side reconstructs the boundaries of what the spec actually constrains. The exercise turned a thin support page into a clear picture of the decisions an implementation has to make and which ones are genuinely underdetermined.
This is the first place the way I was working earned its keep. Reading two standards documents and four reference implementations, then synthesizing them into a single audit of where they agree and diverge, is the kind of breadth-first research that is straightforward to describe and brutal to actually do in an afternoon. Done as a human-AI collaboration, it was tractable in a sitting, and it produced a written audit I would return to in every later session. I will come back to that working method near the end. For now the point is that the research, not the code, was the hard part, and skipping it would have produced a confident, wrong implementation that passed its own tests.
By the end of the research phase I had what the documentation never gave me: an explicit formula, a set of worked reference values, and a catalogue of exactly which behaviors were genuine decisions rather than facts. Only then was it worth writing code.
Testing against an oracle
With a real formula in hand, the implementation went the way the warm-up had taught me: the period-walk, the day-count per basis, the validation, wired in through the project’s canonical pattern. The hard part of this phase was not writing the code. It was knowing whether it was right.
I tested against two oracles, and the gap between them taught me the most useful lesson of the project. The first oracle was a suite of unit tests whose expected values I computed by hand from the formula and the worked examples. They passed. That felt like success, and it was misleading. The second oracle was an Excel-authored fixture: a spreadsheet of ACCRINT calls, saved by real Excel, whose cached results the engine has to reproduce when it re-evaluates the file on load. That round-trip immediately surfaced bugs the hand-computed tests had sailed past, because my hand-computed cases happened to all be semi-annual and the failures lived in the quarterly and the short-span configurations I had not thought to compute by hand. A test suite built entirely from your own reading of the spec validates your reading, not your code. You need an oracle you did not author.
The deepest bug was the odd-long first coupon from earlier, the case where the front stub is normalized against a regular coupon length rather than its own. I could see the engine was off by a small amount on exactly those inputs, but the formula sources did not spell out the rule, so I had to recover it empirically. The technique that cracked it was to stop guessing and instead build an input where the answer inverts to the unknown: pick dates so the accrual falls inside a single quasi-coupon period, and Excel’s output is then a direct readout of the internal numerator it used. From a handful of those isolating cases the rule fell out, the settlement period’s accrued days, minus a constant offset accumulated from the interior periods’ drift against the regular length. I validated that model against fifteen Excel reference points before I trusted it in Rust, and tested every candidate against all of them at once rather than fitting one case and hoping.
Somewhere in the middle of this, the branch I was working on had drifted far behind the upstream project, far enough that a rebase meant grinding the same conflicts across a long chain of commits. The call we made was to not rebase at all: start a fresh branch off current upstream and re-apply the finished, validated work onto it. It sounds drastic, and the first time I reached for it, it felt like throwing work away. It was the opposite. Because every decision and every oracle value was written down, the rebuild was mechanical and fast, and a file-by-file diff against the old branch confirmed the new one was byte-identical where it should be. The one thing that diff caught, and that nothing else would have, was a fixed-size array constant that had moved in upstream: the new code compiled cleanly with the stale number and would have been quietly, invisibly wrong. I did the redo-from-scratch twice over the life of these two functions, and both times it was faster and safer than the rebase it replaced.
The fixture deserves one more word, because it becomes the spine of what follows. It is authored in Excel, which means its cached numbers are not my interpretation of correct, they are the reference, reproducible by anyone who opens the file. Every numeric claim I would later make in public rested on that property. When you can hand someone a file and say “open this and check,” disagreements stop being about authority and start being about evidence.
The divergence that had no rule
Eventually everything matched, except one case. At quarterly frequency, when a coupon-period boundary fell on the last day of a month, the documented formula and shipping Excel disagreed by exactly one day of accrued interest. Every other configuration I tested agreed to the last decimal. This one would not.
The tempting move was to just make the code match Excel. Excel is what people have on their desks; “build what you can actually test against the product” is a reasonable principle. So before touching anything, I tried to learn the rule. I built a grid of twelve cases that varied one thing, whether the relevant boundary landed on an end-of-month date, holding everything else fixed, and I wrote down the value the documented formula predicted for each one before running any of them. Then I computed all twelve in Excel and compared.
The result demolished the easy answer. The deviation was always exactly one day, but its sign flipped: some end-of-month cases came out a day high, one came out a day low, and several end-of-month cases showed no deviation at all. It was not “end-of-month rounds up.” It was not Feb-specific. It was not any rule I could state from the inputs I had varied. “Match Excel exactly” had looked like a one-line change; the probe showed it was an unsolved reverse-engineering problem. I could have tweaked the code to pass the one case that started all this, but the sign-flip guaranteed that any such fit would silently break the cases I had not tested. A patch that passes the example in front of you and quietly fails the ones that are not is worse than an honest, documented difference.
What made sense of the disagreement was not more arithmetic, it was history. The two values come from different generations of the same lineage. The accrued-interest math traces to the industry’s standard formula book from the early 1990s. Excel’s version of the function began life as an add-in port in that era and was folded into the product as a built-in around 2007, formulas carried over largely as-is. Microsoft’s documented formula, the rigorous one with worked examples, came later, with the database-engine generation of the function. Seen that way, the one-day wobble is most plausibly a frozen artifact of a decades-old port, and the documented spec is a newer, cleaner re-derivation of the same underlying math. The question stopped being “two Microsoft numbers disagree, which is right” and became “which generation of the same vendor’s work is the one to anchor to.”
That is not a call a contributor should make silently inside a pull request. So I took it to the maintainer in the open: a public discussion laying out the twelve-case grid, the structural reason the deviation is plus-or-minus a day, the historical provenance of the two sources, and, crucially, the exact formulas so anyone could reproduce every number themselves. I had re-run every value through the engine and checked every source link before posting, because a public artifact with a dead link or a stale number undercuts the very credibility it is meant to build.
The maintainer’s answer, on the public record, was to go with the documented specification, which is what the implementation already did. The decision took him minutes, and I think the reason is worth naming: he was not asked to take my word for anything. He was handed a grid he could paste into his own copy of Excel, a clearly stated hypothesis, and the historical context, and he could verify the whole thing in the time it takes to read it. The disagreement never had to be about authority. It was about evidence anyone could check.
The function merged shortly after. The one divergent case ships as the documented value, with the difference recorded honestly rather than papered over, and it is excluded from the Excel round-trip fixture precisely because it is the one place the engine intentionally does not match the spreadsheet. Every other case in that fixture matches Excel exactly.
How this was actually worked
I have been describing this as one continuous push. It was not. The two functions spanned roughly twenty working sessions over several weeks, fitted around the rest of life. That gap, between how it reads and how it happened, is the part I most want to write about, because the bond math above is ordinary. Plenty of people can derive a day-count formula. What made this genuinely finishable, across weeks of stop-and-start work, was not skill at finance. It was that I was running the whole effort on a framework I built for exactly this kind of problem.
Take a Bite (TAB) is a practice for human-AI collaboration that I designed to make complex, long-running work hold together over weeks. The reason TAB carried a twenty-session project rather than just tidying up a single afternoon is that it is more than a sizing rule: it is a full, versioned methodology that turns the right-sized bite into durable practice. Three properties of this contribution I would like to highlight came out of that by design.
Continuity. Context never lived in a chat window or in my head; it accumulated in durable artifacts as it was produced, decisions and their rationale, every reference value from Excel, the canonical pattern for extending the codebase. The proof is the redo-from-scratch from the testing section. Twice I discarded the working branch and rebuilt the entire change on fresh upstream, and twice it came back byte-identical in minutes, because a rebuild from written decisions is transcription, not recall. You cannot do that from memory after a three-week gap. The framework meant I never had to.
Compounding lessons. The framework captures reasoning as the work happens, and over these two functions that record grew to nearly a hundred specific, reusable lessons: the conflict count past which you stop rebasing and rebuild, always tag a backup before a destructive git step, when you port onto a moved-forward base re-derive the magic constants against the current target. That last one caught the silent array-size bug from the testing section. Not by cleverness in the moment, but because an earlier session had recorded “this class of constant drifts, check it,” and the framework surfaced that lesson when the situation recurred. Mistakes become standing checks, automatically, across sessions. That accumulation is the thing a framework gives you that ad-hoc care cannot.
A human genuinely in the loop. This is the design goal so it is worth being honest about which side carried which weight. Several steps here would have been brutal to do alone in any reasonable time, and the collaboration is what made them tractable: reading two standards documents and four reference implementations into a single map of where they agree and diverge; generating and testing candidate models for the odd-long-coupon rule against fifteen reference points at once; designing a twelve-case probe built specifically to falsify a hypothesis about Excel’s behavior, and some other steps. That is patient, high-volume, error-prone reasoning, and it is what the AI side of the collaboration is good at. TAB’s term for it is magnification: the human brings the direction, the model amplifies it.
But amplification is worthless without something to amplify, and the framework is built around that asymmetry rather than in spite of it. The decision to warm up on the easy function was mine. The domain access that grounded every claim, a real copy of Excel, and a working relationship with the maintainer, was mine. Every pivot in this story was a human call: to abandon the rebase and rebuild, to not blindly match Excel but probe first and keep the documented spec, to reframe a disagreement as a question of historical provenance, to take it public rather than settle it silently in a pull request, and many more. The assistant proposed, computed, checked, and remembered. I set the direction and made the calls following my design in Take a Bite. Even the reproducibility that turned the maintainer’s sign-off into a five-minute read came from both sides at once: the framework’s insistence on producing artifacts anyone can check, and my insistence that a public claim has to be one anyone can verify.
That is what I mean when I say the method is why these functions exist. The code is a few hundred lines of Rust. The framework is what let a part-time contributor carry a genuinely hard problem across three weeks without dropping the thread, and arrive with an answer the maintainer could trust on sight.
What I’d keep: reproducibility as the deliverable
If I had to keep one thing from this project and throw away the rest, it would not be the algorithm. It would be the habit of making every claim reproducible by someone who is not me.
Look back at where the work actually turned. The research phase produced a written audit anyone could check against the same public sources. The testing phase rested on a fixture authored in real Excel, whose numbers are not my opinion of correct but the reference itself, openable by anyone who doubts them. The one genuine disagreement, the end-of-month divergence, was settled not by my arguing for a reading but by handing over a twelve-case grid and the exact formulas, so the maintainer could paste them into his own spreadsheet and watch the pattern appear. At every step, the deliverable was not “trust my result,” it was “here is how to get the result yourself.”
That is what made the ending fast. A maintainer approving a contribution from someone without much standing in the project has every reason to be cautious, and caution usually means delay. What collapsed the delay was that there was nothing to take on faith. The evidence was self-checking. He spent his minutes verifying, not trusting, and a decision built on verification is one that stays decided.
The lesson generalizes past spreadsheets and past Rust. For a part-time contributor to an unfamiliar codebase, the patch is the cheap part. Anyone can write a few hundred lines. What is scarce, and what actually earns a merge, is evidence a reviewer can stand on without having to relitigate your reasoning: a fixture they can open, a formula they can paste, a divergence they can reproduce, a decision recorded in the open with the data attached. The investigation, packaged so someone else can repeat it, is the real artifact. The accrued-interest functions are just where this one happened to live.
References
- ACCRINT pull request: https://github.com/ironcalc/IronCalc/pull/1077
- The end-of-month divergence discussion: https://github.com/ironcalc/IronCalc/discussions/1076
- ACCRINTM pull request: https://github.com/ironcalc/IronCalc/pull/865
- Microsoft DAX
ACCRINT(documented formula and worked examples): https://learn.microsoft.com/en-us/dax/accrint-function-dax - Microsoft Excel
ACCRINTsupport page: https://support.microsoft.com/en-us/office/accrint-function-fe45d089-6722-4fb3-9379-e1f911d8dc74 - Standard Securities Calculation Methods (publisher): https://www.sscmfi.com/
- IronCalc: https://github.com/ironcalc/IronCalc