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:

  1. 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.

  2. 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#).

  3. Handle Bounds with unit: Bounds now carry a .unit field. The .lower and .upper fields are unchanged.

  4. 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.

  5. 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().

  6. 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.

  7. 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.

  8. Go-specific — rename RNG functions: Sample()RngSample(), Resample()RngResample(), Shuffle()RngShuffle(). Method variants: SampleFloat64SampleSlice, ResampleFloat64ResampleSlice, ShuffleFloat64ShuffleSlice.

  9. Go-specific — replace BoundsConfig: Replace BoundsConfig{Misrate: ptr} with direct float64 misrate parameter. If using Seed, switch to the *WithSeed function variant.

  10. 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