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