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