Recurring schedules: total a series with "<amount> <period> for <duration>" - #116
Merged
Conversation
…ration>" `450 monthly for 18 months` now answers 8,100, `2000 every 2 weeks for 6 months` 26,000, and `£450 monthly for 18 months` `£8100.00`. Implemented as a normalizer rewrite to `amount * count`: the payment count is a whole number known from the literal tokens, so the engine's own arithmetic carries the currency and keeps an exact amount exact through the same money-multiply path. The rule matches at the amount, the token before the period word, so it fires ahead of implicit multiplication rather than fighting its word-by-word suppression, and it claims the period word only when the full `<amount> <period> for <N> <unit>` shape is present, leaving bare `monthly`/`weekly` and the investment/rate `for` grammar untouched. Periods: daily, weekly, monthly, yearly (annually), and `every N days/weeks/months/years`. The count is one payment per whole period on a scheduling year (12 months, 52 weeks, 365 days), so a part-period is not counted and a zero interval is a structured error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the headline totals, money riding along and staying exact where the amount is exact (`$0.10 weekly for 3 weeks` is 0.3, not the drifted double), every named period plus the every-N form, the whole-period floor on a part period, and the zero-interval error being contained to its line. A "what must keep working" block guards the investment `for` (`1000 for 3 years at 7%`), the loan-repayment phrase, the rate `$24 a day for a year`, the period words as variable names, and ordinary arithmetic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a "Recurring schedules" section to the money-and-finance syntax page, with runnable `solve` examples the doc test executes, and a changeset in the house voice covering the new forms, the money-exactness guarantee, and the partial-period rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
Benchmark comparison
Per suite
Warnings
No regression over threshold. |
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
A recurring schedule now adds itself up.
<amount> <period> for <duration>gives the total, so a subscription, salary or instalment plan does not have to be worked out elsewhere and typed back in as a number.Periods are
daily,weekly,monthly,yearly(alsoannually), andevery N days/weeks/months/years. Money rides along, and where the per-payment amount is exact so is the total ($12.99 monthly for 2 yearsis exactly$311.76).Why
Subscriptions, salaries and instalments are the most common thing anyone adds up in a note, and there was no way to express the series: the total had to be computed separately and typed in as a number, which is the part worth checking. Multiplication covered the whole-number case (
450 * 18) and nothing else. The date and finance packages both exist and neither models a series, so this sat between them.Closes #95
Tests
__tests__/bugs)npm run test:fullpassespackages/engine/__tests__/hardening/RecurringSchedules.spec.tsis the new hardening spec (34 tests): the headline totals, money riding along and staying exact where the amount is exact ($0.10 weekly for 3 weeksis0.3, not the drifted double, with the bare-decimal boundary asserted alongside), every named period plus the every-N form, the whole-period floor on a part period, and the zero-interval structured error being contained to its line. Its "what must keep working" block guards the investmentfor(1000 for 3 years at 7%), the loan-repayment phrase, the rate$24 a day for a year, the period words as ordinary variable names, and plain arithmetic.packages/engine/__tests__/docs/DocExamples.spec.tsexecutes the four new documentation examples.Documentation
docs/still pass, and any new ones are in asolveblock so they runnpm run lint:unitspasses, if a unit was added or renamed (no unit added)npm run lint:sidebarpasses, if a page was added (no page added)A "Recurring schedules" section was added to
docs/src/content/docs/syntax/money-and-finance.mdwith runnablesolveexamples, and.changeset/recurring-schedules.md(minor) describes it in the house voice.Checks
npm run verifynpm run lintnode scripts/check-comment-style.mjs --allAll green.
npm run test:fullalso passes (292 suites, 7023 tests).Anything worth a closer look
This touches the token normalizer, which runs on every keystroke, so a note on the approach and the two design decisions the issue asked for:
Result shape. The primary result is the total, a plain number or a currency amount. The number of payments is the secondary detail that produces it (total = per-payment amount times the whole payment count). The engine has no channel for a note beside a number, so it is the total that shows; the count is documented rather than rendered. Because the feature is a rewrite to a plain
amount * count, currency and exactness come for free from the existing money-multiply path (£12.99 * 24is exactly£311.76), and a bare decimal stays an ordinary float as it does everywhere else in the engine.Partial periods. The count is one payment per whole period, floored, on a scheduling year where a month is one of twelve, a week one of fifty-two and a day one of 365 (independent of the second-based unit table, which is what makes
every 2 weeks for 6 monthsa clean 13: twenty-six a year over half a year). A final part-period has not come due, soevery 2 weeks for 5 weeksis two payments, not three, and a span shorter than one whole period is zero. A count that lands on a whole number through floating point is snapped to it before the floor, so the clean cases stay clean. A non-positiveevery Ninterval is a structuredRECURRING_INTERVAL_NOT_POSITIVEparse error rather than a division by zero.Collision with
for. The word is shared with the investment grammar ($1,000 for 3 years at 7%) and the uom rate grammar ($24 a day for a year). The rule matches at the amount (the token before the period word) so it fires ahead of implicit multiplication at that position, rather than depending on that rule's word-by-word suppression (which fires beforeweeklybut not beforemonthly, sincemonthlycan start the fused phrasemonthly repayment on). It claims the period word only when the whole<amount> <period> for <N> <unit>shape is present, so a baremonthly/weeklystays a variable name and bothforgrammars are untouched, all asserted in the "what must keep working" block.Performance. The rule is O(1) per token position (a few type checks, returning null immediately for anything that is not a value ender followed by a period word), and each application strictly shrinks the token stream, so it cannot loop.