Functional Weave
Code in Rust

property.service-charge-apportion Unreviewed

Apportion a service charge across units by lease fraction, floor area or fixed percentage, exact to the penny.

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

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

Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified conveyancer or tax adviser has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

Not professional advice. This capability calculates property figures from published rules. It is a software component for developers, not legal or financial advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a conveyancer or tax adviser review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

Shares a building's service charge out between its units the way their leases say, and makes the pennies add up: the lines always total the service charge exactly, never a penny over or under.

Leases state a unit's share in one of three ways, chosen by `basis`:

For example

  • apportion_service_charge(£10,000.00, units ×3, fraction) → ×3 £10,000 in thirds: the odd penny goes to the first flat
  • apportion_service_charge(£100.01, units ×3, fraction) → ×3 1/3, 1/6, 1/2 of £100.01: £33.34, £16.67, £50.00, not £100.02 by separate rounding
  • apportion_service_charge(£100.00, units ×3, fraction) → ×3 a half and two quarters

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 apportion_service_charge(total: &Money, units: &[ServiceChargeUnit], basis: &str) -> Vec<ServiceChargeLine>
totalMoneythe service charge to share out; negative to share out a surplus or credit
unitsServiceChargeUnit[]every unit that pays, in the order the lines come back
basisApportionBasishow each unit's share is written
returnsServiceChargeLine[]one line per unit, in the same order; the charges add up to total exactly

The types it declares, generated into your project

// ApportionBasis is a string in Rust, one of: "fraction", "floor-area", "percentage".
// Parameters take it as &str and results hold it as String.

/// One unit and its share, as its lease states it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceChargeUnit {
    /// the flat or unit's name
    pub unit: String,
    /// fraction: the numerator; floor-area: the area in any one integer unit; percentage: basis points
    pub share: i64,
    /// fraction: the denominator; null for floor-area and percentage
    pub of: Option<i64>,
}

/// What one unit pays.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServiceChargeLine {
    pub unit: String,
    pub charge: Money,
}

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

