Skip to main content

redispatch_xml/types/
common.rs

1//! Common primitive value types for Redispatch 2.0 XML (identifiers, timestamps, decimals, market roles).
2use std::fmt;
3
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use time::OffsetDateTime;
6use time::format_description::well_known::Rfc3339;
7
8use crate::RedispatchXmlError;
9
10// ── DocumentId ───────────────────────────────────────────────────────────────
11
12/// A unique document identifier per sender and document type (max 35 chars,
13/// case-sensitive). Used as `DocumentIdentification`, `OrderIdentification`,
14/// `AllocationIdentification`, `mRID`, etc.
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
16pub struct DocumentId(String);
17
18impl DocumentId {
19    /// Create a new [`DocumentId`], returning an error if the value is empty or
20    /// longer than 35 characters.
21    #[must_use = "this returns the new DocumentId, discarding it is likely a mistake"]
22    pub fn new(s: impl Into<String>) -> Result<Self, RedispatchXmlError> {
23        let s = s.into();
24        if s.is_empty() || s.len() > 35 {
25            return Err(RedispatchXmlError::InvalidDocumentId(s));
26        }
27        Ok(Self(s))
28    }
29
30    /// Return the string value.
31    pub fn as_str(&self) -> &str {
32        &self.0
33    }
34}
35
36impl AsRef<str> for DocumentId {
37    fn as_ref(&self) -> &str {
38        &self.0
39    }
40}
41
42impl TryFrom<String> for DocumentId {
43    type Error = RedispatchXmlError;
44    fn try_from(s: String) -> Result<Self, Self::Error> {
45        Self::new(s)
46    }
47}
48
49impl TryFrom<&str> for DocumentId {
50    type Error = RedispatchXmlError;
51    fn try_from(s: &str) -> Result<Self, Self::Error> {
52        Self::new(s)
53    }
54}
55
56impl fmt::Display for DocumentId {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        self.0.fmt(f)
59    }
60}
61
62impl<'de> Deserialize<'de> for DocumentId {
63    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
64        let s = String::deserialize(d)?;
65        Self::new(s).map_err(serde::de::Error::custom)
66    }
67}
68
69impl Serialize for DocumentId {
70    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
71        self.0.serialize(s)
72    }
73}
74
75// ── Mrid (IEC 62325 style, same constraints as DocumentId) ───────────────────
76
77/// An IEC 62325 message resource identifier (max 35 chars, case-sensitive).
78/// Identifies a resource across its revision history; the current version is
79/// the entry with the highest `revisionNumber` for this `mRID`.
80pub type Mrid = DocumentId;
81
82// ── DocumentVersion / RevisionNumber ─────────────────────────────────────────
83
84/// A document version number (integer 1–999).
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
86pub struct DocumentVersion(u16);
87
88impl DocumentVersion {
89    /// Create a new [`DocumentVersion`], returning an error if outside 1–999.
90    #[must_use = "this returns the new DocumentVersion, discarding it is likely a mistake"]
91    pub fn new(v: u32) -> Result<Self, RedispatchXmlError> {
92        if v == 0 || v > 999 {
93            return Err(RedispatchXmlError::InvalidDocumentVersion(v));
94        }
95        Ok(Self(v as u16))
96    }
97
98    /// Return the numeric value.
99    pub fn get(self) -> u16 {
100        self.0
101    }
102}
103
104impl fmt::Display for DocumentVersion {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        self.0.fmt(f)
107    }
108}
109
110impl<'de> Deserialize<'de> for DocumentVersion {
111    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
112        // XML encodes integer as a string token
113        let v: u32 = Deserialize::deserialize(d)?;
114        Self::new(v).map_err(serde::de::Error::custom)
115    }
116}
117
118impl Serialize for DocumentVersion {
119    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
120        self.0.serialize(s)
121    }
122}
123
124/// An IEC 62325 revision number (integer 1–999).  Alias of [`DocumentVersion`].
125pub type RevisionNumber = DocumentVersion;
126
127// ── UtcDateTime (second-precision, yyyy-mm-ddThh:mm:ssZ) ─────────────────────
128
129/// A UTC-only second-precision timestamp.
130///
131/// All BDEW Redispatch 2.0 datetime fields must end with `Z`. This type
132/// rejects any offset other than UTC at deserialization time.
133#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
134pub struct UtcDateTime(OffsetDateTime);
135
136impl UtcDateTime {
137    /// Create from an [`OffsetDateTime`], returning an error if the offset is
138    /// not UTC.
139    #[must_use = "this returns the new UtcDateTime, discarding it is likely a mistake"]
140    pub fn new(dt: OffsetDateTime) -> Result<Self, RedispatchXmlError> {
141        if dt.offset() != time::UtcOffset::UTC {
142            return Err(RedispatchXmlError::InvalidTimestamp(dt.to_string()));
143        }
144        Ok(Self(dt))
145    }
146
147    /// Return the inner [`OffsetDateTime`] (always UTC).
148    pub fn inner(self) -> OffsetDateTime {
149        self.0
150    }
151}
152
153impl<'de> Deserialize<'de> for UtcDateTime {
154    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
155        let s = String::deserialize(d)?;
156        let dt = OffsetDateTime::parse(&s, &Rfc3339)
157            .map_err(|_| serde::de::Error::custom(format!("invalid UTC timestamp: {s:?}")))?;
158        if dt.offset() != time::UtcOffset::UTC {
159            return Err(serde::de::Error::custom(format!(
160                "timestamp must use UTC (Z suffix): {s:?}"
161            )));
162        }
163        Ok(UtcDateTime(dt))
164    }
165}
166
167impl Serialize for UtcDateTime {
168    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
169        // Serialise back as yyyy-mm-ddThh:mm:ssZ
170        self.0
171            .format(&Rfc3339)
172            .map_err(serde::ser::Error::custom)?
173            .serialize(s)
174    }
175}
176
177// ── UtcMinuteDateTime (minute-precision, yyyy-mm-ddThh:mmZ) ──────────────────
178
179/// A UTC-only minute-precision timestamp used in time interval boundaries.
180///
181/// Format: `yyyy-mm-ddThh:mmZ` (no seconds).
182#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
183pub struct UtcMinuteDateTime(OffsetDateTime);
184
185impl UtcMinuteDateTime {
186    /// Create from an [`OffsetDateTime`], returning an error if not UTC.
187    #[must_use = "this returns the new UtcMinuteDateTime, discarding it is likely a mistake"]
188    pub fn new(dt: OffsetDateTime) -> Result<Self, RedispatchXmlError> {
189        if dt.offset() != time::UtcOffset::UTC {
190            return Err(RedispatchXmlError::InvalidTimestamp(dt.to_string()));
191        }
192        Ok(Self(dt))
193    }
194
195    /// Return the inner [`OffsetDateTime`] (always UTC).
196    pub fn inner(self) -> OffsetDateTime {
197        self.0
198    }
199}
200
201const MINUTE_FMT: &[time::format_description::BorrowedFormatItem<'static>] =
202    time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]Z");
203
204impl<'de> Deserialize<'de> for UtcMinuteDateTime {
205    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
206        let s = String::deserialize(d)?;
207        let naive = time::PrimitiveDateTime::parse(&s, MINUTE_FMT).map_err(|_| {
208            serde::de::Error::custom(format!("invalid UTC minute timestamp: {s:?}"))
209        })?;
210        Ok(UtcMinuteDateTime(naive.assume_offset(time::UtcOffset::UTC)))
211    }
212}
213
214impl Serialize for UtcMinuteDateTime {
215    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
216        self.0
217            .format(MINUTE_FMT)
218            .map_err(serde::ser::Error::custom)?
219            .serialize(s)
220    }
221}
222
223// ── TimeInterval (yyyy-mm-ddThh:mmZ/yyyy-mm-ddThh:mmZ) ───────────────────────
224
225/// An ISO 8601 UTC time interval in the BDEW minute-precision format.
226///
227/// Format: `yyyy-mm-ddThh:mmZ/yyyy-mm-ddThh:mmZ`
228#[derive(Debug, Clone, PartialEq, Eq)]
229pub struct TimeInterval {
230    /// Start of the interval (inclusive), always UTC.
231    pub start: OffsetDateTime,
232    /// End of the interval (exclusive), always UTC.
233    pub end: OffsetDateTime,
234}
235
236impl TimeInterval {
237    /// Create a new interval, validating that both timestamps are UTC and that
238    /// start precedes end.
239    #[must_use = "this returns the new TimeInterval, discarding it is likely a mistake"]
240    pub fn new(start: OffsetDateTime, end: OffsetDateTime) -> Result<Self, RedispatchXmlError> {
241        if start.offset() != time::UtcOffset::UTC || end.offset() != time::UtcOffset::UTC {
242            return Err(RedispatchXmlError::InvalidTimeInterval(
243                "timestamps must be UTC".into(),
244            ));
245        }
246        if start >= end {
247            return Err(RedispatchXmlError::InvalidTimeInterval(
248                "start must be before end".into(),
249            ));
250        }
251        Ok(Self { start, end })
252    }
253}
254
255impl<'de> Deserialize<'de> for TimeInterval {
256    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
257        let s = String::deserialize(d)?;
258        let (start_str, end_str) = s.split_once('/').ok_or_else(|| {
259            serde::de::Error::custom(format!("invalid time interval {s:?}: missing '/'"))
260        })?;
261        let start_naive = time::PrimitiveDateTime::parse(start_str, MINUTE_FMT).map_err(|_| {
262            serde::de::Error::custom(format!("invalid interval start {start_str:?}"))
263        })?;
264        let end_naive = time::PrimitiveDateTime::parse(end_str, MINUTE_FMT)
265            .map_err(|_| serde::de::Error::custom(format!("invalid interval end {end_str:?}")))?;
266        Ok(TimeInterval {
267            start: start_naive.assume_offset(time::UtcOffset::UTC),
268            end: end_naive.assume_offset(time::UtcOffset::UTC),
269        })
270    }
271}
272
273impl Serialize for TimeInterval {
274    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
275        let start = self
276            .start
277            .format(MINUTE_FMT)
278            .map_err(serde::ser::Error::custom)?;
279        let end = self
280            .end
281            .format(MINUTE_FMT)
282            .map_err(serde::ser::Error::custom)?;
283        format!("{start}/{end}").serialize(s)
284    }
285}
286
287impl fmt::Display for TimeInterval {
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        let start = self.start.format(MINUTE_FMT).map_err(|_| fmt::Error)?;
290        let end = self.end.format(MINUTE_FMT).map_err(|_| fmt::Error)?;
291        write!(f, "{start}/{end}")
292    }
293}
294
295// ── MarketParticipantId (13 decimal digits) ───────────────────────────────────
296
297/// A BDEW / GS1 market participant identifier (exactly 13 decimal digits).
298#[derive(Debug, Clone, PartialEq, Eq, Hash)]
299pub struct MarketParticipantId(String);
300
301impl MarketParticipantId {
302    /// Create a new identifier, validating the 13-digit constraint.
303    #[must_use = "this returns the new MarketParticipantId, discarding it is likely a mistake"]
304    pub fn new(s: impl Into<String>) -> Result<Self, RedispatchXmlError> {
305        let s = s.into();
306        if s.len() != 13 || !s.chars().all(|c| c.is_ascii_digit()) {
307            return Err(RedispatchXmlError::InvalidMarketParticipantId(s));
308        }
309        Ok(Self(s))
310    }
311
312    /// Return the string value.
313    pub fn as_str(&self) -> &str {
314        &self.0
315    }
316}
317
318impl AsRef<str> for MarketParticipantId {
319    fn as_ref(&self) -> &str {
320        &self.0
321    }
322}
323
324impl TryFrom<String> for MarketParticipantId {
325    type Error = RedispatchXmlError;
326    fn try_from(s: String) -> Result<Self, Self::Error> {
327        Self::new(s)
328    }
329}
330
331impl TryFrom<&str> for MarketParticipantId {
332    type Error = RedispatchXmlError;
333    fn try_from(s: &str) -> Result<Self, Self::Error> {
334        Self::new(s)
335    }
336}
337
338impl fmt::Display for MarketParticipantId {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        self.0.fmt(f)
341    }
342}
343
344impl<'de> Deserialize<'de> for MarketParticipantId {
345    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
346        let s = String::deserialize(d)?;
347        Self::new(s).map_err(serde::de::Error::custom)
348    }
349}
350
351impl Serialize for MarketParticipantId {
352    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
353        self.0.serialize(s)
354    }
355}
356
357// ── Decimal3 (0–999999.999, up to 3 fractional digits) ───────────────────────
358
359/// A non-negative decimal with at most 3 fractional digits, used for power
360/// quantities (0–999999.999 MW) and percentages (0–100.000 %).
361#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
362pub struct Decimal3(f64);
363
364impl Decimal3 {
365    /// Create a new value.  Returns an error if `v` is negative.
366    ///
367    /// **Note**: due to binary floating-point representation, values with more
368    /// than 3 fractional digits are accepted but will be rounded to 3 places
369    /// on serialization. For exact decimal arithmetic use external rounding
370    /// before construction.
371    #[must_use = "this returns the new Decimal3, discarding it is likely a mistake"]
372    pub fn new(v: f64) -> Result<Self, RedispatchXmlError> {
373        if v < 0.0 {
374            return Err(RedispatchXmlError::StructuralError(format!(
375                "Decimal3 value {v} must be ≥ 0"
376            )));
377        }
378        Ok(Self(v))
379    }
380
381    /// Return the raw `f64` value.
382    pub fn value(self) -> f64 {
383        self.0
384    }
385}
386
387impl<'de> Deserialize<'de> for Decimal3 {
388    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
389        // XML represents decimals as strings; serde will deserialise as f64
390        let v: f64 = Deserialize::deserialize(d)?;
391        Self::new(v).map_err(serde::de::Error::custom)
392    }
393}
394
395impl Serialize for Decimal3 {
396    fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
397        // Serialise with exactly 3 decimal places.
398        // E.g. 100.0 → "100.000", 50.5 → "50.500", 1.001 → "1.001"
399        format!("{:.3}", self.0).serialize(s)
400    }
401}
402
403impl fmt::Display for Decimal3 {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        write!(f, "{:.3}", self.0)
406    }
407}
408
409// ── CodingScheme ──────────────────────────────────────────────────────────────
410
411/// Identifier coding scheme used for market participant IDs and object codes.
412#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
413pub enum CodingScheme {
414    /// GS1 Global Location Number (GLN-13) or Global Service Relation Number
415    /// (GSRN-18).
416    #[serde(rename = "A10")]
417    Gs1,
418    /// German national coding scheme (BDEW-Code, 13-digit).
419    #[serde(rename = "NDE")]
420    Nde,
421    /// Energy Identification Coding Scheme (EIC), maintained by ENTSO-E.
422    #[serde(rename = "A01")]
423    Eic,
424}
425
426// ── MeasureUnit ───────────────────────────────────────────────────────────────
427
428/// Physical unit for quantity values in time series intervals.
429#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
430#[non_exhaustive]
431pub enum MeasureUnit {
432    /// Megawatt (MW) — absolute power quantities.
433    #[serde(rename = "MAW")]
434    Megawatt,
435    /// Percent (%) — relative to installed capacity (0–100).
436    #[serde(rename = "P1")]
437    Percent,
438}
439
440// ── Direction ─────────────────────────────────────────────────────────────────
441
442/// Redispatch direction.
443#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
444#[non_exhaustive]
445pub enum Direction {
446    /// Upward redispatch: increase generation or decrease consumption.
447    #[serde(rename = "A01")]
448    Up,
449    /// Downward redispatch: decrease generation or increase consumption.
450    #[serde(rename = "A02")]
451    Down,
452}
453
454// ── MarketRoleType ────────────────────────────────────────────────────────────
455
456/// ENTSO-E harmonised market role codes used in sender/receiver role fields.
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
458#[non_exhaustive]
459pub enum MarketRoleType {
460    /// Balance responsible party (BKV).
461    #[serde(rename = "A08")]
462    BalanceResponsibleParty,
463    /// Grid operator — TSO (ÜNB) or DSO (VNB).
464    #[serde(rename = "A18")]
465    GridOperator,
466    /// Producer / generation asset owner.
467    #[serde(rename = "A21")]
468    Producer,
469    /// Resource provider / Einspeiseverantwortlicher (EIV).
470    #[serde(rename = "A27")]
471    ResourceProvider,
472    /// Data provider (e.g. metering point operator forwarding data).
473    #[serde(rename = "A39")]
474    DataProvider,
475    /// Supplier (Lieferant).
476    #[serde(rename = "Z01")]
477    Supplier,
478}
479
480// ── ControlZone ───────────────────────────────────────────────────────────────
481
482/// German TSO control zone EIC codes used in `ConnectingArea`,
483/// `AcquiringArea`, and `biddingZone_Domain` fields.
484#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
485#[non_exhaustive]
486pub enum ControlZone {
487    /// TransnetBW.
488    #[serde(rename = "10YDE-ENBW-----N")]
489    TransnetBw,
490    /// TenneT DE.
491    #[serde(rename = "10YDE-EON------1")]
492    TennetDe,
493    /// Amprion.
494    #[serde(rename = "10YDE-RWENET---I")]
495    Amprion,
496    /// 50Hertz.
497    #[serde(rename = "10YDE-VE-------2")]
498    FiftyHertz,
499    /// Schleswig-Holstein / Flensburg.
500    #[serde(rename = "10YFLENSBURG---3")]
501    Flensburg,
502    /// DB Netz AG (railway grid).
503    #[serde(rename = "11YRBAHNSTROM--P")]
504    Bahnstrom,
505}