stats.weighted-average
Weighted average of integers, computed exactly and rounded once to stated decimals with an explicit rounding mode.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 21 tests, run in TypeScript, Python and Rust.
What it does
Sum of value x weight, divided by the sum of the weights: the average price paid over several purchases, a grade point average weighted by credits, a stock's average cost.
Everything up to the last step is integer arithmetic, so the only rounding is the one the caller asked for. The exact quotient sum(v x w) x 10^decimals / sum(w) is rounded once by `math.round-div` in the given mode (half-up, half-even, down or up), and the resulting integer is divided by 10^decimals. That last division returns the nearest binary64 to a decimal with at most fifteen significant digits, which is the same double in every language and prints back as exactly that decimal. There is no float accumulation to drift.
For example
weighted_average(90, 80, 3, 1, 1, half-up)→ 87.5 a grade weighted 3 to 1weighted_average(10, 20, 1, 3, 2, half-up)→ 17.5 weights change the answer: one item at 10 and three at 20 average 17.5, not 15weighted_average(199, 249, 3, 2, 0, half-up)→ 219 prices in pence weighted by quantity
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.
def weighted_average(values: Sequence[int], weights: Sequence[int], decimals: int, mode: RoundingMode) -> float
| values | int[] | whole numbers in their smallest unit (pence, grams, marks) |
| weights | int[] | one per value, 0 or greater, not all 0 (quantities, credits, shares) |
| decimals | int | 0 to 9 places in the result |
| mode | RoundingMode | how the single rounding step breaks ties |
| returns | float |
Your code names it in one line, in the file that uses it
from fune.stats.weighted_average import weighted_average # stats.weighted-average@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import Sequence
from .math_round_div import RoundingMode, round_div ← from math.round-div ^1.0.0 · built alongside by fune
MAX_SAFE = 9007199254740991
def _whole(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool) and abs(value) <= MAX_SAFE
def weighted_average(values: Sequence[int], weights: Sequence[int], decimals: int, mode: RoundingMode) -> float:
"""Weighted average of integers, rounded once at the end.
The numerator and denominator are exact integers, so the rounding mode the
caller names is the only rounding that ever happens.
"""
for seq in (values, weights):
if isinstance(seq, (str, bytes)) or not isinstance(seq, (list, tuple)):
raise TypeError("values and weights must be lists of integers")
if len(values) != len(weights):
raise ValueError("values and weights must be the same length, received %d and %d" % (len(values), len(weights)))
if len(values) == 0:
raise ValueError("values must not be empty")
if isinstance(decimals, bool) or not isinstance(decimals, int) or decimals < 0 or decimals > 9:
raise ValueError("decimals must be a whole number from 0 to 9, received %r" % (decimals,))
numerator = 0
denominator = 0
for v, w in zip(values, weights):
if not _whole(v):
raise TypeError("values must be integers, received %r" % (v,))
if not _whole(w):
raise TypeError("weights must be integers, received %r" % (w,))
if w < 0:
raise ValueError("weights must not be negative, received %d" % w)
numerator += v * w
denominator += w
if denominator == 0:
raise ValueError("weights must not all be zero")
scaled = numerator * 10**decimals
# Python ints never overflow, but TypeScript numbers and Rust i64 do; refuse
# the same inputs everywhere so the languages cannot disagree.
if abs(scaled) > MAX_SAFE or denominator > MAX_SAFE:
raise ValueError("weighted sum is too large to average exactly; lower decimals or rescale the values")
return round_div(scaled, denominator, mode) / float(10**decimals) + 0.0Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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 stats.weighted-average
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./stats.weighted-average-1.0.0-python.fune, or fetch it from a terminal with fune pull stats.weighted-average@1.0.0:python.
The whole function, every language, is one file too: stats.weighted-average-1.0.0.fune, 13,212 bytes, sha256 b7bb1a17d5815e23abb96433395dccabe366ec5e241f044edb1bfa80f4b08b40. 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 stats.weighted-average
after — your function gets the result and the arguments, and returns the final result.
# fune: after stats.weighted-average
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.round-div in stats.weighted-average
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 stats.weighted-average --steps.
# fune: step stats.weighted-average 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 grade weighted 3 to 1 | 90, 80, 3, 1, 1, half-up | → | 87.5 |
| weights change the answer: one item at 10 and three at 20 average 17.5, not 15 | 10, 20, 1, 3, 2, half-up | → | 17.5 |
| prices in pence weighted by quantity | 199, 249, 3, 2, 0, half-up | → | 219 |
| 87.5 to whole numbers rounds half up to 88 | 90, 80, 3, 1, 0, half-up | → | 88 |
| 2.5 half-up is 3 | 2, 3, 1, 1, 0, half-up | → | 3 |
| 2.5 half-even is 2 | 2, 3, 1, 1, 0, half-even | → | 2 |
| 1.5 half-even is 2 | 1, 2, 1, 1, 0, half-even | → | 2 |
| a third to four places | 1, 0, 0, 1, 1, 1, 4, half-up | → | 0.333 |
| a third rounded up | 1, 0, 0, 1, 1, 1, 4, up | → | 0.333 |
| down truncates toward zero for negatives | -2, -1, -1, 1, 1, 1, 2, down | → | -1.33 |
Show the other 11 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| up rounds away from zero for negatives | -2, -1, -1, 1, 1, 1, 2, up | → | -1.34 |
| a zero weight contributes nothing | 100, 5,000, 1, 0, 0, half-up | → | 100 |
| nine decimals of an exact answer | 1, 3, 9, half-up | → | 1 |
| a zero average is 0 | -5, 5, 2, 2, 3, half-up | → | 0 |
| mismatched lengths are an error | 1, 2, 1, 0, half-up | → | error: values and weights must be the same length |
| an empty list is an error | , , 0, half-up | → | error: values must not be empty |
| a negative weight is an error | 1, 2, 1, -1, 0, half-up | → | error: weights must not be negative |
| all-zero weights are an error | 1, 2, 0, 0, 0, half-up | → | error: weights must not all be zero |
| decimals above 9 is an error | 1, 1, 10, half-up | → | error: decimals must be a whole number from 0 to 9 |
| a fractional value is an error | 1.5, 1, 0, half-up | → | error: values must be integers |
| a weighted sum past 2^53 - 1 once scaled is an error | 9,007,199,254,740,991, 1, 1, half-up | → | error: weighted sum is too large to average exactly |
More from the author
Values and weights are integers on purpose. Take money in pence and weights in whole units (or scale fractional weights, 0.25 as 25); then the average is exact before it is rounded.
Weights of zero are allowed and simply contribute nothing. Negative weights are an error, and so is a list where every weight is zero: there is no average of nothing.
The weighted sum, scaled by 10^decimals, must stay within 2^53 - 1 (the range integers share across TypeScript, Python and Rust). Beyond that the capability refuses rather than rounding silently; lower `decimals` or rescale the values.
A naive "average of the averages" (15 for one item at 10 and three at 20) is the classic mistake here; the answer is 17.5.
Files
| Path | Bytes |
|---|---|
| README.md | 1,440 |
| impl/python.py | 2,128 |
| impl/rust.rs | 2,844 |
| impl/typescript.ts | 2,090 |
| vectors.json | 2,475 |