metering 0.25.0

German energy metering quantities: DST-correct Europe/Berlin and 06:00-Gastag calendar arithmetic, exact decimal values, and the market's own code lists. Zero I/O, no async, no clock.
Documentation
+++
title = "Getting started"
description = "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."
weight = 1
[extra]
group = "Start"
+++

## Install

```bash
cargo add metering
cargo add metering --features serde
```

MSRV 1.88, edition 2024; [`serde`](@/docs/design.md#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](@/docs/series-and-readings.md) and
[Identifiers](@/docs/identifiers.md).

```rust
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](@/docs/validation-and-substitution.md).

```rust
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](@/docs/billing-quantities.md).

```rust
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](@/docs/series-and-readings.md).

```rust
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](@/docs/gas-and-units.md).

```rust
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)));
```