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 ///
13 /// - Example: **"PT1H23M45.6789S"**.
14 /// - Requires the "alloc" feature.
15 /// - Does **not** do any time scale conversions prior to output.
16 pub fn to_iso_duration(&self) -> String {
17 if self.is_zero() {
18 return String::from("PT0S");
19 }
20
21 let total = self.to_attos();
22 let negative = total < 0;
23 let mut attos = total.unsigned_abs();
24
25 let mut s = String::with_capacity(48);
26 if negative {
27 s.push('-');
28 }
29 s.push_str("PT");
30
31 const A_PER_S: u128 = ATTOS_PER_SEC as u128;
32 const A_PER_M: u128 = A_PER_S * 60;
33 const A_PER_H: u128 = A_PER_M * 60;
34
35 let hours = attos / A_PER_H;
36 attos %= A_PER_H;
37 let minutes = attos / A_PER_M;
38 attos %= A_PER_M;
39 let seconds = attos / A_PER_S;
40 let frac_attos = attos % A_PER_S;
41
42 if hours > 0 {
43 s.push_str(&alloc::format!("{}", hours));
44 s.push('H');
45 }
46 if minutes > 0 {
47 s.push_str(&alloc::format!("{}", minutes));
48 s.push('M');
49 }
50
51 if seconds > 0 || frac_attos > 0 {
52 s.push_str(&alloc::format!("{}", seconds));
53
54 if frac_attos != 0 {
55 let frac_str = alloc::format!("{frac_attos:018}");
56 let trimmed = frac_str.trim_end_matches('0');
57 s.push('.');
58 s.push_str(trimmed);
59 }
60
61 s.push('S');
62 }
63
64 s
65 }
66
67 /// Formats this [`Dt`] into a String. Requires the `"alloc"` feature.
68 ///
69 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
70 /// time scale before producing the result.
71 ///
72 /// ## Examples
73 ///
74 /// ```rust
75 /// use deep_time::{Dt, Lang, Scale};
76 ///
77 /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
78 /// let s = x.to_str("%F", Lang::En).unwrap();
79 ///
80 /// println!("{}", s);
81 /// ```
82 ///
83 /// ## Errors
84 ///
85 /// Returns [`DtErr`] if the format string contains invalid specifiers
86 /// or if the internal formatting buffer overflows (extremely unlikely
87 /// with [`STRFTIME_SIZE`]).
88 ///
89 /// ## See also
90 ///
91 /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset)
92 /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)
93 #[inline(always)]
94 pub fn to_str(&self, fmt: &str, lang: Lang) -> Result<String, DtErr> {
95 self.to_str_in_offset(fmt, 0, lang)
96 }
97
98 /// Formats this [`Dt`] into a String, applying a fixed offset. Requires the
99 /// `"alloc"` feature.
100 ///
101 /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
102 /// formatting, and the offset is stored so that `%z` / `%:z` format directives
103 /// will reflect it.
104 /// - No IANA timezone name or abbreviation is set.
105 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
106 /// time scale before producing the result.
107 ///
108 /// ## Examples
109 ///
110 /// ```rust
111 /// use deep_time::{Dt, Lang, Scale};
112 ///
113 /// let x = Dt::from_ymd(2000, 1, 1, 0, 0, 0, 0, Scale::UTC);
114 ///
115 /// // offset of minus one hour
116 /// let s = x.to_str_in_offset("%F", -3600, Lang::En).unwrap();
117 ///
118 /// println!("{}", s);
119 /// ```
120 ///
121 /// ## Errors
122 ///
123 /// Returns [`DtErr`] if the format string contains invalid specifiers
124 /// or if the internal formatting buffer overflows (extremely unlikely
125 /// with [`STRFTIME_SIZE`]).
126 ///
127 /// ## See also
128 ///
129 /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
130 /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)
131 #[inline(always)]
132 pub fn to_str_in_offset(&self, fmt: &str, secs: i32, lang: Lang) -> Result<String, DtErr> {
133 self.ymd_with_offset(secs)
134 .to_str(fmt, Some(secs), None, None, lang)
135 }
136
137 /// Formats this [`Dt`] into a string, time adjusted to the given IANA timezone. Requires
138 /// the `"alloc"` feature.
139 ///
140 /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
141 ///
142 /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]'s time for the given
143 /// IANA timezone. This is so that the formatter will have:
144 /// - Accurate wall time for the timezone.
145 /// - Correct numeric offset (for `%z` / `%:z`).
146 /// - Timezone abbreviation (for `%Z`). These **do not** round-trip (the parser
147 /// does not parse them).
148 /// - Full IANA timezone name (for `%Q` / `%:Q`).
149 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
150 /// time scale before producing the result.
151 ///
152 /// ## Examples
153 ///
154 /// You can offset an output that wasn't originally from a zoned input:
155 ///
156 /// ```rust
157 /// # #[cfg(all(feature = "jiff-tz", feature = "parse"))]
158 /// # {
159 /// use deep_time::{Dt, Lang, Scale};
160 ///
161 /// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
162 /// let s = x.to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En).unwrap();
163 /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
164 /// # }
165 /// ```
166 ///
167 /// You can also return to a zoned output from a zoned input:
168 ///
169 /// ```rust
170 /// # #[cfg(all(feature = "jiff-tz", feature = "parse"))]
171 /// # {
172 /// use deep_time::{Dt, Lang, Scale};
173 ///
174 /// let x: Dt = "Saturday, January 01, 2000 07:00:00 America/New_York".parse().unwrap();
175 /// let s = x.to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En).unwrap();
176 /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
177 /// # }
178 /// ```
179 ///
180 /// ## Errors
181 ///
182 /// Returns [`DtErr`] if the format string contains invalid specifiers
183 /// or if the internal formatting buffer overflows (extremely unlikely
184 /// with [`STRFTIME_SIZE`]).
185 ///
186 /// ## See also
187 ///
188 /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
189 /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset)
190 #[inline(always)]
191 pub fn to_str_in_tz(&self, fmt: &str, tz_name: &str, lang: Lang) -> Result<String, DtErr> {
192 let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, true)?;
193 ymd.to_str(
194 fmt,
195 Some(offset),
196 Some(LiteStr::new(tz_name)),
197 Some(abbrev),
198 lang,
199 )
200 }
201
202 /// **RFC 9557** / Temporal format with IANA timezone name in brackets.
203 ///
204 /// - Example: **`"2020-06-15T14:30:00-04:00[America/New_York]"`**.
205 /// - Automatically trims trailing zeros in the fractional part.
206 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
207 /// time scale before producing the result.
208 #[inline(always)]
209 pub fn to_str_rfc9557(&self, tz_name: &str) -> Result<String, DtErr> {
210 self.to_str_in_tz("%Y-%m-%dT%H:%M:%S%.~f%:z[%Q]", tz_name, Lang::En)
211 }
212
213 /// Returns this instant as an **RFC 3339** / ISO 8601 timestamp with a
214 /// `Z` suffix.
215 ///
216 /// - Example: **`"2024-03-14T15:30:45.123Z"`**
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 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
220 /// time scale before producing the result.
221 #[inline(always)]
222 pub fn to_str_rfc3339(&self) -> String {
223 self.to_str_rfc3339_nf(9)
224 }
225
226 /// Same as [`Dt::to_str_rfc3339`](../struct.Dt.html#method.to_str_rfc3339) but
227 /// with a configurable maximum number of fractional digits (0–18). Trailing zeros are
228 /// always trimmed.
229 ///
230 /// - Example: **`"2024-03-14T15:30:45.123Z"`**
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) -> String {
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).unwrap()
242 }
243
244 /// **ISO 8601 / RFC 3339** with **actual offset** (modern `+00:00` style).
245 ///
246 /// - Example: **`"2025-04-16T14:30:45.123+00:00"`**.
247 /// - Uses colon-separated offset (`%:z`) instead of forcing `Z`.
248 /// - Still trims trailing zeros in the fractional part.
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) -> String {
253 self.to_str_in_offset("%Y-%m-%dT%H:%M:%S%.~f%:z", 0, Lang::En)
254 .unwrap()
255 }
256
257 /// **Compact ISO 8601 basic format** (no separators).
258 ///
259 /// - Example: **`"20250416T143045.123456789Z"`**.
260 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
261 /// time scale before producing the result.
262 #[inline(always)]
263 pub fn to_str_iso8601_basic(&self) -> String {
264 self.to_str_in_offset("%Y%m%dT%H%M%S%.~fZ", 0, Lang::En)
265 .unwrap()
266 }
267
268 /// **ISO 8601 week date**.
269 ///
270 /// - Example: **`"2025-W16-3"`**. (year-week-day)
271 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
272 /// time scale before producing the result.
273 #[inline(always)]
274 pub fn to_str_iso_week_date(&self) -> String {
275 self.to_str_in_offset("%G-W%V-%u", 0, Lang::En).unwrap()
276 }
277
278 /// Just the **ISO date** part (no time).
279 ///
280 /// - Example: **`"2025-04-16"`**.
281 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
282 /// time scale before producing the result.
283 #[inline(always)]
284 pub fn to_str_iso_date(&self) -> String {
285 self.to_str_in_offset("%Y-%m-%d", 0, Lang::En).unwrap()
286 }
287
288 /// Just the **time** part with fractional seconds (trimmed).
289 ///
290 /// - Example: **`"14:30:45.123456789"`**.
291 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
292 /// time scale before producing the result.
293 #[inline(always)]
294 pub fn to_str_iso_time(&self) -> String {
295 self.to_str_in_offset("%H:%M:%S%.~f", 0, Lang::En).unwrap()
296 }
297
298 /// **HTTP-date** format (RFC 7231 / RFC 1123) — **always in GMT**.
299 ///
300 /// - Example: **`"Wed, 16 Apr 2025 14:30:45 GMT"`**.
301 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
302 /// time scale before producing the result.
303 #[inline(always)]
304 pub fn to_str_http(&self, lang: Lang) -> String {
305 self.to_str_in_offset("%a, %d %b %Y %H:%M:%S GMT", 0, lang)
306 .unwrap()
307 }
308
309 /// **RFC 2822** date format (used in email `Date` headers).
310 ///
311 /// - Example: **`"Wed, 16 Apr 2025 14:30:45 +0000"`**.
312 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
313 /// time scale before producing the result.
314 #[inline(always)]
315 pub fn to_str_rfc2822(&self, lang: Lang) -> String {
316 self.to_str_in_offset("%a, %d %b %Y %H:%M:%S %z", 0, lang)
317 .unwrap()
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();
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();
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 = "jiff-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();
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 #[cfg(feature = "jiff-tz")]
603 let (offset_secs, abbrev): (i32, LiteStr<49>) = {
604 use jiff::{Timestamp, tz::TimeZone};
605
606 let tz = TimeZone::get(tz_name).map_err(|e| {
607 an_err!(
608 DtErrKind::InvalidTimezoneOffset,
609 "invalid tz {:?}: {}",
610 tz_name,
611 e
612 )
613 })?;
614
615 let unix_sec = self.to_unix().to_sec64();
616
617 let ts = Timestamp::from_second(unix_sec).map_err(|e| {
618 an_err!(
619 DtErrKind::InvalidNumber,
620 "invalid unix {:?} for jiff Timestamp: {}",
621 unix_sec,
622 e
623 )
624 })?;
625
626 let info = tz.to_offset_info(ts);
627 let offset_secs = info.offset().seconds();
628 let abbrev: LiteStr<49> = LiteStr::new(info.abbreviation());
629
630 (offset_secs, abbrev)
631 };
632
633 #[cfg(not(feature = "jiff-tz"))]
634 let (offset_secs, abbrev): (i32, LiteStr<49>) = {
635 if !UTC_ALIASES.contains(&tz_name) {
636 return Err(an_err!(
637 DtErrKind::InvalidBytes,
638 "non-utc tz: {} requires jiff-tz feature",
639 tz_name,
640 ));
641 }
642 // UTC → offset 0, canonical abbrev "UTC"
643 let abbrev: LiteStr<49> = LiteStr::new("UTC");
644 (0i32, abbrev)
645 };
646
647 let ymd = if offset_secs != 0 && apply_offset {
648 self.add_sec(offset_secs as i128).to_ymd()
649 } else {
650 self.to_ymd()
651 };
652
653 Ok((ymd, offset_secs, abbrev))
654 }
655}