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