Functional Weave
Code in Rust

payroll.pro-rata-salary Unreviewed

Salary for part of a pay period and for part-time hours, by calendar days or working days, rounded once.

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

Pinned by 13 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 payroll specialist 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 payroll figures from published rules. It is a software component for developers, not tax or legal 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 payroll specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

The salary due for a starter's or leaver's partial pay period, and for a part-time worker, from the full-time salary for the period:

`pay = periodSalary x fteBasisPoints / 10000 x daysCounted / daysInPeriod`

For example

  • pro_rata_salary(£3,000.00, 100%, 2026-09-01, 2026-09-30, 2026-09-14, 2026-09-30, calendar-days, ) → pay £1,700.00, days counted 17, days in period 30 September starter by calendar days: 17 of 30
  • pro_rata_salary(£3,000.00, 100%, 2026-09-01, 2026-09-30, 2026-09-14, 2026-09-30, working-days, ) → pay £1,772.73, days counted 13, days in period 22 the same starter by working days: 13 of 22 gives a different answer
  • pro_rata_salary(£3,000.00, 60%, 2026-09-01, 2026-09-30, 2026-09-01, 2026-09-30, calendar-days, ) → pay £1,800.00, days counted 30, days in period 30 part-time for a whole 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 pro_rata_salary(period_salary: &Money, fte_basis_points: i64, period_start: &str, period_end: &str, worked_from: &str, worked_to: &str, method: &str, holidays: &[String]) -> ProRata
period_salaryMoneyfull-time salary for the whole pay period, e.g. annual salary / 12 for a monthly payroll
fte_basis_pointsint10000 full time; 6000 = three days of five
period_startdate
period_enddatelast day of the pay period, inclusive
worked_fromdate
worked_todatelast day employed in the period, inclusive
methodProRataMethod
holidaysdate[]dates that are not working days; used only by working-days
returnsProRata

The types it declares, generated into your project

// ProRataMethod is a string in Rust, one of: "calendar-days", "working-days".
// Parameters take it as &str and results hold it as String.

/// The amount and the fraction it came from.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProRata {
    pub pay: Money,
    /// days in the employed part of the period, by the method
    pub days_counted: i64,
    /// days in the whole period, by the same method
    pub days_in_period: i64,
}

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

fune!(payroll.pro-rata-salary@^1);  // then call pro_rata_salary(…)
impl/rust.rs · 73 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::dates_add_days::{add_days, days_between};  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_business_days_between::business_days_between;  ← from dates.business-days-between ^1.0.0 · built alongside by fune
use super::math_round_div::round_div;  ← from math.round-div ^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 count_days(from: &str, to: &str, method: &str, holidays: &[String]) -> i64 {
    match method {
        "calendar-days" => days_between(from, to) + 1,
        "working-days" => business_days_between(from, &add_days(to, 1), holidays),
        other => panic!("unknown pro-rata method \"{}\"", other),
    }
}

