Skip to main content

deep_time/dt/
to_str.rs

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