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}