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}