Functional Weave
Code in Rust

hospitality.tronc-allocation

Share a tips pool (tronc) between staff by hours worked, points, or hours times points, exactly to the penny.

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

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

What it does

Shares a pool of tips, gratuities and service charges between the people in it, in proportion to one of three weights:

- `hours`: minutes worked in the period; - `points`: each person's points under the venue's tipping policy (for example head chef 10, commis 4, kitchen porter 3); - `hours-and-points`: minutes worked times points, the usual "points per hour" scheme.

For example

  • tronc_allocation(£1,000.00, staff ×4, hours) → ×4 by hours: 40, 30, 20 and 10 hours share 1000.00
  • tronc_allocation(£100.00, staff ×3, hours) → ×3 equal hours, the odd penny goes to the first listed
  • tronc_allocation(£500.00, staff ×4, points) → ×4 by points: 10, 7, 4 and 3 points share 500.00 without losing a penny

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 tronc_allocation(pool: &Money, staff: &[TroncMember], method: &str) -> Vec<TroncShare>
poolMoneythe tips, gratuities and service charges to share, after nothing but tax
staffTroncMember[]everyone in the pool for the period, at least one
methodTroncMethodhours, points, or hours-and-points (minutes x points)
returnsTroncShare[]one share per member, in the order given; the shares add up to the pool exactly

The types it declares, generated into your project

/// One person in the pool.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TroncMember {
    /// unique within the pool
    pub id: String,
    /// time worked in the period, in minutes
    pub minutes_worked: i64,
    /// the member's points under the written tipping policy
    pub points: i64,
}

// TroncMethod is a string in Rust, one of: "hours", "points", "hours-and-points".
// Parameters take it as &str and results hold it as String.

/// What one person receives.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TroncShare {
    pub id: String,
    /// the number the pool was shared by: minutes, points, or minutes x points
    pub weight: i64,
    pub share: Money,
}

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

fune!(hospitality.tronc-allocation@^1);  // then call tronc_allocation(…)
impl/rust.rs · 91 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::money_allocate::allocate;  ← from money.allocate ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn weight_of(member: &TroncMember, method: &str) -> i64 {
    match method {
        "hours" => member.minutes_worked,
        "points" => member.points,
        "hours-and-points" => member.minutes_worked * member.points,
        other => panic!("unknown tronc method \"{}\"", other),
    }
}

/// Share a tips pool between staff in proportion to a weight.
///
/// Time is in minutes so a 7.5-hour shift is exact. The pool is split with
/// money.allocate, so the shares add up to the pool exactly: the leftover
/// pennies go to the largest remainders, ties to whoever is listed first.
///
/// # Panics
/// Panics on an empty pool of staff, a negative pool, duplicate ids, negative
/// minutes or points, an unknown method, or a pool nobody has weight in.
pub fn tronc_allocation(pool: &Money, staff: &[TroncMember], method: &str) -> Vec<TroncShare> {
    if staff.is_empty() {
        panic!("staff must have at least one member");
    }
    if pool.minor < 0 {
        panic!("pool must not be negative, received {}", pool.minor);
    }
    let mut ids: Vec<&str> = Vec::new();
    for member in staff {
        if member.id.is_empty() {
            panic!("every member needs an id");
        }
        if ids.contains(&member.id.as_str()) {
            panic!("staff id \"{}\" appears twice", member.id);
        }
        ids.push(&member.id);
        if member.minutes_worked < 0 {
            panic!(
                "minutesWorked must not be negative, received {} for \"{}\"",
                member.minutes_worked, member.id
            );
        }
        if member.points < 0 {
            panic!("points must not be negative, received {} for \"{}\"", member.points, member.id);
        }
    }
    let weights: Vec<i64> = staff.iter().map(|m| weight_of(m, method)).collect();
    if weights.iter().sum::<i64>() == 0 {
        if pool.minor != 0 {
            panic!("nobody in the pool has any weight to share it by");
        }
        return staff
            .iter()
            .map(|m| TroncShare { id: m.id.clone(), weight: 0, share: money(0, &pool.currency) })
            .collect();
    }
    let shares = allocate(pool, &weights);
    staff
        .iter()
        .zip(weights.iter().zip(shares))
        .map(|(m, (w, s))| TroncShare { id: m.id.clone(), weight: *w, share: s })
        .collect()
}

pub fn tronc_member_from_value(v: &Value) -> TroncMember {
    TroncMember {
        id: v.get("id").as_str().to_string(),
        minutes_worked: v.get("minutesWorked").as_i64(),
        points: v.get("points").as_i64(),
    }
}

