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