Skip to main content

deep_time/dt/
to_str.rs

1use crate::{
2    Dt, DtErr, DtErrKind, LiteStr, STRFTIME_SIZE, Scale, YmdHmsRich, an_err,
3    tzdb::offset_info_at_utc,
4};
5
6#[cfg(feature = "alloc")]
7use crate::ATTOS_PER_SEC;
8
9#[cfg(feature = "alloc")]
10impl Dt {
11    /// Converts this `Dt` to an ISO 8601 duration string
12    /// (e.g. `"PT1H23M45.6789S"`, `"-PT0.5S"`, `"PT0.000000000000000001S"`, or `"PT0S"`).
13    ///
14    /// - This method is only available when the **`alloc`** feature is enabled.
15    /// - It returns `alloc::string::String` (no_std + alloc compatible).
16    pub fn to_iso_duration(&self) -> alloc::string::String {
17        if self.is_zero() {
18            return alloc::string::String::from("PT0S");
19        }
20
21        let total = self.to_attos();
22        let negative = total < 0;
23        let mut attos = total.unsigned_abs();
24
25        let mut s = alloc::string::String::with_capacity(48);
26        if negative {
27            s.push('-');
28        }
29        s.push_str("PT");
30
31        const A_PER_S: u128 = ATTOS_PER_SEC as u128;
32        const A_PER_M: u128 = A_PER_S * 60;
33        const A_PER_H: u128 = A_PER_M * 60;
34
35        let hours = attos / A_PER_H;
36        attos %= A_PER_H;
37        let minutes = attos / A_PER_M;
38        attos %= A_PER_M;
39        let seconds = attos / A_PER_S;
40        let frac_attos = attos % A_PER_S;
41
42        if hours > 0 {
43            s.push_str(&alloc::format!("{}", hours));
44            s.push('H');
45        }
46        if minutes > 0 {
47            s.push_str(&alloc::format!("{}", minutes));
48            s.push('M');
49        }
50
51        if seconds > 0 || frac_attos > 0 {
52            s.push_str(&alloc::format!("{}", seconds));
53
54            if frac_attos != 0 {
55                let frac_str = alloc::format!("{frac_attos:018}");
56                let trimmed = frac_str.trim_end_matches('0');
57                s.push('.');
58                s.push_str(trimmed);
59            }
60
61            s.push('S');
62        }
63
64        s
65    }
66
67    /// Formats this [`Dt`] into a String. Requires the `"alloc"` feature.
68    ///
69    /// - It is first converted from the `current` [`Scale`] into
70    ///   the `UTC` time scale.
71    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice`
72    ///   are preserved. See [`Scale`] for more info on time scales.
73    ///
74    /// ## Example
75    ///
76    /// ```rust
77    /// use deep_time::{Dt, Scale};
78    ///
79    /// let x = Dt::from_ymd(2000, 1, 1);
80    /// let s = x.to_str(Scale::TAI, "%F").unwrap();
81    ///
82    /// println!("{}", s);
83    /// ```
84    ///
85    /// ## Errors
86    ///
87    /// Returns [`DtErr`] if the format string contains invalid specifiers
88    /// or if the internal formatting buffer overflows (extremely unlikely
89    /// with [`STRFTIME_SIZE`]).
90    ///
91    /// ## See also
92    ///
93    /// - [`Dt::to_str_with_offset`](../struct.Dt.html#method.to_str_with_offset)
94    /// - [`Dt::to_str_with_tz`](../struct.Dt.html#method.to_str_with_tz)
95    #[inline]
96    pub fn to_str(&self, current: Scale, fmt: &str) -> Result<alloc::string::String, DtErr> {
97        self.to_str_with_offset(current, fmt, 0)
98    }
99
100    /// Formats this [`Dt`] into a String, applying a fixed UTC offset.  Requires the
101    /// `"alloc"` feature.
102    ///
103    /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
104    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
105    ///   will reflect it.
106    /// - Then it's converted from the `current` [`Scale`] into the
107    ///   `UTC` time scale.
108    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice` are preserved.
109    ///   See [`Scale`] for more info on time scales.
110    /// - No IANA timezone name or abbreviation is set.
111    ///
112    /// ## Example
113    ///
114    /// ```rust
115    /// use deep_time::{Dt, Scale};
116    ///
117    /// let x = Dt::from_ymd(2000, 1, 1);
118    ///
119    /// // offset of minus one hour
120    /// let s = x.to_str_with_offset(Scale::TAI, "%F", -3600).unwrap();
121    ///
122    /// println!("{}", s);
123    /// ```
124    ///
125    /// ## Errors
126    ///
127    /// Returns [`DtErr`] if the format string contains invalid specifiers
128    /// or if the internal formatting buffer overflows (extremely unlikely
129    /// with [`STRFTIME_SIZE`]).
130    ///
131    /// ## See also
132    ///
133    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
134    /// - [`Dt::to_str_with_tz`](../struct.Dt.html#method.to_str_with_tz)
135    #[inline]
136    pub fn to_str_with_offset(
137        &self,
138        current: Scale,
139        fmt: &str,
140        secs: i32,
141    ) -> Result<alloc::string::String, DtErr> {
142        let mut buf = [0u8; STRFTIME_SIZE];
143        let n = self._to_u8_with_offset(current, fmt, &mut buf, secs)?;
144        Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
145    }
146
147    /// Formats this [`Dt`] into a string, time adjusted to the given IANA timezone. Requires
148    /// the `"alloc"` feature.
149    ///
150    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
151    ///
152    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]s time for the given
153    ///   IANA timezone. This is so that the formatter will have:
154    ///     - Accurate wall time for the timezone.
155    ///     - Correct numeric offset (for `%z` / `%:z`).
156    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip.
157    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
158    /// - Then it's converted from the `current` [`Scale`] into the
159    ///   `UTC` time scale.
160    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice` are preserved.
161    ///   See [`Scale`] for more info on time scales.
162    /// - No IANA timezone name or abbreviation is set.
163    ///
164    /// ## Example
165    ///
166    /// ```rust
167    /// use deep_time::{Dt, Scale};
168    ///
169    /// let x = Dt::from_ymd(2000, 1, 1);
170    /// let s = x.to_str_with_tz(Scale::TAI, "%F", "America/New_York").unwrap();
171    ///
172    /// println!("{}", s);
173    /// ```
174    ///
175    /// ## Errors
176    ///
177    /// Returns [`DtErr`] if the format string contains invalid specifiers
178    /// or if the internal formatting buffer overflows (extremely unlikely
179    /// with [`STRFTIME_SIZE`]).
180    ///
181    /// ## See also
182    ///
183    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
184    /// - [`Dt::to_str_with_offset`](../struct.Dt.html#method.to_str_with_offset)
185    #[inline]
186    pub fn to_str_with_tz(
187        &self,
188        current: Scale,
189        fmt: &str,
190        tz_name: &str,
191    ) -> Result<alloc::string::String, DtErr> {
192        let mut buf = [0u8; STRFTIME_SIZE];
193        let n = self._to_u8_with_tz(current, fmt, &mut buf, tz_name)?;
194        Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
195    }
196}
197
198impl Dt {
199    /// Formats this [`Dt`] into a fixed-size binary string.
200    ///
201    /// - It is first converted from the `current` [`Scale`] into
202    ///   the `UTC` time scale.
203    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice`
204    ///   are preserved. See [`Scale`] for more info on time scales.
205    ///
206    /// ## Example
207    ///
208    /// ```rust
209    /// use deep_time::{Dt, Scale};
210    ///
211    /// let x = Dt::from_ymd(2000, 1, 1);
212    /// let b = x.to_str_bin(Scale::TAI, "%F").unwrap();
213    /// let s = b.as_str().unwrap();
214    ///
215    /// println!("{}", s);
216    /// ```
217    ///
218    /// ## Errors
219    ///
220    /// Returns [`DtErr`] if the format string contains invalid specifiers
221    /// or if the internal formatting buffer overflows (extremely unlikely
222    /// with [`STRFTIME_SIZE`]).
223    ///
224    /// ## See also
225    ///
226    /// - [`Dt::to_str_bin_with_offset`](../struct.Dt.html#method.to_str_bin_with_offset)
227    /// - [`Dt::to_str_bin_with_tz`](../struct.Dt.html#method.to_str_bin_with_tz)
228    pub fn to_str_bin(&self, current: Scale, fmt: &str) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
229        let mut gt = self.to_ymdhms_rich_on(current, current.to_utc());
230        gt.set_offset(Some(0)).set_tz_abbrev(None);
231        let mut buf = [0u8; STRFTIME_SIZE];
232        let mut pos = 0usize;
233        gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
234        Ok(LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))?)
235    }
236
237    /// Formats this [`Dt`] into a fixed-size binary string, applying a fixed UTC offset.
238    ///
239    /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
240    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
241    ///   will reflect it.
242    /// - Then it's converted from the `current` [`Scale`] into the
243    ///   `UTC` time scale.
244    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice` are preserved.
245    ///   See [`Scale`] for more info on time scales.
246    /// - No IANA timezone name or abbreviation is set.
247    ///
248    /// ## Example
249    ///
250    /// ```rust
251    /// use deep_time::{Dt, Scale};
252    ///
253    /// let x = Dt::from_ymd(2000, 1, 1);
254    ///
255    /// // offset of minus one hour
256    /// let b = x.to_str_bin_with_offset(Scale::TAI, "%F", -3600).unwrap();
257    /// let s = b.as_str().unwrap();
258    ///
259    /// println!("{}", s);
260    /// ```
261    ///
262    /// ## Errors
263    ///
264    /// Returns [`DtErr`] if the format string contains invalid specifiers
265    /// or if the internal formatting buffer overflows (extremely unlikely
266    /// with [`STRFTIME_SIZE`]).
267    ///
268    /// ## See also
269    ///
270    /// - [`Dt::to_str_bin`](../struct.Dt.html#method.to_str_bin)
271    /// - [`Dt::to_str_bin_with_tz`](../struct.Dt.html#method.to_str_bin_with_tz)
272    pub fn to_str_bin_with_offset(
273        &self,
274        current: Scale,
275        fmt: &str,
276        secs: i32,
277    ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
278        let gt = self.ymdhms_rich_with_offset(current, secs);
279        let mut buf = [0u8; STRFTIME_SIZE];
280        let mut pos = 0usize;
281        gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
282        Ok(LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))?)
283    }
284
285    /// Formats this [`Dt`] into a fixed-size binary string, time adjusted to the given
286    /// IANA timezone.
287    ///
288    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
289    ///
290    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]s time for the given
291    ///   IANA timezone. This is so that the formatter will have:
292    ///     - Accurate wall time for the timezone.
293    ///     - Correct numeric offset (for `%z` / `%:z`).
294    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip.
295    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
296    /// - Then it's converted from the `current` [`Scale`] into the
297    ///   `UTC` time scale.
298    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice` are preserved.
299    ///   See [`Scale`] for more info on time scales.
300    /// - No IANA timezone name or abbreviation is set.
301    ///
302    /// ## Example
303    ///
304    /// ```rust
305    /// use deep_time::{Dt, Scale};
306    ///
307    /// let x = Dt::from_ymd(2000, 1, 1);
308    ///
309    /// let b = x.to_str_bin_with_tz(Scale::TAI, "%F", "America/New_York").unwrap();
310    /// let s = b.as_str().unwrap();
311    ///
312    /// println!("{}", s);
313    /// ```
314    ///
315    /// ## Errors
316    ///
317    /// Returns [`DtErr`] if the format string contains invalid specifiers
318    /// or if the internal formatting buffer overflows (extremely unlikely
319    /// with [`STRFTIME_SIZE`]).
320    ///
321    /// ## See also
322    ///
323    /// - [`Dt::to_str_bin`](../struct.Dt.html#method.to_str_bin)
324    /// - [`Dt::to_str_bin_with_offset`](../struct.Dt.html#method.to_str_bin_with_offset)
325    pub fn to_str_bin_with_tz(
326        &self,
327        current: Scale,
328        fmt: &str,
329        tz_name: &str,
330    ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
331        let gt = self.ymdhms_rich_with_tz(current, tz_name);
332        let mut buf = [0u8; STRFTIME_SIZE];
333        let mut pos = 0usize;
334        gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
335        Ok(LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))?)
336    }
337
338    /// Low-level no-alloc formatter that writes into a caller-provided slice,
339    /// using a fixed UTC offset.
340    ///
341    /// Same logic as [`Self::to_str_bin_with_offset`], but writes directly into
342    /// `dest` (truncated to `dest.len()`) and returns the number of bytes written.
343    pub fn _to_u8_with_offset(
344        &self,
345        current: Scale,
346        fmt: &str,
347        dest: &mut [u8],
348        secs: i32,
349    ) -> Result<usize, DtErr> {
350        let gt = self.ymdhms_rich_with_offset(current, secs);
351        let mut internal_buf = [0u8; STRFTIME_SIZE];
352        let mut pos = 0usize;
353        gt.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
354        let written = pos.min(dest.len());
355        if written > 0 {
356            dest[0..written].copy_from_slice(&internal_buf[0..written]);
357        }
358        Ok(written)
359    }
360
361    /// Low-level no-alloc formatter that writes into a caller-provided slice,
362    /// using a full IANA timezone.
363    ///
364    /// Same logic as [`Self::to_str_bin_with_tz`], but writes directly into
365    /// `dest` (truncated to `dest.len()`) and returns the number of bytes written.
366    pub fn _to_u8_with_tz(
367        &self,
368        current: Scale,
369        fmt: &str,
370        dest: &mut [u8],
371        tz_name: &str,
372    ) -> Result<usize, DtErr> {
373        let gt = self.ymdhms_rich_with_tz(current, tz_name);
374        let mut internal_buf = [0u8; STRFTIME_SIZE];
375        let mut pos = 0usize;
376        gt.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
377        let written = pos.min(dest.len());
378        if written > 0 {
379            dest[0..written].copy_from_slice(&internal_buf[0..written]);
380        }
381        Ok(written)
382    }
383
384    /// Returns `(is_negative, hours, minutes)`.
385    #[inline]
386    pub(crate) const fn sec_as_hhmm(seconds: i32) -> (bool, u8, u8) {
387        let total = seconds.saturating_abs();
388        let hours = (total / 3600) as u8;
389        let minutes = ((total % 3600) / 60) as u8;
390        (seconds < 0, hours, minutes)
391    }
392
393    /// Helper for creating an offset adjusted YmdHmsRich.
394    pub(crate) fn ymdhms_rich_with_offset(&self, current: Scale, secs: i32) -> YmdHmsRich {
395        let local_tp = if secs != 0 {
396            *self + Dt::new(secs as i64, 0)
397        } else {
398            *self
399        };
400        let mut gt = local_tp.to_ymdhms_rich_on(current, current.to_utc());
401        gt.set_offset(Some(secs));
402        gt
403    }
404
405    /// Helper for creating a timezone-adjusted YmdHmsRich.
406    ///
407    /// Always converts to UTC first, then does a correct UTC-based lookup
408    /// in the IANA transition table. This avoids the previous bug where
409    /// a non-UTC `unix_ts` was being passed to `offset_info_at_local`.
410    pub(crate) fn ymdhms_rich_with_tz(&self, current: Scale, tz_name: &str) -> YmdHmsRich {
411        // 1. Get the true UTC Unix timestamp (this is what we search with)
412        let utc_unix = self
413            .to(current, current.to_utc())
414            .to_diff_raw(Dt::UNIX_EPOCH);
415
416        // 2. Look up offset + abbrev at that exact UTC instant
417        let (offset_secs, abbrev) = match offset_info_at_utc(tz_name, utc_unix.sec) {
418            Some(info) => (info.offset, info.abbrev),
419            None => (0, "UTC"), // fallback for unknown timezone
420        };
421
422        // 3. Build local time = UTC + offset
423        let span = Dt::new(offset_secs as i64, 0);
424        let local_tp = *self + span;
425
426        let mut gt = local_tp.to_ymdhms_rich_on(current, current.to_utc());
427        gt.set_offset(Some(offset_secs));
428        gt.set_tz(Some(tz_name));
429        gt.set_tz_abbrev(Some(abbrev));
430        gt
431    }
432}