/// Pro-rate a period's salary by time employed and FTE, rounding once.
///
/// # Panics
/// Panics on an FTE outside 1-10000, reversed or out-of-period dates, an
/// unknown method, or a working-days period with no working days.
#[allow(clippy::too_many_arguments)]
pub fn pro_rata_salary(
    period_salary: &Money,
    fte_basis_points: i64,
    period_start: &str,
    period_end: &str,
    worked_from: &str,
    worked_to: &str,
    method: &str,
    holidays: &[String],
) -> ProRata {
    if !(1..=10000).contains(&fte_basis_points) {
        panic!("fteBasisPoints must be a whole number from 1 to 10000, received {}", fte_basis_points);
    }
    if days_between(period_start, period_end) < 0 {
        panic!("periodEnd is before periodStart");
    }
    if days_between(worked_from, worked_to) < 0 {
        panic!("workedTo is before workedFrom");
    }
    if worked_from < period_start || worked_to > period_end {
        panic!("the worked dates must fall inside the pay period");
    }
    let days_in_period = count_days(period_start, period_end, method, holidays);
    if days_in_period == 0 {
        panic!("the pay period has no working days to pro-rate over");
    }
    let days_counted = count_days(worked_from, worked_to, method, holidays);
    let minor = round_div(
        period_salary.minor * fte_basis_points * days_counted,
        10000 * days_in_period,
        "half-up",
    );
    ProRata { pay: money(minor, &period_salary.currency), days_counted, days_in_period }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let holidays: Vec<String> = args[7].as_arr().iter().map(|v| v.as_str().to_string()).collect();
    let r = pro_rata_salary(
        &money_from_value(&args[0]),
        args[1].as_i64(),
        args[2].as_str(),
        args[3].as_str(),
        args[4].as_str(),
        args[5].as_str(),
        args[6].as_str(),
        &holidays,
    );
    Value::obj(vec![
        ("pay", money_to_value(&r.pay)),
        ("daysCounted", Value::Int(r.days_counted)),
        ("daysInPeriod", Value::Int(r.days_in_period)),
    ])
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 4 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 payroll.pro-rata-salary
Download for Rust payroll.pro-rata-salary-1.0.1-rust.fune · 11,800 bytes sha256 6cebcb4576b95d0147269b831406905a96552ec7dac407c8aa66bbe3ecec932d

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./payroll.pro-rata-salary-1.0.1-rust.fune, or fetch it from a terminal with fune pull payroll.pro-rata-salary@1.0.1:rust.

The whole function, every language, is one file too: payroll.pro-rata-salary-1.0.1.fune, 15,940 bytes, sha256 f8bb34881684fca05492f85329cff6f12cb9fef7c6277d1903e38ba24f5bfdf0. 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 payroll.pro-rata-salary

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

// fune: after payroll.pro-rata-salary

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 dates.add-days in payroll.pro-rata-salary
// fune: replace dates.business-days-between in payroll.pro-rata-salary
// fune: replace math.round-div in payroll.pro-rata-salary
// fune: replace money.amount in payroll.pro-rata-salary

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 payroll.pro-rata-salary --steps.

// fune: step payroll.pro-rata-salary 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
September starter by calendar days: 17 of 30 £3,000.00, 100%, 2026-09-01, 2026-09-30, 2026-09-14, 2026-09-30, calendar-days, → pay £1,700.00, days counted 17, days in period 30
the same starter by working days: 13 of 22 gives a different answer £3,000.00, 100%, 2026-09-01, 2026-09-30, 2026-09-14, 2026-09-30, working-days, → pay £1,772.73, days counted 13, days in period 22
part-time for a whole month £3,000.00, 60%, 2026-09-01, 2026-09-30, 2026-09-01, 2026-09-30, calendar-days, → pay £1,800.00, days counted 30, days in period 30
leaver on the 10th £3,000.00, 100%, 2026-09-01, 2026-09-30, 2026-09-01, 2026-09-10, calendar-days, → pay £1,000.00, days counted 10, days in period 30
a bank holiday comes out of both counts £3,000.00, 100%, 2026-08-01, 2026-08-31, 2026-08-17, 2026-08-31, working-days, 2026-08-31 → pay £1,500.00, days counted 10, days in period 20
a leap February has 29 days £2,500.00, 100%, 2024-02-01, 2024-02-29, 2024-02-15, 2024-02-29, calendar-days, → pay £1,293.10, days counted 15, days in period 29
part-time and part-period together, rounded once £3,000.00, 80%, 2026-09-01, 2026-09-30, 2026-09-14, 2026-09-30, working-days, → pay £1,418.18, days counted 13, days in period 22
half a penny rounds up £10.01, 100%, 2026-09-01, 2026-09-02, 2026-09-02, 2026-09-02, calendar-days, → pay £5.01, days counted 1, days in period 2
worked dates outside the period are an error £3,000.00, 100%, 2026-09-01, 2026-09-30, 2026-08-25, 2026-09-30, calendar-days, → error: inside the pay period
reversed worked dates are an error £3,000.00, 100%, 2026-09-01, 2026-09-30, 2026-09-20, 2026-09-10, calendar-days, → error: workedTo is before workedFrom
Show the other 3 tests
CaseArgumentsExpected
an FTE of zero is an error £3,000.00, 0%, 2026-09-01, 2026-09-30, 2026-09-01, 2026-09-30, calendar-days, → error: fteBasisPoints must be
a weekend-only period has no working days £3,000.00, 100%, 2026-09-19, 2026-09-20, 2026-09-19, 2026-09-20, working-days, → error: no working days
an unknown method is an error £3,000.00, 100%, 2026-09-01, 2026-09-30, 2026-09-01, 2026-09-30, hours, → error: unknown pro-rata method

More from the author

computed exactly and rounded half-up to the penny once, at the end (math.round-div), so the fraction of a penny is never rounded twice.

Two methods, because employers use both and they give different answers:

- **calendar-days** counts every day: 14 to 30 September is 17 of 30 days. - **working-days** counts Monday to Friday, less the `holidays` supplied (dates.business-days-between): 14 to 30 September 2026 is 13 of 22 days.

Which one applies is a contractual choice, not a legal one. A third common method, 1/260th or 1/365th of the annual salary per day, is the calendar or working-days method applied to a year-long period: pass the annual salary with the tax year as the period.

Both date ranges are inclusive. The employed range must sit inside the pay period. A working-days period with no working days in it is an error, since the fraction would be 0/0.

## Before you rely on this

**Not professional advice.** This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal 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 payroll specialist 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 payroll specialist 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.md2,204
impl/python.py2,014
impl/rust.rs2,650
impl/typescript.ts1,963
vectors.json3,437