property.floor-area Unreviewed
Floor area from room lengths and widths, exactly, with the total in both square metres and square feet.
1.0.2 · published 2026-10-03 by charlie · Anterra
Pinned by 20 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 conveyancer or tax adviser 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 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 ftfloor_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.529999floor_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
| rooms | RoomDimensions[] | each room's length and width, as decimal text |
| length_unit | FloorAreaUnit | the unit the dimensions are measured in |
| decimals | int | 0 to 12; places for squareMetres and squareFeet, rounded half away from zero |
| returns | FloorArea |
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(…)
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
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./property.floor-area-1.0.2-rust.fune, or fetch it from a terminal with fune pull property.floor-area@1.0.2:rust.
The whole function, every language, is one file too: property.floor-area-1.0.2.fune, 21,244 bytes, sha256 f59699a3b1318c87c90a61fe248c8ef838984c17b27452ee4d538dcd807727ed. 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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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 10 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| 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 |
| a trailing newline is not part of a length | rooms ×1, m, 2 | → | error: room "Hall": length must be a decimal below 1000 |
| a trailing newline is not part of a width | rooms ×1, m, 2 | → | error: room "Hall": width must be a decimal below 1000 |
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.
1.0.1 fixes Python accepting a trailing newline in a room's length or width; adds tests.
## Before you rely on this
**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 above, 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.
**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 conveyancer or tax adviser 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.2 marks it unreviewed. The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| README.md | 2,714 |
| impl/python.py | 2,108 |
| impl/rust.rs | 3,851 |
| impl/typescript.ts | 2,232 |
| vectors.json | 5,718 |