Skip to main content

deep_time/dt/
to_str.rs

1use crate::{
2    AsciiStr, Dt, DtErr, GregorianTime, STRFTIME_SIZE, Scale, TSpan, tzdb::offset_info_at_utc,
3};
4
5impl Dt {
6    /// High-level alloc version (defaults to UTC label-only formatting).
7    #[cfg(feature = "alloc")]
8    #[inline]
9    pub fn to_str(&self, fmt: &str) -> Result<alloc::string::String, DtErr> {
10        self.to_str_with_offset(fmt, 0)
11    }
12
13    /// High-level alloc version with explicit offset (label-only).
14    #[cfg(feature = "alloc")]
15    #[inline]
16    pub fn to_str_with_offset(&self, fmt: &str, secs: i32) -> Result<alloc::string::String, DtErr> {
17        let mut buf = [0u8; STRFTIME_SIZE];
18        let n = self.to_u8_with_offset(fmt, &mut buf, secs)?;
19        Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
20    }
21
22    /// High-level alloc version for full IANA timezone formatting (with civil-time adjustment).
23    #[cfg(feature = "alloc")]
24    #[inline]
25    pub fn to_str_with_tz(&self, fmt: &str, tz_name: &str) -> Result<alloc::string::String, DtErr> {
26        let mut buf = [0u8; STRFTIME_SIZE];
27        let n = self.to_u8_with_tz(fmt, &mut buf, tz_name)?;
28        Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
29    }
30
31    /// No-alloc label-only formatting.
32    pub fn to_str_bin(&self, fmt: &str) -> Result<AsciiStr<STRFTIME_SIZE>, DtErr> {
33        let mut gt = self.to_gregorian_time();
34        gt.set_offset(Some(0)).set_tz_abbrev(None);
35        let mut buf = [0u8; STRFTIME_SIZE];
36        let mut pos = 0usize;
37        gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
38        Ok(AsciiStr::from_filled_buffer(buf))
39    }
40
41    /// No-alloc label-only formatting.
42    pub fn to_str_bin_with_offset(
43        &self,
44        fmt: &str,
45        secs: i32,
46    ) -> Result<AsciiStr<STRFTIME_SIZE>, DtErr> {
47        let gt = self.gregorian_time_with_offset(secs);
48        let mut buf = [0u8; STRFTIME_SIZE];
49        let mut pos = 0usize;
50        gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
51        Ok(AsciiStr::from_filled_buffer(buf))
52    }
53
54    /// No-alloc full IANA adjusted formatting (civil time is adjusted to local wall time).
55    pub fn to_str_bin_with_tz(
56        &self,
57        fmt: &str,
58        tz_name: &str,
59    ) -> Result<AsciiStr<STRFTIME_SIZE>, DtErr> {
60        let gt = self.gregorian_time_with_tz(tz_name);
61        let mut buf = [0u8; STRFTIME_SIZE];
62        let mut pos = 0usize;
63        gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
64        Ok(AsciiStr::from_filled_buffer(buf))
65    }
66
67    /// Returns `(is_negative, hours, minutes)`.
68    #[inline]
69    pub const fn sec_as_hhmm(seconds: i32) -> (bool, u8, u8) {
70        let total = seconds.abs();
71        let hours = (total / 3600) as u8;
72        let minutes = ((total % 3600) / 60) as u8;
73        (seconds < 0, hours, minutes)
74    }
75
76    /// Helper for to_str.
77    pub fn to_u8_with_offset(&self, fmt: &str, dest: &mut [u8], secs: i32) -> Result<usize, DtErr> {
78        let gt = self.gregorian_time_with_offset(secs);
79        let mut internal_buf = [0u8; STRFTIME_SIZE];
80        let mut pos = 0usize;
81        gt.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
82        let written = pos.min(dest.len());
83        if written > 0 {
84            dest[0..written].copy_from_slice(&internal_buf[0..written]);
85        }
86        Ok(written)
87    }
88
89    /// Helper for to_str.
90    pub fn to_u8_with_tz(&self, fmt: &str, dest: &mut [u8], tz_name: &str) -> Result<usize, DtErr> {
91        let gt = self.gregorian_time_with_tz(tz_name);
92        let mut internal_buf = [0u8; STRFTIME_SIZE];
93        let mut pos = 0usize;
94        gt.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
95        let written = pos.min(dest.len());
96        if written > 0 {
97            dest[0..written].copy_from_slice(&internal_buf[0..written]);
98        }
99        Ok(written)
100    }
101
102    /// Helper for creating an offset adjusted GregorianTime.
103    pub(crate) fn gregorian_time_with_offset(&self, secs: i32) -> GregorianTime {
104        let local_tp = if secs != 0 {
105            *self + TSpan::new(secs as i64, 0)
106        } else {
107            *self
108        };
109        let mut gt = local_tp.to_gregorian_time();
110        gt.set_offset(Some(secs));
111        gt
112    }
113
114    /// Helper for creating a timezone-adjusted GregorianTime.
115    ///
116    /// Always converts to UTC first, then does a correct UTC-based lookup
117    /// in the IANA transition table. This avoids the previous bug where
118    /// a non-UTC `unix_ts` was being passed to `offset_info_at_local`.
119    pub(crate) fn gregorian_time_with_tz(&self, tz_name: &str) -> GregorianTime {
120        // 1. Get the true UTC Unix timestamp (this is what we search with)
121        let utc_unix = self.to_epoch(Dt::UNIX_EPOCH, Scale::UTC).to_sec();
122
123        // 2. Look up offset + abbrev at that exact UTC instant
124        let (offset_secs, abbrev) = match offset_info_at_utc(tz_name, utc_unix) {
125            Some(info) => (info.offset, info.abbrev),
126            None => (0, "UTC"), // fallback for unknown timezone
127        };
128
129        // 3. Build local time = UTC + offset
130        let span = TSpan::new(offset_secs as i64, 0);
131        let local_tp = *self + span;
132
133        let mut gt = local_tp.to_gregorian_time();
134        gt.set_offset(Some(offset_secs));
135        gt.set_tz(Some(tz_name));
136        gt.set_tz_abbrev(Some(abbrev));
137        gt
138    }
139}