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