Functional Weave
Code in TypeScript

health.dose-weight-based Unreviewed

Weight-based dose (per kg) capped at a maximum single dose and rounded to a measurable volume, in exact micro-units.

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

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

Unreviewed. Not for clinical use; regulatory review pending. 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 pharmacist 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 health figures from published rules. It is a software component for developers, not medical 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 pharmacist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

Not a medical device. It is not intended to diagnose, treat or support clinical decisions about any individual. Anyone building it into clinical software is responsible for that software’s regulatory status, and must validate it under their own clinical governance.

What it does

Status: needs review and sign-off by a qualified clinician before it is published. Not a medical device; for decision support only; always follow local clinical guidelines.

Works out a weight-based dose (so much per kilogram), caps it at a maximum single dose, and turns it into a volume the measuring device can actually show: a whole number of 0.1 mL, 0.01 mL or 1 mL steps. It holds no drug data. The prescriber's dose per kg, the maximum single dose and the product strength all come from the caller, from the prescription and the product in hand.

For example

  • weightBasedDose(12,400, 15,000, —, 120,000, 5,000, 100) → calculated micrograms 186,000, capped at maximum false, target micrograms 186,000, volume microlitres 7,700, delivered micrograms 184,800, difference basis points -0.65% 15 mg/kg for 12.4 kg is 186 mg; at 120 mg/5 mL that is 7.75 mL, an exact tie between 0.1 mL steps, so it goes down to 7.7 mL (184.8 mg, 0.65% under)
  • weightBasedDose(12,600, 15,000, 500,000, 120,000, 5,000, 100) → calculated micrograms 189,000, capped at maximum false, target micrograms 189,000, volume microlitres 7,900, delivered micrograms 189,600, difference basis points 0.32% 15 mg/kg for 12.6 kg is 189 mg = 7.875 mL, nearest 0.1 mL step is up: 7.9 mL (189.6 mg, 0.32% over)
  • weightBasedDose(60,000, 20,000, 1,000,000, 500,000, 5,000, 500) → calculated micrograms 1,200,000, capped at maximum true, target micrograms 1,000,000, volume microlitres 10,000, delivered micrograms 1,000,000, difference basis points 0% 20 mg/kg for 60 kg is 1200 mg, capped at the 1000 mg maximum: 500 mg/5 mL in 0.5 mL steps is exactly 10 mL

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 weightBasedDose(weightGrams: number, dosePerKgMicrograms: number, maxSingleDoseMicrograms: number | null, strengthMicrograms: number, strengthVolumeMicrolitres: number, measuringIncrementMicrolitres: number): WeightBasedDose
weightGramsintthe patient's weight in grams, 200 to 500000; 12.4 kg is 12400
dosePerKgMicrogramsintthe prescribed dose per kg in micrograms, 1 to 1000000; 15 mg/kg is 15000
maxSingleDoseMicrogramsint?the maximum single dose in micrograms, 1 to 100000000; null for none
strengthMicrogramsintthe product strength: this many micrograms, 1 to 100000000 ...
strengthVolumeMicrolitresint... in this many microlitres, 1 to 1000000; 120 mg/5 mL is 120000 in 5000
measuringIncrementMicrolitresintthe smallest step the measuring device shows, 1 to 100000; 0.1 mL is 100
returnsWeightBasedDose

The type it declares, generated into your project

/** The dose worked out, the volume to give, and how far the measurable volume is from the dose. */
export interface WeightBasedDose {
  /** weight × dose per kg, half-up to a whole microgram, before any cap */
  readonly calculatedMicrograms: number;
  /** true when the calculated dose was above the maximum single dose */
  readonly cappedAtMaximum: boolean;
  /** the dose aimed for: the maximum when capped, else the calculated dose; half-up */
  readonly targetMicrograms: number;
  /** the volume to give, a whole number of measuring increments */
  readonly volumeMicrolitres: number;
  /** what that volume contains, half-up to a whole microgram */
  readonly deliveredMicrograms: number;
  /** delivered against target, signed; -65 = 0.65% under; half away from zero */
  readonly differenceBasisPoints: number;
}

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

import { weightBasedDose } from "#fune/health.dose-weight-based@^1";
impl/typescript.ts · 75 lines · open · raw
import { type WeightBasedDose } from "./health_dose_weight_based_types.ts";

