Functional Weave
Code in Rust

subscriptions.mrr-arr

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

1.0.0 (not the latest) · 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.

pub fn mrr_arr(subscriptions: &[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

// BillingInterval is a string in Rust, one of: "day", "week", "month", "quarter", "year".
// Parameters take it as &str and results hold it as String.

// SubscriptionStatus is a string in Rust, one of: "active", "past-due", "trialing", "paused", "canceled".
// Parameters take it as &str and results hold it as String.

/// One subscription's recurring charge.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecurringCharge {
    /// charged every intervalCount intervals, after recurring discounts, before tax
    pub amount: Money,
    pub interval: String,
    /// 1 or more: month with 6 is every six months
    pub interval_count: i64,
    /// only active and past-due count towards MRR
    pub status: String,
}

/// Recurring revenue, each figure rounded once from the exact total.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MrrArr {
    pub mrr: Money,
    pub arr: Money,
    /// subscriptions included: active and past-due
    pub counted: i64,
    /// subscriptions left out: trialing, paused and canceled
    pub excluded: i64,
}

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

fune!(subscriptions.mrr-arr@^1);  // then call mrr_arr(…)
impl/rust.rs · 77 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.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_rational::{add_rational, multiply_rational, rational, rational_to_integer};  ← from math.rational ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value};  ← from money.amount ^1.0.0 · built alongside by fune

/// 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.
///
/// # Panics
/// Panics on a currency mismatch, a negative amount, an interval count below
/// 1, or an unknown interval or status.
pub fn mrr_arr(subscriptions: &[RecurringCharge], currency: &str) -> MrrArr {
    let zero = money(0, currency);
    let mut annual = rational(0, 1);
    let mut counted = 0;
    let mut excluded = 0;
    for s in subscriptions {
        assert_same_currency(&zero, &s.amount);
        if s.amount.minor < 0 {
            panic!("recurring amount must not be negative, received {}", s.amount.minor);
        }
        if s.interval_count < 1 {
            panic!("interval count must be 1 or more, received {}", s.interval_count);
        }
        let per_year: i64 = match s.interval.as_str() {
            "day" => 365,
            "week" => 52,
            "month" => 12,
            "quarter" => 4,
            "year" => 1,
            other => panic!(
                "unknown billing interval \"{}\": expected day, week, month, quarter or year",
                other
            ),
        };
        match s.status.as_str() {
            "active" | "past-due" => {
                counted += 1;
                annual = add_rational(&annual, &rational(s.amount.minor * per_year, s.interval_count));
            }
            "trialing" | "paused" | "canceled" => excluded += 1,
            other => panic!(
                "unknown subscription status \"{}\": expected active, past-due, trialing, paused or canceled",
                other
            ),
        }
    }
    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,
        excluded,
    }
}

pub fn recurring_charge_from_value(v: &Value) -> RecurringCharge {
    RecurringCharge {
        amount: money_from_value(v.get("amount")),
        interval: v.get("interval").as_str().to_string(),
        interval_count: v.get("intervalCount").as_i64(),
        status: v.get("status").as_str().to_string(),
    }
}

pub fn mrr_arr_to_value(r: &MrrArr) -> Value {
    Value::obj(vec![
        ("mrr", money_to_value(&r.mrr)),
        ("arr", money_to_value(&r.arr)),
        ("counted", Value::Int(r.counted)),
        ("excluded", Value::Int(r.excluded)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let subs: Vec<RecurringCharge> = args[0].as_arr().iter().map(recurring_charge_from_value).collect();
    mrr_arr_to_value(&mrr_arr(&subs, args[1].as_str()))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add subscriptions.mrr-arr
Download for Rust subscriptions.mrr-arr-1.0.0-rust.fune · 15,493 bytes sha256 c82499f2aef6bffe1f4cf6dbe271f00dce29dbd223e99801e0e68126503bfb90

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

The whole function, every language, is one file too: subscriptions.mrr-arr-1.0.0.fune, 19,756 bytes, sha256 c365b390da882b1e456067e309fd722583bc83f1878ef19af2c14429fd7b479c. 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

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.

Files

PathBytes
README.md1,814
impl/python.py2,038
impl/rust.rs2,957
impl/typescript.ts2,031
vectors.json6,335