Skip to main content

deep_time/dt/
to_str.rs

1use crate::{Dt, DtErr, DtErrKind, LiteStr, STRFTIME_SIZE, YmdHmsRich, an_err, tz::offset_for_utc};
2
3#[cfg(feature = "alloc")]
4use crate::ATTOS_PER_SEC;
5
6#[cfg(feature = "alloc")]
7impl Dt {
8    /// Converts this `Dt` to an ISO 8601 duration string
9    /// (e.g. `"PT1H23M45.6789S"`, `"-PT0.5S"`, `"PT0.000000000000000001S"`, or `"PT0S"`).
10    ///
11    /// - This method is only available when the **`alloc`** feature is enabled.
12    /// - It returns `alloc::string::String` (no_std + alloc compatible).
13    /// - Performs no time scale conversions prior to output.
14    pub fn to_iso_duration(&self) -> alloc::string::String {
15        if self.is_zero() {
16            return alloc::string::String::from("PT0S");
17        }
18
19        let total = self.to_attos();
20        let negative = total < 0;
21        let mut attos = total.unsigned_abs();
22
23        let mut s = alloc::string::String::with_capacity(48);
24        if negative {
25            s.push('-');
26        }
27        s.push_str("PT");
28
29        const A_PER_S: u128 = ATTOS_PER_SEC as u128;
30        const A_PER_M: u128 = A_PER_S * 60;
31        const A_PER_H: u128 = A_PER_M * 60;
32
33        let hours = attos / A_PER_H;
34        attos %= A_PER_H;
35        let minutes = attos / A_PER_M;
36        attos %= A_PER_M;
37        let seconds = attos / A_PER_S;
38        let frac_attos = attos % A_PER_S;
39
40        if hours > 0 {
41            s.push_str(&alloc::format!("{}", hours));
42            s.push('H');
43        }
44        if minutes > 0 {
45            s.push_str(&alloc::format!("{}", minutes));
46            s.push('M');
47        }
48
49        if seconds > 0 || frac_attos > 0 {
50            s.push_str(&alloc::format!("{}", seconds));
51
52            if frac_attos != 0 {
53                let frac_str = alloc::format!("{frac_attos:018}");
54                let trimmed = frac_str.trim_end_matches('0');
55                s.push('.');
56                s.push_str(trimmed);
57            }
58
59            s.push('S');
60        }
61
62        s
63    }
64
65    /// Formats this [`Dt`] into a String. Requires the `"alloc"` feature.
66    ///
67    /// Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
68    /// time scale before producing the result.
69    ///
70    /// ## Examples
71    ///
72    /// ```rust
73    /// use deep_time::{Dt, Scale};
74    ///
75    /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
76    /// let s = x.to_str("%F").unwrap();
77    ///
78    /// println!("{}", s);
79    /// ```
80    ///
81    /// ## Errors
82    ///
83    /// Returns [`DtErr`] if the format string contains invalid specifiers
84    /// or if the internal formatting buffer overflows (extremely unlikely
85    /// with [`STRFTIME_SIZE`]).
86    ///
87    /// ## See also
88    ///
89    /// - [`Dt::to_str_with_offset`](../struct.Dt.html#method.to_str_with_offset)
90    /// - [`Dt::to_str_with_tz`](../struct.Dt.html#method.to_str_with_tz)
91    #[inline]
92    pub fn to_str(&self, fmt: &str) -> Result<alloc::string::String, DtErr> {
93        self.to_str_with_offset(fmt, 0)
94    }
95
96    /// Formats this [`Dt`] into a String, applying a fixed UTC offset.  Requires the
97    /// `"alloc"` feature.
98    ///
99    /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
100    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
101    ///   will reflect it.
102    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
103    ///   time scale before producing the result.
104    /// - No IANA timezone name or abbreviation is set.
105    ///
106    /// ## Examples
107    ///
108    /// ```rust
109    /// use deep_time::{Dt, Scale};
110    ///
111    /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
112    ///
113    /// // offset of minus one hour
114    /// let s = x.to_str_with_offset("%F", -3600).unwrap();
115    ///
116    /// println!("{}", s);
117    /// ```
118    ///
119    /// ## Errors
120    ///
121    /// Returns [`DtErr`] if the format string contains invalid specifiers
122    /// or if the internal formatting buffer overflows (extremely unlikely
123    /// with [`STRFTIME_SIZE`]).
124    ///
125    /// ## See also
126    ///
127    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
128    /// - [`Dt::to_str_with_tz`](../struct.Dt.html#method.to_str_with_tz)
129    #[inline]
130    pub fn to_str_with_offset(&self, fmt: &str, secs: i32) -> Result<alloc::string::String, DtErr> {
131        let mut buf = [0u8; STRFTIME_SIZE];
132        let n = self._to_u8_with_offset(fmt, &mut buf, secs)?;
133        Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
134    }
135
136    /// Formats this [`Dt`] into a string, time adjusted to the given IANA timezone. Requires
137    /// the `"alloc"` feature.
138    ///
139    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
140    ///
141    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]s time for the given
142    ///   IANA timezone. This is so that the formatter will have:
143    ///     - Accurate wall time for the timezone.
144    ///     - Correct numeric offset (for `%z` / `%:z`).
145    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip.
146    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
147    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
148    ///   time scale before producing the result.
149    /// - No IANA timezone name or abbreviation is set.
150    ///
151    /// ## Examples
152    ///
153    /// ```
154    /// # #[cfg(all(feature = "tz", feature = "parse"))]
155    /// # {
156    /// use deep_time::{Dt, Scale};
157    ///
158    /// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
159    ///
160    /// let s = x.to_str_with_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York").unwrap();
161    ///
162    /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
163    /// # }
164    /// ```
165    ///
166    /// ## Errors
167    ///
168    /// Returns [`DtErr`] if the format string contains invalid specifiers
169    /// or if the internal formatting buffer overflows (extremely unlikely
170    /// with [`STRFTIME_SIZE`]).
171    ///
172    /// ## See also
173    ///
174    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
175    /// - [`Dt::to_str_with_offset`](../struct.Dt.html#method.to_str_with_offset)
176    #[inline]
177    pub fn to_str_with_tz(&self, fmt: &str, tz_name: &str) -> Result<alloc::string::String, DtErr> {
178        let mut buf = [0u8; STRFTIME_SIZE];
179        let n = self._to_u8_with_tz(fmt, &mut buf, tz_name)?;
180        Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
181    }
182
183    /// Returns this instant as an **RFC 3339** / ISO 8601 timestamp with a
184    /// `Z` suffix.
185    ///
186    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
187    ///   time scale before producing the result.
188    /// - Default = 9 digits (nanoseconds) but **automatically trims trailing zeros**.
189    /// - If fractional part is zero → no decimal point at all (e.g. `...45Z`).
190    /// - Example: `"2024-03-14T15:30:45.123Z"`
191    #[inline]
192    pub fn to_str_rfc3339(&self) -> Result<String, DtErr> {
193        self.to_str_rfc3339_nf(9)
194    }
195
196    /// Same as [`Dt::to_str_rfc3339`](../struct.Dt.html#method.to_str_rfc3339) but
197    /// with a configurable maximum number of fractional digits (0–18). Trailing zeros are
198    /// always trimmed.
199    ///
200    /// Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
201    /// time scale before producing the result.
202    pub fn to_str_rfc3339_nf(&self, max_precision: usize) -> Result<String, DtErr> {
203        let prec = max_precision.min(18);
204        // Uses the formatter with the `~` "trim trailing zeros" flag.
205        // The formatter already handles:
206        //   - correct 4-digit years (with sign) for |yr| < 10000
207        //   - full-width years otherwise
208        //   - suppressing the decimal point entirely when the trimmed fraction is zero
209        let fmt = alloc::format!("%Y-%m-%dT%H:%M:%S%.{}~fZ", prec);
210        self.to_str_with_offset(&fmt, 0)
211    }
212
213    /// **ISO 8601 / RFC 3339** with **actual offset** (modern `+00:00` style).
214    ///
215    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
216    ///   time scale before producing the result.
217    /// - Uses colon-separated offset (`%:z`) instead of forcing `Z`.
218    /// - Still trims trailing zeros in the fractional part.
219    /// - Example: `"2025-04-16T14:30:45.123+00:00"`
220    #[inline]
221    pub fn to_str_iso8601(&self) -> Result<String, DtErr> {
222        self.to_str_with_offset("%Y-%m-%dT%H:%M:%S%.~f%:z", 0)
223    }
224
225    /// **Compact ISO 8601 basic format** (no separators).
226    ///
227    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
228    ///   time scale before producing the result.
229    /// - Useful for filenames, URLs, database keys, etc.
230    /// - Example: `"20250416T143045.123456789Z"`
231    #[inline]
232    pub fn to_str_iso8601_basic(&self) -> Result<String, DtErr> {
233        self.to_str_with_offset("%Y%m%dT%H%M%S%.~fZ", 0)
234    }
235
236    /// **HTTP-date** format (RFC 7231 / RFC 1123) — **always in GMT**.
237    ///
238    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
239    ///   time scale before producing the result.
240    /// - This is the format used in `Date`, `Expires`, `Last-Modified` headers.
241    /// - Example: `"Wed, 16 Apr 2025 14:30:45 GMT"`
242    #[inline]
243    pub fn to_str_http(&self) -> Result<String, DtErr> {
244        self.to_str_with_offset("%a, %d %b %Y %H:%M:%S GMT", 0)
245    }
246
247    /// **RFC 2822** date format (used in email `Date` headers).
248    ///
249    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
250    ///   time scale before producing the result.
251    /// - Example: `"Wed, 16 Apr 2025 14:30:45 +0000"`
252    #[inline]
253    pub fn to_str_rfc2822(&self) -> Result<String, DtErr> {
254        self.to_str_with_offset("%a, %d %b %Y %H:%M:%S %z", 0)
255    }
256
257    /// **ISO 8601 week date**.
258    ///
259    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
260    ///   time scale before producing the result.
261    /// - Example: `"2025-W16-3"` (year-week-day)
262    #[inline]
263    pub fn to_str_iso_week_date(&self) -> Result<String, DtErr> {
264        self.to_str_with_offset("%G-W%V-%u", 0)
265    }
266
267    /// Just the **ISO date** part (no time).
268    ///
269    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
270    ///   time scale before producing the result.
271    /// - Example: `"2025-04-16"`
272    #[inline]
273    pub fn to_str_iso_date(&self) -> Result<String, DtErr> {
274        self.to_str_with_offset("%Y-%m-%d", 0)
275    }
276
277    /// Just the **time** part with fractional seconds (trimmed).
278    ///
279    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
280    ///   time scale before producing the result.
281    /// - Example: `"14:30:45.123456789"`
282    #[inline]
283    pub fn to_str_iso_time(&self) -> Result<String, DtErr> {
284        self.to_str_with_offset("%H:%M:%S%.~f", 0)
285    }
286}
287
288impl Dt {
289    /// Formats this [`Dt`] into a fixed-size binary string.
290    ///
291    /// Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
292    /// time scale before producing the result.
293    ///
294    /// ## Examples
295    ///
296    /// ```rust
297    /// use deep_time::{Dt, Scale};
298    ///
299    /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
300    /// let b = x.to_str_bin("%F").unwrap();
301    /// let s = b.as_str().unwrap();
302    ///
303    /// println!("{}", s);
304    /// ```
305    ///
306    /// ## Errors
307    ///
308    /// Returns [`DtErr`] if the format string contains invalid specifiers
309    /// or if the internal formatting buffer overflows (extremely unlikely
310    /// with [`STRFTIME_SIZE`]).
311    ///
312    /// ## See also
313    ///
314    /// - [`Dt::to_str_bin_with_offset`](../struct.Dt.html#method.to_str_bin_with_offset)
315    /// - [`Dt::to_str_bin_with_tz`](../struct.Dt.html#method.to_str_bin_with_tz)
316    pub fn to_str_bin(&self, fmt: &str) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
317        let mut ymdhms = self.to_ymd_rich();
318        ymdhms.set_offset(Some(0)).set_tz_abbrev(None);
319        let mut buf = [0u8; STRFTIME_SIZE];
320        let mut pos = 0usize;
321        ymdhms.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
322        LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))
323    }
324
325    /// Formats this [`Dt`] into a fixed-size binary string, applying a fixed UTC offset.
326    ///
327    /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
328    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
329    ///   will reflect it.
330    /// - No IANA timezone name or abbreviation is set.
331    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
332    ///   time scale before producing the result.
333    ///
334    /// ## Examples
335    ///
336    /// ```rust
337    /// use deep_time::{Dt, Scale};
338    ///
339    /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
340    ///
341    /// // offset of minus one hour
342    /// let b = x.to_str_bin_with_offset("%F", -3600).unwrap();
343    /// let s = b.as_str().unwrap();
344    ///
345    /// println!("{}", s);
346    /// ```
347    ///
348    /// ## Errors
349    ///
350    /// Returns [`DtErr`] if the format string contains invalid specifiers
351    /// or if the internal formatting buffer overflows (extremely unlikely
352    /// with [`STRFTIME_SIZE`]).
353    ///
354    /// ## See also
355    ///
356    /// - [`Dt::to_str_bin`](../struct.Dt.html#method.to_str_bin)
357    /// - [`Dt::to_str_bin_with_tz`](../struct.Dt.html#method.to_str_bin_with_tz)
358    pub fn to_str_bin_with_offset(
359        &self,
360        fmt: &str,
361        secs: i32,
362    ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
363        let ymdhms = self.ymdhms_rich_with_offset(secs);
364        let mut buf = [0u8; STRFTIME_SIZE];
365        let mut pos = 0usize;
366        ymdhms.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
367        LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))
368    }
369
370    /// Formats this [`Dt`] into a fixed-size binary string, time adjusted to the given
371    /// IANA timezone.
372    ///
373    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
374    ///
375    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]s time for the given
376    ///   IANA timezone. This is so that the formatter will have:
377    ///     - Accurate wall time for the timezone.
378    ///     - Correct numeric offset (for `%z` / `%:z`).
379    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip.
380    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
381    /// - No IANA timezone name or abbreviation is set.
382    /// - Converts from the [`Dt`]s current time `scale` to the [`Dt`]s `target`
383    ///   time scale before producing the result.
384    ///
385    /// ## Examples
386    ///
387    /// ```rust
388    /// use deep_time::{Dt, Scale};
389    ///
390    /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
391    ///
392    /// let b = x.to_str_bin_with_tz("%F", "America/New_York").unwrap();
393    /// let s = b.as_str().unwrap();
394    ///
395    /// println!("{}", s);
396    /// ```
397    ///
398    /// ## Errors
399    ///
400    /// Returns [`DtErr`] if the format string contains invalid specifiers
401    /// or if the internal formatting buffer overflows (extremely unlikely
402    /// with [`STRFTIME_SIZE`]).
403    ///
404    /// ## See also
405    ///
406    /// - [`Dt::to_str_bin`](../struct.Dt.html#method.to_str_bin)
407    /// - [`Dt::to_str_bin_with_offset`](../struct.Dt.html#method.to_str_bin_with_offset)
408    pub fn to_str_bin_with_tz(
409        &self,
410        fmt: &str,
411        tz_name: &str,
412    ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
413        let ymdhms = self.ymdhms_rich_with_tz(tz_name);
414        let mut buf = [0u8; STRFTIME_SIZE];
415        let mut pos = 0usize;
416        ymdhms.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
417        LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))
418    }
419
420    /// Low-level no-alloc formatter that writes into a caller-provided slice,
421    /// using a fixed UTC offset.
422    ///
423    /// Same logic as
424    /// [`Dt::to_str_bin_with_offset`](../struct.Dt.html#method.to_str_bin_with_offset)
425    /// but writes directly into `dest` (truncated to `dest.len()`) and returns the
426    /// number of bytes written.
427    pub fn _to_u8_with_offset(
428        &self,
429        fmt: &str,
430        dest: &mut [u8],
431        secs: i32,
432    ) -> Result<usize, DtErr> {
433        let ymdhms = self.ymdhms_rich_with_offset(secs);
434        let mut internal_buf = [0u8; STRFTIME_SIZE];
435        let mut pos = 0usize;
436        ymdhms.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
437        let written = pos.min(dest.len());
438        if written > 0 {
439            dest[0..written].copy_from_slice(&internal_buf[0..written]);
440        }
441        Ok(written)
442    }
443
444    /// Low-level no-alloc formatter that writes into a caller-provided slice,
445    /// using a full IANA timezone.
446    ///
447    /// Same logic as
448    /// [`Dt::to_str_bin_with_tz`](../struct.Dt.html#method.to_str_bin_with_tz)
449    /// but writes directly into `dest` (truncated to `dest.len()`) and returns the
450    /// number of bytes written.
451    pub fn _to_u8_with_tz(
452        &self,
453        fmt: &str,
454        dest: &mut [u8],
455        tz_name: &str,
456    ) -> Result<usize, DtErr> {
457        let ymdhms = self.ymdhms_rich_with_tz(tz_name);
458        let mut internal_buf = [0u8; STRFTIME_SIZE];
459        let mut pos = 0usize;
460        ymdhms.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
461        let written = pos.min(dest.len());
462        if written > 0 {
463            dest[0..written].copy_from_slice(&internal_buf[0..written]);
464        }
465        Ok(written)
466    }
467
468    /// Returns `(is_negative, hours, minutes)`.
469    #[inline]
470    pub(crate) const fn sec_as_hhmm(seconds: i32) -> (bool, u8, u8) {
471        let total = seconds.saturating_abs();
472        let hours = (total / 3600) as u8;
473        let minutes = ((total % 3600) / 60) as u8;
474        (seconds < 0, hours, minutes)
475    }
476
477    /// Helper for creating an offset adjusted YmdHmsRich.
478    pub(crate) fn ymdhms_rich_with_offset(&self, secs: i32) -> YmdHmsRich {
479        let local_tp = if secs != 0 {
480            self.add_sec(secs as i128)
481        } else {
482            *self
483        };
484        let mut ymdhms = local_tp.to_ymd_rich();
485        ymdhms.set_offset(Some(secs));
486        ymdhms
487    }
488
489    /// Helper for creating a timezone-adjusted YmdHmsRich.
490    pub(crate) fn ymdhms_rich_with_tz(&self, tz_name: &str) -> YmdHmsRich {
491        // 1. Get the true UTC Unix timestamp
492        let utc_unix = self.to_unix();
493
494        // 2. Look up offset + abbrev at that exact UTC instant
495        let unix_sec = Dt::attos_to_sec_i64(utc_unix.to_attos());
496        let (offset_secs, abbrev) = match offset_for_utc(tz_name, unix_sec) {
497            Some(info) => (info.offset, info.abbrev),
498            None => (0, "UTC"), // fallback for unknown timezone
499        };
500
501        // 3. Build local time = UTC + offset
502        let local_tp = self.add_sec(offset_secs as i128);
503
504        let mut ymdhms = local_tp.to_ymd_rich();
505        ymdhms.set_offset(Some(offset_secs));
506        ymdhms.set_tz(Some(tz_name));
507        ymdhms.set_tz_abbrev(Some(abbrev));
508        ymdhms
509    }
510}