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