Skip to main content

deep_time/dt/
to_str.rs

1use crate::Lang;
2use crate::{Dt, DtErr, DtErrKind, LiteStr, STRFTIME_SIZE, YmdHms, an_err, tz::offset_for_utc};
3
4#[cfg(feature = "alloc")]
5use {crate::ATTOS_PER_SEC, alloc::string::String};
6
7#[cfg(feature = "alloc")]
8impl Dt {
9    /// # Time Scale Handling
10    ///
11    /// All formatting methods in this `impl` block (except [`to_iso_duration`])
12    /// convert from the [`Dt`]'s current time `scale` to its `target` scale
13    /// before producing output.
14
15    /// Converts this `Dt` to an ISO 8601 duration string
16    /// (e.g. `"PT1H23M45.6789S"`, `"-PT0.5S"`, `"PT0.000000000000000001S"`, or `"PT0S"`).
17    ///
18    /// - This method is only available when the **`alloc`** feature is enabled.
19    /// - It returns `alloc::string::String` (no_std + alloc compatible).
20    /// - Performs no time scale conversions prior to output.
21    pub fn to_iso_duration(&self) -> String {
22        if self.is_zero() {
23            return String::from("PT0S");
24        }
25
26        let total = self.to_attos();
27        let negative = total < 0;
28        let mut attos = total.unsigned_abs();
29
30        let mut s = String::with_capacity(48);
31        if negative {
32            s.push('-');
33        }
34        s.push_str("PT");
35
36        const A_PER_S: u128 = ATTOS_PER_SEC as u128;
37        const A_PER_M: u128 = A_PER_S * 60;
38        const A_PER_H: u128 = A_PER_M * 60;
39
40        let hours = attos / A_PER_H;
41        attos %= A_PER_H;
42        let minutes = attos / A_PER_M;
43        attos %= A_PER_M;
44        let seconds = attos / A_PER_S;
45        let frac_attos = attos % A_PER_S;
46
47        if hours > 0 {
48            s.push_str(&alloc::format!("{}", hours));
49            s.push('H');
50        }
51        if minutes > 0 {
52            s.push_str(&alloc::format!("{}", minutes));
53            s.push('M');
54        }
55
56        if seconds > 0 || frac_attos > 0 {
57            s.push_str(&alloc::format!("{}", seconds));
58
59            if frac_attos != 0 {
60                let frac_str = alloc::format!("{frac_attos:018}");
61                let trimmed = frac_str.trim_end_matches('0');
62                s.push('.');
63                s.push_str(trimmed);
64            }
65
66            s.push('S');
67        }
68
69        s
70    }
71
72    /// Formats this [`Dt`] into a String. Requires the `"alloc"` feature.
73    ///
74    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
75    ///   time scale before producing the result.
76    ///
77    /// ## Examples
78    ///
79    /// ```rust
80    /// use deep_time::{Dt, Lang, Scale};
81    ///
82    /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
83    /// let s = x.to_str("%F", Lang::En).unwrap();
84    ///
85    /// println!("{}", s);
86    /// ```
87    ///
88    /// ## Errors
89    ///
90    /// Returns [`DtErr`] if the format string contains invalid specifiers
91    /// or if the internal formatting buffer overflows (extremely unlikely
92    /// with [`STRFTIME_SIZE`]).
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 `secs` 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, 0, 0, 0, 0, Scale::UTC);
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    /// or if the internal formatting buffer overflows (extremely unlikely
130    /// with [`STRFTIME_SIZE`]).
131    ///
132    /// ## See also
133    ///
134    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
135    /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)
136    #[inline(always)]
137    pub fn to_str_in_offset(&self, fmt: &str, secs: i32, lang: Lang) -> Result<String, DtErr> {
138        self.ymd_with_offset(secs)
139            .to_str(fmt, Some(secs), None, None, lang)
140    }
141
142    /// Formats this [`Dt`] into a string, time adjusted to the given IANA timezone. Requires
143    /// the `"alloc"` feature.
144    ///
145    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
146    ///
147    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]'s time for the given
148    ///   IANA timezone. This is so that the formatter will have:
149    ///     - Accurate wall time for the timezone.
150    ///     - Correct numeric offset (for `%z` / `%:z`).
151    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip (the parser
152    ///       does not parse them).
153    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
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 = "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 = "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    /// - Automatically trims trailing zeros in the fractional part.
210    /// - Example: `"2020-06-15T14:30:00-04:00[America/New_York]"`
211    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
212    ///   time scale before producing the result.
213    #[inline(always)]
214    pub fn to_str_rfc9557(&self, tz_name: &str) -> Result<String, DtErr> {
215        self.to_str_in_tz("%Y-%m-%dT%H:%M:%S%.~f%:z[%Q]", tz_name, Lang::En)
216    }
217
218    /// Returns this instant as an **RFC 3339** / ISO 8601 timestamp with a
219    /// `Z` suffix.
220    ///
221    /// - Default = 9 digits (nanoseconds) but **automatically trims trailing zeros**.
222    /// - If fractional part is zero → no decimal point at all (e.g. `...45Z`).
223    /// - Example: `"2024-03-14T15:30:45.123Z"`
224    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
225    ///   time scale before producing the result.
226    #[inline(always)]
227    pub fn to_str_rfc3339(&self) -> Result<String, DtErr> {
228        self.to_str_rfc3339_nf(9)
229    }
230
231    /// Same as [`Dt::to_str_rfc3339`](../struct.Dt.html#method.to_str_rfc3339) but
232    /// with a configurable maximum number of fractional digits (0–18). Trailing zeros are
233    /// always trimmed.
234    ///
235    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
236    ///   time scale before producing the result.
237    pub fn to_str_rfc3339_nf(&self, max_precision: usize) -> Result<String, DtErr> {
238        let prec = max_precision.min(18);
239        // Uses the formatter with the `~` "trim trailing zeros" flag.
240        // The formatter already handles:
241        //   - correct 4-digit years (with sign) for |yr| < 10000
242        //   - full-width years otherwise
243        //   - suppressing the decimal point entirely when the trimmed fraction is zero
244        let fmt = alloc::format!("%Y-%m-%dT%H:%M:%S%.{}~fZ", prec);
245        self.to_str_in_offset(&fmt, 0, Lang::En)
246    }
247
248    /// **ISO 8601 / RFC 3339** with **actual offset** (modern `+00:00` style).
249    ///
250    /// - Uses colon-separated offset (`%:z`) instead of forcing `Z`.
251    /// - Still trims trailing zeros in the fractional part.
252    /// - Example: `"2025-04-16T14:30:45.123+00:00"`
253    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
254    ///   time scale before producing the result.
255    #[inline(always)]
256    pub fn to_str_iso8601(&self) -> Result<String, DtErr> {
257        self.to_str_in_offset("%Y-%m-%dT%H:%M:%S%.~f%:z", 0, Lang::En)
258    }
259
260    /// **Compact ISO 8601 basic format** (no separators).
261    ///
262    /// - Example: `"20250416T143045.123456789Z"`
263    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
264    ///   time scale before producing the result.
265    #[inline(always)]
266    pub fn to_str_iso8601_basic(&self) -> Result<String, DtErr> {
267        self.to_str_in_offset("%Y%m%dT%H%M%S%.~fZ", 0, Lang::En)
268    }
269
270    /// **ISO 8601 week date**.
271    ///
272    /// - Example: `"2025-W16-3"` (year-week-day)
273    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
274    ///   time scale before producing the result.
275    #[inline(always)]
276    pub fn to_str_iso_week_date(&self) -> Result<String, DtErr> {
277        self.to_str_in_offset("%G-W%V-%u", 0, Lang::En)
278    }
279
280    /// Just the **ISO date** part (no time).
281    ///
282    /// - Example: `"2025-04-16"`
283    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
284    ///   time scale before producing the result.
285    #[inline(always)]
286    pub fn to_str_iso_date(&self) -> Result<String, DtErr> {
287        self.to_str_in_offset("%Y-%m-%d", 0, Lang::En)
288    }
289
290    /// Just the **time** part with fractional seconds (trimmed).
291    ///
292    /// - Example: `"14:30:45.123456789"`
293    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
294    ///   time scale before producing the result.
295    #[inline(always)]
296    pub fn to_str_iso_time(&self) -> Result<String, DtErr> {
297        self.to_str_in_offset("%H:%M:%S%.~f", 0, Lang::En)
298    }
299
300    /// **HTTP-date** format (RFC 7231 / RFC 1123) — **always in GMT**.
301    ///
302    /// - Example: `"Wed, 16 Apr 2025 14:30:45 GMT"`
303    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
304    ///   time scale before producing the result.
305    #[inline(always)]
306    pub fn to_str_http(&self, lang: Lang) -> Result<String, DtErr> {
307        self.to_str_in_offset("%a, %d %b %Y %H:%M:%S GMT", 0, lang)
308    }
309
310    /// **RFC 2822** date format (used in email `Date` headers).
311    ///
312    /// - Example: `"Wed, 16 Apr 2025 14:30:45 +0000"`
313    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
314    ///   time scale before producing the result.
315    #[inline(always)]
316    pub fn to_str_rfc2822(&self, lang: Lang) -> Result<String, DtErr> {
317        self.to_str_in_offset("%a, %d %b %Y %H:%M:%S %z", 0, lang)
318    }
319
320    /// Formats this [`Dt`] into a `String`, attaching an offset **as a label only**.
321    ///
322    /// - The actual datetime components are **not** shifted or adjusted.
323    /// - The given `offset` is used **only** for `%z` / `%:z` format directives.
324    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
325    ///   time scale before producing the result.
326    ///
327    /// ## Errors
328    ///
329    /// Returns [`DtErr`] if the format string contains invalid specifiers
330    /// or if the internal formatting buffer overflows (extremely unlikely
331    /// with [`STRFTIME_SIZE`]).
332    ///
333    /// ## See also
334    ///
335    /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset) —
336    ///   shifts the datetime by the offset
337    #[inline(always)]
338    pub fn to_str_with_offset_label(
339        &self,
340        fmt: &str,
341        offset: i32,
342        lang: Lang,
343    ) -> Result<String, DtErr> {
344        self.to_ymd().to_str(fmt, Some(offset), None, None, lang)
345    }
346
347    /// Formats this [`Dt`] into a `String`, attaching a timezone **as a label only**.
348    ///
349    /// - The actual datetime components are **not** shifted or adjusted.
350    /// - The timezone is used to provide correct values for `%z`, `%:z`, `%Z`, `%Q`, and `%:Q`.
351    /// - The timezone abbreviation is automatically looked up from tzdata.
352    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
353    ///   time scale before producing the result.
354    ///
355    /// ## Errors
356    ///
357    /// Returns [`DtErr`] if the format string contains invalid specifiers,
358    /// if the timezone name is invalid, or if the internal formatting buffer
359    /// overflows (extremely unlikely with [`STRFTIME_SIZE`]).
360    ///
361    /// ## See also
362    ///
363    /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz) —
364    ///   shifts the datetime into the given timezone
365    #[inline(always)]
366    pub fn to_str_with_tz_label(
367        &self,
368        fmt: &str,
369        tz_name: &str,
370        lang: Lang,
371    ) -> Result<String, DtErr> {
372        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, false)?;
373        ymd.to_str(
374            fmt,
375            Some(offset),
376            Some(LiteStr::new(tz_name)),
377            Some(abbrev),
378            lang,
379        )
380    }
381}
382
383impl Dt {
384    /// Formats this [`Dt`] into a fixed-size binary string.
385    ///
386    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
387    ///   time scale before producing the result.
388    ///
389    /// ## Examples
390    ///
391    /// ```rust
392    /// use deep_time::{Dt, Lang, Scale};
393    ///
394    /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
395    /// let b = x.to_str_lite("%F", Lang::En).unwrap();
396    /// let s = b.as_str().unwrap();
397    ///
398    /// println!("{}", s);
399    /// ```
400    ///
401    /// ## Errors
402    ///
403    /// Returns [`DtErr`] if the format string contains invalid specifiers
404    /// or if the internal formatting buffer overflows (extremely unlikely
405    /// with [`STRFTIME_SIZE`]).
406    ///
407    /// ## See also
408    ///
409    /// - [`Dt::to_str_lite_in_offset`](../struct.Dt.html#method.to_str_lite_in_offset)
410    /// - [`Dt::to_str_lite_in_tz`](../struct.Dt.html#method.to_str_lite_in_tz)
411    #[inline(always)]
412    pub fn to_str_lite(&self, fmt: &str, lang: Lang) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
413        self.to_ymd().to_str_lite(fmt, None, None, None, lang)
414    }
415
416    /// Formats this [`Dt`] into a fixed-size binary string, applying a fixed UTC offset.
417    ///
418    /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
419    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
420    ///   will reflect it.
421    /// - No IANA timezone name or abbreviation is set.
422    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
423    ///   time scale before producing the result.
424    ///
425    /// ## Examples
426    ///
427    /// ```rust
428    /// use deep_time::{Dt, Lang, Scale};
429    ///
430    /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
431    ///
432    /// // offset of minus one hour
433    /// let b = x.to_str_lite_in_offset("%F", -3600, Lang::En).unwrap();
434    /// let s = b.as_str().unwrap();
435    ///
436    /// println!("{}", s);
437    /// ```
438    ///
439    /// ## Errors
440    ///
441    /// Returns [`DtErr`] if the format string contains invalid specifiers
442    /// or if the internal formatting buffer overflows (extremely unlikely
443    /// with [`STRFTIME_SIZE`]).
444    ///
445    /// ## See also
446    ///
447    /// - [`Dt::to_str_lite`](../struct.Dt.html#method.to_str_lite)
448    /// - [`Dt::to_str_lite_in_tz`](../struct.Dt.html#method.to_str_lite_in_tz)
449    #[inline(always)]
450    pub fn to_str_lite_in_offset(
451        &self,
452        fmt: &str,
453        secs: i32,
454        lang: Lang,
455    ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
456        self.ymd_with_offset(secs)
457            .to_str_lite(fmt, Some(secs), None, None, lang)
458    }
459
460    /// Formats this [`Dt`] into a fixed-size binary string, time adjusted to the given
461    /// IANA timezone.
462    ///
463    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
464    ///
465    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]'s time for the given
466    ///   IANA timezone. This is so that the formatter will have:
467    ///     - Accurate wall time for the timezone.
468    ///     - Correct numeric offset (for `%z` / `%:z`).
469    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip.
470    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
471    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
472    ///   time scale before producing the result.
473    ///
474    /// ## Examples
475    ///
476    /// ```rust
477    /// # #[cfg(feature = "tz")]
478    /// # {
479    /// use deep_time::{Dt, Lang, Scale};
480    ///
481    /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
482    ///
483    /// let b = x.to_str_lite_in_tz("%F", "America/New_York", Lang::En).unwrap();
484    /// let s = b.as_str().unwrap();
485    ///
486    /// println!("{}", s);
487    /// # }
488    /// ```
489    ///
490    /// ## Errors
491    ///
492    /// Returns [`DtErr`] if the format string contains invalid specifiers
493    /// or if the internal formatting buffer overflows (extremely unlikely
494    /// with [`STRFTIME_SIZE`]).
495    ///
496    /// ## See also
497    ///
498    /// - [`Dt::to_str_lite`](../struct.Dt.html#method.to_str_lite)
499    /// - [`Dt::to_str_lite_in_offset`](../struct.Dt.html#method.to_str_lite_in_offset)
500    #[inline(always)]
501    pub fn to_str_lite_in_tz(
502        &self,
503        fmt: &str,
504        tz_name: &str,
505        lang: Lang,
506    ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
507        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, true)?;
508        ymd.to_str_lite(
509            fmt,
510            Some(offset),
511            Some(LiteStr::new(tz_name)),
512            Some(abbrev),
513            lang,
514        )
515    }
516
517    /// Formats this [`Dt`] into a `LiteStr`, attaching an offset **as a label only**.
518    ///
519    /// - The actual datetime components are **not** shifted or adjusted.
520    /// - The given `offset` is used **only** for `%z` / `%:z` format directives.
521    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
522    ///   time scale before producing the result.
523    ///
524    /// ## Errors
525    ///
526    /// Returns [`DtErr`] if the format string contains invalid specifiers
527    /// or if the internal formatting buffer overflows (extremely unlikely
528    /// with [`STRFTIME_SIZE`]).
529    ///
530    /// ## See also
531    ///
532    /// - [`Dt::to_str_lite_in_offset`](../struct.Dt.html#method.to_str_lite_in_offset) —
533    ///   shifts the datetime by the offset
534    #[inline(always)]
535    pub fn to_str_lite_with_offset_label(
536        &self,
537        fmt: &str,
538        offset: i32,
539        lang: Lang,
540    ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
541        self.to_ymd()
542            .to_str_lite(fmt, Some(offset), None, None, lang)
543    }
544
545    /// Formats this [`Dt`] into a `LiteStr`, attaching a timezone **as a label only**.
546    ///
547    /// - The actual datetime components are **not** shifted or adjusted.
548    /// - The timezone is used to provide correct values for `%z`, `%:z`, `%Z`, `%Q`, and `%:Q`.
549    /// - The timezone abbreviation is automatically looked up from tzdata.
550    /// - Converts from this [`Dt`]'s current time `scale` to its `target`
551    ///   time scale before producing the result.
552    ///
553    /// ## Errors
554    ///
555    /// Returns [`DtErr`] if the format string contains invalid specifiers,
556    /// if the timezone name is invalid, or if the internal formatting buffer
557    /// overflows (extremely unlikely with [`STRFTIME_SIZE`]).
558    ///
559    /// ## See also
560    ///
561    /// - [`Dt::to_str_lite_in_tz`](../struct.Dt.html#method.to_str_lite_in_tz) —
562    ///   shifts the datetime into the given timezone
563    #[inline(always)]
564    pub fn to_str_lite_with_tz_label(
565        &self,
566        fmt: &str,
567        tz_name: &str,
568        lang: Lang,
569    ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
570        let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, false)?;
571        ymd.to_str_lite(
572            fmt,
573            Some(offset),
574            Some(LiteStr::new(tz_name)),
575            Some(abbrev),
576            lang,
577        )
578    }
579
580    /// Returns `(is_negative, hours, minutes)`.
581    #[inline]
582    pub(crate) const fn sec_as_hhmm(seconds: i32) -> (bool, u8, u8) {
583        let total = seconds.saturating_abs();
584        let hours = (total / 3600) as u8;
585        let minutes = ((total % 3600) / 60) as u8;
586        (seconds < 0, hours, minutes)
587    }
588
589    pub(crate) fn ymd_with_offset(&self, secs: i32) -> YmdHms {
590        if secs != 0 {
591            self.add_sec(secs as i128).to_ymd()
592        } else {
593            self.to_ymd()
594        }
595    }
596
597    pub(crate) fn ymd_with_tz(
598        &self,
599        tz_name: &str,
600        apply_offset: bool,
601    ) -> Result<(YmdHms, i32, LiteStr<49>), DtErr> {
602        // Look up offset + abbrev at that exact UTC instant
603        let unix_sec = self.to_unix().to_sec64();
604        let (offset_secs, abbrev) = match offset_for_utc(tz_name, unix_sec) {
605            Some(info) => (info.offset, info.abbrev),
606            None => return Err(an_err!(DtErrKind::InvalidTimezoneOffset, "{}", tz_name)),
607        };
608        let ab = LiteStr::new(abbrev);
609        let ymd = if offset_secs != 0 && apply_offset {
610            self.add_sec(offset_secs as i128).to_ymd()
611        } else {
612            self.to_ymd()
613        };
614
615        Ok((ymd, offset_secs, ab))
616    }
617}