Skip to main content

bo4e_core/bo/
time_series.rs

1//! Time series (Zeitreihe) business object.
2//!
3//! Represents a generic time series of data values.
4
5use serde::{Deserialize, Serialize};
6
7use crate::com::{TimePeriod, TimeSeriesValue};
8use crate::enums::{Division, MeasurementType, Unit};
9use crate::traits::{Bo4eMeta, Bo4eObject};
10
11/// A time series of data values.
12///
13/// German: Zeitreihe
14///
15/// Time series contain sequences of timestamped data values
16/// for various purposes (forecasts, historical data, etc.).
17///
18/// # Example
19///
20/// ```rust
21/// use bo4e_core::bo::TimeSeries;
22/// use bo4e_core::enums::Division;
23///
24/// let series = TimeSeries {
25///     time_series_id: Some("TS001".to_string()),
26///     division: Some(Division::Electricity),
27///     ..Default::default()
28/// };
29/// ```
30#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
31#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
32#[cfg_attr(feature = "json-schema", schemars(rename = "Zeitreihe"))]
33#[serde(rename_all = "camelCase")]
34pub struct TimeSeries {
35    /// BO4E metadata
36    #[serde(flatten)]
37    pub meta: Bo4eMeta,
38
39    /// Time series ID (Zeitreihe-ID)
40    #[serde(skip_serializing_if = "Option::is_none")]
41    #[cfg_attr(feature = "json-schema", schemars(rename = "zeitreiheId"))]
42    pub time_series_id: Option<String>,
43
44    /// Energy division (Sparte)
45    #[serde(skip_serializing_if = "Option::is_none")]
46    #[cfg_attr(feature = "json-schema", schemars(rename = "sparte"))]
47    pub division: Option<Division>,
48
49    /// Measurement type (Messart)
50    #[serde(skip_serializing_if = "Option::is_none")]
51    #[cfg_attr(feature = "json-schema", schemars(rename = "messart"))]
52    pub measurement_type: Option<MeasurementType>,
53
54    /// Unit of measurement (Einheit)
55    #[serde(skip_serializing_if = "Option::is_none")]
56    #[cfg_attr(feature = "json-schema", schemars(rename = "einheit"))]
57    pub unit: Option<Unit>,
58
59    /// Validity period (Gueltigkeitszeitraum)
60    #[serde(skip_serializing_if = "Option::is_none")]
61    #[cfg_attr(feature = "json-schema", schemars(rename = "gueltigkeitszeitraum"))]
62    pub validity_period: Option<TimePeriod>,
63
64    /// Time series values (Zeitreihenwerte)
65    #[serde(default, skip_serializing_if = "Vec::is_empty")]
66    #[cfg_attr(feature = "json-schema", schemars(rename = "zeitreihenwerte"))]
67    pub values: Vec<TimeSeriesValue>,
68
69    /// Associated market location ID
70    #[serde(skip_serializing_if = "Option::is_none")]
71    #[cfg_attr(feature = "json-schema", schemars(rename = "marktlokationsId"))]
72    pub market_location_id: Option<String>,
73
74    /// Associated metering location ID
75    #[serde(skip_serializing_if = "Option::is_none")]
76    #[cfg_attr(feature = "json-schema", schemars(rename = "messlokationsId"))]
77    pub metering_location_id: Option<String>,
78
79    /// Description (Beschreibung)
80    #[serde(skip_serializing_if = "Option::is_none")]
81    #[cfg_attr(feature = "json-schema", schemars(rename = "beschreibung"))]
82    pub description: Option<String>,
83
84    /// OBIS code
85    #[serde(skip_serializing_if = "Option::is_none")]
86    #[cfg_attr(feature = "json-schema", schemars(rename = "obisKennzahl"))]
87    pub obis_code: Option<String>,
88
89    /// Version of the time series
90    #[serde(skip_serializing_if = "Option::is_none")]
91    #[cfg_attr(feature = "json-schema", schemars(rename = "version"))]
92    pub series_version: Option<String>,
93
94    /// Resolution/interval in minutes (Aufloesung)
95    #[serde(skip_serializing_if = "Option::is_none")]
96    #[cfg_attr(feature = "json-schema", schemars(rename = "aufloesung"))]
97    pub resolution_minutes: Option<i32>,
98}
99
100impl Bo4eObject for TimeSeries {
101    fn type_name_german() -> &'static str {
102        "Zeitreihe"
103    }
104
105    fn type_name_english() -> &'static str {
106        "TimeSeries"
107    }
108
109    fn meta(&self) -> &Bo4eMeta {
110        &self.meta
111    }
112
113    fn meta_mut(&mut self) -> &mut Bo4eMeta {
114        &mut self.meta
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_time_series_creation() {
124        let series = TimeSeries {
125            time_series_id: Some("TS001".to_string()),
126            division: Some(Division::Electricity),
127            ..Default::default()
128        };
129
130        assert_eq!(series.time_series_id, Some("TS001".to_string()));
131    }
132
133    #[test]
134    fn test_serialize() {
135        let series = TimeSeries {
136            meta: Bo4eMeta::with_type("Zeitreihe"),
137            time_series_id: Some("TS001".to_string()),
138            ..Default::default()
139        };
140
141        let json = serde_json::to_string(&series).unwrap();
142        assert!(json.contains(r#""_typ":"Zeitreihe""#));
143    }
144
145    #[test]
146    fn test_roundtrip() {
147        let series = TimeSeries {
148            meta: Bo4eMeta::with_type("Zeitreihe"),
149            time_series_id: Some("TS001".to_string()),
150            division: Some(Division::Electricity),
151            unit: Some(Unit::KilowattHour),
152            resolution_minutes: Some(15),
153            description: Some("Test time series".to_string()),
154            ..Default::default()
155        };
156
157        let json = serde_json::to_string(&series).unwrap();
158        let parsed: TimeSeries = serde_json::from_str(&json).unwrap();
159        assert_eq!(series, parsed);
160    }
161
162    #[test]
163    fn test_bo4e_object_impl() {
164        assert_eq!(TimeSeries::type_name_german(), "Zeitreihe");
165        assert_eq!(TimeSeries::type_name_english(), "TimeSeries");
166    }
167}