Skip to main content

deep_time/dt/
to_str.rs

1use crate::{BufStr, Dt, DtErr, DtErrKind, Lang, STRTIME_SIZE, YmdHms, an_err};
2
3#[cfg(feature = "alloc")]
4use {
5    crate::ATTOS_PER_SEC_U128,
6    alloc::string::{String, ToString},
7};
8
9#[cfg(not(any(feature = "jiff-tz-bundle", feature = "jiff-tz")))]
10use crate::tz::UTC_ALIASES;
11
12#[cfg(feature = "alloc")]
13impl Dt {
14    /// Converts this `Dt` to an ISO 8601 duration string.
15    ///
16    /// - Example: **"PT1H23M45.6789S"**.
17    /// - Requires the `alloc` feature.
18    /// - Does **not** do any time scale conversions prior to output.
19    pub fn to_iso_duration(&self) -> String {
20        if self.is_zero() {
21            return String::from("PT0S");
22        }
23
24        let total = self.to_attos();
25        let negative = total < 0;
26        let mut attos = total.unsigned_abs();
27
28        let mut s = String::with_capacity(48);
29        if negative {
30            s.push('-');
31        }
32        s.push_str("PT");
33
34        const A_PER_M: u128 = ATTOS_PER_SEC_U128 * 60;
35        const A_PER_H: u128 = A_PER_M * 60;
36
37        let hours = attos / A_PER_H;
38        attos %= A_PER_H;
39        let minutes = attos / A_PER_M;
40        attos %= A_PER_M;
41        let seconds = attos / ATTOS_PER_SEC_U128;
42        let frac_attos = attos % ATTOS_PER_SEC_U128;
43
44        if hours > 0 {
45            s.push_str(&alloc::format!("{}", hours));
46            s.push('H');
47        }
48        if minutes > 0 {
49            s.push_str(&alloc::format!("{}", minutes));
50            s.push('M');
51        }
52
53        if seconds > 0 || frac_attos > 0 {
54            s.push_str(&alloc::format!("{}", seconds));
55
56            if frac_attos != 0 {
57                let frac_str = alloc::format!("{frac_attos:018}");
58                let trimmed = frac_str.trim_end_matches('0');
59                s.push('.');
60                s.push_str(trimmed);
61            }
62
63            s.push('S');
64        }
65
66        s
67    }
68
69    /// Formats this [`Dt`] into a String. Requires the `alloc` feature.
70    ///
71    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
72    ///   time scale before producing the result.
73    ///
74    /// ## Examples
75    ///
76    /// ```rust
77    /// use deep_time::{Dt, Lang, Scale};
78    ///
79    /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
80    /// let s = x.to_str("%F", Lang::En).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 [`STRTIME_SIZE`]).
90    ///
91    /// ## See also
92    ///
93    /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset)
94    /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)
95    #[inline(always)]
96    pub fn to_str(&self, fmt: &str, lang: Lang) -> Result<String, DtErr> {
97        self.to_str_in_offset(fmt, 0, lang)
98    }
99
100    /// Formats this [`Dt`] into a String, applying a fixed offset. Requires the
101    /// `"alloc"` feature.
102    ///
103    /// - A copy of the [`Dt`] is adjusted by the given `sec` offset **before**
104    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
105    ///   will reflect it.
106    /// - No IANA timezone name or abbreviation is set.
107    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
108    ///   time scale before producing the result.
109    ///
110    /// ## Examples
111    ///
112    /// ```rust
113    /// use deep_time::{Dt, Lang, Scale};
114    ///
115    /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
116    ///
117    /// // offset of minus one hour
118    /// let s = x.to_str_in_offset("%F", -3600, Lang::En).unwrap();
119    ///
120    /// println!("{}", s);
121    /// ```
122    ///
123    /// ## Errors
124    ///
125    /// Returns [`DtErr`] if the format string contains invalid specifiers
126    /// or if the internal formatting buffer overflows (extremely unlikely
127    /// with [`STRTIME_SIZE`]).
128    ///
129    /// ## See also
130    ///
131    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
132    /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)
133    #[inline(always)]
134    pub fn to_str_in_offset(&self, fmt: &str, sec: i32, lang: Lang) -> Result<String, DtErr> {
135        self.ymd_with_offset(sec)
136            ._to_str(fmt, Some(sec), None, None, lang)
137    }
138
139    /// Formats this [`Dt`] into a string, time adjusted to the given IANA timezone.
140    ///
141    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
142    ///
143    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]'s time for the given
144    ///   IANA timezone. This is so that the formatter will have:
145    ///     - Accurate wall time for the timezone.
146    ///     - Correct numeric offset (for `%z` / `%:z`).
147    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip (the parser
148    ///       does not parse them).
149    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
150    /// - Converts to the provided timezone, if your [`Dt`] is already in
151    ///   the timezone then use the label function instead:
152    ///   [`Dt::to_str_with_tz_label`](../struct.Dt.html#method.to_str_with_tz_label).
153    ///   This is unlikely to be case because when a date with a timezone is parsed
154    ///   the returned [`Dt`] is not in local time. But, label only functions are
155    ///   provided just in case anyway.
156    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
157    ///   time scale before producing the result.
158    /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
159    ///
160    /// ## Examples
161    ///
162    /// You can offset an output that wasn't originally from a zoned input:
163    ///
164    /// ```rust
165    /// # #[cfg(all(any(feature = "jiff-tz", feature = "jiff-tz-bundle"), feature = "parse"))]
166    /// # {
167    /// use deep_time::{Dt, Lang, Scale};
168    ///
169    /// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
170    /// let s = x.to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En).unwrap();
171    /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
172    /// # }
173    /// ```
174    ///
175    /// You can also return to a zoned output from a zoned input:
176    ///
177    /// ```rust
178    /// # #[cfg(all(any(feature = "jiff-tz", feature = "jiff-tz-bundle"), feature = "parse"))]
179    /// # {
180    /// use deep_time::{Dt, Lang, Scale};
181    ///
182    /// let x: Dt = "Saturday, January 01, 2000 07:00:00 America/New_York".parse().unwrap();
183    /// let s = x.to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En).unwrap();
184    /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
185    /// # }
186    /// ```
187    ///
188    /// ## Errors
189    ///
190    /// Returns [`DtErr`] if the format string contains invalid specifiers
191    /// or if the internal formatting buffer overflows (extremely unlikely
192    /// with [`STRTIME_SIZE`]).
193    ///
194    /// ## See also
195    ///
196    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
197    /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset)
198    #[inline(always)]
199    pub fn to_str_in_tz(&self, fmt: &str, tz_name: &str, lang: Lang) -> Result<String, DtErr> {
200        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, true)?;
201        ymd._to_str(
202            fmt,
203            Some(offset),
204            Some(BufStr::new(tz_name)),
205            Some(abbrev),
206            lang,
207        )
208    }
209
210    /// **RFC 9557** / Temporal format with IANA timezone name in brackets.
211    ///
212    /// - Example: **`"2020-06-15T14:30:00-04:00[America/New_York]"`**.
213    /// - Converts to the provided timezone, if your [`Dt`] is already in
214    ///   the timezone then use the label function instead:
215    ///   [`Dt::to_str_with_tz_label`](../struct.Dt.html#method.to_str_with_tz_label).
216    ///   This is unlikely to be case because when a date with a timezone is parsed
217    ///   the returned [`Dt`] is not in local time. But, label only functions are
218    ///   provided just in case anyway.
219    /// - Automatically trims trailing zeros in the fractional part.
220    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
221    ///   time scale before producing the result.
222    #[inline(always)]
223    pub fn to_str_rfc9557(&self, tz_name: &str) -> Result<String, DtErr> {
224        self.to_str_in_tz("%Y-%m-%dT%H:%M:%S%.~f%:z[%Q]", tz_name, Lang::En)
225    }
226
227    /// Returns this instant as an **RFC 3339** / ISO 8601 timestamp with a
228    /// `Z` suffix.
229    ///
230    /// - Example: **`"2024-03-14T15:30:45.123Z"`**
231    /// - Default = 9 digits (nanoseconds) but **automatically trims trailing zeros**.
232    /// - If fractional part is zero → no decimal point at all (e.g. `...45Z`).
233    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
234    ///   time scale before producing the result.
235    #[inline(always)]
236    pub fn to_str_rfc3339(&self) -> String {
237        self.to_str_rfc3339_nf(9)
238    }
239
240    /// Same as [`Dt::to_str_rfc3339`](../struct.Dt.html#method.to_str_rfc3339) but
241    /// with a configurable maximum number of fractional digits (0–18). Trailing zeros are
242    /// always trimmed.
243    ///
244    /// - Example: **`"2024-03-14T15:30:45.123Z"`**
245    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
246    ///   time scale before producing the result.
247    #[allow(clippy::unwrap_used)]
248    pub fn to_str_rfc3339_nf(&self, max_precision: usize) -> String {
249        let prec = max_precision.min(18);
250        // Uses the formatter with the `~` "trim trailing zeros" flag.
251        // The formatter already handles:
252        //   - correct 4-digit years (with sign) for |yr| < 10000
253        //   - full-width years otherwise
254        //   - suppressing the decimal point entirely when the trimmed fraction is zero
255        let fmt = alloc::format!("%Y-%m-%dT%H:%M:%S%.{}~fZ", prec);
256        self.to_str_in_offset(&fmt, 0, Lang::En).unwrap()
257    }
258
259    /// **ISO 8601 / RFC 3339** with **actual offset** (modern `+00:00` style).
260    ///
261    /// - Example: **`"2025-04-16T14:30:45.123+00:00"`**.
262    /// - Uses colon-separated offset (`%:z`) instead of forcing `Z`.
263    /// - Still trims trailing zeros in the fractional part.
264    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
265    ///   time scale before producing the result.
266    #[allow(clippy::unwrap_used)]
267    #[inline(always)]
268    pub fn to_str_iso8601(&self) -> String {
269        self.to_str_in_offset("%Y-%m-%dT%H:%M:%S%.~f%:z", 0, Lang::En)
270            .unwrap()
271    }
272
273    /// **Compact ISO 8601 basic format** (no separators).
274    ///
275    /// - Example: **`"20250416T143045.123456789Z"`**.
276    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
277    ///   time scale before producing the result.
278    #[allow(clippy::unwrap_used)]
279    #[inline(always)]
280    pub fn to_str_iso8601_basic(&self) -> String {
281        self.to_str_in_offset("%Y%m%dT%H%M%S%.~fZ", 0, Lang::En)
282            .unwrap()
283    }
284
285    /// **ISO 8601 week date**.
286    ///
287    /// - Example: **`"2025-W16-3"`**. (year-week-day)
288    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
289    ///   time scale before producing the result.
290    #[allow(clippy::unwrap_used)]
291    #[inline(always)]
292    pub fn to_str_iso_week_date(&self) -> String {
293        self.to_str_in_offset("%G-W%V-%u", 0, Lang::En).unwrap()
294    }
295
296    /// Just the **ISO date** part (no time).
297    ///
298    /// - Example: **`"2025-04-16"`**.
299    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
300    ///   time scale before producing the result.
301    #[allow(clippy::unwrap_used)]
302    #[inline(always)]
303    pub fn to_str_iso_date(&self) -> String {
304        self.to_str_in_offset("%Y-%m-%d", 0, Lang::En).unwrap()
305    }
306
307    /// Just the **time** part with fractional seconds (trimmed).
308    ///
309    /// - Example: **`"14:30:45.123456789"`**.
310    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
311    ///   time scale before producing the result.
312    #[allow(clippy::unwrap_used)]
313    #[inline(always)]
314    pub fn to_str_iso_time(&self) -> String {
315        self.to_str_in_offset("%H:%M:%S%.~f", 0, Lang::En).unwrap()
316    }
317
318    /// **HTTP-date** format (RFC 7231 / RFC 1123) — **always in GMT**.
319    ///
320    /// - Example: **`"Wed, 16 Apr 2025 14:30:45 GMT"`**.
321    /// - Always outputs in GMT (equivalent to UTC+00:00). Does not apply
322    ///   regional DST rules.
323    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
324    ///   time scale before producing the result.
325    #[allow(clippy::unwrap_used)]
326    #[inline(always)]
327    pub fn to_str_http(&self, lang: Lang) -> String {
328        self.to_str_in_offset("%a, %d %b %Y %H:%M:%S GMT", 0, lang)
329            .unwrap()
330    }
331
332    /// **RFC 2822** date format (used in email `Date` headers).
333    ///
334    /// - Example: **`"Wed, 16 Apr 2025 14:30:45 +0000"`**.
335    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
336    ///   time scale before producing the result.
337    #[allow(clippy::unwrap_used)]
338    #[inline(always)]
339    pub fn to_str_rfc2822(&self, lang: Lang) -> String {
340        self.to_str_in_offset("%a, %d %b %Y %H:%M:%S %z", 0, lang)
341            .unwrap()
342    }
343
344    /// Formats this [`Dt`] into a `String`, attaching an offset **as a label only**.
345    ///
346    /// - The actual datetime components are **not** shifted or adjusted.
347    /// - The given `offset` is used **only** for `%z` / `%:z` format directives.
348    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
349    ///   time scale before producing the result.
350    ///
351    /// ## Errors
352    ///
353    /// Returns [`DtErr`] if the format string contains invalid specifiers
354    /// or if the internal formatting buffer overflows (extremely unlikely
355    /// with [`STRTIME_SIZE`]).
356    ///
357    /// ## See also
358    ///
359    /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset) —
360    ///   shifts the datetime by the offset
361    #[inline(always)]
362    pub fn to_str_with_offset_label(
363        &self,
364        fmt: &str,
365        offset: i32,
366        lang: Lang,
367    ) -> Result<String, DtErr> {
368        self.to_ymd()._to_str(fmt, Some(offset), None, None, lang)
369    }
370
371    /// Formats this [`Dt`] into a `String`, attaching a timezone **as a label only**.
372    ///
373    /// - The actual datetime components are **not** shifted or adjusted.
374    /// - The timezone is used to provide correct values for `%z`, `%:z`, `%Z`, `%Q`, and `%:Q`.
375    /// - The timezone abbreviation is automatically looked up from tzdata.
376    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
377    ///   time scale before producing the result.
378    /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
379    ///
380    /// ## Errors
381    ///
382    /// Returns [`DtErr`] if the format string contains invalid specifiers,
383    /// if the timezone name is invalid, or if the internal formatting buffer
384    /// overflows (extremely unlikely with [`STRTIME_SIZE`]).
385    ///
386    /// ## See also
387    ///
388    /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz) —
389    ///   shifts the datetime into the given timezone
390    #[inline(always)]
391    pub fn to_str_with_tz_label(
392        &self,
393        fmt: &str,
394        tz_name: &str,
395        lang: Lang,
396    ) -> Result<String, DtErr> {
397        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, false)?;
398        ymd._to_str(
399            fmt,
400            Some(offset),
401            Some(BufStr::new(tz_name)),
402            Some(abbrev),
403            lang,
404        )
405    }
406}
407
408impl Dt {
409    /// Formats this [`Dt`] into a fixed-size [`BufStr`] (no-alloc byte buffer).
410    ///
411    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
412    ///   time scale before producing the result.
413    ///
414    /// ## Examples
415    ///
416    /// ```rust
417    /// use deep_time::{Dt, Lang, Scale};
418    ///
419    /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
420    /// let b = x.to_str_b("%F", Lang::En).unwrap();
421    /// let s = b.as_str();
422    ///
423    /// println!("{}", s);
424    /// ```
425    ///
426    /// ## Errors
427    ///
428    /// Returns [`DtErr`] if the format string contains invalid specifiers
429    /// or if the internal formatting buffer overflows (extremely unlikely
430    /// with [`STRTIME_SIZE`]).
431    ///
432    /// ## See also
433    ///
434    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str) — alloc `String` variant
435    /// - [`Dt::to_str_b_in_offset`](../struct.Dt.html#method.to_str_b_in_offset)
436    /// - [`Dt::to_str_b_in_tz`](../struct.Dt.html#method.to_str_b_in_tz)
437    #[inline(always)]
438    pub fn to_str_b(&self, fmt: &str, lang: Lang) -> Result<BufStr<STRTIME_SIZE>, DtErr> {
439        self.to_ymd()._to_str_b(fmt, None, None, None, lang)
440    }
441
442    /// Formats this [`Dt`] into a fixed-size [`BufStr`], applying a fixed UTC offset.
443    ///
444    /// - A copy of the [`Dt`] is adjusted by the given `sec` offset **before**
445    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
446    ///   will reflect it.
447    /// - No IANA timezone name or abbreviation is set.
448    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
449    ///   time scale before producing the result.
450    ///
451    /// ## Examples
452    ///
453    /// ```rust
454    /// use deep_time::{Dt, Lang, Scale};
455    ///
456    /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
457    ///
458    /// // offset of minus one hour
459    /// let b = x.to_str_b_in_offset("%F", -3600, Lang::En).unwrap();
460    /// let s = b.as_str();
461    ///
462    /// println!("{}", s);
463    /// ```
464    ///
465    /// ## Errors
466    ///
467    /// Returns [`DtErr`] if the format string contains invalid specifiers
468    /// or if the internal formatting buffer overflows (extremely unlikely
469    /// with [`STRTIME_SIZE`]).
470    ///
471    /// ## See also
472    ///
473    /// - [`Dt::to_str_b`](../struct.Dt.html#method.to_str_b)
474    /// - [`Dt::to_str_b_in_tz`](../struct.Dt.html#method.to_str_b_in_tz)
475    #[inline(always)]
476    pub fn to_str_b_in_offset(
477        &self,
478        fmt: &str,
479        sec: i32,
480        lang: Lang,
481    ) -> Result<BufStr<STRTIME_SIZE>, DtErr> {
482        self.ymd_with_offset(sec)
483            ._to_str_b(fmt, Some(sec), None, None, lang)
484    }
485
486    /// Formats this [`Dt`] into a fixed-size [`BufStr`], time adjusted to the given
487    /// IANA timezone.
488    ///
489    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
490    ///
491    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]'s time for the given
492    ///   IANA timezone. This is so that the formatter will have:
493    ///     - Accurate wall time for the timezone.
494    ///     - Correct numeric offset (for `%z` / `%:z`).
495    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip.
496    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
497    /// - Converts to the provided timezone, if your [`Dt`] is already in
498    ///   the timezone then use the label function instead:
499    ///   [`Dt::to_str_b_with_tz_label`](../struct.Dt.html#method.to_str_b_with_tz_label).
500    ///   This is unlikely to be case because when a date with a timezone is parsed
501    ///   the returned [`Dt`] is not in local time. But, label only functions are
502    ///   provided just in case anyway.
503    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
504    ///   time scale before producing the result.
505    /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
506    ///
507    /// ## Examples
508    ///
509    /// ```rust
510    /// # #[cfg(any(feature = "jiff-tz-bundle", feature = "jiff-tz"))]
511    /// # {
512    /// use deep_time::{Dt, Lang, Scale};
513    ///
514    /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
515    ///
516    /// let b = x.to_str_b_in_tz("%F", "America/New_York", Lang::En).unwrap();
517    /// let s = b.as_str();
518    ///
519    /// println!("{}", s);
520    /// # }
521    /// ```
522    ///
523    /// ## Errors
524    ///
525    /// Returns [`DtErr`] if the format string contains invalid specifiers
526    /// or if the internal formatting buffer overflows (extremely unlikely
527    /// with [`STRTIME_SIZE`]).
528    ///
529    /// ## See also
530    ///
531    /// - [`Dt::to_str_b`](../struct.Dt.html#method.to_str_b)
532    /// - [`Dt::to_str_b_in_offset`](../struct.Dt.html#method.to_str_b_in_offset)
533    /// - [`Dt::to_str_b_with_tz_label`](../struct.Dt.html#method.to_str_b_with_tz_label)
534    #[inline(always)]
535    pub fn to_str_b_in_tz(
536        &self,
537        fmt: &str,
538        tz_name: &str,
539        lang: Lang,
540    ) -> Result<BufStr<STRTIME_SIZE>, DtErr> {
541        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, true)?;
542        ymd._to_str_b(
543            fmt,
544            Some(offset),
545            Some(BufStr::new(tz_name)),
546            Some(abbrev),
547            lang,
548        )
549    }
550
551    /// Formats this [`Dt`] into a `BufStr`, attaching an offset **as a label only**.
552    ///
553    /// - The actual datetime components are **not** shifted or adjusted.
554    /// - The given `offset` is used **only** for `%z` / `%:z` format directives.
555    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
556    ///   time scale before producing the result.
557    ///
558    /// ## Errors
559    ///
560    /// Returns [`DtErr`] if the format string contains invalid specifiers
561    /// or if the internal formatting buffer overflows (extremely unlikely
562    /// with [`STRTIME_SIZE`]).
563    ///
564    /// ## See also
565    ///
566    /// - [`Dt::to_str_b_in_offset`](../struct.Dt.html#method.to_str_b_in_offset) —
567    ///   shifts the datetime by the offset
568    #[inline(always)]
569    pub fn to_str_b_with_offset_label(
570        &self,
571        fmt: &str,
572        offset: i32,
573        lang: Lang,
574    ) -> Result<BufStr<STRTIME_SIZE>, DtErr> {
575        self.to_ymd()._to_str_b(fmt, Some(offset), None, None, lang)
576    }
577
578    /// Formats this [`Dt`] into a `BufStr`, attaching a timezone **as a label only**.
579    ///
580    /// - The actual datetime components are **not** shifted or adjusted.
581    /// - The timezone is used to provide correct values for `%z`, `%:z`, `%Z`, `%Q`, and `%:Q`.
582    /// - The timezone abbreviation is automatically looked up from tzdata.
583    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
584    ///   time scale before producing the result.
585    /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
586    ///
587    /// ## Errors
588    ///
589    /// Returns [`DtErr`] if the format string contains invalid specifiers,
590    /// if the timezone name is invalid, or if the internal formatting buffer
591    /// overflows (extremely unlikely with [`STRTIME_SIZE`]).
592    ///
593    /// ## See also
594    ///
595    /// - [`Dt::to_str_b_in_tz`](../struct.Dt.html#method.to_str_b_in_tz) —
596    ///   shifts the datetime into the given timezone
597    #[inline(always)]
598    pub fn to_str_b_with_tz_label(
599        &self,
600        fmt: &str,
601        tz_name: &str,
602        lang: Lang,
603    ) -> Result<BufStr<STRTIME_SIZE>, DtErr> {
604        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, false)?;
605        ymd._to_str_b(
606            fmt,
607            Some(offset),
608            Some(BufStr::new(tz_name)),
609            Some(abbrev),
610            lang,
611        )
612    }
613
614    /// **ISO 8601 / RFC 3339** style with a time scale suffix and without
615    /// an offset, as a fixed-size no-alloc [`BufStr`].
616    ///
617    /// - Example: **`"2025-04-16T14:30:45.123 TAI"`**.
618    /// - Trims trailing zeros in the fractional part.
619    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
620    ///   time scale before producing the result.
621    #[allow(clippy::unwrap_used)]
622    #[inline(always)]
623    pub fn to_str_b_iso_sc(&self) -> BufStr<STRTIME_SIZE> {
624        self.to_str_b_in_offset("%Y-%m-%dT%H:%M:%S%.~f %L", 0, Lang::En)
625            .unwrap()
626    }
627
628    /// **ISO 8601 / RFC 3339** with **actual offset** (modern `+00:00` style)
629    /// as a fixed-size no-alloc [`BufStr`].
630    ///
631    /// - Example: **`"2025-04-16T14:30:45.123+00:00"`**.
632    /// - Uses colon-separated offset (`%:z`) instead of forcing `Z`.
633    /// - Trims trailing zeros in the fractional part.
634    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
635    ///   time scale before producing the result.
636    #[allow(clippy::unwrap_used)]
637    #[inline(always)]
638    pub fn to_str_b_iso8601(&self) -> BufStr<STRTIME_SIZE> {
639        self.to_str_b_in_offset("%Y-%m-%dT%H:%M:%S%.~f%:z", 0, Lang::En)
640            .unwrap()
641    }
642
643    /// **RFC 9557** / Temporal format with IANA timezone name in brackets
644    /// as a fixed-size no-alloc [`BufStr`].
645    ///
646    /// - Example: **`"2020-06-15T14:30:00-04:00[America/New_York]"`**.
647    /// - Automatically trims trailing zeros in the fractional part.
648    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
649    ///   time scale before producing the result.
650    #[inline(always)]
651    pub fn to_str_b_rfc9557(&self, tz_name: &str) -> Result<BufStr<STRTIME_SIZE>, DtErr> {
652        self.to_str_b_in_tz("%Y-%m-%dT%H:%M:%S%.~f%:z[%Q]", tz_name, Lang::En)
653    }
654
655    /// **HTTP-date** format (RFC 7231 / RFC 1123) — **always in GMT**
656    /// as a fixed-size no-alloc [`BufStr`].
657    ///
658    /// - Example: **`"Wed, 16 Apr 2025 14:30:45 GMT"`**.
659    /// - Always outputs in GMT (equivalent to UTC+00:00). Does not apply
660    ///   regional DST rules.
661    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
662    ///   time scale before producing the result.
663    #[allow(clippy::unwrap_used)]
664    #[inline(always)]
665    pub fn to_str_b_http(&self, lang: Lang) -> BufStr<STRTIME_SIZE> {
666        self.to_str_b_in_offset("%a, %d %b %Y %H:%M:%S GMT", 0, lang)
667            .unwrap()
668    }
669
670    /// Returns `(is_negative, hours, minutes)`.
671    #[inline]
672    pub(crate) const fn sec_as_hhmm(seconds: i32) -> (bool, u8, u8) {
673        let total = seconds.saturating_abs();
674        let hours = (total / 3600) as u8;
675        let minutes = ((total % 3600) / 60) as u8;
676        (seconds < 0, hours, minutes)
677    }
678
679    #[inline(always)]
680    pub(crate) fn ymd_with_offset(&self, sec: i32) -> YmdHms {
681        if sec != 0 {
682            self.add_sec(sec as i128).to_ymd()
683        } else {
684            self.to_ymd()
685        }
686    }
687
688    pub(crate) fn ymd_with_tz(
689        &self,
690        tz_name: &str,
691        apply_offset: bool,
692    ) -> Result<(YmdHms, i32, BufStr<49>), DtErr> {
693        #[cfg(any(feature = "jiff-tz-bundle", feature = "jiff-tz"))]
694        let (offset_sec, abbrev): (i32, BufStr<49>) = {
695            use jiff::{Timestamp, tz::TimeZone};
696
697            let tz =
698                TimeZone::get(tz_name).map_err(|e| an_err!(DtErrKind::InvalidTimeZone, "{}", e))?;
699
700            let unix_sec = self.to_unix().to_sec64_floor();
701
702            let ts = Timestamp::from_second(unix_sec)
703                .map_err(|e| an_err!(DtErrKind::InvalidTimestamp, "{}", e))?;
704
705            let info = tz.to_offset_info(ts);
706            let offset_sec = info.offset().seconds();
707            let abbrev: BufStr<49> = BufStr::new(info.abbreviation());
708
709            (offset_sec, abbrev)
710        };
711
712        #[cfg(not(any(feature = "jiff-tz-bundle", feature = "jiff-tz")))]
713        let (offset_sec, abbrev): (i32, BufStr<49>) = {
714            if !UTC_ALIASES.contains(&tz_name) {
715                return Err(an_err!(DtErrKind::MissingFeature));
716            }
717            // UTC → offset 0, canonical abbrev "UTC"
718            let abbrev: BufStr<49> = BufStr::new("UTC");
719            (0i32, abbrev)
720        };
721
722        let ymd = if offset_sec != 0 && apply_offset {
723            self.add_sec(offset_sec as i128).to_ymd()
724        } else {
725            self.to_ymd()
726        };
727
728        Ok((ymd, offset_sec, abbrev))
729    }
730}
731
732impl Dt {
733    /// Formats the duration using the common media/video player style
734    /// (e.g. `"0:45"`, `"9:41"`, `"1:23:45"`, `"1:07:54:30"`).
735    #[cfg(feature = "alloc")]
736    #[inline(always)]
737    pub fn to_str_media_duration(&self) -> String {
738        self.to_str_b_media_duration().to_string()
739    }
740
741    /// Formats the duration using the common media/video player style
742    /// (e.g. `"0:45"`, `"9:41"`, `"1:23:45"`, `"1:07:54:30"`).
743    #[inline(always)]
744    pub fn to_str_b_media_duration(&self) -> BufStr<STRTIME_SIZE> {
745        let (buf, len) = self.format_media_duration();
746        BufStr::from_bytes(&buf[..len])
747    }
748
749    /// Returns a stack buffer + the number of valid bytes written.
750    fn format_media_duration(&self) -> ([u8; 64], usize) {
751        let mut buf = [0u8; 64];
752        let mut pos = 0;
753
754        if self.is_zero() {
755            buf[0] = b'0';
756            buf[1] = b':';
757            buf[2] = b'0';
758            buf[3] = b'0';
759            return (buf, 4);
760        }
761
762        let negative = self.attos < 0;
763        let total = self.to_sec_round().unsigned_abs();
764
765        if negative {
766            buf[pos] = b'-';
767            pos += 1;
768        }
769
770        let days = total / 86400;
771        let rem = total % 86400;
772        let hours = rem / 3600;
773        let rem = rem % 3600;
774        let mins = rem / 60;
775        let sec = rem % 60;
776
777        if days > 0 {
778            pos += write_u128(&mut buf[pos..], days);
779            buf[pos] = b':';
780            pos += 1;
781            pos += write_u128_padded(&mut buf[pos..], hours);
782            buf[pos] = b':';
783            pos += 1;
784            pos += write_u128_padded(&mut buf[pos..], mins);
785            buf[pos] = b':';
786            pos += 1;
787            pos += write_u128_padded(&mut buf[pos..], sec);
788        } else if hours > 0 {
789            pos += write_u128(&mut buf[pos..], hours);
790            buf[pos] = b':';
791            pos += 1;
792            pos += write_u128_padded(&mut buf[pos..], mins);
793            buf[pos] = b':';
794            pos += 1;
795            pos += write_u128_padded(&mut buf[pos..], sec);
796        } else {
797            pos += write_u128(&mut buf[pos..], mins);
798            buf[pos] = b':';
799            pos += 1;
800            pos += write_u128_padded(&mut buf[pos..], sec);
801        }
802
803        (buf, pos)
804    }
805}
806
807/// Write number with no leading zeros. Returns bytes written.
808fn write_u128(buf: &mut [u8], mut n: u128) -> usize {
809    if n == 0 {
810        buf[0] = b'0';
811        return 1;
812    }
813    let mut i = buf.len();
814    while n > 0 {
815        i -= 1;
816        buf[i] = b'0' + (n % 10) as u8;
817        n /= 10;
818    }
819    let len = buf.len() - i;
820    buf.copy_within(i.., 0);
821    len
822}
823
824/// Write number padded to exactly 2 digits (assumes n < 100).
825fn write_u128_padded(buf: &mut [u8], n: u128) -> usize {
826    buf[0] = b'0' + ((n / 10) % 10) as u8;
827    buf[1] = b'0' + (n % 10) as u8;
828    2
829}