deep_time/dt/to_str.rs
1use crate::{Dt, DtErr, DtErrKind, Lang, LiteStr, STRTIME_SIZE, YmdHms, an_err};
2
3#[cfg(feature = "alloc")]
4use {
5 crate::ATTOS_PER_SEC_U128,
6 alloc::string::{String, ToString},
7};
8
9#[cfg(not(feature = "jiff-tz"))]
10use crate::tz::UTC_ALIASES;
11
12#[allow(clippy::unwrap_used)]
13#[cfg(feature = "alloc")]
14impl Dt {
15 /// Converts this `Dt` to an ISO 8601 duration string.
16 ///
17 /// - Example: **"PT1H23M45.6789S"**.
18 /// - Requires the `alloc` feature.
19 /// - Does **not** do any time scale conversions prior to output.
20 pub fn to_iso_duration(&self) -> String {
21 if self.is_zero() {
22 return String::from("PT0S");
23 }
24
25 let total = self.to_attos();
26 let negative = total < 0;
27 let mut attos = total.unsigned_abs();
28
29 let mut s = String::with_capacity(48);
30 if negative {
31 s.push('-');
32 }
33 s.push_str("PT");
34
35 const A_PER_M: u128 = ATTOS_PER_SEC_U128 * 60;
36 const A_PER_H: u128 = A_PER_M * 60;
37
38 let hours = attos / A_PER_H;
39 attos %= A_PER_H;
40 let minutes = attos / A_PER_M;
41 attos %= A_PER_M;
42 let seconds = attos / ATTOS_PER_SEC_U128;
43 let frac_attos = attos % ATTOS_PER_SEC_U128;
44
45 if hours > 0 {
46 s.push_str(&alloc::format!("{}", hours));
47 s.push('H');
48 }
49 if minutes > 0 {
50 s.push_str(&alloc::format!("{}", minutes));
51 s.push('M');
52 }
53
54 if seconds > 0 || frac_attos > 0 {
55 s.push_str(&alloc::format!("{}", seconds));
56
57 if frac_attos != 0 {
58 let frac_str = alloc::format!("{frac_attos:018}");
59 let trimmed = frac_str.trim_end_matches('0');
60 s.push('.');
61 s.push_str(trimmed);
62 }
63
64 s.push('S');
65 }
66
67 s
68 }
69
70 /// Formats this [`Dt`] into a String. Requires the `alloc` feature.
71 ///
72 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
73 /// time scale before producing the result.
74 ///
75 /// ## Examples
76 ///
77 /// ```rust
78 /// use deep_time::{Dt, Lang, Scale};
79 ///
80 /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
81 /// let s = x.to_str("%F", Lang::En).unwrap();
82 ///
83 /// println!("{}", s);
84 /// ```
85 ///
86 /// ## Errors
87 ///
88 /// Returns [`DtErr`] if the format string contains invalid specifiers
89 /// or if the internal formatting buffer overflows (extremely unlikely
90 /// with [`STRTIME_SIZE`]).
91 ///
92 /// ## See also
93 ///
94 /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset)
95 /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)
96 #[inline(always)]
97 pub fn to_str(&self, fmt: &str, lang: Lang) -> Result<String, DtErr> {
98 self.to_str_in_offset(fmt, 0, lang)
99 }
100
101 /// Formats this [`Dt`] into a String, applying a fixed offset. Requires the
102 /// `"alloc"` feature.
103 ///
104 /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
105 /// formatting, and the offset is stored so that `%z` / `%:z` format directives
106 /// will reflect it.
107 /// - No IANA timezone name or abbreviation is set.
108 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
109 /// time scale before producing the result.
110 ///
111 /// ## Examples
112 ///
113 /// ```rust
114 /// use deep_time::{Dt, Lang, Scale};
115 ///
116 /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
117 ///
118 /// // offset of minus one hour
119 /// let s = x.to_str_in_offset("%F", -3600, Lang::En).unwrap();
120 ///
121 /// println!("{}", s);
122 /// ```
123 ///
124 /// ## Errors
125 ///
126 /// Returns [`DtErr`] if the format string contains invalid specifiers
127 /// or if the internal formatting buffer overflows (extremely unlikely
128 /// with [`STRTIME_SIZE`]).
129 ///
130 /// ## See also
131 ///
132 /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
133 /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz)
134 #[inline(always)]
135 pub fn to_str_in_offset(&self, fmt: &str, secs: i32, lang: Lang) -> Result<String, DtErr> {
136 self.ymd_with_offset(secs)
137 .to_str(fmt, Some(secs), None, None, lang)
138 }
139
140 /// Formats this [`Dt`] into a string, time adjusted to the given IANA timezone.
141 ///
142 /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
143 ///
144 /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]'s time for the given
145 /// IANA timezone. This is so that the formatter will have:
146 /// - Accurate wall time for the timezone.
147 /// - Correct numeric offset (for `%z` / `%:z`).
148 /// - Timezone abbreviation (for `%Z`). These **do not** round-trip (the parser
149 /// does not parse them).
150 /// - Full IANA timezone name (for `%Q` / `%:Q`).
151 /// - Converts to the provided timezone, if your [`Dt`] is already in
152 /// the timezone then use the label function instead:
153 /// [`Dt::to_str_with_tz_label`](../struct.Dt.html#method.to_str_with_tz_label).
154 /// This is unlikely to be case because when a date with a timezone is parsed
155 /// the returned [`Dt`] is not in local time. But, label only functions are
156 /// provided just in case anyway.
157 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
158 /// time scale before producing the result.
159 /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
160 ///
161 /// ## Examples
162 ///
163 /// You can offset an output that wasn't originally from a zoned input:
164 ///
165 /// ```rust
166 /// # #[cfg(all(feature = "jiff-tz", feature = "parse"))]
167 /// # {
168 /// use deep_time::{Dt, Lang, Scale};
169 ///
170 /// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
171 /// let s = x.to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En).unwrap();
172 /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
173 /// # }
174 /// ```
175 ///
176 /// You can also return to a zoned output from a zoned input:
177 ///
178 /// ```rust
179 /// # #[cfg(all(feature = "jiff-tz", feature = "parse"))]
180 /// # {
181 /// use deep_time::{Dt, Lang, Scale};
182 ///
183 /// let x: Dt = "Saturday, January 01, 2000 07:00:00 America/New_York".parse().unwrap();
184 /// let s = x.to_str_in_tz("%A, %B %d, %Y %H:%M:%S %Q", "America/New_York", Lang::En).unwrap();
185 /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
186 /// # }
187 /// ```
188 ///
189 /// ## Errors
190 ///
191 /// Returns [`DtErr`] if the format string contains invalid specifiers
192 /// or if the internal formatting buffer overflows (extremely unlikely
193 /// with [`STRTIME_SIZE`]).
194 ///
195 /// ## See also
196 ///
197 /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
198 /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset)
199 #[inline(always)]
200 pub fn to_str_in_tz(&self, fmt: &str, tz_name: &str, lang: Lang) -> Result<String, DtErr> {
201 let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, true)?;
202 ymd.to_str(
203 fmt,
204 Some(offset),
205 Some(LiteStr::new(tz_name)),
206 Some(abbrev),
207 lang,
208 )
209 }
210
211 /// **RFC 9557** / Temporal format with IANA timezone name in brackets.
212 ///
213 /// - Example: **`"2020-06-15T14:30:00-04:00[America/New_York]"`**.
214 /// - Converts to the provided timezone, if your [`Dt`] is already in
215 /// the timezone then use the label function instead:
216 /// [`Dt::to_str_with_tz_label`](../struct.Dt.html#method.to_str_with_tz_label).
217 /// This is unlikely to be case because when a date with a timezone is parsed
218 /// the returned [`Dt`] is not in local time. But, label only functions are
219 /// provided just in case anyway.
220 /// - Automatically trims trailing zeros in the fractional part.
221 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
222 /// time scale before producing the result.
223 #[inline(always)]
224 pub fn to_str_rfc9557(&self, tz_name: &str) -> Result<String, DtErr> {
225 self.to_str_in_tz("%Y-%m-%dT%H:%M:%S%.~f%:z[%Q]", tz_name, Lang::En)
226 }
227
228 /// Returns this instant as an **RFC 3339** / ISO 8601 timestamp with a
229 /// `Z` suffix.
230 ///
231 /// - Example: **`"2024-03-14T15:30:45.123Z"`**
232 /// - Default = 9 digits (nanoseconds) but **automatically trims trailing zeros**.
233 /// - If fractional part is zero → no decimal point at all (e.g. `...45Z`).
234 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
235 /// time scale before producing the result.
236 #[inline(always)]
237 pub fn to_str_rfc3339(&self) -> String {
238 self.to_str_rfc3339_nf(9)
239 }
240
241 /// Same as [`Dt::to_str_rfc3339`](../struct.Dt.html#method.to_str_rfc3339) but
242 /// with a configurable maximum number of fractional digits (0–18). Trailing zeros are
243 /// always trimmed.
244 ///
245 /// - Example: **`"2024-03-14T15:30:45.123Z"`**
246 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
247 /// time scale before producing the result.
248 pub fn to_str_rfc3339_nf(&self, max_precision: usize) -> String {
249 let prec = max_precision.min(18);
250 // Uses the formatter with the `~` "trim trailing zeros" flag.
251 // The formatter already handles:
252 // - correct 4-digit years (with sign) for |yr| < 10000
253 // - full-width years otherwise
254 // - suppressing the decimal point entirely when the trimmed fraction is zero
255 let fmt = alloc::format!("%Y-%m-%dT%H:%M:%S%.{}~fZ", prec);
256 self.to_str_in_offset(&fmt, 0, Lang::En).unwrap()
257 }
258
259 /// **ISO 8601 / RFC 3339** with **actual offset** (modern `+00:00` style).
260 ///
261 /// - Example: **`"2025-04-16T14:30:45.123+00:00"`**.
262 /// - Uses colon-separated offset (`%:z`) instead of forcing `Z`.
263 /// - Still trims trailing zeros in the fractional part.
264 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
265 /// time scale before producing the result.
266 #[inline(always)]
267 pub fn to_str_iso8601(&self) -> String {
268 self.to_str_in_offset("%Y-%m-%dT%H:%M:%S%.~f%:z", 0, Lang::En)
269 .unwrap()
270 }
271
272 /// **Compact ISO 8601 basic format** (no separators).
273 ///
274 /// - Example: **`"20250416T143045.123456789Z"`**.
275 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
276 /// time scale before producing the result.
277 #[inline(always)]
278 pub fn to_str_iso8601_basic(&self) -> String {
279 self.to_str_in_offset("%Y%m%dT%H%M%S%.~fZ", 0, Lang::En)
280 .unwrap()
281 }
282
283 /// **ISO 8601 week date**.
284 ///
285 /// - Example: **`"2025-W16-3"`**. (year-week-day)
286 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
287 /// time scale before producing the result.
288 #[inline(always)]
289 pub fn to_str_iso_week_date(&self) -> String {
290 self.to_str_in_offset("%G-W%V-%u", 0, Lang::En).unwrap()
291 }
292
293 /// Just the **ISO date** part (no time).
294 ///
295 /// - Example: **`"2025-04-16"`**.
296 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
297 /// time scale before producing the result.
298 #[inline(always)]
299 pub fn to_str_iso_date(&self) -> String {
300 self.to_str_in_offset("%Y-%m-%d", 0, Lang::En).unwrap()
301 }
302
303 /// Just the **time** part with fractional seconds (trimmed).
304 ///
305 /// - Example: **`"14:30:45.123456789"`**.
306 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
307 /// time scale before producing the result.
308 #[inline(always)]
309 pub fn to_str_iso_time(&self) -> String {
310 self.to_str_in_offset("%H:%M:%S%.~f", 0, Lang::En).unwrap()
311 }
312
313 /// **HTTP-date** format (RFC 7231 / RFC 1123) — **always in GMT**.
314 ///
315 /// - Example: **`"Wed, 16 Apr 2025 14:30:45 GMT"`**.
316 /// - Always outputs in GMT (equivalent to UTC+00:00). Does not apply
317 /// regional DST rules.
318 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
319 /// time scale before producing the result.
320 #[inline(always)]
321 pub fn to_str_http(&self, lang: Lang) -> String {
322 self.to_str_in_offset("%a, %d %b %Y %H:%M:%S GMT", 0, lang)
323 .unwrap()
324 }
325
326 /// **RFC 2822** date format (used in email `Date` headers).
327 ///
328 /// - Example: **`"Wed, 16 Apr 2025 14:30:45 +0000"`**.
329 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
330 /// time scale before producing the result.
331 #[inline(always)]
332 pub fn to_str_rfc2822(&self, lang: Lang) -> String {
333 self.to_str_in_offset("%a, %d %b %Y %H:%M:%S %z", 0, lang)
334 .unwrap()
335 }
336
337 /// Formats this [`Dt`] into a `String`, attaching an offset **as a label only**.
338 ///
339 /// - The actual datetime components are **not** shifted or adjusted.
340 /// - The given `offset` is used **only** for `%z` / `%:z` format directives.
341 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
342 /// time scale before producing the result.
343 ///
344 /// ## Errors
345 ///
346 /// Returns [`DtErr`] if the format string contains invalid specifiers
347 /// or if the internal formatting buffer overflows (extremely unlikely
348 /// with [`STRTIME_SIZE`]).
349 ///
350 /// ## See also
351 ///
352 /// - [`Dt::to_str_in_offset`](../struct.Dt.html#method.to_str_in_offset) —
353 /// shifts the datetime by the offset
354 #[inline(always)]
355 pub fn to_str_with_offset_label(
356 &self,
357 fmt: &str,
358 offset: i32,
359 lang: Lang,
360 ) -> Result<String, DtErr> {
361 self.to_ymd().to_str(fmt, Some(offset), None, None, lang)
362 }
363
364 /// Formats this [`Dt`] into a `String`, attaching a timezone **as a label only**.
365 ///
366 /// - The actual datetime components are **not** shifted or adjusted.
367 /// - The timezone is used to provide correct values for `%z`, `%:z`, `%Z`, `%Q`, and `%:Q`.
368 /// - The timezone abbreviation is automatically looked up from tzdata.
369 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
370 /// time scale before producing the result.
371 /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
372 ///
373 /// ## Errors
374 ///
375 /// Returns [`DtErr`] if the format string contains invalid specifiers,
376 /// if the timezone name is invalid, or if the internal formatting buffer
377 /// overflows (extremely unlikely with [`STRTIME_SIZE`]).
378 ///
379 /// ## See also
380 ///
381 /// - [`Dt::to_str_in_tz`](../struct.Dt.html#method.to_str_in_tz) —
382 /// shifts the datetime into the given timezone
383 #[inline(always)]
384 pub fn to_str_with_tz_label(
385 &self,
386 fmt: &str,
387 tz_name: &str,
388 lang: Lang,
389 ) -> Result<String, DtErr> {
390 let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, false)?;
391 ymd.to_str(
392 fmt,
393 Some(offset),
394 Some(LiteStr::new(tz_name)),
395 Some(abbrev),
396 lang,
397 )
398 }
399}
400
401impl Dt {
402 /// Formats this [`Dt`] into a fixed-size binary string.
403 ///
404 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
405 /// time scale before producing the result.
406 ///
407 /// ## Examples
408 ///
409 /// ```rust
410 /// use deep_time::{Dt, Lang, Scale};
411 ///
412 /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
413 /// let b = x.to_str_lite("%F", Lang::En).unwrap();
414 /// let s = b.as_str();
415 ///
416 /// println!("{}", s);
417 /// ```
418 ///
419 /// ## Errors
420 ///
421 /// Returns [`DtErr`] if the format string contains invalid specifiers
422 /// or if the internal formatting buffer overflows (extremely unlikely
423 /// with [`STRTIME_SIZE`]).
424 ///
425 /// ## See also
426 ///
427 /// - [`Dt::to_str_lite_in_offset`](../struct.Dt.html#method.to_str_lite_in_offset)
428 /// - [`Dt::to_str_lite_in_tz`](../struct.Dt.html#method.to_str_lite_in_tz)
429 #[inline(always)]
430 pub fn to_str_lite(&self, fmt: &str, lang: Lang) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
431 self.to_ymd().to_str_lite(fmt, None, None, None, lang)
432 }
433
434 /// Formats this [`Dt`] into a fixed-size binary string, applying a fixed UTC offset.
435 ///
436 /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
437 /// formatting, and the offset is stored so that `%z` / `%:z` format directives
438 /// will reflect it.
439 /// - No IANA timezone name or abbreviation is set.
440 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
441 /// time scale before producing the result.
442 ///
443 /// ## Examples
444 ///
445 /// ```rust
446 /// use deep_time::{Dt, Lang, Scale};
447 ///
448 /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
449 ///
450 /// // offset of minus one hour
451 /// let b = x.to_str_lite_in_offset("%F", -3600, Lang::En).unwrap();
452 /// let s = b.as_str();
453 ///
454 /// println!("{}", s);
455 /// ```
456 ///
457 /// ## Errors
458 ///
459 /// Returns [`DtErr`] if the format string contains invalid specifiers
460 /// or if the internal formatting buffer overflows (extremely unlikely
461 /// with [`STRTIME_SIZE`]).
462 ///
463 /// ## See also
464 ///
465 /// - [`Dt::to_str_lite`](../struct.Dt.html#method.to_str_lite)
466 /// - [`Dt::to_str_lite_in_tz`](../struct.Dt.html#method.to_str_lite_in_tz)
467 #[inline(always)]
468 pub fn to_str_lite_in_offset(
469 &self,
470 fmt: &str,
471 secs: i32,
472 lang: Lang,
473 ) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
474 self.ymd_with_offset(secs)
475 .to_str_lite(fmt, Some(secs), None, None, lang)
476 }
477
478 /// Formats this [`Dt`] into a fixed-size binary string, time adjusted to the given
479 /// IANA timezone.
480 ///
481 /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
482 ///
483 /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]'s time for the given
484 /// IANA timezone. This is so that the formatter will have:
485 /// - Accurate wall time for the timezone.
486 /// - Correct numeric offset (for `%z` / `%:z`).
487 /// - Timezone abbreviation (for `%Z`). These **do not** round-trip.
488 /// - Full IANA timezone name (for `%Q` / `%:Q`).
489 /// - Converts to the provided timezone, if your [`Dt`] is already in
490 /// the timezone then use the label function instead:
491 /// [`Dt::to_str_lite_with_tz_label`](../struct.Dt.html#method.to_str_lite_with_tz_label).
492 /// This is unlikely to be case because when a date with a timezone is parsed
493 /// the returned [`Dt`] is not in local time. But, label only functions are
494 /// provided just in case anyway.
495 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
496 /// time scale before producing the result.
497 /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
498 ///
499 /// ## Examples
500 ///
501 /// ```rust
502 /// # #[cfg(feature = "jiff-tz")]
503 /// # {
504 /// use deep_time::{Dt, Lang, Scale};
505 ///
506 /// let x = Dt::from_ymd(2000, 1, 1, Scale::UTC, 0, 0, 0, 0);
507 ///
508 /// let b = x.to_str_lite_in_tz("%F", "America/New_York", Lang::En).unwrap();
509 /// let s = b.as_str();
510 ///
511 /// println!("{}", s);
512 /// # }
513 /// ```
514 ///
515 /// ## Errors
516 ///
517 /// Returns [`DtErr`] if the format string contains invalid specifiers
518 /// or if the internal formatting buffer overflows (extremely unlikely
519 /// with [`STRTIME_SIZE`]).
520 ///
521 /// ## See also
522 ///
523 /// - [`Dt::to_str_lite`](../struct.Dt.html#method.to_str_lite)
524 /// - [`Dt::to_str_lite_in_offset`](../struct.Dt.html#method.to_str_lite_in_offset)
525 /// - [`Dt::to_str_lite_with_tz_label`](../struct.Dt.html#method.to_str_lite_with_tz_label)
526 #[inline(always)]
527 pub fn to_str_lite_in_tz(
528 &self,
529 fmt: &str,
530 tz_name: &str,
531 lang: Lang,
532 ) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
533 let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, true)?;
534 ymd.to_str_lite(
535 fmt,
536 Some(offset),
537 Some(LiteStr::new(tz_name)),
538 Some(abbrev),
539 lang,
540 )
541 }
542
543 /// Formats this [`Dt`] into a `LiteStr`, attaching an offset **as a label only**.
544 ///
545 /// - The actual datetime components are **not** shifted or adjusted.
546 /// - The given `offset` is used **only** for `%z` / `%:z` format directives.
547 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
548 /// time scale before producing the result.
549 ///
550 /// ## Errors
551 ///
552 /// Returns [`DtErr`] if the format string contains invalid specifiers
553 /// or if the internal formatting buffer overflows (extremely unlikely
554 /// with [`STRTIME_SIZE`]).
555 ///
556 /// ## See also
557 ///
558 /// - [`Dt::to_str_lite_in_offset`](../struct.Dt.html#method.to_str_lite_in_offset) —
559 /// shifts the datetime by the offset
560 #[inline(always)]
561 pub fn to_str_lite_with_offset_label(
562 &self,
563 fmt: &str,
564 offset: i32,
565 lang: Lang,
566 ) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
567 self.to_ymd()
568 .to_str_lite(fmt, Some(offset), None, None, lang)
569 }
570
571 /// Formats this [`Dt`] into a `LiteStr`, attaching a timezone **as a label only**.
572 ///
573 /// - The actual datetime components are **not** shifted or adjusted.
574 /// - The timezone is used to provide correct values for `%z`, `%:z`, `%Z`, `%Q`, and `%:Q`.
575 /// - The timezone abbreviation is automatically looked up from tzdata.
576 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
577 /// time scale before producing the result.
578 /// - Requires the `jiff-tz` feature for timezone names other than UTC aliases.
579 ///
580 /// ## Errors
581 ///
582 /// Returns [`DtErr`] if the format string contains invalid specifiers,
583 /// if the timezone name is invalid, or if the internal formatting buffer
584 /// overflows (extremely unlikely with [`STRTIME_SIZE`]).
585 ///
586 /// ## See also
587 ///
588 /// - [`Dt::to_str_lite_in_tz`](../struct.Dt.html#method.to_str_lite_in_tz) —
589 /// shifts the datetime into the given timezone
590 #[inline(always)]
591 pub fn to_str_lite_with_tz_label(
592 &self,
593 fmt: &str,
594 tz_name: &str,
595 lang: Lang,
596 ) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
597 let (ymd, offset, abbrev) = self.ymd_with_tz(tz_name, false)?;
598 ymd.to_str_lite(
599 fmt,
600 Some(offset),
601 Some(LiteStr::new(tz_name)),
602 Some(abbrev),
603 lang,
604 )
605 }
606
607 /// **ISO 8601 / RFC 3339** with **actual offset** (modern `+00:00` style)
608 /// as a fixed size no-alloc binary string.
609 ///
610 /// - Example: **`"2025-04-16T14:30:45.123+00:00"`**.
611 /// - Uses colon-separated offset (`%:z`) instead of forcing `Z`.
612 /// - Trims trailing zeros in the fractional part.
613 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
614 /// time scale before producing the result.
615 #[inline(always)]
616 pub fn to_str_lite_iso8601(&self) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
617 self.to_str_lite_in_offset("%Y-%m-%dT%H:%M:%S%.~f%:z", 0, Lang::En)
618 }
619
620 /// **RFC 9557** / Temporal format with IANA timezone name in brackets
621 /// as a fixed size no-alloc binary string.
622 ///
623 /// - Example: **`"2020-06-15T14:30:00-04:00[America/New_York]"`**.
624 /// - Automatically trims trailing zeros in the fractional part.
625 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
626 /// time scale before producing the result.
627 #[inline(always)]
628 pub fn to_str_lite_rfc9557(&self, tz_name: &str) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
629 self.to_str_lite_in_tz("%Y-%m-%dT%H:%M:%S%.~f%:z[%Q]", tz_name, Lang::En)
630 }
631
632 /// **HTTP-date** format (RFC 7231 / RFC 1123) — **always in GMT**
633 /// as a fixed size no-alloc binary string.
634 ///
635 /// - Example: **`"Wed, 16 Apr 2025 14:30:45 GMT"`**.
636 /// - Always outputs in GMT (equivalent to UTC+00:00). Does not apply
637 /// regional DST rules.
638 /// - Converts from this [`Dt`]'s current time `scale` to its `target`
639 /// time scale before producing the result.
640 #[inline(always)]
641 pub fn to_str_lite_http(&self, lang: Lang) -> Result<LiteStr<STRTIME_SIZE>, DtErr> {
642 self.to_str_lite_in_offset("%a, %d %b %Y %H:%M:%S GMT", 0, lang)
643 }
644
645 /// Returns `(is_negative, hours, minutes)`.
646 #[inline]
647 pub(crate) const fn sec_as_hhmm(seconds: i32) -> (bool, u8, u8) {
648 let total = seconds.saturating_abs();
649 let hours = (total / 3600) as u8;
650 let minutes = ((total % 3600) / 60) as u8;
651 (seconds < 0, hours, minutes)
652 }
653
654 #[inline(always)]
655 pub(crate) fn ymd_with_offset(&self, secs: i32) -> YmdHms {
656 if secs != 0 {
657 self.add_sec(secs as i128).to_ymd()
658 } else {
659 self.to_ymd()
660 }
661 }
662
663 pub(crate) fn ymd_with_tz(
664 &self,
665 tz_name: &str,
666 apply_offset: bool,
667 ) -> Result<(YmdHms, i32, LiteStr<49>), DtErr> {
668 #[cfg(feature = "jiff-tz")]
669 let (offset_secs, abbrev): (i32, LiteStr<49>) = {
670 use jiff::{Timestamp, tz::TimeZone};
671
672 let tz = TimeZone::get(tz_name).map_err(|e| {
673 an_err!(
674 DtErrKind::InvalidTimezoneOffset,
675 "invalid tz {:?}: {}",
676 tz_name,
677 e
678 )
679 })?;
680
681 let unix_sec = self.to_unix().to_sec64();
682
683 let ts = Timestamp::from_second(unix_sec).map_err(|e| {
684 an_err!(
685 DtErrKind::InvalidNumber,
686 "invalid unix {:?} for jiff Timestamp: {}",
687 unix_sec,
688 e
689 )
690 })?;
691
692 let info = tz.to_offset_info(ts);
693 let offset_secs = info.offset().seconds();
694 let abbrev: LiteStr<49> = LiteStr::new(info.abbreviation());
695
696 (offset_secs, abbrev)
697 };
698
699 #[cfg(not(feature = "jiff-tz"))]
700 let (offset_secs, abbrev): (i32, LiteStr<49>) = {
701 if !UTC_ALIASES.contains(&tz_name) {
702 return Err(an_err!(
703 DtErrKind::InvalidBytes,
704 "non-utc tz: {} requires jiff-tz feature",
705 tz_name,
706 ));
707 }
708 // UTC → offset 0, canonical abbrev "UTC"
709 let abbrev: LiteStr<49> = LiteStr::new("UTC");
710 (0i32, abbrev)
711 };
712
713 let ymd = if offset_secs != 0 && apply_offset {
714 self.add_sec(offset_secs as i128).to_ymd()
715 } else {
716 self.to_ymd()
717 };
718
719 Ok((ymd, offset_secs, abbrev))
720 }
721}
722
723impl Dt {
724 /// Formats the duration using the common media/video player style
725 /// (e.g. `"0:45"`, `"9:41"`, `"1:23:45"`, `"1:07:54:30"`).
726 #[cfg(feature = "alloc")]
727 #[inline(always)]
728 pub fn to_str_media_duration(&self) -> String {
729 self.to_str_lite_media_duration().to_string()
730 }
731
732 /// Same as [`to_media_duration`](Self::to_media_duration) but returns a
733 /// stack-allocated [`LiteStr`].
734 #[inline(always)]
735 pub fn to_str_lite_media_duration(&self) -> LiteStr<STRTIME_SIZE> {
736 let (buf, len) = self.format_media_duration();
737 LiteStr::from_bytes(&buf[..len])
738 }
739
740 /// Returns a stack buffer + the number of valid bytes written.
741 fn format_media_duration(&self) -> ([u8; 64], usize) {
742 let mut buf = [0u8; 64];
743 let mut pos = 0;
744
745 if self.is_zero() {
746 buf[0] = b'0';
747 buf[1] = b':';
748 buf[2] = b'0';
749 buf[3] = b'0';
750 return (buf, 4);
751 }
752
753 let negative = self.attos < 0;
754 let total = self.to_sec_rounded().unsigned_abs();
755
756 if negative {
757 buf[pos] = b'-';
758 pos += 1;
759 }
760
761 let days = total / 86400;
762 let rem = total % 86400;
763 let hours = rem / 3600;
764 let rem = rem % 3600;
765 let mins = rem / 60;
766 let secs = rem % 60;
767
768 if days > 0 {
769 pos += write_u128(&mut buf[pos..], days);
770 buf[pos] = b':';
771 pos += 1;
772 pos += write_u128_padded(&mut buf[pos..], hours);
773 buf[pos] = b':';
774 pos += 1;
775 pos += write_u128_padded(&mut buf[pos..], mins);
776 buf[pos] = b':';
777 pos += 1;
778 pos += write_u128_padded(&mut buf[pos..], secs);
779 } else if hours > 0 {
780 pos += write_u128(&mut buf[pos..], hours);
781 buf[pos] = b':';
782 pos += 1;
783 pos += write_u128_padded(&mut buf[pos..], mins);
784 buf[pos] = b':';
785 pos += 1;
786 pos += write_u128_padded(&mut buf[pos..], secs);
787 } else {
788 pos += write_u128(&mut buf[pos..], mins);
789 buf[pos] = b':';
790 pos += 1;
791 pos += write_u128_padded(&mut buf[pos..], secs);
792 }
793
794 (buf, pos)
795 }
796}
797
798/// Write number with no leading zeros. Returns bytes written.
799fn write_u128(buf: &mut [u8], mut n: u128) -> usize {
800 if n == 0 {
801 buf[0] = b'0';
802 return 1;
803 }
804 let mut i = buf.len();
805 while n > 0 {
806 i -= 1;
807 buf[i] = b'0' + (n % 10) as u8;
808 n /= 10;
809 }
810 let len = buf.len() - i;
811 buf.copy_within(i.., 0);
812 len
813}
814
815/// Write number padded to exactly 2 digits (assumes n < 100).
816fn write_u128_padded(buf: &mut [u8], n: u128) -> usize {
817 buf[0] = b'0' + ((n / 10) % 10) as u8;
818 buf[1] = b'0' + (n % 10) as u8;
819 2
820}