Skip to main content

redispatch_xml/types/
common.rs

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