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