Skip to main content

orbital_base_components/form/datetime/
format.rs

1use chrono::{FixedOffset, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc};
2
3use super::convert::{ToUnixSeconds, TryFromUnixSeconds};
4use super::orbital_datetime::OrbitalDateTime;
5use super::timezone::DatetimeTimezone;
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
8pub enum DatetimeFormat {
9    IsoDate,
10    #[default]
11    UsDate,
12    Time24,
13    Time12,
14}
15
16pub fn format_unix(secs: i64, format: DatetimeFormat, tz: DatetimeTimezone) -> String {
17    match tz {
18        DatetimeTimezone::Local => Local
19            .timestamp_opt(secs, 0)
20            .single()
21            .map(|dt| dt.format(fmt_pattern(format)).to_string())
22            .unwrap_or_default(),
23        DatetimeTimezone::Utc => Utc
24            .timestamp_opt(secs, 0)
25            .single()
26            .map(|dt| dt.format(fmt_pattern(format)).to_string())
27            .unwrap_or_default(),
28        DatetimeTimezone::FixedOffset(offset_secs) => FixedOffset::east_opt(offset_secs)
29            .and_then(|offset| offset.timestamp_opt(secs, 0).single())
30            .map(|dt| dt.format(fmt_pattern(format)).to_string())
31            .unwrap_or_default(),
32    }
33}
34
35pub fn parse_to_unix(input: &str, format: DatetimeFormat, tz: DatetimeTimezone) -> Option<i64> {
36    let input = input.trim();
37    if input.is_empty() {
38        return None;
39    }
40
41    let naive = match format {
42        DatetimeFormat::IsoDate => {
43            let date = NaiveDate::parse_from_str(input, "%Y-%m-%d").ok()?;
44            date.and_hms_opt(0, 0, 0)?
45        }
46        DatetimeFormat::UsDate => {
47            let date = NaiveDate::parse_from_str(input, "%m/%d/%Y").ok()?;
48            date.and_hms_opt(0, 0, 0)?
49        }
50        DatetimeFormat::Time24 => {
51            let time = NaiveTime::parse_from_str(input, "%H:%M").ok()?;
52            let epoch = NaiveDate::from_ymd_opt(1970, 1, 1)?;
53            epoch.and_time(time)
54        }
55        DatetimeFormat::Time12 => {
56            let time = NaiveTime::parse_from_str(input, "%I:%M %p").ok()?;
57            let epoch = NaiveDate::from_ymd_opt(1970, 1, 1)?;
58            epoch.and_time(time)
59        }
60    };
61
62    timestamp_for_timezone(naive, tz)
63}
64
65fn timestamp_for_timezone(naive: NaiveDateTime, tz: DatetimeTimezone) -> Option<i64> {
66    match tz {
67        DatetimeTimezone::Local => Local
68            .from_local_datetime(&naive)
69            .single()
70            .map(|dt| dt.timestamp()),
71        DatetimeTimezone::Utc => Some(Utc.from_utc_datetime(&naive).timestamp()),
72        DatetimeTimezone::FixedOffset(offset_secs) => FixedOffset::east_opt(offset_secs)?
73            .from_local_datetime(&naive)
74            .single()
75            .map(|dt| dt.timestamp()),
76    }
77}
78
79/// Display a bound datetime value using its embedded timezone.
80pub fn format_datetime(dt: OrbitalDateTime, format: DatetimeFormat) -> String {
81    format_unix(dt.to_unix_seconds(), format, dt.timezone())
82}
83
84/// Parse display text into a bound datetime value.
85pub fn parse_datetime(
86    input: &str,
87    format: DatetimeFormat,
88    timezone: DatetimeTimezone,
89) -> Option<OrbitalDateTime> {
90    parse_to_unix(input, format, timezone)
91        .and_then(|secs| OrbitalDateTime::try_from_unix_seconds(secs, timezone).ok())
92}
93
94fn fmt_pattern(format: DatetimeFormat) -> &'static str {
95    match format {
96        DatetimeFormat::IsoDate => "%Y-%m-%d",
97        DatetimeFormat::UsDate => "%m/%d/%Y",
98        DatetimeFormat::Time24 => "%H:%M",
99        DatetimeFormat::Time12 => "%I:%M %p",
100    }
101}