Skip to main content

deep_time/dt/
to_str.rs

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