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#[serde(rename_all = "camelCase")]
32pub struct TimeSeries {
33    /// BO4E metadata
34    #[serde(flatten)]
35    pub meta: Bo4eMeta,
36
37    /// Time series ID (Zeitreihe-ID)
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub time_series_id: Option<String>,
40
41    /// Energy division (Sparte)
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub division: Option<Division>,
44
45    /// Measurement type (Messart)
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub measurement_type: Option<MeasurementType>,
48
49    /// Unit of measurement (Einheit)
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub unit: Option<Unit>,
52
53    /// Validity period (Gueltigkeitszeitraum)
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub validity_period: Option<TimePeriod>,
56
57    /// Time series values (Zeitreihenwerte)
58    #[serde(default, skip_serializing_if = "Vec::is_empty")]
59    pub values: Vec<TimeSeriesValue>,
60
61    /// Associated market location ID
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub market_location_id: Option<String>,
64
65    /// Associated metering location ID
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub metering_location_id: Option<String>,
68
69    /// Description (Beschreibung)
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub description: Option<String>,
72
73    /// OBIS code
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub obis_code: Option<String>,
76
77    /// Version of the time series
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub series_version: Option<String>,
80
81    /// Resolution/interval in minutes (Aufloesung)
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub resolution_minutes: Option<i32>,
84}
85
86impl Bo4eObject for TimeSeries {
87    fn type_name_german() -> &'static str {
88        "Zeitreihe"
89    }
90
91    fn type_name_english() -> &'static str {
92        "TimeSeries"
93    }
94
95    fn meta(&self) -> &Bo4eMeta {
96        &self.meta
97    }
98
99    fn meta_mut(&mut self) -> &mut Bo4eMeta {
100        &mut self.meta
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn test_time_series_creation() {
110        let series = TimeSeries {
111            time_series_id: Some("TS001".to_string()),
112            division: Some(Division::Electricity),
113            ..Default::default()
114        };
115
116        assert_eq!(series.time_series_id, Some("TS001".to_string()));
117    }
118
119    #[test]
120    fn test_serialize() {
121        let series = TimeSeries {
122            meta: Bo4eMeta::with_type("Zeitreihe"),
123            time_series_id: Some("TS001".to_string()),
124            ..Default::default()
125        };
126
127        let json = serde_json::to_string(&series).unwrap();
128        assert!(json.contains(r#""_typ":"Zeitreihe""#));
129    }
130
131    #[test]
132    fn test_roundtrip() {
133        let series = TimeSeries {
134            meta: Bo4eMeta::with_type("Zeitreihe"),
135            time_series_id: Some("TS001".to_string()),
136            division: Some(Division::Electricity),
137            unit: Some(Unit::KilowattHour),
138            resolution_minutes: Some(15),
139            description: Some("Test time series".to_string()),
140            ..Default::default()
141        };
142
143        let json = serde_json::to_string(&series).unwrap();
144        let parsed: TimeSeries = serde_json::from_str(&json).unwrap();
145        assert_eq!(series, parsed);
146    }
147
148    #[test]
149    fn test_bo4e_object_impl() {
150        assert_eq!(TimeSeries::type_name_german(), "Zeitreihe");
151        assert_eq!(TimeSeries::type_name_english(), "TimeSeries");
152    }
153}