Functional Weave
Code in Python

subscriptions.mrr-arr

Monthly and annual recurring revenue from a list of subscriptions, normalising intervals and leaving out trials.

1.1.0 · published 2026-10-03 by charlie · Anterra

Pinned by 19 tests, run in TypeScript, Python and Rust.

What it does

MRR (monthly recurring revenue) and ARR (annual recurring revenue) from the subscriptions a business has on one day. Every subscription is first turned into what it earns in a year, exactly:

| interval | a year is | |----------|----------------| | year | 1 interval | | quarter | 4 intervals | | month | 12 intervals | | week | 52 intervals | | day | 365 intervals |

For example

  • mrr_arr(subscriptions ×1, GBP) → mrr £49.00, arr £588.00, counted 1, excluded 0 one monthly plan at 49.00
  • mrr_arr(subscriptions ×1, GBP) → mrr £82.50, arr £990.00, counted 1, excluded 0 one annual plan at 990.00 is 82.50 a month
  • mrr_arr(subscriptions ×1, GBP) → mrr £8.33, arr £100.00, counted 1, excluded 0 an annual plan of 100.00 is 8.33 a month

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

def mrr_arr(subscriptions: Sequence[RecurringCharge], currency: str) -> MrrArr
subscriptionsRecurringCharge[]one entry per subscription, all in currency
currencystringthe currency of the result, required so an empty list still has one
returnsMrrArr

The types it declares, generated into your project

SubscriptionStatus = Literal["active", "past-due", "trialing", "paused", "canceled"]

@dataclass(frozen=True)
class RecurringCharge:
    """One subscription's recurring charge."""

    #: charged every intervalCount intervals, after recurring discounts, before tax
    amount: Money
    interval: BillingInterval
    #: 1 or more: month with 6 is every six months
    interval_count: int
    #: only active and past-due count towards MRR
    status: SubscriptionStatus

@dataclass(frozen=True)
class MrrArr:
    """Recurring revenue, each figure rounded once from the exact total."""

    mrr: Money
    arr: Money
    #: subscriptions included: active and past-due
    counted: int
    #: subscriptions left out: trialing, paused and canceled
    excluded: int

Your code names it in one line, in the file that uses it

from fune.subscriptions.mrr_arr import mrr_arr  # subscriptions.mrr-arr@^1
impl/python.py · 49 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import Dict, Sequence

from .math_rational import add_rational, multiply_rational, rational, rational_to_integer  ← from math.rational ^1.0.0 · built alongside by fune
from .money_amount import assert_same_currency, money  ← from money.amount ^1.0.0 · built alongside by fune
from .subscriptions_mrr_arr_types import MrrArr, RecurringCharge

# 1.0.0 declared its own BillingInterval, which callers imported from this
# module; it is now subscriptions.next-billing-date's, imported here so they
# still can.
from .subscriptions_next_billing_date import BillingInterval  ← from subscriptions.next-billing-date ^1.0.0 · built alongside by fune

PER_YEAR: Dict[BillingInterval, int] = {"day": 365, "week": 52, "month": 12, "quarter": 4, "year": 1}


