Functional Weave
Code in Rust

property.floor-area

Floor area from room lengths and widths, exactly, with the total in both square metres and square feet.

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

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

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

The floor area of a property from its room measurements, as estate agents' floor plans and EPCs quote it: every room's length × width, their total, and the total in both square metres and square feet.

`floorArea([{name: "Lounge", length: "4", width: "3"}, {name: "Bed", length: "2.5", width: "2"}], "m", 2)` gives rooms of 12 and 5 m², a total of 17 m², and 182.99 sq ft.

For example

  • floor_area(rooms ×2, m, 2) → room areas 12, 5, total 17, square metres 17, square feet 182.99 a 4 × 3 m lounge and a 2.5 × 2 m bedroom: 17 m², 182.99 sq ft
  • floor_area(rooms ×1, m, 2) → room areas 13.53, total 13.53, square metres 13.53, square feet 145.64 decimal dimensions multiply exactly: 4.1 × 3.3 is 13.53, not 13.529999
  • floor_area(rooms ×2, ft, 2) → room areas 120, 174.375, total 294.375, square metres 27.35, square feet 294.38 measured in feet: 12 × 10 and 15.5 × 11.25 is 294.375 sq ft, 27.35 m²

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 floor_area(rooms: &[RoomDimensions], length_unit: &str, decimals: i64) -> FloorArea
roomsRoomDimensions[]each room's length and width, as decimal text
length_unitFloorAreaUnitthe unit the dimensions are measured in
decimalsint0 to 12; places for squareMetres and squareFeet, rounded half away from zero
returnsFloorArea

The types it declares, generated into your project

// FloorAreaUnit is a string in Rust, one of: "m", "ft".
// Parameters take it as &str and results hold it as String.

/// One rectangular room or area.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoomDimensions {
    pub name: String,
    /// plain decimal text, at most 3 decimal places, greater than 0 and below 1000
    pub length: String,
    /// the same
    pub width: String,
}

/// Room areas and the total, exact in the measured unit and rounded in both.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FloorArea {
    /// each room's length × width, exact, in square lengthUnit, in input order
    pub room_areas: Vec<String>,
    /// the sum of roomAreas, exact, in square lengthUnit
    pub total: String,
    /// the total in m², rounded to decimals
    pub square_metres: String,
    /// the total in sq ft, rounded to decimals
    pub square_feet: String,
}

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

fune!(property.floor-area@^1);  // then call floor_area(…)
impl/rust.rs · 104 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::units_convert::convert_units;  ← from units.convert ^1.0.0 · built alongside by fune

const MAX_ROOMS: usize = 1000;

/// A dimension as an integer number of thousandths.
fn thousandths(room: &str, field: &str, text: &str) -> i64 {
    let bad = || -> ! {
        panic!(
            "room \"{}\": {} must be a decimal below 1000 with at most 3 decimal places, received \"{}\"",
            room, field, text
        )
    };
    let (whole, fraction) = match text.split_once('.') {
        Some((w, f)) => (w, Some(f)),
        None => (text, None),
    };
    if whole.is_empty() || whole.len() > 3 || !whole.bytes().all(|b| b.is_ascii_digit()) {
        bad();
    }
    let mut value = whole.parse::<i64>().unwrap() * 1000;
    if let Some(f) = fraction {
        if f.is_empty() || f.len() > 3 || !f.bytes().all(|b| b.is_ascii_digit()) {
            bad();
        }
        value += format!("{:0<3}", f).parse::<i64>().unwrap();
    }
    if value == 0 {
        panic!("room \"{}\": {} must be greater than zero", room, field);
    }
    value
}

/// An integer number of millionths as the shortest exact decimal.
fn decimal(millionths: i64) -> String {
    let whole = millionths / 1_000_000;
    let fraction = format!("{:06}", millionths % 1_000_000);
    let fraction = fraction.trim_end_matches('0');
    if fraction.is_empty() {
        whole.to_string()
    } else {
        format!("{}.{}", whole, fraction)
    }
}

/// Room areas and their total, exact, from length × width of each room, and
/// the total in square metres and square feet rounded once at the end.
///
/// # Panics
/// Panics on an unknown unit, no rooms or too many, or a dimension that is
/// not a positive decimal below 1000 with at most 3 places.
pub fn floor_area(rooms: &[RoomDimensions], length_unit: &str, decimals: i64) -> FloorArea {
    if length_unit != "m" && length_unit != "ft" {
        panic!("unknown length unit \"{}\": use m or ft", length_unit);
    }
    if rooms.is_empty() {
        panic!("rooms must not be empty");
    }
    if rooms.len() > MAX_ROOMS {
        panic!("at most {} rooms, received {}", MAX_ROOMS, rooms.len());
    }
    let areas: Vec<i64> = rooms
        .iter()
        .map(|r| thousandths(&r.name, "length", &r.length) * thousandths(&r.name, "width", &r.width))
        .collect();
    let total = decimal(areas.iter().sum());
    let from = if length_unit == "m" { "m2" } else { "ft2" };
    FloorArea {
        room_areas: areas.iter().map(|a| decimal(*a)).collect(),
        square_metres: convert_units(&total, from, "m2", decimals),
        square_feet: convert_units(&total, from, "ft2", decimals),
        total,
    }
}

