Skip to main content

deep_time/dt/
to_str.rs

1use crate::{AsciiStr, Dt, DtErr, GregorianTime, STRFTIME_SIZE, Scale, tzdb::offset_info_at_utc};
2
3#[cfg(feature = "alloc")]
4use crate::ATTOS_PER_SEC;
5
6#[cfg(feature = "alloc")]
7impl Dt {
8    /// High-level alloc version (defaults to UTC label-only formatting).
9    #[inline]
10    pub fn to_str(&self, current: Scale, fmt: &str) -> Result<alloc::string::String, DtErr> {
11        self.to_str_with_offset(current, fmt, 0)
12    }
13
14    /// High-level alloc version with explicit offset (label-only).
15    #[inline]
16    pub fn to_str_with_offset(
17        &self,
18        current: Scale,
19        fmt: &str,
20        secs: i32,
21    ) -> Result<alloc::string::String, DtErr> {
22        let mut buf = [0u8; STRFTIME_SIZE];
23        let n = self.to_u8_with_offset(current, fmt, &mut buf, secs)?;
24        Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
25    }
26
27    /// High-level alloc version for full IANA timezone formatting (with civil-time adjustment).
28    #[inline]
29    pub fn to_str_with_tz(
30        &self,
31        current: Scale,
32        fmt: &str,
33        tz_name: &str,
34    ) -> Result<alloc::string::String, DtErr> {
35        let mut buf = [0u8; STRFTIME_SIZE];
36        let n = self.to_u8_with_tz(current, fmt, &mut buf, tz_name)?;
37        Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
38    }
39
40    /// Converts this `Dt` to an ISO 8601 duration string
41    /// (e.g. `"PT1H23M45.6789S"`, `"-PT0.5S"`, `"PT0.000000000000000001S"`, or `"PT0S"`).
42    ///
43    /// This method is only available when the **`alloc`** feature is enabled.
44    /// It returns `alloc::string::String` (no_std + alloc compatible).
45    pub fn to_iso_duration(&self) -> alloc::string::String {
46        if self.is_zero() {
47            return alloc::string::String::from("PT0S");
48        }
49
50        let total = self.to_attos();
51        let negative = total < 0;
52        let mut attos = total.unsigned_abs() as u128;
53
54        let mut s = alloc::string::String::with_capacity(48);
55        if negative {
56            s.push('-');
57        }
58        s.push_str("PT");
59
60        const A_PER_S: u128 = ATTOS_PER_SEC as u128;
61        const A_PER_M: u128 = A_PER_S * 60;
62        const A_PER_H: u128 = A_PER_M * 60;
63
64        let hours = attos / A_PER_H;
65        attos %= A_PER_H;
66        let minutes = attos / A_PER_M;
67        attos %= A_PER_M;
68        let seconds = attos / A_PER_S;
69        let frac_attos = attos % A_PER_S;
70
71        if hours > 0 {
72            s.push_str(&alloc::format!("{}", hours));
73            s.push('H');
74        }
75        if minutes > 0 {
76            s.push_str(&alloc::format!("{}", minutes));
77            s.push('M');
78        }
79
80        if seconds > 0 || frac_attos > 0 {
81            s.push_str(&alloc::format!("{}", seconds));
82
83            if frac_attos != 0 {
84                let frac_str = alloc::format!("{frac_attos:018}");
85                let trimmed = frac_str.trim_end_matches('0');
86                s.push('.');
87                s.push_str(trimmed);
88            }
89
90            s.push('S');
91        }
92
93        s
94    }
95}
96
97impl Dt {
98    /// No-alloc label-only formatting.
99    pub fn to_str_bin(&self, current: Scale, fmt: &str) -> Result<AsciiStr<STRFTIME_SIZE>, DtErr> {
100        let mut gt = self.to_gregorian_time(current);
101        gt.set_offset(Some(0)).set_tz_abbrev(None);
102        let mut buf = [0u8; STRFTIME_SIZE];
103        let mut pos = 0usize;
104        gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
105        Ok(AsciiStr::from_filled_buffer(buf))
106    }
107
108    /// No-alloc label-only formatting.
109    pub fn to_str_bin_with_offset(
110        &self,
111        current: Scale,
112        fmt: &str,
113        secs: i32,
114    ) -> Result<AsciiStr<STRFTIME_SIZE>, DtErr> {
115        let gt = self.gregorian_time_with_offset(current, secs);
116        let mut buf = [0u8; STRFTIME_SIZE];
117        let mut pos = 0usize;
118        gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
119        Ok(AsciiStr::from_filled_buffer(buf))
120    }
121
122    /// No-alloc full IANA adjusted formatting (civil time is adjusted to local wall time).
123    pub fn to_str_bin_with_tz(
124        &self,
125        current: Scale,
126        fmt: &str,
127        tz_name: &str,
128    ) -> Result<AsciiStr<STRFTIME_SIZE>, DtErr> {
129        let gt = self.gregorian_time_with_tz(current, tz_name);
130        let mut buf = [0u8; STRFTIME_SIZE];
131        let mut pos = 0usize;
132        gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
133        Ok(AsciiStr::from_filled_buffer(buf))
134    }
135
136    /// Returns `(is_negative, hours, minutes)`.
137    #[inline]
138    pub const fn sec_as_hhmm(seconds: i32) -> (bool, u8, u8) {
139        let total = seconds.saturating_abs();
140        let hours = (total / 3600) as u8;
141        let minutes = ((total % 3600) / 60) as u8;
142        (seconds < 0, hours, minutes)
143    }
144
145    /// Helper for to_str.
146    pub fn to_u8_with_offset(
147        &self,
148        current: Scale,
149        fmt: &str,
150        dest: &mut [u8],
151        secs: i32,
152    ) -> Result<usize, DtErr> {
153        let gt = self.gregorian_time_with_offset(current, secs);
154        let mut internal_buf = [0u8; STRFTIME_SIZE];
155        let mut pos = 0usize;
156        gt.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
157        let written = pos.min(dest.len());
158        if written > 0 {
159            dest[0..written].copy_from_slice(&internal_buf[0..written]);
160        }
161        Ok(written)
162    }
163
164    /// Helper for to_str.
165    pub fn to_u8_with_tz(
166        &self,
167        current: Scale,
168        fmt: &str,
169        dest: &mut [u8],
170        tz_name: &str,
171    ) -> Result<usize, DtErr> {
172        let gt = self.gregorian_time_with_tz(current, tz_name);
173        let mut internal_buf = [0u8; STRFTIME_SIZE];
174        let mut pos = 0usize;
175        gt.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
176        let written = pos.min(dest.len());
177        if written > 0 {
178            dest[0..written].copy_from_slice(&internal_buf[0..written]);
179        }
180        Ok(written)
181    }
182
183    /// Helper for creating an offset adjusted GregorianTime.
184    pub(crate) fn gregorian_time_with_offset(&self, current: Scale, secs: i32) -> GregorianTime {
185        let local_tp = if secs != 0 {
186            *self + Dt::new(secs as i64, 0)
187        } else {
188            *self
189        };
190        let mut gt = local_tp.to_gregorian_time(current);
191        gt.set_offset(Some(secs));
192        gt
193    }
194
195    /// Helper for creating a timezone-adjusted GregorianTime.
196    ///
197    /// Always converts to UTC first, then does a correct UTC-based lookup
198    /// in the IANA transition table. This avoids the previous bug where
199    /// a non-UTC `unix_ts` was being passed to `offset_info_at_local`.
200    pub(crate) fn gregorian_time_with_tz(&self, current: Scale, tz_name: &str) -> GregorianTime {
201        // 1. Get the true UTC Unix timestamp (this is what we search with)
202        let utc_unix = self
203            .to(current, current.to_ut())
204            .to_diff_raw(Dt::UNIX_EPOCH);
205
206        // 2. Look up offset + abbrev at that exact UTC instant
207        let (offset_secs, abbrev) = match offset_info_at_utc(tz_name, utc_unix.sec) {
208            Some(info) => (info.offset, info.abbrev),
209            None => (0, "UTC"), // fallback for unknown timezone
210        };
211
212        // 3. Build local time = UTC + offset
213        let span = Dt::new(offset_secs as i64, 0);
214        let local_tp = *self + span;
215
216        let mut gt = local_tp.to_gregorian_time(current);
217        gt.set_offset(Some(offset_secs));
218        gt.set_tz(Some(tz_name));
219        gt.set_tz_abbrev(Some(abbrev));
220        gt
221    }
222}