def mrr_arr(subscriptions: Sequence[RecurringCharge], currency: str) -> MrrArr:
    """MRR and ARR from active and past-due subscriptions. Every charge becomes
    an exact annual amount first, and each total is rounded once at the end,
    so a list of annual plans does not lose a penny per plan.
    """
    zero = money(0, currency)
    annual = rational(0, 1)
    counted = 0
    excluded = 0
    for s in subscriptions:
        assert_same_currency(zero, s.amount)
        if s.amount.minor < 0:
            raise ValueError("recurring amount must not be negative, received %d" % (s.amount.minor,))
        if isinstance(s.interval_count, bool) or not isinstance(s.interval_count, int) or s.interval_count < 1:
            raise ValueError("interval count must be 1 or more, received %r" % (s.interval_count,))
        if s.interval not in PER_YEAR:
            raise ValueError(
                'unknown billing interval "%s": expected day, week, month, quarter or year' % (s.interval,)
            )
        if s.status in ("active", "past-due"):
            counted += 1
            annual = add_rational(annual, rational(s.amount.minor * PER_YEAR[s.interval], s.interval_count))
        elif s.status in ("trialing", "paused", "canceled"):
            excluded += 1
        else:
            raise ValueError(
                'unknown subscription status "%s": expected active, past-due, trialing, paused or canceled'
                % (s.status,)
            )
    return MrrArr(
        mrr=money(rational_to_integer(multiply_rational(annual, rational(1, 12)), "half-up"), currency),
        arr=money(rational_to_integer(annual, "half-up"), currency),
        counted=counted,
        excluded=excluded,
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add subscriptions.mrr-arr
Download for Python subscriptions.mrr-arr-1.1.0-python.fune · 15,133 bytes sha256 dfa091b1b0bfbd68854af9be88cdac473576ed67c6512e8c05b76b299dbfe498

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./subscriptions.mrr-arr-1.1.0-python.fune, or fetch it from a terminal with fune pull subscriptions.mrr-arr@1.1.0:python.

The whole function, every language, is one file too: subscriptions.mrr-arr-1.1.0.fune, 20,656 bytes, sha256 00720786d3b507dce9b22f2e8966b5ec051a2a993e0df8eef8ed7f42d5596938. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

# fune: before subscriptions.mrr-arr

after — your function gets the result and the arguments, and returns the final result.

# fune: after subscriptions.mrr-arr

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

# fune: replace math.rational in subscriptions.mrr-arr
# fune: replace money.amount in subscriptions.mrr-arr
# fune: replace subscriptions.next-billing-date in subscriptions.mrr-arr

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show subscriptions.mrr-arr --steps.

# fune: step subscriptions.mrr-arr after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
one monthly plan at 49.00 subscriptions ×1, GBP → mrr £49.00, arr £588.00, counted 1, excluded 0
one annual plan at 990.00 is 82.50 a month subscriptions ×1, GBP → mrr £82.50, arr £990.00, counted 1, excluded 0
an annual plan of 100.00 is 8.33 a month subscriptions ×1, GBP → mrr £8.33, arr £100.00, counted 1, excluded 0
three annual plans of 100.00 are 25.00 of MRR, not 3 x 8.33 = 24.99 subscriptions ×3, GBP → mrr £25.00, arr £300.00, counted 3, excluded 0
monthly, annual and quarterly plans together subscriptions ×3, GBP → mrr £67.33, arr £808.00, counted 3, excluded 0
a trial is left out of MRR and counted as excluded subscriptions ×2, GBP → mrr £49.00, arr £588.00, counted 1, excluded 1
past-due still counts; paused and canceled do not subscriptions ×3, GBP → mrr £49.00, arr £588.00, counted 1, excluded 2
a weekly plan uses 52 weeks to the year subscriptions ×1, GBP → mrr £43.33, arr £520.00, counted 1, excluded 0
a daily plan uses 365 days to the year subscriptions ×1, GBP → mrr £30.42, arr £365.00, counted 1, excluded 0
every six months: the price is earned twice a year subscriptions ×1, GBP → mrr £50.00, arr £600.00, counted 1, excluded 0
Show the other 9 tests
CaseArgumentsExpected
every two years: half the price each year subscriptions ×1, GBP → mrr £8.33, arr £100.00, counted 1, excluded 0
half a minor unit of MRR rounds up subscriptions ×1, GBP → mrr £0.01, arr £0.06, counted 1, excluded 0
no subscriptions is zero in the requested currency , EUR → mrr €0.00, arr €0.00, counted 0, excluded 0
only trials: nothing counted subscriptions ×1, GBP → mrr £0.00, arr £0.00, counted 0, excluded 1
yen subscriptions ×2, JPY → mrr ¥1,980, arr ¥23,760, counted 2, excluded 0
a subscription in another currency is an error subscriptions ×1, GBP → error: currency mismatch
a negative amount is an error subscriptions ×1, GBP → error: recurring amount must not be negative
an interval count of zero is an error subscriptions ×1, GBP → error: interval count must be 1 or more
an unknown status is an error subscriptions ×1, GBP → error: unknown subscription status

More from the author

divided by `intervalCount` (a 6-monthly plan earns its price twice a year). ARR is the sum of those, and MRR is the same sum divided by 12. Both are rounded once, half-up to the minor unit, from the exact fraction (math.rational). Rounding each subscription's monthly share first and adding up is the naive way and it drifts: three annual plans of 100.00 are 25.00 of MRR, but 8.33 + 8.33 + 8.33 is 24.99. Because each figure is rounded separately, MRR x 12 can differ from ARR by a few minor units; both are right.

Weeks and days are conventions, not calendar facts: 52 weeks and 365 days to the year. Businesses that use 52.14 weeks or 365.25 days get slightly different figures for weekly and daily plans; monthly, quarterly and annual plans are exact under any convention.

What counts. `active` and `past-due` subscriptions count: a failed payment still in dunning has not churned yet, so it stays in MRR until it is cancelled. (Metrics tools differ on this; filter past-due out before calling if yours does.) `trialing` subscriptions are left out (no revenue yet), as are `paused` and `canceled` ones. The amount should be the recurring price after recurring discounts and before tax; one-off charges, usage and setup fees are not recurring revenue and do not belong here.

Errors: an amount in a currency other than `currency`, a negative amount, and an interval count below 1.

Since 1.1.0 the `interval` of a `RecurringCharge` is the `BillingInterval` declared by `subscriptions.next-billing-date` (the same five values, `day`, `week`, `month`, `quarter` and `year`), rather than a copy declared here, so a subscription record typed for one capability is accepted by the other. `BillingInterval` can still be imported from this capability's module in TypeScript and Python, which re-export it. The answers are the same as 1.0.0's.

Files

PathBytes
README.md2,269
impl/python.py2,299
impl/rust.rs2,957
impl/typescript.ts2,317
vectors.json6,335