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=0was indistinguishable from "use default" (changed to*float64, then later replaced by direct parameter) - Go: Fixed integer overflow in
float64conversions —float64(a+b)split intofloat64(a)+float64(b)to avoid overflow in integer domain - C#: Fixed
MeasurementUnit.Equalsto compareId,Family, andBaseUnits(not justId), preventing collisions between units with same id but different base units - TypeScript: Added sample name (
'x'/'y') tocheckNonWeightederror messages for consistency with other languages - R: Fixed
as.numeric()dispatch onMeasurement— renamed toas.double.Measurement(R dispatches onas.double, notas.numeric) - R: Made
avg_spreadinternal (removed fromNAMESPACE) 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 ford>0) - Rust: Deduplicated
derive_seedintofnv1a::hash_f64_slice(was duplicated infast_center.rsandfast_spread.rs) - C#: Removed unused
FormatMessagemethod
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 inSample.of(data)(TS),Sample(data)(Py/Kt),Sample::new(data)?(Rust),NewSample(data, nil, nil)(Go),Sample$new(data)(R), ornew Sample(data)(C#). If the data has a unit, use thewithUnit/with_unitvariant.Handle
Measurementreturns: Estimators now returnMeasurement(value + unit) instead of a plain number. If the calling code expects a number, access.value(all languages) or.Value(C#).Handle
Boundswith unit: Bounds now carry a.unitfield. The.lowerand.upperfields are unchanged.Replace
relSpread: Search forrelSpread,rel_spread,RelSpreadcalls and replace withspread(x) / abs(center(x)). Note that bothspreadandcenternow returnMeasurement, so extract.valuebefore dividing.Rust-specific — remove
Box<dyn MeasurementUnit>: Replace allBox<dyn MeasurementUnit>withMeasurementUnit. ReplaceNumberUnitwithMeasurementUnit::number(),RatioUnitwithMeasurementUnit::ratio(),DisparityUnitwithMeasurementUnit::disparity(),CustomUnit::new(...)withMeasurementUnit::new(...). Replace.clone_box()with.clone().C#-specific — update standard unit references: Replace
NumberUnit.InstancewithMeasurementUnit.Number,RatioUnit.InstancewithMeasurementUnit.Ratio,DisparityUnit.InstancewithMeasurementUnit.Disparity. Remove subclass definitions if any.Kotlin-specific — update
CustomUnitusage: ReplaceCustomUnit(...)withMeasurementUnit(...). Removeis StandardUnit/is NumberUnittype checks (use value equality instead). Note:List<Double>overloads are now internal; useSample-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: ReplaceBoundsConfig{Misrate: ptr}with directfloat64misrate parameter. If usingSeed, switch to the*WithSeedfunction variant.Validation errors now come from
Sampleconstruction, not from estimator calls. If you catch validity errors (empty data, NaN, Inf), the error now fires atSamplecreation. 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