Functional Weave
Code in TypeScript

construction.roof-pitch

Roof pitch in degrees, rafter length and roof slope area from span, rise and length.

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

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

What it does

The pitch of a roof, the length of one rafter and the area of the roof slopes, from the span, the rise and the length along the ridge. All dimensions are in metres.

- **duo** (a pitched roof with a ridge in the middle): the run of each rafter is half the span, and there are two slopes. - **mono** (a lean-to or mono-pitch): the run is the whole span, and there is one slope.

For example

  • roofPitch(8, 2, 10, duo) → pitch degrees 26.57, rafter length 4.472, roof area 89.44 duo pitch, 8 m span, 2 m rise: run 4, tan 0.5
  • roofPitch(6, 3, 5, duo) → pitch degrees 45, rafter length 4.243, roof area 42.43 rise equal to the run is 45 degrees
  • roofPitch(8, 3, 12, duo) → pitch degrees 36.87, rafter length 5, roof area 120 a 3-4-5 triangle gives an exact 5 m rafter

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 roofPitch(span: number, rise: number, roofLength: number, roofType: RoofType): RoofPitch
spanfloatmetres, wall plate to wall plate; for a duo pitch the run is half of it
risefloatmetres, wall plate to ridge; 0 for a flat roof
roofLengthfloatmetres, along the ridge (gable to gable)
roofTypeRoofTypeduo: two equal slopes meeting at a ridge; mono: one slope (lean-to)
returnsRoofPitch

The types it declares, generated into your project

export type RoofType = "duo" | "mono";

/** The pitch, one rafter and the whole roof's slope area. */
export interface RoofPitch {
  /** angle of the slope from horizontal, to 2 decimal places */
  readonly pitchDegrees: number;
  /** metres along the slope from plate to ridge, no overhang, to 3 decimal places (the nearest millimetre) */
  readonly rafterLength: number;
  /** square metres of slope, every slope together, no overhang, to 2 decimal places */
  readonly roofArea: number;
}

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

import { roofPitch } from "#fune/construction.roof-pitch@^1";
impl/typescript.ts · 48 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 { atan } from "./math_atan.ts";  ← from math.atan ^1.0.0 · built alongside by fune
import { roundFloat } from "./math_round_float.ts";  ← from math.round-float ^1.0.0 · built alongside by fune
import { type RoofPitch, type RoofType } from "./construction_roof_pitch_types.ts";

// 180 / pi as a double, written out so no language computes it differently.
const DEGREES_PER_RADIAN = 57.29577951308232;
const LIMIT = 10000;

function checkLength(name: string, value: number, allowZero: boolean): void {
  const ok = typeof value === "number" && Number.isFinite(value) && value <= LIMIT && (allowZero ? value >= 0 : value > 0);
  if (!ok) {
    const want = allowZero ? "zero or more" : "greater than zero";
    throw new RangeError(`${name} must be a finite number of metres, ${want} and at most ${LIMIT}, received ${String(value)}`);
  }
}

/**
 * Pitch, rafter length and slope area of a duo- or mono-pitch roof.
 *
 * The area is worked from the unrounded rafter, not the one rounded to a
 * millimetre for display: over a long roof the difference shows in the
 * second decimal place.
 */
