Skip to main content

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