pub fn tronc_share_to_value(s: &TroncShare) -> Value {
    Value::obj(vec![
        ("id", Value::str(&s.id)),
        ("weight", Value::Int(s.weight)),
        ("share", money_to_value(&s.share)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let staff: Vec<TroncMember> = args[1].as_arr().iter().map(tronc_member_from_value).collect();
    Value::Arr(
        tronc_allocation(&money_from_value(&args[0]), &staff, args[2].as_str())
            .iter()
            .map(tronc_share_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 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 hospitality.tronc-allocation
Download for Rust hospitality.tronc-allocation-1.0.0-rust.fune · 18,260 bytes sha256 5415855f96fb5ec24c376febfbd5090b1eb996e0511ace5b76afe79d9507ef25

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

The whole function, every language, is one file too: hospitality.tronc-allocation-1.0.0.fune, 23,042 bytes, sha256 2a06249aa9fe592eb58b2943d4d639adb5cac80e12b5c5743c97efbeab417afe. 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 hospitality.tronc-allocation

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

// fune: after hospitality.tronc-allocation

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 money.allocate in hospitality.tronc-allocation
// fune: replace money.amount in hospitality.tronc-allocation

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 hospitality.tronc-allocation --steps.

// fune: step hospitality.tronc-allocation 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
by hours: 40, 30, 20 and 10 hours share 1000.00 £1,000.00, staff ×4, hours → ×4
equal hours, the odd penny goes to the first listed £100.00, staff ×3, hours → ×3
by points: 10, 7, 4 and 3 points share 500.00 without losing a penny £500.00, staff ×4, points → ×4
hours and points: minutes worked times points £840.00, staff ×3, hours-and-points → ×3
hours and points on the floor team £1,000.00, staff ×4, hours-and-points → ×4
a 7.5 hour shift is exact in minutes £10.00, staff ×2, hours → ×2
someone who worked no hours gets nothing £10.01, staff ×2, hours → ×2
the leftover pennies go to the largest remainders, not the first listed £1.00, staff ×4, hours → ×4
an empty pool shares nothing £0.00, staff ×4, hours → ×4
an empty pool with nobody weighted is still nothing £0.00, staff ×1, hours → ×1
Show the other 9 tests
CaseArgumentsExpected
one person takes the whole pool €123.45, staff ×1, points → ×1
no staff is an error £1.00, , hours → error: staff must have at least one member
a negative pool is an error -£1.00, staff ×4, hours → error: pool must not be negative
a duplicate id is an error £1.00, staff ×2, hours → error: staff id "a" appears twice
negative minutes are an error £1.00, staff ×1, hours → error: minutesWorked must not be negative
negative points are an error £1.00, staff ×1, points → error: points must not be negative
a pool with nobody to share it by is an error £1.00, staff ×2, points → error: nobody in the pool has any weight to share it by
an unknown method is an error £1.00, staff ×4, seniority → error: unknown tronc method "seniority"
an empty id is an error £1.00, staff ×1, hours → error: every member needs an id

More from the author

The shares always add up to the pool exactly. The split is `money.allocate`: each person gets their proportional share rounded down, and the pennies left over go one at a time to the largest remainders, ties to whoever is listed first. So the same inputs always give the same split, and nobody's share is nudged by rounding more than a penny.

## Why minutes

Hours are often fractional (a 7.5-hour shift), and a fraction of an hour as a float is exactly the kind of drift this registry avoids. Minutes are whole numbers, and nothing smaller matters for a tronc.

## Edge cases

- Someone with a weight of 0 (no hours, or no points) receives 0. - An empty pool gives everyone 0. A pool with money in it but no weight on anybody is an error, because there is no fair way to split it. - Ids must be unique and not empty; minutes and points must not be negative.

## The law this does not enforce

Since 1 October 2024 the **Employment (Allocation of Tips) Act 2023** (https://www.legislation.gov.uk/ukpga/2023/13/contents), which inserted Part 2B into the Employment Rights Act 1996, requires employers in Great Britain to pass on all tips, gratuities and service charges they control or significantly influence to workers, without deductions other than tax, and to do so fairly. The **Code of Practice on Fair and Transparent Distribution of Tips**, brought into force by SI 2024/831 (https://www.legislation.gov.uk/uksi/2024/831/made; guidance at https://www.gov.uk/government/publications/distributing-tips-fairly-revised-statutory-code-of-practice and https://www.acas.org.uk/tips-and-service-charges), sets out what fair means in practice. A revised draft Code was published on 29 June 2026 and withdrawn on 29 July 2026 pending consultation, so the 2024 Code still applies.

This function does the arithmetic of a policy. It cannot check that the policy is lawful. In particular, the employer is responsible for:

- allocating fairly (ERA 1996 s27D): choosing fair factors and weights and applying them consistently. The Code discusses factors such as role, hours, seniority and performance; - **not** keeping a share for the business. An employer who is an individual and works alongside staff may only take part as the Act and the Code allow; - including agency workers who worked at the venue; - paying tips no later than the end of the month after the month they were paid (s27G); - having a written tipping policy available to workers (s27I), keeping records of the tips and how they were shared for three years, and answering a worker's written request for them within four weeks (s27J); - consulting workers on the tipping policy, a duty the Employment Rights Act 2025 adds. Check what is in force on the date you use this.

Tax and National Insurance on tronc payments (whether a troncmaster scheme takes the payments out of Class 1 NICs) are payroll matters and are not done here.

Files

PathBytes
README.md3,317
impl/python.py2,336
impl/rust.rs3,239
impl/typescript.ts2,260
vectors.json7,388