fune!(property.service-charge-apportion@^1);  // then call apportion_service_charge(…)
impl/rust.rs · 100 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_gcd_lcm::lcm;  ← from math.gcd-lcm ^1.0.0 · built alongside by fune
use super::money_allocate::allocate;  ← from money.allocate ^1.0.0 · built alongside by fune
use super::money_amount::{money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

const MAX_SAFE: i128 = 9_007_199_254_740_991;

/// Share a service charge between units by lease fraction, floor area or
/// fixed percentage. Every share becomes an integer weight, and
/// money.allocate does the one split, so the lines always add up to the total
/// exactly.
///
/// # Panics
/// Panics on an unknown basis, no units, a negative share, a missing or
/// unexpected denominator, fractions or percentages that do not add up to the
/// whole, or shares too fine to split exactly.
pub fn apportion_service_charge(total: &Money, units: &[ServiceChargeUnit], basis: &str) -> Vec<ServiceChargeLine> {
    if basis != "fraction" && basis != "floor-area" && basis != "percentage" {
        panic!("unknown basis \"{}\": use fraction, floor-area or percentage", basis);
    }
    if units.is_empty() {
        panic!("units must not be empty");
    }
    for u in units {
        if u.share < 0 || u.share as i128 > MAX_SAFE {
            panic!("unit \"{}\": share must be a whole number, zero or more, received {}", u.unit, u.share);
        }
        if basis == "fraction" {
            match u.of {
                Some(of) if of > 0 && of as i128 <= MAX_SAFE => {}
                _ => panic!("unit \"{}\": a fraction needs a denominator \"of\" greater than zero", u.unit),
            }
        } else if u.of.is_some() {
            panic!("unit \"{}\": \"of\" is only for the fraction basis", u.unit);
        }
    }
    let weights: Vec<i128>;
    if basis == "fraction" {
        let mut common: i64 = 1;
        for u in units {
            common = lcm(common, u.of.unwrap());
        }
        weights = units.iter().map(|u| u.share as i128 * (common / u.of.unwrap()) as i128).collect();
        let sum: i128 = weights.iter().sum();
        if sum > MAX_SAFE {
            panic!("the fractions are too fine to apportion exactly");
        }
        if sum != common as i128 {
            panic!("the fractions add up to {}/{}, not exactly 1", sum, common);
        }
    } else {
        weights = units.iter().map(|u| u.share as i128).collect();
        let sum: i128 = weights.iter().sum();
        if basis == "percentage" && sum != 10000 {
            panic!("the percentages add up to {} basis points, not 10000", sum);
        }
        if sum == 0 {
            panic!("the floor areas add up to zero");
        }
    }
    let sum: i128 = weights.iter().sum();
    if sum > MAX_SAFE || (total.minor as i128).abs() * sum > MAX_SAFE {
        panic!("the fractions are too fine to apportion exactly");
    }
    let ratios: Vec<i64> = weights.iter().map(|w| *w as i64).collect();
    let parts = allocate(total, &ratios);
    units
        .iter()
        .zip(parts)
        .map(|(u, charge)| ServiceChargeLine { unit: u.unit.clone(), charge })
        .collect()
}

pub fn service_charge_unit_from_value(v: &Value) -> ServiceChargeUnit {
    let name = v.get("unit").as_str().to_string();
    let share = match v.get("share") {
        Value::Int(s) => *s,
        other => panic!("unit \"{}\": share must be a whole number, zero or more, received {:?}", name, other),
    };
    let of = match v.get("of") {
        Value::Null => None,
        Value::Int(o) => Some(*o),
        _ => panic!("unit \"{}\": a fraction needs a denominator \"of\" greater than zero", name),
    };
    ServiceChargeUnit { unit: name, share, of }
}

pub fn service_charge_line_to_value(line: &ServiceChargeLine) -> Value {
    Value::obj(vec![("unit", Value::str(&line.unit)), ("charge", money_to_value(&line.charge))])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let units: Vec<ServiceChargeUnit> = args[1].as_arr().iter().map(service_charge_unit_from_value).collect();
    Value::Arr(
        apportion_service_charge(&money_from_value(&args[0]), &units, args[2].as_str())
            .iter()
            .map(service_charge_line_to_value)
            .collect(),
    )
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 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 property.service-charge-apportion
Download for Rust property.service-charge-apportion-1.0.1-rust.fune · 21,258 bytes sha256 4d82e546c11a5252768e3865fedf30335a6e2641f40dd12297fbca3e9e6508b5

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./property.service-charge-apportion-1.0.1-rust.fune, or fetch it from a terminal with fune pull property.service-charge-apportion@1.0.1:rust.

The whole function, every language, is one file too: property.service-charge-apportion-1.0.1.fune, 26,765 bytes, sha256 5e9bff52e0f5eefba43b53d17b7c1b4ee788f4c0b2362f3c0ed1410b67baeaf1. 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 property.service-charge-apportion

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

// fune: after property.service-charge-apportion

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.gcd-lcm in property.service-charge-apportion
// fune: replace money.allocate in property.service-charge-apportion
// fune: replace money.amount in property.service-charge-apportion

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 property.service-charge-apportion --steps.

// fune: step property.service-charge-apportion 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
£10,000 in thirds: the odd penny goes to the first flat £10,000.00, units ×3, fraction → ×3
1/3, 1/6, 1/2 of £100.01: £33.34, £16.67, £50.00, not £100.02 by separate rounding £100.01, units ×3, fraction → ×3
a half and two quarters £100.00, units ×3, fraction → ×3
twentieths and quarters mixed: 3/20, 7/20, 1/4, 1/4 of £4,000 £4,000.00, units ×4, fraction → ×4
floor areas 50, 70 and 80 m² share £1,000 £1,000.00, units ×3, floor-area → ×3
areas in hundredths of a m²: the spare penny goes to the largest remainder £2,345.67, units ×3, floor-area → ×3
fixed percentages 33.33%, 33.33%, 33.34% of £100 £100.00, units ×3, percentage → ×3
a unit with a zero share pays nothing but keeps its line £500.00, units ×2, floor-area → ×2
a surplus is shared out the same way -£100.00, units ×3, fraction → ×3
one unit takes it all £1,234.56, units ×1, fraction → ×1
Show the other 9 tests
CaseArgumentsExpected
fractions that do not make a whole are refused £100.00, units ×2, fraction → error: the fractions add up to 2/3, not exactly 1
fractions over a whole are refused £100.00, units ×2, fraction → error: the fractions add up to 7/6, not exactly 1
percentages short of 100% are refused £100.00, units ×2, percentage → error: the percentages add up to 9999 basis points, not 10000
a fraction without a denominator is refused £100.00, units ×2, fraction → error: unit "A": a fraction needs a denominator
a denominator on a floor area is refused £100.00, units ×1, floor-area → error: unit "A": "of" is only for the fraction basis
a negative share is refused £100.00, units ×2, floor-area → error: unit "A": share must be a whole number
no units is refused £100.00, , floor-area → error: units must not be empty
all-zero floor areas are refused £100.00, units ×2, floor-area → error: the floor areas add up to zero
an unknown basis is refused £100.00, units ×1, rateable-value → error: unknown basis

More from the author

- **fraction**: "one-third", "3/20ths". Each unit gives `share` over `of`. The fractions must add up to exactly 1, since a lease scheme that does not recover the whole cost (or recovers more) is something to raise with the landlord, not to paper over. - **floor-area**: each unit's area, in any one integer unit (square feet, square metres, or square metres × 100 for 0.01 m² precision). The share is the unit's area over the total area. - **percentage**: basis points (2500 = 25%), adding up to exactly 10,000.

## Exact to the penny

Each unit's exact share is total × share, and the pennies left over after rounding every share down are given one at a time to the units with the largest remainders (ties to the earlier unit), by `money.allocate`. So a £100.01 charge split 1/3, 1/6, 1/2 is £33.34, £16.67 and £50.00. Rounding each share to the nearest penny separately would give £33.34, £16.67 and £50.01: £100.02, a penny that was never spent.

Fractions are brought to a common denominator exactly (with `math.gcd-lcm`), never converted to decimals.

## Edge cases

- A unit with a zero share pays nothing but still gets a line. - A negative total (a surplus refunded, a credit) is shared the same way. - `of` must be null unless the basis is fraction, and must be given (and be positive) when it is. - The shares' total, times the service charge in minor units, must stay below 2^53, so that every language computes the same split exactly. Denominators whose lowest common multiple is too large to share a charge that precisely are refused rather than approximated.

## What it does not do

It does not weight different cost heads differently (a lift schedule that excludes ground-floor flats is a second call with its own units), cap any unit's contribution, or handle reserve fund contributions separately.

## Before you rely on this

**Not professional advice.** This capability calculates property figures from published rules. It is a software component for developers, not legal or financial advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a conveyancer or tax adviser review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.

**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified conveyancer or tax adviser has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

1.0.1 marks it unreviewed. The code and the tests are unchanged.

Files

PathBytes
README.md3,264
impl/python.py2,628
impl/rust.rs4,061
impl/typescript.ts2,647
vectors.json9,079