Functional Weave
Code in Rust

payroll.tax-period Unreviewed

The UK PAYE tax week, tax month and payroll period number that a pay date falls in, weeks 53, 54 and 56 included.

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

Pinned by 16 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

Every PAYE and National Insurance calculation starts by asking which tax week or tax month a payment falls in. The answer depends only on the date the employee is paid, never on the dates the work was done: a monthly salary paid on 28 April is month 1, and one paid on 3 May is still month 1.

- Tax week 1 is 6 to 12 April, week 2 is 13 to 19 April, and so on. Week 52 ends on 3 or 4 April, so the last one or two days of every tax year (5 April, or 4 and 5 April when the tax year contains 29 February) are **week 53**. - Tax month 1 is 6 April to 5 May, month 2 is 6 May to 5 June, and so on to month 12, 6 March to 5 April. - `period` is what `payroll.income-tax` and the other payroll capabilities take. For monthly pay it is the tax month. For weekly, fortnightly and four-weekly pay it is the tax week of the pay date, because HMRC's cumulative tables for those frequencies are indexed by week: a fortnightly payroll paid in weeks 2, 4, 6 uses the week 2, 4 and 6 figures. A payday in week 53 is the extra payday of the year and HMRC calls it week 53 (weekly), week 54 (fortnightly) or week 56 (four-weekly); those periods are taxed on a non-cumulative basis, which is why they get their own numbers.

For example

  • tax_period(2026-04-06, monthly) → tax year 2026/27, tax year start 2026-04-06, week 1, month 1, period 1 6 April is week 1 and month 1
  • tax_period(2026-04-12, weekly) → tax year 2026/27, tax year start 2026-04-06, week 1, month 1, period 1 12 April is still week 1
  • tax_period(2026-04-13, weekly) → tax year 2026/27, tax year start 2026-04-06, week 2, month 1, period 2 13 April starts week 2

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 tax_period(pay_date: &str, frequency: &str) -> TaxPeriod
pay_datedatethe date the employee is paid, not the end of the period worked
frequencyPayFrequencyhow often this employee is paid
returnsTaxPeriod

The types it declares, generated into your project

// PayFrequency is a string in Rust, one of: "weekly", "fortnightly", "four-weekly", "monthly".
// Parameters take it as &str and results hold it as String.

/// Where a pay date sits in the UK tax year.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaxPeriod {
    /// HMRC's label, e.g. 2026/27
    pub tax_year: String,
    /// 6 April of that tax year
    pub tax_year_start: String,
    /// tax week 1-53: week 1 is 6-12 April
    pub week: i64,
    /// tax month 1-12: month 1 is 6 April to 5 May
    pub month: i64,
    /// the period PAYE and NICs are worked out for: the month when paid monthly, otherwise the tax week, with week 53 reported as 53, 54 or 56 for weekly, fortnightly or four-weekly pay
    pub period: i64,
}

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

fune!(payroll.tax-period@^1);  // then call tax_period(…)
impl/rust.rs · 62 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::{days_between, parse_iso_date};  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_uk_tax_year::uk_tax_year;  ← from dates.uk-tax-year ^1.0.0 · built alongside by fune

/// The tax week, tax month and payroll period a pay date falls in.
///
/// The tax year is 365 or 366 days, which is 52 weeks and one or two days over.
/// Those spare days are week 53, and they are the reason `period` exists: HMRC
/// numbers the extra payday 53, 54 or 56 by frequency and taxes it
/// non-cumulatively, so a payroll that just counts weeks gets it wrong.
///
/// # Panics
/// Panics on a malformed date or an unknown frequency.
pub fn tax_period(pay_date: &str, frequency: &str) -> TaxPeriod {
    if !matches!(frequency, "weekly" | "fortnightly" | "four-weekly" | "monthly") {
        panic!("unknown pay frequency \"{}\"", frequency);
    }
    let year = uk_tax_year(pay_date);
    let week = days_between(&year.start, pay_date) / 7 + 1;

    // Tax months run from the 6th to the 5th, so the calendar month moves on
    // five days before the tax month does.
    let date = parse_iso_date(pay_date);
    let mut month = if date.day >= 6 { date.month - 3 } else { date.month - 4 };
    if month <= 0 {
        month += 12;
    }

    let period = if frequency == "monthly" {
        month
    } else if week < 53 {
        week
    } else {
        match frequency {
            "weekly" => 53,
            "fortnightly" => 54,
            _ => 56,
        }
    };

    TaxPeriod {
        tax_year: year.label,
        tax_year_start: year.start,
        week,
        month,
        period,
    }
}

