Functional Weave
Code in TypeScript

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

  • floorArea(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
  • floorArea(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
  • floorArea(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.

export function floorArea(rooms: readonly RoomDimensions[], lengthUnit: FloorAreaUnit, decimals: number): FloorArea
roomsRoomDimensions[]each room's length and width, as decimal text
lengthUnitFloorAreaUnitthe 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

export type FloorAreaUnit = "m" | "ft";

/** One rectangular room or area. */
export interface RoomDimensions {
  readonly name: string;
  /** plain decimal text, at most 3 decimal places, greater than 0 and below 1000 */
  readonly length: string;
  /** the same */
  readonly width: string;
}

/** Room areas and the total, exact in the measured unit and rounded in both. */
export interface FloorArea {
  /** each room's length × width, exact, in square lengthUnit, in input order */
  readonly roomAreas: readonly string[];
  /** the sum of roomAreas, exact, in square lengthUnit */
  readonly total: string;
  /** the total in m², rounded to decimals */
  readonly squareMetres: string;
  /** the total in sq ft, rounded to decimals */
  readonly squareFeet: string;
}

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

import { floorArea } from "#fune/property.floor-area@^1";
impl/typescript.ts · 51 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.

import { convertUnits } from "./units_convert.ts";  ← from units.convert ^1.0.0 · built alongside by fune
import { type FloorArea, type FloorAreaUnit, type RoomDimensions } from "./property_floor_area_types.ts";

const DIMENSION = /^[0-9]{1,3}(\.[0-9]{1,3})?$/;
const MAX_ROOMS = 1000;

/** A dimension as an integer number of thousandths. */
function thousandths(room: string, field: string, text: string): number {
  if (typeof text !== "string" || !DIMENSION.test(text)) {
    throw new RangeError(`room "${room}": ${field} must be a decimal below 1000 with at most 3 decimal places, received "${text}"`);
  }
  const [whole, fraction = ""] = text.split(".");
  const value = Number(whole) * 1000 + Number(fraction.padEnd(3, "0"));
  if (value === 0) {
    throw new RangeError(`room "${room}": ${field} must be greater than zero`);
  }
  return value;
}

/** An integer number of millionths as the shortest exact decimal. */
function decimal(millionths: number): string {
  const whole = Math.floor(millionths / 1_000_000);
  const fraction = String(millionths % 1_000_000).padStart(6, "0").replace(/0+$/, "");
  return fraction === "" ? String(whole) : `${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.
 */
export function floorArea(rooms: readonly RoomDimensions[], lengthUnit: FloorAreaUnit, decimals: number): FloorArea {
  if (lengthUnit !== "m" && lengthUnit !== "ft") {
    throw new RangeError(`unknown length unit "${lengthUnit}": use m or ft`);
  }
  if (rooms.length === 0) {
    throw new RangeError("rooms must not be empty");
  }
  if (rooms.length > MAX_ROOMS) {
    throw new RangeError(`at most ${MAX_ROOMS} rooms, received ${rooms.length}`);
  }
  // Each area is below 1e12 millionths and the total below 1e15: exact numbers.
  const areas = rooms.map((r) => thousandths(r.name, "length", r.length) * thousandths(r.name, "width", r.width));
  const total = decimal(areas.reduce((a, b) => a + b, 0));
  const from = lengthUnit === "m" ? "m2" : "ft2";
  return {
    roomAreas: areas.map(decimal),
    total,
    squareMetres: convertUnits(total, from, "m2", decimals),
    squareFeet: convertUnits(total, from, "ft2", decimals),
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript 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. Or pin a range in fune.project and build in one step:

fune add property.floor-area
Download for TypeScript property.floor-area-1.0.2-typescript.fune · 14,971 bytes sha256 17e825e563fa0b71ca2fb2715d8e64d6752617f92f883f3fcec4e917cd05ff6b

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

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.

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 10 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
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

PathBytes
README.md2,714
impl/python.py2,108
impl/rust.rs3,851
impl/typescript.ts2,232
vectors.json5,718