function checkRange(name: string, value: number, low: number, high: number): void {
  if (!Number.isInteger(value) || value < low || value > high) {
    throw new RangeError(`${name} must be a whole number from ${low} to ${high}, received ${value}`);
  }
}

/** num / den rounded half-up, for num >= 0 and den > 0. */
function halfUp(num: bigint, den: bigint): bigint {
  return (2n * num + den) / (2n * den);
}

/** num / den rounded half away from zero, for den > 0. */
function halfAwayFromZero(num: bigint, den: bigint): bigint {
  const magnitude = halfUp(num < 0n ? -num : num, den);
  return num < 0n ? -magnitude : magnitude;
}

/**
 * A weight-based dose, capped at the maximum single dose and turned into a
 * volume the measuring device can show. Every quantity is a whole number of
 * micrograms, microlitres or grams and every step is an exact fraction, so a
 * 0.1 mL step never drifts to 0.30000000000000004 mL. The volume is the
 * nearest whole number of increments, a tie going down, and never one that
 * would deliver more than the maximum single dose.
 */
export function weightBasedDose(
  weightGrams: number,
  dosePerKgMicrograms: number,
  maxSingleDoseMicrograms: number | null,
  strengthMicrograms: number,
  strengthVolumeMicrolitres: number,
  measuringIncrementMicrolitres: number,
): WeightBasedDose {
  checkRange("weightGrams", weightGrams, 200, 500000);
  checkRange("dosePerKgMicrograms", dosePerKgMicrograms, 1, 1000000);
  if (maxSingleDoseMicrograms !== null) checkRange("maxSingleDoseMicrograms", maxSingleDoseMicrograms, 1, 100000000);
  checkRange("strengthMicrograms", strengthMicrograms, 1, 100000000);
  checkRange("strengthVolumeMicrolitres", strengthVolumeMicrolitres, 1, 1000000);
  checkRange("measuringIncrementMicrolitres", measuringIncrementMicrolitres, 1, 100000);

  const perKg = BigInt(weightGrams) * BigInt(dosePerKgMicrograms); // micrograms × 1000
  const max = maxSingleDoseMicrograms === null ? null : BigInt(maxSingleDoseMicrograms);
  const capped = max !== null && perKg > max * 1000n;
  // The target dose as the exact fraction targetNum / targetDen micrograms.
  const targetNum = capped ? (max as bigint) : perKg;
  const targetDen = capped ? 1n : 1000n;

  const strength = BigInt(strengthMicrograms);
  const volume = BigInt(strengthVolumeMicrolitres);
  const increment = BigInt(measuringIncrementMicrolitres);
  // Increments needed: target × volume / (strength × increment), nearest, a tie down.
  const stepsNum = targetNum * volume;
  const stepsDen = targetDen * strength * increment;
  let steps = stepsNum / stepsDen;
  if (2n * (stepsNum % stepsDen) > stepsDen) steps += 1n;
  // Rounding up to the nearest step must not take the dose over the maximum.
  if (max !== null && steps * increment * strength > max * volume) steps -= 1n;
  if (steps === 0n) {
    throw new RangeError("dose is less than one measuring increment; use a more dilute product or a finer measure");
  }

  const deliveredNum = steps * increment * strength; // micrograms × volume
  return {
    calculatedMicrograms: Number(halfUp(perKg, 1000n)),
    cappedAtMaximum: capped,
    targetMicrograms: Number(halfUp(targetNum, targetDen)),
    volumeMicrolitres: Number(steps * increment),
    deliveredMicrograms: Number(halfUp(deliveredNum, volume)),
    differenceBasisPoints: Number(
      halfAwayFromZero((deliveredNum * targetDen - targetNum * volume) * 10000n, targetNum * volume),
    ),
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and nothing else, 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 health.dose-weight-based
Download for TypeScript health.dose-weight-based-1.0.1-typescript.fune · 17,562 bytes sha256 5320a3e35d389b3c05e08c6f87ad05339c22794e5146bc445ead794ece5bc74b

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./health.dose-weight-based-1.0.1-typescript.fune, or fetch it from a terminal with fune pull health.dose-weight-based@1.0.1:typescript.

The whole function, every language, is one file too: health.dose-weight-based-1.0.1.fune, 26,519 bytes, sha256 fb3a319fd96fe97fc55f9f76484cb17fd73dfdd268483893ef40cb6d90639fa5. 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 health.dose-weight-based

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

// fune: after health.dose-weight-based

replace — it requires no other capability, so there is no dependency to replace.

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 health.dose-weight-based --steps.

// fune: step health.dose-weight-based 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
15 mg/kg for 12.4 kg is 186 mg; at 120 mg/5 mL that is 7.75 mL, an exact tie between 0.1 mL steps, so it goes down to 7.7 mL (184.8 mg, 0.65% under) 12,400, 15,000, —, 120,000, 5,000, 100 → calculated micrograms 186,000, capped at maximum false, target micrograms 186,000, volume microlitres 7,700, delivered micrograms 184,800, difference basis points -0.65%
15 mg/kg for 12.6 kg is 189 mg = 7.875 mL, nearest 0.1 mL step is up: 7.9 mL (189.6 mg, 0.32% over) 12,600, 15,000, 500,000, 120,000, 5,000, 100 → calculated micrograms 189,000, capped at maximum false, target micrograms 189,000, volume microlitres 7,900, delivered micrograms 189,600, difference basis points 0.32%
20 mg/kg for 60 kg is 1200 mg, capped at the 1000 mg maximum: 500 mg/5 mL in 0.5 mL steps is exactly 10 mL 60,000, 20,000, 1,000,000, 500,000, 5,000, 500 → calculated micrograms 1,200,000, capped at maximum true, target micrograms 1,000,000, volume microlitres 10,000, delivered micrograms 1,000,000, difference basis points 0%
capped at 1000 mg, 120 mg/5 mL is 41.667 mL; the nearest 0.1 mL step (41.7 mL, 1000.8 mg) would exceed the maximum, so 41.6 mL 60,000, 20,000, 1,000,000, 120,000, 5,000, 100 → calculated micrograms 1,200,000, capped at maximum true, target micrograms 1,000,000, volume microlitres 41,600, delivered micrograms 998,400, difference basis points -0.16%
a dose exactly at the maximum is not capped 50,000, 20,000, 1,000,000, 500,000, 5,000, 500 → calculated micrograms 1,000,000, capped at maximum false, target micrograms 1,000,000, volume microlitres 10,000, delivered micrograms 1,000,000, difference basis points 0%
a neonate on an oral syringe marked in 0.01 mL: 3.45 kg x 15 mg/kg = 51.75 mg = 2.15625 mL, nearest 2.16 mL 3,450, 15,000, —, 120,000, 5,000, 10 → calculated micrograms 51,750, capped at maximum false, target micrograms 51,750, volume microlitres 2,160, delivered micrograms 51,840, difference basis points 0.17%
a 1 mL measure: 157.5 mg at 250 mg/5 mL is 3.15 mL, so 3 mL (150 mg, 4.76% under) 21,000, 7,500, —, 250,000, 5,000, 1,000 → calculated micrograms 157,500, capped at maximum false, target micrograms 157,500, volume microlitres 3,000, delivered micrograms 150,000, difference basis points -4.76%
half a microgram: the target is shown half-up (1235) but the difference is measured from the exact 1234.5, and the tie in steps goes down 12,345, 100, —, 1,000, 1,000, 1 → calculated micrograms 1,235, capped at maximum false, target micrograms 1,235, volume microlitres 1,234, delivered micrograms 1,234, difference basis points -0.04%
an awkward strength stays exact: 1 g in 3 mL, 10 mg/kg for 20 kg is 200 mg = 0.6 mL 20,000, 10,000, —, 1,000,000, 3,000, 100 → calculated micrograms 200,000, capped at maximum false, target micrograms 200,000, volume microlitres 600, delivered micrograms 200,000, difference basis points 0%
a dose smaller than one measuring increment is an error, not zero 1,000, 100, —, 120,000, 5,000, 100 → error: dose is less than one measuring increment
Show the other 10 tests
CaseArgumentsExpected
an exact half increment rounds down to nothing and is refused 1,000, 1,200, —, 120,000, 5,000, 100 → error: dose is less than one measuring increment
weight below 200 g is refused 199, 15,000, —, 120,000, 5,000, 100 → error: weightGrams must be a whole number from 200 to 500000
weight in kilograms by mistake (12.4) is refused, not rounded 12.4, 15,000, —, 120,000, 5,000, 100 → error: weightGrams must be a whole number from 200 to 500000
a fractional weight in grams is refused 12,400.5, 15,000, —, 120,000, 5,000, 100 → error: weightGrams must be a whole number from 200 to 500000
weight above 500 kg is refused 500,001, 15,000, —, 120,000, 5,000, 100 → error: weightGrams must be a whole number from 200 to 500000
a zero dose per kg is refused 12,400, 0, —, 120,000, 5,000, 100 → error: dosePerKgMicrograms must be a whole number from 1 to 1000000
a zero maximum is refused, not read as no maximum 12,400, 15,000, 0, 120,000, 5,000, 100 → error: maxSingleDoseMicrograms must be a whole number from 1 to 100000000
a zero strength is refused 12,400, 15,000, —, 0, 5,000, 100 → error: strengthMicrograms must be a whole number from 1 to 100000000
a zero strength volume is refused 12,400, 15,000, —, 120,000, 0, 100 → error: strengthVolumeMicrolitres must be a whole number from 1 to 1000000
a measuring increment over 100 mL is refused 12,400, 15,000, —, 120,000, 5,000, 100,001 → error: measuringIncrementMicrolitres must be a whole number from 1 to 100000

More from the author

## Units

Everything is a whole number in small units, so no step drifts. In floating point, three 0.1 mL steps are 0.30000000000000004 mL. Here they are 300 µL.

| you have | pass | |----------|---------------------------| | 1 kg | 1000 g | | 1 mg | 1000 µg | | 1 mL | 1000 µL | | 120 mg/5 mL | strength 120000 µg in 5000 µL | | 1 g in 3 mL | strength 1000000 µg in 3000 µL (no 333.33… mg/mL rounding) |

A weight of 12.4 is refused, not read as grams, because a slip between kilograms and grams is a thousand-fold error.

## Worked example

15 mg/kg for a 12.4 kg child is 186 mg. At 120 mg in 5 mL that is 7.75 mL. The syringe is marked in 0.1 mL, and 7.75 lies exactly between 7.7 and 7.8. The tie goes down, so the answer is 7.7 mL, which holds 184.8 mg. `differenceBasisPoints` is -65, meaning the given dose is 0.65% below the calculated dose.

## Rounding rules

- **Cap first.** When weight × dose per kg is more than the maximum single dose, the maximum becomes the target and `cappedAtMaximum` is true. A dose exactly at the maximum is not capped. - **Nearest measurable volume, a tie goes down.** When the dose is exactly halfway between two steps, the smaller one is chosen, because that is the more cautious choice. - **Never over the maximum.** If rounding to the nearest step would deliver more than the maximum single dose, the volume drops one step. - **Never zero.** If the dose rounds to no increments at all, the function raises an error. The fix is a more dilute product or a finer measure, not silently giving nothing.

Local policy may round differently. For example, some policies always round down, and some allow up to ±10%. `differenceBasisPoints` compares the delivered dose with the target (signed, 100 = 1%), so a caller can enforce its own tolerance. The integer outputs (`calculatedMicrograms`, `targetMicrograms`, `deliveredMicrograms`) are rounded half-up to a whole microgram for display. The comparisons and the difference use the exact fractions.

## Limits (refused, not clamped)

weightGrams 200 to 500000; dosePerKgMicrograms 1 to 1000000; maxSingleDoseMicrograms 1 to 100000000 or null for none; strengthMicrograms 1 to 100000000; strengthVolumeMicrolitres 1 to 1000000; measuringIncrementMicrolitres 1 to 100000.

It does not check that the dose per kg, the maximum or the product are right for the drug, the patient's age or renal function, or the route. It does not convert to dosing by body surface area, and it does not cap by a daily maximum.

## Before you rely on this

**Not professional advice.** This capability calculates health figures from published rules. It is a software component for developers, not medical 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 pharmacist review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.

**Not a medical device.** It is not intended to diagnose, treat or support clinical decisions about any individual. Anyone building it into clinical software is responsible for that software's regulatory status, and must validate it under their own clinical governance.

**Not for clinical use; regulatory review pending.** Whether publishing this capability makes it a medical device is under regulatory review. Until that is settled it is a developer library only.

**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 pharmacist 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.1 marks it unreviewed (not for clinical use; regulatory review pending). The code and the tests are unchanged.

Files

PathBytes
README.md4,777
impl/python.py3,480
impl/rust.rs5,178
impl/typescript.ts3,533
vectors.json5,176