use crate::generated::v202607::Mengeneinheit;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum Dimension {
Energy,
Power,
ReactiveEnergy,
ReactivePower,
Volume,
Time,
Count,
Ratio,
Frequency,
EnergyPerTemperature,
Dimensionless,
}
impl Dimension {
#[must_use]
pub const fn base_unit(self) -> Mengeneinheit {
match self {
Dimension::Energy => Mengeneinheit::Wh,
Dimension::Power => Mengeneinheit::W,
Dimension::ReactiveEnergy => Mengeneinheit::Varh,
Dimension::ReactivePower => Mengeneinheit::Var,
Dimension::Volume => Mengeneinheit::Kubikmeter,
Dimension::Time => Mengeneinheit::Sekunde,
Dimension::Count => Mengeneinheit::Stueck,
Dimension::Ratio => Mengeneinheit::Prozent,
Dimension::Frequency => Mengeneinheit::Hz,
Dimension::EnergyPerTemperature => Mengeneinheit::Kwhk,
Dimension::Dimensionless => Mengeneinheit::Dimensionslos,
}
}
#[must_use]
pub const fn is_extensive(self) -> bool {
matches!(
self,
Dimension::Energy
| Dimension::ReactiveEnergy
| Dimension::Volume
| Dimension::Count
| Dimension::Time
)
}
}
impl Mengeneinheit {
#[must_use]
pub const fn dimension(self) -> Option<Dimension> {
Some(match self {
Mengeneinheit::Wh | Mengeneinheit::Kwh | Mengeneinheit::Mwh => Dimension::Energy,
Mengeneinheit::W | Mengeneinheit::Kw | Mengeneinheit::Mw => Dimension::Power,
Mengeneinheit::Varh | Mengeneinheit::Kvarh => Dimension::ReactiveEnergy,
Mengeneinheit::Var | Mengeneinheit::Kvar => Dimension::ReactivePower,
Mengeneinheit::Kubikmeter => Dimension::Volume,
Mengeneinheit::Sekunde
| Mengeneinheit::Minute
| Mengeneinheit::ViertelStunde
| Mengeneinheit::Stunde
| Mengeneinheit::Tag
| Mengeneinheit::Woche
| Mengeneinheit::Monat
| Mengeneinheit::Quartal
| Mengeneinheit::Halbjahr
| Mengeneinheit::Jahr => Dimension::Time,
Mengeneinheit::Stueck => Dimension::Count,
Mengeneinheit::Prozent => Dimension::Ratio,
Mengeneinheit::Hz => Dimension::Frequency,
Mengeneinheit::Kwhk => Dimension::EnergyPerTemperature,
Mengeneinheit::Dimensionslos => Dimension::Dimensionless,
Mengeneinheit::Unknown => return None,
})
}
#[must_use]
pub const fn is_calendar(self) -> bool {
matches!(
self,
Mengeneinheit::Monat
| Mengeneinheit::Quartal
| Mengeneinheit::Halbjahr
| Mengeneinheit::Jahr
)
}
#[must_use]
pub const fn is_extensive(self) -> bool {
match self.dimension() {
Some(d) => d.is_extensive(),
None => false,
}
}
#[must_use]
pub const fn energy_unit(self) -> Option<Mengeneinheit> {
Some(match self {
Mengeneinheit::W => Mengeneinheit::Wh,
Mengeneinheit::Kw => Mengeneinheit::Kwh,
Mengeneinheit::Mw => Mengeneinheit::Mwh,
Mengeneinheit::Var => Mengeneinheit::Varh,
Mengeneinheit::Kvar => Mengeneinheit::Kvarh,
_ => return None,
})
}
#[must_use]
pub const fn power_unit(self) -> Option<Mengeneinheit> {
Some(match self {
Mengeneinheit::Wh => Mengeneinheit::W,
Mengeneinheit::Kwh => Mengeneinheit::Kw,
Mengeneinheit::Mwh => Mengeneinheit::Mw,
Mengeneinheit::Varh => Mengeneinheit::Var,
Mengeneinheit::Kvarh => Mengeneinheit::Kvar,
_ => return None,
})
}
#[cfg(feature = "decimal")]
#[cfg_attr(docsrs, doc(cfg(feature = "decimal")))]
#[must_use]
pub fn factor_to_base(self) -> Option<rust_decimal::Decimal> {
use rust_decimal::Decimal;
Some(Decimal::from(match self {
Mengeneinheit::Wh
| Mengeneinheit::W
| Mengeneinheit::Varh
| Mengeneinheit::Var
| Mengeneinheit::Kubikmeter
| Mengeneinheit::Stueck
| Mengeneinheit::Prozent
| Mengeneinheit::Hz
| Mengeneinheit::Kwhk
| Mengeneinheit::Dimensionslos
| Mengeneinheit::Sekunde => 1u32,
Mengeneinheit::Kwh | Mengeneinheit::Kw | Mengeneinheit::Kvarh | Mengeneinheit::Kvar => {
1_000
}
Mengeneinheit::Mwh | Mengeneinheit::Mw => 1_000_000,
Mengeneinheit::Minute => 60,
Mengeneinheit::ViertelStunde => 900,
Mengeneinheit::Stunde => 3_600,
Mengeneinheit::Tag => 86_400,
Mengeneinheit::Woche => 604_800,
Mengeneinheit::Monat
| Mengeneinheit::Quartal
| Mengeneinheit::Halbjahr
| Mengeneinheit::Jahr
| Mengeneinheit::Unknown => return None,
}))
}
#[cfg(feature = "decimal")]
#[cfg_attr(docsrs, doc(cfg(feature = "decimal")))]
#[must_use]
pub fn conversion_factor(self, target: Mengeneinheit) -> Option<rust_decimal::Decimal> {
if self.dimension()? != target.dimension()? {
return None;
}
self.factor_to_base()?.checked_div(target.factor_to_base()?)
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
#[must_use]
pub const fn exact_duration(self) -> Option<time::Duration> {
Some(time::Duration::seconds(match self {
Mengeneinheit::Sekunde => 1,
Mengeneinheit::Minute => 60,
Mengeneinheit::ViertelStunde => 900,
Mengeneinheit::Stunde => 3_600,
Mengeneinheit::Tag => 86_400,
Mengeneinheit::Woche => 604_800,
_ => return None,
}))
}
}
#[cfg(feature = "decimal")]
mod menge_impl {
use super::Dimension;
use crate::generated::v202607::{Menge, Mengeneinheit};
impl Menge {
#[must_use]
pub fn dimension(&self) -> Option<Dimension> {
self.einheit?.dimension()
}
#[must_use]
pub fn convert_to(&self, target: Mengeneinheit) -> Option<Menge> {
let source = self.einheit?;
if source.dimension()? != target.dimension()? {
return None;
}
let wert = self
.wert?
.checked_mul(source.factor_to_base()?)?
.checked_div(target.factor_to_base()?)?;
Some(Menge {
wert: Some(wert),
einheit: Some(target),
..self.clone()
})
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
#[must_use]
pub fn as_duration(&self) -> Option<time::Duration> {
if self.einheit?.dimension()? != Dimension::Time {
return None;
}
let seconds = self.wert?.checked_mul(self.einheit?.factor_to_base()?)?;
super::decimal_to_duration(seconds)
}
#[cfg(feature = "time")]
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
#[must_use]
pub fn energy_over(&self, duration: time::Duration) -> Option<Menge> {
let target = self.einheit?.energy_unit()?;
let hours = super::duration_to_hours(duration)?;
let wert = self.wert?.checked_mul(hours)?;
Some(Menge {
wert: Some(wert),
einheit: Some(target),
..self.clone()
})
}
}
}
#[cfg(all(feature = "decimal", feature = "time"))]
pub(crate) fn duration_to_seconds(duration: time::Duration) -> Option<rust_decimal::Decimal> {
let nanos = i64::try_from(duration.whole_nanoseconds()).ok()?;
rust_decimal::Decimal::try_new(nanos, 9).ok()
}
#[cfg(all(feature = "decimal", feature = "time"))]
pub(crate) fn duration_to_hours(duration: time::Duration) -> Option<rust_decimal::Decimal> {
duration_to_seconds(duration)?.checked_div(rust_decimal::Decimal::from(3_600))
}
#[cfg(all(feature = "decimal", feature = "time"))]
pub(crate) fn decimal_to_duration(seconds: rust_decimal::Decimal) -> Option<time::Duration> {
use rust_decimal::prelude::ToPrimitive;
let nanos = seconds.checked_mul(rust_decimal::Decimal::from(1_000_000_000u32))?;
Some(time::Duration::nanoseconds(nanos.trunc().to_i64()?))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_known_unit_has_a_dimension() {
for unit in Mengeneinheit::iter_known() {
assert!(
unit.dimension().is_some(),
"{} has no Dimension",
unit.as_wire()
);
}
assert_eq!(Mengeneinheit::Unknown.dimension(), None);
}
#[cfg(feature = "decimal")]
#[test]
fn base_units_are_self_consistent() {
use rust_decimal::Decimal;
for unit in Mengeneinheit::iter_known() {
let Some(dim) = unit.dimension() else {
continue;
};
let base = dim.base_unit();
assert_eq!(base.dimension(), Some(dim), "{base:?} left its dimension");
assert_eq!(base.factor_to_base(), Some(Decimal::ONE), "{base:?}");
}
}
#[cfg(feature = "decimal")]
#[test]
fn only_calendar_units_lack_a_factor() {
for unit in Mengeneinheit::iter_known() {
assert_eq!(
unit.factor_to_base().is_none(),
unit.is_calendar(),
"{} disagrees with is_calendar()",
unit.as_wire()
);
}
}
#[cfg(feature = "decimal")]
#[test]
fn conversion_round_trips_within_a_dimension() {
use rust_decimal::Decimal;
for a in Mengeneinheit::iter_known() {
for b in Mengeneinheit::iter_known() {
let factor = a.conversion_factor(b);
if a.dimension() != b.dimension() || a.is_calendar() || b.is_calendar() {
assert!(factor.is_none(), "{a:?} -> {b:?} should not convert");
continue;
}
let there = factor.expect("same dimension, both exact");
let back = b.conversion_factor(a).expect("symmetric");
assert!(
(there * back - Decimal::ONE).abs() < Decimal::new(1, 20),
"{a:?} <-> {b:?}: {there} * {back}"
);
}
}
}
#[test]
fn energy_and_power_units_pair_up() {
for unit in Mengeneinheit::iter_known() {
if let Some(energy) = unit.energy_unit() {
assert_eq!(energy.power_unit(), Some(unit), "{unit:?}");
assert!(energy.is_extensive(), "{energy:?} must be summable");
}
if let Some(power) = unit.power_unit() {
assert_eq!(power.energy_unit(), Some(unit), "{unit:?}");
}
}
}
#[test]
fn extensivity_matches_the_dimension() {
assert!(Mengeneinheit::Kwh.is_extensive());
assert!(Mengeneinheit::Kubikmeter.is_extensive());
assert!(!Mengeneinheit::Kw.is_extensive());
assert!(!Mengeneinheit::Hz.is_extensive());
assert!(!Mengeneinheit::Prozent.is_extensive());
assert!(!Mengeneinheit::Unknown.is_extensive());
}
#[cfg(feature = "time")]
#[test]
fn exact_durations_agree_with_the_factor() {
for unit in Mengeneinheit::iter_known() {
let is_exact_time = unit.dimension() == Some(Dimension::Time) && !unit.is_calendar();
assert_eq!(
unit.exact_duration().is_some(),
is_exact_time,
"{} disagrees",
unit.as_wire()
);
#[cfg(feature = "decimal")]
if let Some(d) = unit.exact_duration() {
assert_eq!(
super::duration_to_seconds(d),
unit.factor_to_base(),
"{}",
unit.as_wire()
);
}
}
}
#[cfg(feature = "decimal")]
#[test]
fn converting_through_the_base_unit_stays_exact() {
use crate::generated::v202607::Menge;
use rust_decimal::Decimal;
let two_minutes = Menge {
wert: Some(Decimal::from(120)),
einheit: Some(Mengeneinheit::Sekunde),
..Default::default()
}
.convert_to(Mengeneinheit::Minute)
.unwrap();
assert_eq!(two_minutes.wert, Some(Decimal::from(2)));
let rounded = Decimal::from(120)
* Mengeneinheit::Sekunde
.conversion_factor(Mengeneinheit::Minute)
.unwrap();
assert_ne!(rounded, Decimal::from(2));
}
#[cfg(all(feature = "decimal", feature = "time"))]
#[test]
fn power_integrates_into_energy() {
use crate::generated::v202607::Menge;
use rust_decimal::Decimal;
let load = Menge {
wert: Some(Decimal::from(400)),
einheit: Some(Mengeneinheit::Kw),
..Default::default()
};
let energy = load.energy_over(time::Duration::minutes(15)).unwrap();
assert_eq!(energy.wert, Some(Decimal::from(100)));
assert_eq!(energy.einheit, Some(Mengeneinheit::Kwh));
assert!(energy.energy_over(time::Duration::HOUR).is_none());
}
#[cfg(all(feature = "decimal", feature = "time"))]
#[test]
fn menge_reads_as_a_duration() {
use crate::generated::v202607::Menge;
use rust_decimal::Decimal;
let quarter = Menge {
wert: Some(Decimal::ONE),
einheit: Some(Mengeneinheit::ViertelStunde),
..Default::default()
};
assert_eq!(quarter.as_duration(), Some(time::Duration::minutes(15)));
let half_hour = Menge {
wert: Some(Decimal::new(5, 1)),
einheit: Some(Mengeneinheit::Stunde),
..Default::default()
};
assert_eq!(half_hour.as_duration(), Some(time::Duration::minutes(30)));
let energy = Menge {
wert: Some(Decimal::ONE),
einheit: Some(Mengeneinheit::Kwh),
..Default::default()
};
assert_eq!(energy.as_duration(), None);
}
}