Functional Weave
Code in Rust

hospitality.occupancy-adr-revpar

Hotel occupancy, average daily rate (ADR) and revenue per available room (RevPAR) for a period.

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

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

What it does

The three headline hotel figures for a period, from three totals:

- **Occupancy** = room-nights sold / room-nights available, in basis points (7500 is 75%). - **ADR** (average daily rate) = room revenue / room-nights sold. It is `null` when nothing was sold, because the average of no sales is undefined, not 0. - **RevPAR** (revenue per available room) = room revenue / room-nights available.

For example

  • occupancy_adr_revpar(3,000, 2,250, £247,500.00) → room nights available 3,000, room nights sold 2,250, room revenue £247,500.00, occupancy basis points 75%, adr £110.00, revpar £82.50 100 rooms over 30 nights, 2250 sold for 247,500.00
  • occupancy_adr_revpar(50, 50, £5,000.00) → room nights available 50, room nights sold 50, room revenue £5,000.00, occupancy basis points 100%, adr £100.00, revpar £100.00 a full house: occupancy 100%, ADR equals RevPAR
  • occupancy_adr_revpar(30, 0, £0.00) → room nights available 30, room nights sold 0, room revenue £0.00, occupancy basis points 0%, adr —, revpar £0.00 nothing sold: ADR is undefined, not zero

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 occupancy_adr_revpar(room_nights_available: i64, room_nights_sold: i64, room_revenue: &Money) -> HotelPerformance
room_nights_availableintrooms available x nights in the period, at least 1
room_nights_soldintroom-nights sold in the period, 0 to roomNightsAvailable
room_revenueMoneyroom revenue only, excluding VAT, food and extras
returnsHotelPerformance

The type it declares, generated into your project

/// The three headline figures, each worked out from the raw totals.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HotelPerformance {
    pub room_nights_available: i64,
    pub room_nights_sold: i64,
    pub room_revenue: Money,
    /// sold / available; 7500 = 75%
    pub occupancy_basis_points: i64,
    /// revenue / sold; null when nothing was sold
    pub adr: Option<Money>,
    /// revenue / available
    pub revpar: Money,
}

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

fune!(hospitality.occupancy-adr-revpar@^1);  // then call occupancy_adr_revpar(…)
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::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

/// Occupancy, ADR and RevPAR for a period.
///
/// Each figure is divided out of the raw totals and rounded half-up once.
/// RevPAR is revenue over available rooms, not ADR times occupancy:
/// multiplying two figures that were each rounded already can be a penny out.
///
/// # Panics
/// Panics when there are no rooms available, the counts are out of range or
/// the revenue is negative.
pub fn occupancy_adr_revpar(room_nights_available: i64, room_nights_sold: i64, room_revenue: &Money) -> HotelPerformance {
    if room_nights_available < 1 {
        panic!("roomNightsAvailable must be at least 1, received {}", room_nights_available);
    }
    if room_nights_sold < 0 {
        panic!("roomNightsSold must not be negative, received {}", room_nights_sold);
    }
    if room_nights_sold > room_nights_available {
        panic!(
            "roomNightsSold ({}) cannot exceed roomNightsAvailable ({})",
            room_nights_sold, room_nights_available
        );
    }
    if room_revenue.minor < 0 {
        panic!("roomRevenue must not be negative, received {}", room_revenue.minor);
    }
    HotelPerformance {
        room_nights_available,
        room_nights_sold,
        room_revenue: room_revenue.clone(),
        occupancy_basis_points: round_div(room_nights_sold * 10000, room_nights_available, "half-up"),
        adr: if room_nights_sold == 0 {
            None
        } else {
            Some(money(
                round_div(room_revenue.minor, room_nights_sold, "half-up"),
                &room_revenue.currency,
            ))
        },
        revpar: money(
            round_div(room_revenue.minor, room_nights_available, "half-up"),
            &room_revenue.currency,
        ),
    }
}