pub fn tax_period_to_value(p: &TaxPeriod) -> Value {
    Value::obj(vec![
        ("taxYear", Value::str(&p.tax_year)),
        ("taxYearStart", Value::str(&p.tax_year_start)),
        ("week", Value::Int(p.week)),
        ("month", Value::Int(p.month)),
        ("period", Value::Int(p.period)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    tax_period_to_value(&tax_period(args[0].as_str(), 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 payroll.tax-period
Download for Rust payroll.tax-period-1.0.1-rust.fune · 11,459 bytes sha256 7f399f75a3d63022aadfdb31e029aefb71d654c65ca63f813b0ab31e8231f863

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

The whole function, every language, is one file too: payroll.tax-period-1.0.1.fune, 14,493 bytes, sha256 aec12ac339e717be493bd6db1e7a8778fd8e04e4e8fe86674c1d4853d504fe83. 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.tax-period

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

// fune: after payroll.tax-period

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.tax-period
// fune: replace dates.uk-tax-year in payroll.tax-period

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.tax-period --steps.

// fune: step payroll.tax-period 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
6 April is week 1 and month 1 2026-04-06, monthly → tax year 2026/27, tax year start 2026-04-06, week 1, month 1, period 1
12 April is still week 1 2026-04-12, weekly → tax year 2026/27, tax year start 2026-04-06, week 1, month 1, period 1
13 April starts week 2 2026-04-13, weekly → tax year 2026/27, tax year start 2026-04-06, week 2, month 1, period 2
5 May is the last day of month 1 2026-05-05, monthly → tax year 2026/27, tax year start 2026-04-06, week 5, month 1, period 1
6 May is month 2 2026-05-06, monthly → tax year 2026/27, tax year start 2026-04-06, week 5, month 2, period 2
a fortnightly payday is placed by its tax week 2026-09-23, fortnightly → tax year 2026/27, tax year start 2026-04-06, week 25, month 6, period 25
5 January is still tax month 9 2026-01-05, monthly → tax year 2025/26, tax year start 2025-04-06, week 40, month 9, period 9
4 April is week 52 2027-04-04, weekly → tax year 2026/27, tax year start 2026-04-06, week 52, month 12, period 52
5 April is week 53 for a weekly payroll 2027-04-05, weekly → tax year 2026/27, tax year start 2026-04-06, week 53, month 12, period 53
5 April is week 54 for a fortnightly payroll 2027-04-05, fortnightly → tax year 2026/27, tax year start 2026-04-06, week 53, month 12, period 54
Show the other 6 tests
CaseArgumentsExpected
5 April is week 56 for a four-weekly payroll 2027-04-05, four-weekly → tax year 2026/27, tax year start 2026-04-06, week 53, month 12, period 56
5 April is just month 12 for a monthly payroll 2027-04-05, monthly → tax year 2026/27, tax year start 2026-04-06, week 53, month 12, period 12
a tax year containing 29 February has two week-53 days: 4 April is one 2024-04-04, weekly → tax year 2023/24, tax year start 2023-04-06, week 53, month 12, period 53
29 February itself 2024-02-29, four-weekly → tax year 2023/24, tax year start 2023-04-06, week 48, month 11, period 48
an impossible date is an error 2026-02-30, monthly → error: is not a real calendar date
an unknown frequency is an error 2026-04-06, daily → error: unknown pay frequency

More from the author

This capability does not check that a payroll really is fortnightly or four-weekly (that the paydays are 14 or 28 days apart); it places one date.

Sources: HMRC, "Employer further guide to PAYE and NICs" (CWG2), sections on tax weeks and months and on week 53 payments, https://www.gov.uk/government/publications/cwg2-further-guide-to-paye-and-national-insurance-contributions; HMRC, "Specification for PAYE tax table routines" v24.0, paragraph 14 (payments in weeks 53, 54 or 56), https://www.gov.uk/government/publications/payroll-technical-specifications-income-tax.

## 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.

## Notices

Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).

1.0.1 marks it unreviewed and adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE181
README.md3,133
impl/python.py1,409
impl/rust.rs1,985
impl/typescript.ts1,480
vectors.json2,938