orbital_base_components/form/datetime/
convert.rs1use chrono::{DateTime, NaiveDate, TimeZone, Utc};
2use orbital_data::DataValue;
3
4use super::error::DatetimeError;
5use super::orbital_datetime::OrbitalDateTime;
6use super::timezone::DatetimeTimezone;
7
8pub trait TryFromUnixSeconds {
10 fn try_from_unix_seconds(
11 secs: i64,
12 timezone: DatetimeTimezone,
13 ) -> Result<OrbitalDateTime, DatetimeError>;
14}
15
16pub trait TryFromIso8601 {
18 fn try_from_iso8601(
19 input: &str,
20 timezone: DatetimeTimezone,
21 ) -> Result<OrbitalDateTime, DatetimeError>;
22}
23
24pub trait TryFromDataValue {
26 fn try_from_data_value(
27 value: &DataValue,
28 timezone: DatetimeTimezone,
29 ) -> Result<OrbitalDateTime, DatetimeError>;
30}
31
32pub trait ToUnixSeconds {
34 fn to_unix_seconds(&self) -> i64;
35}
36
37pub trait ToIso8601 {
39 fn to_iso8601(&self) -> String;
40}
41
42pub trait ToDataValue {
44 fn to_data_value(&self) -> DataValue;
45}
46
47impl TryFromUnixSeconds for OrbitalDateTime {
48 fn try_from_unix_seconds(secs: i64, timezone: DatetimeTimezone) -> Result<Self, DatetimeError> {
49 let instant = Utc
50 .timestamp_opt(secs, 0)
51 .single()
52 .ok_or(DatetimeError::OutOfRange)?;
53 Ok(Self::from_instant(instant, timezone))
54 }
55}
56
57impl TryFromIso8601 for OrbitalDateTime {
58 fn try_from_iso8601(input: &str, timezone: DatetimeTimezone) -> Result<Self, DatetimeError> {
59 let trimmed = input.trim();
60 if trimmed.is_empty() {
61 return Err(DatetimeError::InvalidInput);
62 }
63
64 if let Ok(parsed) = DateTime::parse_from_rfc3339(trimmed) {
65 return Ok(Self::from_instant(parsed.with_timezone(&Utc), timezone));
66 }
67
68 if let Ok(date) = NaiveDate::parse_from_str(trimmed, "%Y-%m-%d") {
69 return Self::from_naive_date(date, timezone).ok_or(DatetimeError::OutOfRange);
70 }
71
72 Err(DatetimeError::InvalidInput)
73 }
74}
75
76impl TryFromDataValue for OrbitalDateTime {
77 fn try_from_data_value(
78 value: &DataValue,
79 timezone: DatetimeTimezone,
80 ) -> Result<Self, DatetimeError> {
81 match value {
82 DataValue::Date(date) => {
83 Self::from_naive_date(*date, timezone).ok_or(DatetimeError::OutOfRange)
84 }
85 _ => Err(DatetimeError::InvalidInput),
86 }
87 }
88}
89
90impl ToUnixSeconds for OrbitalDateTime {
91 fn to_unix_seconds(&self) -> i64 {
92 self.instant().timestamp()
93 }
94}
95
96impl ToIso8601 for OrbitalDateTime {
97 fn to_iso8601(&self) -> String {
98 self.instant().to_rfc3339()
99 }
100}
101
102impl ToDataValue for OrbitalDateTime {
103 fn to_data_value(&self) -> DataValue {
104 let date = self
105 .wall_date()
106 .unwrap_or_else(|| self.instant().date_naive());
107 DataValue::Date(date)
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use chrono::{NaiveDate, TimeZone, Utc};
115
116 #[test]
117 fn unix_round_trip() {
118 let secs = 1_735_689_600_i64;
119 let dt =
120 OrbitalDateTime::try_from_unix_seconds(secs, DatetimeTimezone::Utc).expect("valid");
121 assert_eq!(dt.to_unix_seconds(), secs);
122 }
123
124 #[test]
125 fn iso8601_round_trip() {
126 let dt = OrbitalDateTime::try_from_iso8601("2025-01-01T00:00:00Z", DatetimeTimezone::Utc)
127 .expect("valid");
128 assert!(dt.to_iso8601().starts_with("2025-01-01"));
129 }
130
131 #[test]
132 fn iso8601_naive_date_uses_timezone() {
133 let dt =
134 OrbitalDateTime::try_from_iso8601("2025-06-15", DatetimeTimezone::Utc).expect("valid");
135 assert_eq!(dt.hour_minute_second(), Some((0, 0, 0)));
136 }
137
138 #[test]
139 fn data_value_date_to_start_of_day() {
140 let date = NaiveDate::from_ymd_opt(2025, 4, 10).expect("valid");
141 let dt =
142 OrbitalDateTime::try_from_data_value(&DataValue::Date(date), DatetimeTimezone::Utc)
143 .expect("valid");
144 assert_eq!(dt.hour_minute_second(), Some((0, 0, 0)));
145 }
146
147 #[test]
148 fn to_data_value_returns_wall_date() {
149 let instant = Utc
150 .with_ymd_and_hms(2025, 7, 4, 15, 30, 0)
151 .single()
152 .expect("valid");
153 let dt = OrbitalDateTime::from_instant(instant, DatetimeTimezone::Utc);
154 match dt.to_data_value() {
155 DataValue::Date(d) => assert_eq!(d, NaiveDate::from_ymd_opt(2025, 7, 4).unwrap()),
156 _ => panic!("expected date"),
157 }
158 }
159
160 #[test]
161 fn invalid_data_value_variant() {
162 let err = OrbitalDateTime::try_from_data_value(
163 &DataValue::Text("nope".into()),
164 DatetimeTimezone::Utc,
165 );
166 assert_eq!(err, Err(DatetimeError::InvalidInput));
167 }
168}