pub fn hotel_performance_to_value(p: &HotelPerformance) -> Value {
    Value::obj(vec![
        ("roomNightsAvailable", Value::Int(p.room_nights_available)),
        ("roomNightsSold", Value::Int(p.room_nights_sold)),
        ("roomRevenue", money_to_value(&p.room_revenue)),
        ("occupancyBasisPoints", Value::Int(p.occupancy_basis_points)),
        (
            "adr",
            match &p.adr {
                Some(a) => money_to_value(a),
                None => Value::Null,
            },
        ),
        ("revpar", money_to_value(&p.revpar)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    hotel_performance_to_value(&occupancy_adr_revpar(
        args[0].as_i64(),
        args[1].as_i64(),
        &money_from_value(&args[2]),
    ))
}

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.occupancy-adr-revpar
Download for Rust hospitality.occupancy-adr-revpar-1.0.0-rust.fune · 11,408 bytes sha256 9665990d363ccbf198a82d1a8ff9fe308667fe73317905d3090643491ed0a85d

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

The whole function, every language, is one file too: hospitality.occupancy-adr-revpar-1.0.0.fune, 15,023 bytes, sha256 a5b276ca41ec79434791e086583943b38f276c9e5b13ff990bce6edf2f1bfc4f. 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.occupancy-adr-revpar

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

// fune: after hospitality.occupancy-adr-revpar

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.round-div in hospitality.occupancy-adr-revpar
// fune: replace money.amount in hospitality.occupancy-adr-revpar

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.occupancy-adr-revpar --steps.

// fune: step hospitality.occupancy-adr-revpar 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
100 rooms over 30 nights, 2250 sold for 247,500.00 3,000, 2,250, £247,500.00 → room nights available 3,000, room nights sold 2,250, room revenue £247,500.00, occupancy basis points 75%, adr £110.00, revpar £82.50
a full house: occupancy 100%, ADR equals RevPAR 50, 50, £5,000.00 → room nights available 50, room nights sold 50, room revenue £5,000.00, occupancy basis points 100%, adr £100.00, revpar £100.00
nothing sold: ADR is undefined, not zero 30, 0, £0.00 → room nights available 30, room nights sold 0, room revenue £0.00, occupancy basis points 0%, adr —, revpar £0.00
one night of three sold rounds occupancy down to 3333 3, 1, £90.00 → room nights available 3, room nights sold 1, room revenue £90.00, occupancy basis points 33.33%, adr £90.00, revpar £30.00
two nights of three sold rounds occupancy up to 6667 3, 2, £200.00 → room nights available 3, room nights sold 2, room revenue £200.00, occupancy basis points 66.67%, adr £100.00, revpar £66.67
RevPAR rounds half-up from the totals: 4 over 8 room-nights is 0.5, so 1 8, 1, £0.04 → room nights available 8, room nights sold 1, room revenue £0.04, occupancy basis points 12.5%, adr £0.04, revpar £0.01
ADR rounds half-up: 100.00 over 3 nights 7, 3, £100.00 → room nights available 7, room nights sold 3, room revenue £100.00, occupancy basis points 42.86%, adr £33.33, revpar £14.29
RevPAR from the totals, not ADR x occupancy: 1000.01 over 3 of 5 room-nights 5, 3, £1,000.01 → room nights available 5, room nights sold 3, room revenue £1,000.01, occupancy basis points 60%, adr £333.34, revpar £200.00
complimentary rooms sold for nothing 10, 5, £0.00 → room nights available 10, room nights sold 5, room revenue £0.00, occupancy basis points 50%, adr £0.00, revpar £0.00
yen have no minor unit to round to 4, 3, ¥55,000 → room nights available 4, room nights sold 3, room revenue ¥55,000, occupancy basis points 75%, adr ¥18,333, revpar ¥13,750
Show the other 4 tests
CaseArgumentsExpected
no rooms available is an error 0, 0, £0.00 → error: roomNightsAvailable must be at least 1
negative rooms sold is an error 10, -1, £0.00 → error: roomNightsSold must not be negative
selling more than is available is an error 10, 11, £10.00 → error: cannot exceed roomNightsAvailable
negative revenue is an error 10, 5, -£1.00 → error: roomRevenue must not be negative

More from the author

## Why it is shaped this way

Each figure is divided out of the raw totals and rounded half-up once, to the minor unit or the basis point. RevPAR is *not* worked out as ADR x occupancy: both of those are already rounded, and multiplying rounded figures can land a penny away from the true value. ADR x occupancy is the identity to explain RevPAR with, not the way to compute it.

Room-nights, not rooms: a 100-room hotel over 30 nights has 3,000 room-nights available.

## What the caller decides

The industry definitions (STR's glossary, for example) leave choices to the operator, and this function takes the totals as given:

- whether out-of-order rooms come off the rooms available; - whether complimentary and house-use rooms count as sold (they add nights but no revenue, pulling ADR down); - what counts as room revenue. Use it net of VAT, and without breakfast, packages' food element or other extras, so figures compare across hotels.

## Errors

At least one room-night must be available; sold must be from 0 to available; revenue must not be negative.

Files

PathBytes
README.md1,507
impl/python.py1,848
impl/rust.rs2,639
impl/typescript.ts1,632
vectors.json4,074