Getting started
Install the crate and do the five everyday things: build a series, validate and substitute it, aggregate it, derive it from readings, and convert gas.
Install
cargo add metering
cargo add metering --features serde
MSRV 1.88, edition 2024; serde is optional. The prelude holds the core nouns, the pipeline entry points and Decimal with dec!.
Instants are UTC, periods are local (Europe/Berlin) — the market's split, EDI@Energy Allgemeine Festlegungen 6.1d, Kap. 3: "Die Angabe von Zeiten in einer EDIFACT Nachricht erfolgt in koordinierter Weltzeit (Coordinated Universal Time, UTC)." and "Alle in den Prozessen genannten Zeitpunkte … nutzen die gesetzliche deutsche Zeit."
1. Build intervals, a series and identifiers
A MeterInterval is one value over [from, to) with an explicit QualityFlag. A Series is sorted, unique, on one grid, one channel. See Series and readings and Identifiers.
use metering::prelude::*;
use time::{Duration, macros::date};
let day = DayBoundary::Strom.day(date!(2026 - 06 - 01)).unwrap();
let slots = (0..96)
.map(|i| MeterInterval::quarter_hour(day.start() + Duration::minutes(15 * i), dec!(0.5), QualityFlag::Measured))
.collect::<Result<Vec<_>, _>>().unwrap();
let series = Series::new(Resolution::QUARTER_HOUR, DayBoundary::Strom, slots).unwrap();
assert_eq!(series.billable_total(), Some(dec!(48)));
// Identifiers verify their check digit at the parse.
let malo: MaloId = "41373559241".parse().unwrap();
assert_eq!(malo.check_digit(), 1);
assert!("41373559214".parse::<MaloId>().is_err());
2. Validate, then substitute
validate returns findings, the rules that ran, coverage and a grade; substitute replaces exactly the slots an error finding covers. See Validation and substitution.
use metering::prelude::*;
use metering::time::holiday::Bundesland;
use metering::vee::substitute::SubstitutionReason;
use metering::vee::validation::{Grade, Rule};
use time::{Duration, macros::{date, datetime}};
let day = DayBoundary::Strom.day(date!(2026 - 06 - 01)).unwrap();
let slots = (0..96)
.filter(|i| !(40..44).contains(i)) // one hour missing
.map(|i| MeterInterval::quarter_hour(day.start() + Duration::minutes(15 * i), dec!(1), QualityFlag::Measured))
.collect::<Result<Vec<_>, _>>().unwrap();
let series = Series::new(Resolution::QUARTER_HOUR, DayBoundary::Strom, slots).unwrap();
let rules = Rules::strom(day, datetime!(2026-07-01 0:00 UTC), None);
let report = validate(&series, &rules);
assert_eq!(report.grade(), Grade::C);
assert_eq!(report.by_rule(Rule::Gap).count(), 1);
let policy = Policy::strom(Bundesland::Be, SubstitutionReason::CommunicationFailure);
let filled = substitute(&series, &report, &policy).unwrap();
assert_eq!(filled.substitutes.len(), 4);
assert_eq!(filled.substitutes[0].code, Some("Z92")); // interpolation
assert_eq!(filled.substitutes[0].reason.code(), "Z75"); // STS+Z40
assert_eq!(filled.series.len(), 96);
3. Aggregate, and split into tariff registers
See Billing quantities.
use metering::billing::zaehlzeit::{HT, NT, Zaehlzeitdefinition};
use metering::prelude::*;
use time::{Duration, macros::date};
let day = DayBoundary::Strom.day(date!(2026 - 06 - 01)).unwrap(); // a Monday
let slots = (0..96)
.map(|i| MeterInterval::quarter_hour(day.start() + Duration::minutes(15 * i), dec!(1), QualityFlag::Measured))
.collect::<Result<Vec<_>, _>>().unwrap();
let series = Series::new(Resolution::QUARTER_HOUR, DayBoundary::Strom, slots).unwrap();
let period = aggregate(&series, day).unwrap();
assert_eq!(period.arbeitsmenge, dec!(96));
assert_eq!(period.spitzenleistung_kw, Some(dec!(4)));
// HT 06:00–22:00 on weekdays, NT for the rest.
let zzd = Zaehlzeitdefinition::ht_nt("NB-1", date!(2026 - 01 - 01), 6 * 60, 22 * 60).unwrap();
let split = zzd.split_energy(series.as_slice()).unwrap();
assert_eq!(split.per_register[HT], dec!(64));
assert_eq!(split.per_register[NT], dec!(32));
assert!(split.is_complete());
4. From readings to a series
to_lastgang differences a Zählerstandsgang into one Lastgang per register. See Series and readings.
use metering::prelude::*;
use time::{Duration, macros::datetime};
let t = datetime!(2026-06-01 0:00 UTC);
let readings: Vec<MeterReading> = [dec!(1000.0), dec!(1002.5), dec!(1004.8), dec!(1007.0)]
.into_iter()
.enumerate()
.map(|(i, v)| MeterReading::measured(t + Duration::minutes(15 * i as i64), v))
.collect();
let lastgang = &to_lastgang(&readings, &LastgangConfig::strom())[0];
assert!(lastgang.is_clean());
let series = lastgang.to_series(Resolution::QUARTER_HOUR, DayBoundary::Strom).unwrap();
assert_eq!(series.len(), 3); // n readings, n − 1 intervals
assert_eq!(series.billable_total(), Some(dec!(7.0)));
5. The Gastag and gas conversion
The Gastag runs 06:00 to 06:00 local; kWh_Hs = m³ × Brennwert × Zustandszahl. See Gas, units and load profiles.
use metering::gas::conversion::gas_m3_to_kwh_hs;
use metering::prelude::*;
use time::macros::{date, datetime};
let gastag = DayBoundary::Gas.day(date!(2026 - 01 - 15)).unwrap();
assert_eq!(gastag.start(), datetime!(2026-01-15 5:00 UTC)); // 06:00 MEZ
// 100 m³ at a Brennwert of 11.2 kWh/m³ and a Zustandszahl of 0.95.
assert_eq!(gas_m3_to_kwh_hs(dec!(100), dec!(11.2), dec!(0.95)), Some(dec!(1064.000)));