Highlights
v11.0.0 introduces the metrology system across all 7 languages (C#, Go, Kotlin, Python, R, Rust, TypeScript). Estimators now accept Sample objects and return Measurement/Bounds values with automatic unit propagation. The release also simplifies MeasurementUnit to a single concrete type in all languages and removes the deprecated relSpread API.
New Features
Metrology System
All 7 languages now have a unified metrology API:
| Type |
Purpose |
Sample |
Validated collection of values with optional unit and weights |
Measurement |
A numeric value paired with a MeasurementUnit |
MeasurementUnit |
Describes a unit of measurement (id, family, abbreviation, fullName, baseUnits) |
Bounds |
Lower/upper confidence interval paired with a MeasurementUnit |
UnitRegistry |
Registry for resolving units by id |
Estimator signature changes. All estimators now accept Sample and return Measurement or Bounds:
center(sample) → Measurement (unit = sample.unit)
spread(sample) → Measurement (unit = sample.unit)
shift(x, y) → Measurement (unit = finer(x.unit, y.unit))
ratio(x, y) → Measurement (unit = RatioUnit)
disparity(x, y) → Measurement (unit = DisparityUnit)
*Bounds(...) → Bounds (unit follows the same rules)
Validation at construction. Sample validates data at construction time (non-empty, all values finite). Estimators no longer perform inline validity checks.
Weighted sample rejection. All estimators reject weighted samples (weighted algorithms are deferred to a future release).
Unit propagation rules:
- Center/spread: result unit = input sample's unit
- Shift: inputs auto-converted to finer unit; result unit = finer of the two
- Ratio: result unit =
RATIO (dimensionless ratio)
- Disparity: result unit =
DISPARITY (dimensionless disparity)
Cross-Language Test Fixtures
New test suites in tests/:
sample-construction/ — 7 fixtures covering valid construction and error cases (empty, NaN, Inf, -Inf)
unit-propagation/ — 6 fixtures covering unit preservation, ratio/disparity unit assignment, and weighted rejection
Breaking Changes
1. relSpread / rel_spread removed (all languages)
The relSpread estimator, deprecated in v10.0.0, is now removed from all 7 languages. Use spread(x) / abs(center(x)) instead.
Removed symbols:
- C#:
Toolkit.RelSpread()
- Go:
RelSpread()
- Kotlin:
relSpread()
- Python:
rel_spread()
- R:
rel_spread()
- Rust:
rel_spread()
- TypeScript:
relSpread()
2. Estimator signatures changed (all languages)
All estimators now accept Sample instead of raw arrays/slices and return Measurement/Bounds instead of raw numbers.
3. MeasurementUnit simplified to a single concrete type (Rust, Kotlin, C#)
Rust: trait MeasurementUnit + NumberUnit/RatioUnit/DisparityUnit/CustomUnit structs replaced by a single MeasurementUnit struct. Box<dyn MeasurementUnit> eliminated.
Kotlin: MeasurementUnit interface + StandardUnit sealed interface + NumberUnit/RatioUnit/DisparityUnit data objects + CustomUnit data class collapsed into a single MeasurementUnit data class.
C#: MeasurementUnit made non-abstract. NumberUnit/RatioUnit/DisparityUnit subclasses replaced by static fields MeasurementUnit.Number, .Ratio, .Disparity. Value wrappers NumberValue/RatioValue/DisparityValue deleted.
4. Go: BoundsConfig replaced with direct parameters
BoundsConfig struct removed. Bounds functions now take misrate float64 directly. Seed-requiring variants split into separate *WithSeed functions.
5. Go: RNG collection functions renamed
To avoid collision with the new Sample type:
Sample() → RngSample()
Resample() → RngResample()
Shuffle() → RngShuffle()
Rng.SampleFloat64() → Rng.SampleSlice()
Rng.ResampleFloat64() → Rng.ResampleSlice()
Rng.ShuffleFloat64() → Rng.ShuffleSlice()
6. Kotlin: List<Double> estimator overloads made internal
Top-level estimator functions that accept List<Double> are now internal. Only the Sample-based API is public.
7. Kotlin: Demo isolated in separate Gradle subproject
Demo code moved from kt/src/ to kt/demo/ subproject. The application plugin and mainClass config removed from the main build.gradle.kts.
Bug Fixes
- Go: Fixed zero-value bug where
BoundsConfig.Misrate=0 was indistinguishable from "use default" (changed to *float64, then later replaced by direct parameter)
- Go: Fixed integer overflow in
float64 conversions — float64(a+b) split into float64(a)+float64(b) to avoid overflow in integer domain
- C#: Fixed
MeasurementUnit.Equals to compare Id, Family, and BaseUnits (not just Id), preventing collisions between units with same id but different base units
- TypeScript: Added sample name (
'x'/'y') to checkNonWeighted error messages for consistency with other languages
- R: Fixed
as.numeric() dispatch on Measurement — renamed to as.double.Measurement (R dispatches on as.double, not as.numeric)
- R: Made
avg_spread internal (removed from NAMESPACE) to match all other languages
Refactoring
- Go, Rust: Removed dead branch in
fast_spread (the condition |k-1-c| <= |c-k| always evaluates to true for d>0)
- Rust: Deduplicated
derive_seed into fnv1a::hash_f64_slice (was duplicated in fast_center.rs and fast_spread.rs)
- C#: Removed unused
FormatMessage method
Migration Guide
Quick Reference: Symbol Renames
| Language |
Before |
After |
| All |
relSpread(x) / rel_spread(x) |
spread(x) / abs(center(x)) |
| C# |
NumberUnit.Instance |
MeasurementUnit.Number |
| C# |
RatioUnit.Instance |
MeasurementUnit.Ratio |
| C# |
DisparityUnit.Instance |
MeasurementUnit.Disparity |
| C# |
new CustomUnit(...) |
new MeasurementUnit(...) |
| Rust |
Box<dyn MeasurementUnit> |
MeasurementUnit |
| Rust |
NumberUnit |
MeasurementUnit::number() |
| Rust |
RatioUnit |
MeasurementUnit::ratio() |
| Rust |
DisparityUnit |
MeasurementUnit::disparity() |
| Rust |
CustomUnit::new(...) |
MeasurementUnit::new(...) |
| Kotlin |
CustomUnit(...) |
MeasurementUnit(...) |
| Kotlin |
NumberUnit (data object) |
NumberUnit (top-level val) |
| Kotlin |
is NumberUnit / is StandardUnit |
Value equality check |
| Go |
Sample(rng, x, k) |
RngSample(rng, x, k) |
| Go |
Resample(rng, x, k) |
RngResample(rng, x, k) |
| Go |
Shuffle(rng, x) |
RngShuffle(rng, x) |
| Go |
rng.SampleFloat64(x, k) |
rng.SampleSlice(x, k) |
| Go |
rng.ResampleFloat64(x, k) |
rng.ResampleSlice(x, k) |
| Go |
rng.ShuffleFloat64(x) |
rng.ShuffleSlice(x) |
Migrating Estimator Calls (All Languages)
The core change: wrap raw data in Sample, extract .value from Measurement results where you need a plain number.
TypeScript
// v10 — raw arrays
import { center, spread, shift, shiftBounds } from 'pragmastat';
const c = center(values); // number
const s = spread(values); // number
const sh = shift(x, y); // number
const b = shiftBounds(x, y, 0.001); // { lower, upper }
// v11 — Sample-based
import { Sample, center, spread, shift, shiftBounds } from 'pragmastat';
const sx = Sample.of(values);
const c = center(sx); // Measurement { value, unit }
const s = spread(sx); // Measurement { value, unit }
const sy = Sample.of(yValues);
const sh = shift(sx, sy); // Measurement { value, unit }
const b = shiftBounds(sx, sy, 0.001); // Bounds { lower, upper, unit }
// To get plain numbers:
const cValue = center(sx).value;
Python
# v10 — raw lists
from pragmastat import center, spread, shift
c = center(values) # float
sh = shift(x, y) # float
# v11 — Sample-based
from pragmastat import Sample, center, spread, shift
sx = Sample(values)
c = center(sx) # Measurement(value, unit)
sh = shift(sx, Sample(y)) # Measurement(value, unit)
# To get plain numbers:
c_value = center(sx).value
Rust
// v10 — raw slices
use pragmastat::{center, spread, shift};
let c = center(&values)?;
let sh = shift(&x, &y)?;
// v11 — Sample-based (raw API preserved in estimators::raw)
use pragmastat::{Sample, center, spread, shift};
let sx = Sample::new(values)?;
let c = center(&sx)?; // Measurement { value, unit }
let sh = shift(&sx, &sy)?; // Measurement { value, unit }
// Raw slice API still available:
use pragmastat::estimators::raw;
let c = raw::center(&values)?; // f64
Go
// v10 — raw slices + BoundsConfig
c, _ := Center(values)
sh, _ := Shift(x, y)
b, _ := ShiftBounds(x, y, BoundsConfig{Misrate: float64Ptr(0.05)})
sampled := Sample(rng, data, 10)
// v11 — *Sample + direct misrate
sx, _ := NewSample(values, nil, nil)
c, _ := Center(sx) // Measurement
sh, _ := Shift(sx, sy) // Measurement
b, _ := ShiftBounds(sx, sy, 0.05) // Bounds (misrate is direct float64)
sampled := RngSample(rng, data, 10) // renamed to avoid collision
Kotlin
// v10 — List<Double>
val c = center(values) // Double
val sh = shift(x, y) // Double
// v11 — Sample-based (List<Double> overloads now internal)
val sx = Sample(values)
val c = center(sx) // Measurement
val sh = shift(sx, sy) // Measurement
// CustomUnit migration:
// v10: CustomUnit("ns", "Time", "ns", "Nanosecond", 1_000_000)
// v11: MeasurementUnit("ns", "Time", "ns", "Nanosecond", 1_000_000)
C#
// v10 — Sample without unit-aware returns
var c = Toolkit.Center(sample); // double
var b = Toolkit.CenterBounds(sample, 0.05); // Bounds (no unit)
// v11 — returns Measurement/Bounds with unit
var c = Toolkit.Center(sample); // Measurement { Value, Unit }
var b = Toolkit.CenterBounds(sample, 0.05); // Bounds { Lower, Upper, Unit }
// Unit migration:
// v10: NumberUnit.Instance / RatioUnit.Instance / DisparityUnit.Instance
// v11: MeasurementUnit.Number / MeasurementUnit.Ratio / MeasurementUnit.Disparity
// Custom unit migration:
// v10: class MyUnit : MeasurementUnit { ... } (abstract base)
// v11: new MeasurementUnit("ns", "Time", "ns", "Nanosecond", 1_000_000)
R
# R has backward-compatible dual interface:
# - Pass numeric vector → get numeric result (unchanged)
# - Pass Sample object → get Measurement/Bounds with unit
# v10 behavior (still works):
c <- center(values) # numeric
# v11 new behavior:
sx <- Sample$new(values, unit = my_unit)
c <- center(sx) # Measurement with unit
c$value # extract numeric
Migrating MeasurementUnit (Rust)
// v10 — trait + dynamic dispatch
use pragmastat::{MeasurementUnit, NumberUnit, CustomUnit};
let unit: Box<dyn MeasurementUnit> = Box::new(NumberUnit);
let custom: Box<dyn MeasurementUnit> = Box::new(CustomUnit::new("ns", "Time", "ns", "Nanosecond", 1_000_000));
let sample = Sample::with_unit(values, Box::new(NumberUnit));
let cloned = unit.clone_box();
// v11 — single struct, no boxing
use pragmastat::MeasurementUnit;
let unit = MeasurementUnit::number();
let custom = MeasurementUnit::new("ns", "Time", "ns", "Nanosecond", 1_000_000);
let sample = Sample::with_unit(values, MeasurementUnit::number());
let cloned = unit.clone();
// Now supports PartialEq/Eq/Hash
assert_eq!(MeasurementUnit::number(), MeasurementUnit::number());
Migrating BoundsConfig (Go)
// v10 — variadic BoundsConfig
b, _ := ShiftBounds(x, y, BoundsConfig{Misrate: float64Ptr(0.05)})
b, _ := SpreadBounds(x, BoundsConfig{Misrate: float64Ptr(0.05), Seed: "my-seed"})
// v11 — direct parameters
b, _ := ShiftBounds(sx, sy, 0.05)
b, _ := SpreadBoundsWithSeed(sx, 0.05, "my-seed") // seed now via separate function
Migrating relSpread (All Languages)
// v10
relSpread(x) // or rel_spread(x), Toolkit.RelSpread(x), etc.
// v11 — manual composition
spread(x).value / Math.abs(center(x).value) // TS/Kotlin/C#
spread(x).value / abs(center(x).value) // Python/R
spread(&sx)?.value / center(&sx)?.value.abs() // Rust
LLM Migration Instructions
When migrating code from pragmastat v10 to v11, apply these transformations:
Wrap raw data in Sample: Find all estimator calls that pass raw arrays/slices/lists. Wrap the data in Sample.of(data) (TS), Sample(data) (Py/Kt), Sample::new(data)? (Rust), NewSample(data, nil, nil) (Go), Sample$new(data) (R), or new Sample(data) (C#). If the data has a unit, use the withUnit / with_unit variant.
Handle Measurement returns: Estimators now return Measurement (value + unit) instead of a plain number. If the calling code expects a number, access .value (all languages) or .Value (C#).
Handle Bounds with unit: Bounds now carry a .unit field. The .lower and .upper fields are unchanged.
Replace relSpread: Search for relSpread, rel_spread, RelSpread calls and replace with spread(x) / abs(center(x)). Note that both spread and center now return Measurement, so extract .value before dividing.
Rust-specific — remove Box<dyn MeasurementUnit>: Replace all Box<dyn MeasurementUnit> with MeasurementUnit. Replace NumberUnit with MeasurementUnit::number(), RatioUnit with MeasurementUnit::ratio(), DisparityUnit with MeasurementUnit::disparity(), CustomUnit::new(...) with MeasurementUnit::new(...). Replace .clone_box() with .clone().
C#-specific — update standard unit references: Replace NumberUnit.Instance with MeasurementUnit.Number, RatioUnit.Instance with MeasurementUnit.Ratio, DisparityUnit.Instance with MeasurementUnit.Disparity. Remove subclass definitions if any.
Kotlin-specific — update CustomUnit usage: Replace CustomUnit(...) with MeasurementUnit(...). Remove is StandardUnit / is NumberUnit type checks (use value equality instead). Note: List<Double> overloads are now internal; use Sample-based API.
Go-specific — rename RNG functions: Sample() → RngSample(), Resample() → RngResample(), Shuffle() → RngShuffle(). Method variants: SampleFloat64 → SampleSlice, ResampleFloat64 → ResampleSlice, ShuffleFloat64 → ShuffleSlice.
Go-specific — replace BoundsConfig: Replace BoundsConfig{Misrate: ptr} with direct float64 misrate parameter. If using Seed, switch to the *WithSeed function variant.
Validation errors now come from Sample construction, not from estimator calls. If you catch validity errors (empty data, NaN, Inf), the error now fires at Sample creation. The error subject is "x" by default; for a y-sample in two-sample estimators, use the factory method that sets subject to "y" (e.g., Sample.of(yValues, 'y') in TS).
Full Changelog: https://github.com/AndreyAkinshin/pragmastat/compare/v10.0.6...v11.0.0