pub fn room_dimensions_from_value(v: &Value) -> RoomDimensions {
    let name = v.get("name").as_str().to_string();
    let text = |field: &str| match v.get(field) {
        Value::Str(s) => s.clone(),
        other => panic!(
            "room \"{}\": {} must be a decimal below 1000 with at most 3 decimal places, received \"{:?}\"",
            name, field, other
        ),
    };
    RoomDimensions { length: text("length"), width: text("width"), name: name.clone() }
}

pub fn floor_area_to_value(area: &FloorArea) -> Value {
    Value::obj(vec![
        ("roomAreas", Value::Arr(area.room_areas.iter().map(|a| Value::str(a)).collect())),
        ("total", Value::str(&area.total)),
        ("squareMetres", Value::str(&area.square_metres)),
        ("squareFeet", Value::str(&area.square_feet)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let rooms: Vec<RoomDimensions> = args[0].as_arr().iter().map(room_dimensions_from_value).collect();
    let decimals = match &args[2] {
        Value::Int(d) => *d,
        other => panic!("decimals must be a whole number from 0 to 12, received {:?}", other),
    };
    floor_area_to_value(&floor_area(&rooms, args[1].as_str(), decimals))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, 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.floor-area
Download for Rust property.floor-area-1.0.0-rust.fune · 14,828 bytes sha256 092f46bed319464be501d8893c9d683e6fab115dc95d3d55c6bc87538313aa65

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

The whole function, every language, is one file too: property.floor-area-1.0.0.fune, 19,410 bytes, sha256 1d61ab42c79adb922f1fa4e036ba46b288c591b0579a04ac64e2de69a39b9d9b. 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.floor-area

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

// fune: after property.floor-area

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 units.convert in property.floor-area

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.floor-area --steps.

// fune: step property.floor-area 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
a 4 × 3 m lounge and a 2.5 × 2 m bedroom: 17 m², 182.99 sq ft rooms ×2, m, 2 → room areas 12, 5, total 17, square metres 17, square feet 182.99
decimal dimensions multiply exactly: 4.1 × 3.3 is 13.53, not 13.529999 rooms ×1, m, 2 → room areas 13.53, total 13.53, square metres 13.53, square feet 145.64
measured in feet: 12 × 10 and 15.5 × 11.25 is 294.375 sq ft, 27.35 m² rooms ×2, ft, 2 → room areas 120, 174.375, total 294.375, square metres 27.35, square feet 294.38
the same at 6 places keeps the exact square feet rooms ×2, ft, 6 → room areas 120, 174.375, total 294.375, square metres 27.348332, square feet 294.375
100 sq ft to whole square metres is 9 rooms ×1, ft, 0 → room areas 100, total 100, square metres 9, square feet 100
100 sq ft to 4 places is 9.2903 m² rooms ×1, ft, 4 → room areas 100, total 100, square metres 9.2903, square feet 100
one square metre at 12 places: 10.76391041671 sq ft rooms ×1, m, 12 → room areas 1, total 1, square metres 1, square feet 10.76391041671
millimetre precision: 0.001 × 0.001 m is a square millimetre rooms ×1, m, 6 → room areas 0.000001, total 0.000001, square metres 0.000001, square feet 0.000011
the largest room: 999.999 × 999.999 m rooms ×1, m, 0 → room areas 999998.000001, total 999998.000001, square metres 999998, square feet 10763889
trailing zeros are accepted and dropped: 3.50 × 2.00 rooms ×1, m, 2 → room areas 7, total 7, square metres 7, square feet 75.35
Show the other 8 tests
CaseArgumentsExpected
no rooms is refused , m, 2 → error: rooms must not be empty
a zero dimension is refused rooms ×1, m, 2 → error: room "Cupboard": length must be greater than zero
four decimal places is refused rooms ×1, m, 2 → error: room "Hall": length must be a decimal below 1000
1000 or more is refused rooms ×1, m, 2 → error: room "Field": length must be a decimal below 1000
feet and inches are not decimal feet rooms ×1, ft, 2 → error: room "Hall": length must be a decimal below 1000
a negative width is refused rooms ×1, m, 2 → error: room "Hall": width must be a decimal below 1000
an unknown unit is refused rooms ×1, yd, 2 → error: unknown length unit
decimals beyond 12 are refused by units.convert rooms ×1, m, 13 → error: decimals must be a whole number from 0 to 12

More from the author

## Why decimal text

Dimensions are read as exact decimals, never floats, so 4.1 × 3.3 is 13.53, not 13.529999999999999. Each room's area and the total are exact (up to six decimal places, from dimensions with up to three), and the only rounding is the final one to `decimals` places in each unit, done by `units.convert` with the exact definition 1 ft = 0.3048 m (1 sq ft = 0.09290304 m²). The total is rounded once, not the rooms: rounding each room first and adding can be off in the last place.

## Edge cases and limits

- Rooms are rectangles. An L-shaped room is two entries; a bay is its own entry; deduct nothing, since the function never subtracts. - Dimensions are greater than 0 and below 1000 of the unit, with at most 3 decimal places (a millimetre in metres), and there may be at most 1000 rooms. An empty list is an error, as is a room of zero size. - Feet and inches must be converted to decimal feet first (10 ft 6 in is "10.5"). - This is the area of the rooms measured, not a RICS gross internal area: walls, stairs and circulation space count only if they are measured in.

Files

PathBytes
README.md1,506
impl/python.py2,106
impl/rust.rs3,851
impl/typescript.ts2,232
vectors.json5,223