export function roofPitch(span: number, rise: number, roofLength: number, roofType: RoofType): RoofPitch {
  checkLength("span", span, false);
  checkLength("rise", rise, true);
  checkLength("roofLength", roofLength, false);
  let run: number;
  let slopes: number;
  if (roofType === "duo") {
    run = span / 2;
    slopes = 2;
  } else if (roofType === "mono") {
    run = span;
    slopes = 1;
  } else {
    throw new RangeError(`unknown roof type "${String(roofType)}"`);
  }
  const radians = atan(rise / run);
  // sqrt is one of the operations IEEE 754 requires to be correctly rounded,
  // so Math.sqrt is the same double in every language.
  const rafter = Math.sqrt(run * run + rise * rise);
  return {
    pitchDegrees: roundFloat(radians * DEGREES_PER_RADIAN, 2),
    rafterLength: roundFloat(rafter, 3),
    roofArea: roundFloat(rafter * roofLength * slopes, 2),
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, 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 construction.roof-pitch
Download for TypeScript construction.roof-pitch-1.0.0-typescript.fune · 9,247 bytes sha256 80983d07cf33f10b3120ad6542165d44feb8c1287be2a1121ffcc9f9e35817f3

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./construction.roof-pitch-1.0.0-typescript.fune, or fetch it from a terminal with fune pull construction.roof-pitch@1.0.0:typescript.

The whole function, every language, is one file too: construction.roof-pitch-1.0.0.fune, 13,927 bytes, sha256 a8a35da395a9273cd0e552f4eaf3ea782a8dc8357310c2c616bc80d54187f6f9. 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 construction.roof-pitch

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

// fune: after construction.roof-pitch

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.atan in construction.roof-pitch
// fune: replace math.round-float in construction.roof-pitch

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 construction.roof-pitch --steps.

// fune: step construction.roof-pitch 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
duo pitch, 8 m span, 2 m rise: run 4, tan 0.5 8, 2, 10, duo → pitch degrees 26.57, rafter length 4.472, roof area 89.44
rise equal to the run is 45 degrees 6, 3, 5, duo → pitch degrees 45, rafter length 4.243, roof area 42.43
a 3-4-5 triangle gives an exact 5 m rafter 8, 3, 12, duo → pitch degrees 36.87, rafter length 5, roof area 120
area from the unrounded rafter: 4.32666 x 40 is 173.07, not 4.327 x 40 = 173.08 7.2, 2.4, 20, duo → pitch degrees 33.69, rafter length 4.327, roof area 173.07
a rise of 2.887 on a 10 m span is a 30 degree roof 10, 2.887, 8, duo → pitch degrees 30, rafter length 5.774, roof area 92.38
steep duo pitch: tan 6 2, 6, 1, duo → pitch degrees 80.54, rafter length 6.083, roof area 12.17
mono pitch uses the whole span as the run and has one slope 4, 1, 3, mono → pitch degrees 14.04, rafter length 4.123, roof area 12.37
shallow lean-to 9.6, 1.2, 15.5, mono → pitch degrees 7.13, rafter length 9.675, roof area 149.96
zero rise is a flat roof: pitch 0, rafter equal to the run 5, 0, 4, mono → pitch degrees 0, rafter length 5, roof area 20
the same span as a duo pitch halves the run 8, 2, 10, mono → pitch degrees 14.04, rafter length 8.246, roof area 82.46
Show the other 5 tests
CaseArgumentsExpected
a zero span is an error 0, 2, 10, duo → error: span must be a finite number of metres
a negative rise is an error 8, -1, 10, duo → error: rise must be a finite number of metres
a zero roof length is an error 8, 2, 0, duo → error: roofLength must be a finite number of metres
a span given as text is an error 8, 2, 10, duo → error: span must be a finite number of metres
an unknown roof type is an error 8, 2, 10, hip → error: unknown roof type "hip"

More from the author

The pitch is `atan(rise / run)` in degrees, the rafter is `sqrt(run² + rise²)`, and the area is rafter × roof length × slopes.

## Rounding

The pitch is rounded to 2 decimal places, the rafter to 3 (the nearest millimetre) and the area to 2, each with `math.round-float`, half away from zero. The area is worked from the **unrounded** rafter: on a 7.2 m span with a 2.4 m rise and a 20 m ridge the rafter is 4.32666 m, so the area is 173.07 m², while 4.327 × 40 would give 173.08.

## Why every language agrees

The angle comes from `math.atan` (fdlibm's arctangent from + − × ÷ only), not `Math.atan`, whose last bit differs between platforms. The square root is the platform's own: IEEE 754 requires square root to be correctly rounded, like + − × ÷, so it is the same double everywhere (`geo.distance` relies on the same guarantee). Degrees are radians × 57.29577951308232, 180/π as a double, written out.

## What it does not do

- Overhangs (eaves and verges), ridge and fascia boards, and the rafter's plumb cuts: the rafter length is plate to ridge along the slope, and the area is the slope over the building's footprint. Add overhang to the run and roof length yourself for tiling or felt quantities, then add waste. - Hips, valleys and unequal pitches. A hipped roof's slope area does equal plan area ÷ cos(pitch) when every slope has the same pitch, but its rafters do not; that is a different calculation. - Pitch from a known angle (the reverse); rise = run × tan(pitch).

A rise of 0 is allowed (a flat roof: pitch 0, rafter equal to the run). Span and roof length must be above zero, and every dimension at most 10,000 m.

Files

PathBytes
README.md2,075
impl/python.py1,916
impl/rust.rs2,556
impl/typescript.ts1,876
vectors.json2,332