bo4e_core/com/
time_period.rs

1//! Time period (Zeitraum) component.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::traits::{Bo4eMeta, Bo4eObject};
7
8/// A time period with start and end timestamps.
9///
10/// German: Zeitraum
11///
12/// # Example
13///
14/// ```rust
15/// use bo4e_core::com::TimePeriod;
16/// use chrono::Utc;
17///
18/// let period = TimePeriod {
19///     start: Some(Utc::now()),
20///     end: None, // Open-ended
21///     ..Default::default()
22/// };
23/// ```
24#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct TimePeriod {
27    /// BO4E metadata
28    #[serde(flatten)]
29    pub meta: Bo4eMeta,
30
31    /// Start of the period (Startdatum)
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub start: Option<DateTime<Utc>>,
34
35    /// End of the period (Enddatum)
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub end: Option<DateTime<Utc>>,
38}
39
40impl Bo4eObject for TimePeriod {
41    fn type_name_german() -> &'static str {
42        "Zeitraum"
43    }
44
45    fn type_name_english() -> &'static str {
46        "TimePeriod"
47    }
48
49    fn meta(&self) -> &Bo4eMeta {
50        &self.meta
51    }
52
53    fn meta_mut(&mut self) -> &mut Bo4eMeta {
54        &mut self.meta
55    }
56}
57
58impl TimePeriod {
59    /// Create a time period from start to end.
60    pub fn new(start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
61        Self {
62            start: Some(start),
63            end: Some(end),
64            ..Default::default()
65        }
66    }
67
68    /// Create an open-ended period starting from a given time.
69    pub fn starting_from(start: DateTime<Utc>) -> Self {
70        Self {
71            start: Some(start),
72            end: None,
73            ..Default::default()
74        }
75    }
76
77    /// Check if this period contains a given timestamp.
78    pub fn contains(&self, timestamp: DateTime<Utc>) -> bool {
79        let after_start = self.start.map_or(true, |s| timestamp >= s);
80        let before_end = self.end.map_or(true, |e| timestamp < e);
81        after_start && before_end
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use chrono::TimeZone;
89
90    #[test]
91    fn test_time_period_creation() {
92        let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
93        let end = Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap();
94
95        let period = TimePeriod::new(start, end);
96        assert_eq!(period.start, Some(start));
97        assert_eq!(period.end, Some(end));
98    }
99
100    #[test]
101    fn test_contains() {
102        let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
103        let end = Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap();
104        let period = TimePeriod::new(start, end);
105
106        let mid = Utc.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
107        assert!(period.contains(mid));
108
109        let before = Utc.with_ymd_and_hms(2023, 12, 31, 0, 0, 0).unwrap();
110        assert!(!period.contains(before));
111    }
112
113    #[test]
114    fn test_serialize_iso8601() {
115        let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
116        let period = TimePeriod::starting_from(start);
117
118        let json = serde_json::to_string(&period).unwrap();
119        assert!(json.contains("2024-01-01"));
120    }
121
122    #[test]
123    fn test_roundtrip() {
124        let start = Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap();
125        let end = Utc.with_ymd_and_hms(2024, 12, 31, 23, 59, 59).unwrap();
126        let period = TimePeriod::new(start, end);
127
128        let json = serde_json::to_string(&period).unwrap();
129        let parsed: TimePeriod = serde_json::from_str(&json).unwrap();
130        assert_eq!(period, parsed);
131    }
132
133    #[test]
134    fn test_bo4e_object_impl() {
135        assert_eq!(TimePeriod::type_name_german(), "Zeitraum");
136        assert_eq!(TimePeriod::type_name_english(), "TimePeriod");
137    }
138}