Skip to main content

deep_time/dt/
to_str.rs

1use crate::{
2    Dt, DtErr, DtErrKind, LiteStr, STRFTIME_SIZE, Scale, YmdHmsRich, an_err,
3    tzdb::offset_info_at_utc,
4};
5
6#[cfg(feature = "alloc")]
7use crate::ATTOS_PER_SEC;
8
9#[cfg(feature = "alloc")]
10impl Dt {
11    /// Converts this `Dt` to an ISO 8601 duration string
12    /// (e.g. `"PT1H23M45.6789S"`, `"-PT0.5S"`, `"PT0.000000000000000001S"`, or `"PT0S"`).
13    ///
14    /// - This method is only available when the **`alloc`** feature is enabled.
15    /// - It returns `alloc::string::String` (no_std + alloc compatible).
16    pub fn to_iso_duration(&self) -> alloc::string::String {
17        if self.is_zero() {
18            return alloc::string::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 = alloc::string::String::with_capacity(48);
26        if negative {
27            s.push('-');
28        }
29        s.push_str("PT");
30
31        const A_PER_S: u128 = ATTOS_PER_SEC as u128;
32        const A_PER_M: u128 = A_PER_S * 60;
33        const A_PER_H: u128 = A_PER_M * 60;
34
35        let hours = attos / A_PER_H;
36        attos %= A_PER_H;
37        let minutes = attos / A_PER_M;
38        attos %= A_PER_M;
39        let seconds = attos / A_PER_S;
40        let frac_attos = attos % A_PER_S;
41
42        if hours > 0 {
43            s.push_str(&alloc::format!("{}", hours));
44            s.push('H');
45        }
46        if minutes > 0 {
47            s.push_str(&alloc::format!("{}", minutes));
48            s.push('M');
49        }
50
51        if seconds > 0 || frac_attos > 0 {
52            s.push_str(&alloc::format!("{}", seconds));
53
54            if frac_attos != 0 {
55                let frac_str = alloc::format!("{frac_attos:018}");
56                let trimmed = frac_str.trim_end_matches('0');
57                s.push('.');
58                s.push_str(trimmed);
59            }
60
61            s.push('S');
62        }
63
64        s
65    }
66
67    /// Formats this [`Dt`] into a String. Requires the `"alloc"` feature.
68    ///
69    /// - It is first converted from the `current` [`Scale`] into
70    ///   the `UTC` time scale.
71    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice`
72    ///   are preserved. See [`Scale`] for more info on time scales.
73    ///
74    /// ## Example
75    ///
76    /// ```rust
77    /// use deep_time::{Dt, Scale};
78    ///
79    /// let x = Dt::from_ymd(2000, 1, 1);
80    /// let s = x.to_str(Scale::TAI, "%F").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 [`STRFTIME_SIZE`]).
90    ///
91    /// ## See also
92    ///
93    /// - [`Dt::to_str_with_offset`](../struct.Dt.html#method.to_str_with_offset)
94    /// - [`Dt::to_str_with_tz`](../struct.Dt.html#method.to_str_with_tz)
95    #[inline]
96    pub fn to_str(&self, current: Scale, fmt: &str) -> Result<alloc::string::String, DtErr> {
97        self.to_str_with_offset(current, fmt, 0)
98    }
99
100    /// Formats this [`Dt`] into a String, applying a fixed UTC offset.  Requires the
101    /// `"alloc"` feature.
102    ///
103    /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
104    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
105    ///   will reflect it.
106    /// - Then it's converted from the `current` [`Scale`] into the
107    ///   `UTC` time scale.
108    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice` are preserved.
109    ///   See [`Scale`] for more info on time scales.
110    /// - No IANA timezone name or abbreviation is set.
111    ///
112    /// ## Example
113    ///
114    /// ```rust
115    /// use deep_time::{Dt, Scale};
116    ///
117    /// let x = Dt::from_ymd(2000, 1, 1);
118    ///
119    /// // offset of minus one hour
120    /// let s = x.to_str_with_offset(Scale::TAI, "%F", -3600).unwrap();
121    ///
122    /// println!("{}", s);
123    /// ```
124    ///
125    /// ## Errors
126    ///
127    /// Returns [`DtErr`] if the format string contains invalid specifiers
128    /// or if the internal formatting buffer overflows (extremely unlikely
129    /// with [`STRFTIME_SIZE`]).
130    ///
131    /// ## See also
132    ///
133    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
134    /// - [`Dt::to_str_with_tz`](../struct.Dt.html#method.to_str_with_tz)
135    #[inline]
136    pub fn to_str_with_offset(
137        &self,
138        current: Scale,
139        fmt: &str,
140        secs: i32,
141    ) -> Result<alloc::string::String, DtErr> {
142        let mut buf = [0u8; STRFTIME_SIZE];
143        let n = self._to_u8_with_offset(current, fmt, &mut buf, secs)?;
144        Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
145    }
146
147    /// Formats this [`Dt`] into a string, time adjusted to the given IANA timezone. Requires
148    /// the `"alloc"` feature.
149    ///
150    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
151    ///
152    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]s time for the given
153    ///   IANA timezone. This is so that the formatter will have:
154    ///     - Accurate wall time for the timezone.
155    ///     - Correct numeric offset (for `%z` / `%:z`).
156    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip.
157    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
158    /// - Then it's converted from the `current` [`Scale`] into the
159    ///   `UTC` time scale.
160    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice` are preserved.
161    ///   See [`Scale`] for more info on time scales.
162    /// - No IANA timezone name or abbreviation is set.
163    ///
164    /// ## Example
165    ///
166    /// ```
167    /// # #[cfg(all(feature = "tz", feature = "parse"))]
168    /// # {
169    /// use deep_time::{Dt, Scale};
170    ///
171    /// let x: Dt = "2000-01-01 12:00:00".parse().unwrap();
172    ///
173    /// let s = x.to_str_with_tz(Scale::TAI, "%A, %B %d, %Y %H:%M:%S %Q", "America/New_York").unwrap();
174    ///
175    /// assert_eq!(s, "Saturday, January 01, 2000 07:00:00 America/New_York");
176    /// # }
177    /// ```
178    ///
179    /// ## Errors
180    ///
181    /// Returns [`DtErr`] if the format string contains invalid specifiers
182    /// or if the internal formatting buffer overflows (extremely unlikely
183    /// with [`STRFTIME_SIZE`]).
184    ///
185    /// ## See also
186    ///
187    /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
188    /// - [`Dt::to_str_with_offset`](../struct.Dt.html#method.to_str_with_offset)
189    #[inline]
190    pub fn to_str_with_tz(
191        &self,
192        current: Scale,
193        fmt: &str,
194        tz_name: &str,
195    ) -> Result<alloc::string::String, DtErr> {
196        let mut buf = [0u8; STRFTIME_SIZE];
197        let n = self._to_u8_with_tz(current, fmt, &mut buf, tz_name)?;
198        Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
199    }
200
201    /// Returns this instant as an **RFC 3339** / ISO 8601 timestamp in **UTC**
202    /// with the `Z` suffix.
203    ///
204    /// - Always uses UTC (`Z` = Zulu = UTC).
205    /// - Default = 9 digits (nanoseconds) but **automatically trims trailing zeros**.
206    /// - If fractional part is zero → no decimal point at all (e.g. `...45Z`).
207    /// - Example: `"2024-03-14T15:30:45.123Z"`
208    #[inline]
209    pub fn to_str_rfc3339(&self, current: Scale) -> Result<String, DtErr> {
210        self.to_str_rfc3339_nf(current, 9)
211    }
212
213    /// Same as [`Dt::to_str_rfc3339`](../struct.Dt.html#method.to_str_rfc3339) but
214    /// with a configurable maximum number of fractional digits (0–18). Trailing zeros are
215    /// always trimmed.
216    pub fn to_str_rfc3339_nf(&self, current: Scale, max_precision: usize) -> Result<String, DtErr> {
217        let prec = max_precision.min(18);
218        // Uses the formatter with the `~` "trim trailing zeros" flag.
219        // The formatter already handles:
220        //   - correct 4-digit years (with sign) for |yr| < 10000
221        //   - full-width years otherwise
222        //   - suppressing the decimal point entirely when the trimmed fraction is zero
223        let fmt = alloc::format!("%Y-%m-%dT%H:%M:%S%.{}~fZ", prec);
224        self.to_str_with_offset(current, &fmt, 0)
225    }
226
227    /// **ISO 8601 / RFC 3339** with **actual offset** (modern `+00:00` style).
228    ///
229    /// - Uses colon-separated offset (`%:z`) instead of forcing `Z`.
230    /// - Still trims trailing zeros in the fractional part.
231    /// - Example: `"2025-04-16T14:30:45.123+00:00"`
232    #[inline]
233    pub fn to_str_iso8601(&self, current: Scale) -> Result<String, DtErr> {
234        self.to_str_with_offset(current, "%Y-%m-%dT%H:%M:%S%.~f%:z", 0)
235    }
236
237    /// **Compact ISO 8601 basic format** (no separators).
238    ///
239    /// - Useful for filenames, URLs, database keys, etc.
240    /// - Example: `"20250416T143045.123456789Z"`
241    #[inline]
242    pub fn to_str_iso8601_basic(&self, current: Scale) -> Result<String, DtErr> {
243        self.to_str_with_offset(current, "%Y%m%dT%H%M%S%.~fZ", 0)
244    }
245
246    /// **HTTP-date** format (RFC 7231 / RFC 1123) — **always in GMT**.
247    ///
248    /// This is the format used in `Date`, `Expires`, `Last-Modified` headers.
249    /// Example: `"Wed, 16 Apr 2025 14:30:45 GMT"`
250    #[inline]
251    pub fn to_str_http(&self, current: Scale) -> Result<String, DtErr> {
252        self.to_str_with_offset(current, "%a, %d %b %Y %H:%M:%S GMT", 0)
253    }
254
255    /// **RFC 2822** date format (used in email `Date` headers).
256    ///
257    /// Example: `"Wed, 16 Apr 2025 14:30:45 +0000"`
258    #[inline]
259    pub fn to_str_rfc2822(&self, current: Scale) -> Result<String, DtErr> {
260        self.to_str_with_offset(current, "%a, %d %b %Y %H:%M:%S %z", 0)
261    }
262
263    /// **ISO 8601 week date**.
264    ///
265    /// Example: `"2025-W16-3"` (year-week-day)
266    #[inline]
267    pub fn to_str_iso_week_date(&self, current: Scale) -> Result<String, DtErr> {
268        self.to_str_with_offset(current, "%G-W%V-%u", 0)
269    }
270
271    /// Just the **ISO date** part (no time).
272    ///
273    /// Example: `"2025-04-16"`
274    #[inline]
275    pub fn to_str_iso_date(&self, current: Scale) -> Result<String, DtErr> {
276        self.to_str_with_offset(current, "%Y-%m-%d", 0)
277    }
278
279    /// Just the **time** part with fractional seconds (trimmed).
280    ///
281    /// Example: `"14:30:45.123456789"`
282    #[inline]
283    pub fn to_str_iso_time(&self, current: Scale) -> Result<String, DtErr> {
284        self.to_str_with_offset(current, "%H:%M:%S%.~f", 0)
285    }
286}
287
288impl Dt {
289    /// Formats this [`Dt`] into a fixed-size binary string.
290    ///
291    /// - It is first converted from the `current` [`Scale`] into
292    ///   the `UTC` time scale.
293    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice`
294    ///   are preserved. See [`Scale`] for more info on time scales.
295    ///
296    /// ## Example
297    ///
298    /// ```rust
299    /// use deep_time::{Dt, Scale};
300    ///
301    /// let x = Dt::from_ymd(2000, 1, 1);
302    /// let b = x.to_str_bin(Scale::TAI, "%F").unwrap();
303    /// let s = b.as_str().unwrap();
304    ///
305    /// println!("{}", s);
306    /// ```
307    ///
308    /// ## Errors
309    ///
310    /// Returns [`DtErr`] if the format string contains invalid specifiers
311    /// or if the internal formatting buffer overflows (extremely unlikely
312    /// with [`STRFTIME_SIZE`]).
313    ///
314    /// ## See also
315    ///
316    /// - [`Dt::to_str_bin_with_offset`](../struct.Dt.html#method.to_str_bin_with_offset)
317    /// - [`Dt::to_str_bin_with_tz`](../struct.Dt.html#method.to_str_bin_with_tz)
318    pub fn to_str_bin(&self, current: Scale, fmt: &str) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
319        let mut ymdhms = self.to_ymdhms_rich_on(current, current.to_utc());
320        ymdhms.set_offset(Some(0)).set_tz_abbrev(None);
321        let mut buf = [0u8; STRFTIME_SIZE];
322        let mut pos = 0usize;
323        ymdhms.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
324        Ok(LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))?)
325    }
326
327    /// Formats this [`Dt`] into a fixed-size binary string, applying a fixed UTC offset.
328    ///
329    /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
330    ///   formatting, and the offset is stored so that `%z` / `%:z` format directives
331    ///   will reflect it.
332    /// - Then it's converted from the `current` [`Scale`] into the
333    ///   `UTC` time scale.
334    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice` are preserved.
335    ///   See [`Scale`] for more info on time scales.
336    /// - No IANA timezone name or abbreviation is set.
337    ///
338    /// ## Example
339    ///
340    /// ```rust
341    /// use deep_time::{Dt, Scale};
342    ///
343    /// let x = Dt::from_ymd(2000, 1, 1);
344    ///
345    /// // offset of minus one hour
346    /// let b = x.to_str_bin_with_offset(Scale::TAI, "%F", -3600).unwrap();
347    /// let s = b.as_str().unwrap();
348    ///
349    /// println!("{}", s);
350    /// ```
351    ///
352    /// ## Errors
353    ///
354    /// Returns [`DtErr`] if the format string contains invalid specifiers
355    /// or if the internal formatting buffer overflows (extremely unlikely
356    /// with [`STRFTIME_SIZE`]).
357    ///
358    /// ## See also
359    ///
360    /// - [`Dt::to_str_bin`](../struct.Dt.html#method.to_str_bin)
361    /// - [`Dt::to_str_bin_with_tz`](../struct.Dt.html#method.to_str_bin_with_tz)
362    pub fn to_str_bin_with_offset(
363        &self,
364        current: Scale,
365        fmt: &str,
366        secs: i32,
367    ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
368        let ymdhms = self.ymdhms_rich_with_offset(current, secs);
369        let mut buf = [0u8; STRFTIME_SIZE];
370        let mut pos = 0usize;
371        ymdhms.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
372        Ok(LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))?)
373    }
374
375    /// Formats this [`Dt`] into a fixed-size binary string, time adjusted to the given
376    /// IANA timezone.
377    ///
378    /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
379    ///
380    /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]s time for the given
381    ///   IANA timezone. This is so that the formatter will have:
382    ///     - Accurate wall time for the timezone.
383    ///     - Correct numeric offset (for `%z` / `%:z`).
384    ///     - Timezone abbreviation (for `%Z`). These **do not** round-trip.
385    ///     - Full IANA timezone name (for `%Q` / `%:Q`).
386    /// - Then it's converted from the `current` [`Scale`] into the
387    ///   `UTC` time scale.
388    /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice` are preserved.
389    ///   See [`Scale`] for more info on time scales.
390    /// - No IANA timezone name or abbreviation is set.
391    ///
392    /// ## Example
393    ///
394    /// ```rust
395    /// use deep_time::{Dt, Scale};
396    ///
397    /// let x = Dt::from_ymd(2000, 1, 1);
398    ///
399    /// let b = x.to_str_bin_with_tz(Scale::TAI, "%F", "America/New_York").unwrap();
400    /// let s = b.as_str().unwrap();
401    ///
402    /// println!("{}", s);
403    /// ```
404    ///
405    /// ## Errors
406    ///
407    /// Returns [`DtErr`] if the format string contains invalid specifiers
408    /// or if the internal formatting buffer overflows (extremely unlikely
409    /// with [`STRFTIME_SIZE`]).
410    ///
411    /// ## See also
412    ///
413    /// - [`Dt::to_str_bin`](../struct.Dt.html#method.to_str_bin)
414    /// - [`Dt::to_str_bin_with_offset`](../struct.Dt.html#method.to_str_bin_with_offset)
415    pub fn to_str_bin_with_tz(
416        &self,
417        current: Scale,
418        fmt: &str,
419        tz_name: &str,
420    ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
421        let ymdhms = self.ymdhms_rich_with_tz(current, tz_name);
422        let mut buf = [0u8; STRFTIME_SIZE];
423        let mut pos = 0usize;
424        ymdhms.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
425        Ok(LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))?)
426    }
427
428    /// Low-level no-alloc formatter that writes into a caller-provided slice,
429    /// using a fixed UTC offset.
430    ///
431    /// Same logic as [`Self::to_str_bin_with_offset`], but writes directly into
432    /// `dest` (truncated to `dest.len()`) and returns the number of bytes written.
433    pub fn _to_u8_with_offset(
434        &self,
435        current: Scale,
436        fmt: &str,
437        dest: &mut [u8],
438        secs: i32,
439    ) -> Result<usize, DtErr> {
440        let ymdhms = self.ymdhms_rich_with_offset(current, secs);
441        let mut internal_buf = [0u8; STRFTIME_SIZE];
442        let mut pos = 0usize;
443        ymdhms.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
444        let written = pos.min(dest.len());
445        if written > 0 {
446            dest[0..written].copy_from_slice(&internal_buf[0..written]);
447        }
448        Ok(written)
449    }
450
451    /// Low-level no-alloc formatter that writes into a caller-provided slice,
452    /// using a full IANA timezone.
453    ///
454    /// Same logic as [`Self::to_str_bin_with_tz`], but writes directly into
455    /// `dest` (truncated to `dest.len()`) and returns the number of bytes written.
456    pub fn _to_u8_with_tz(
457        &self,
458        current: Scale,
459        fmt: &str,
460        dest: &mut [u8],
461        tz_name: &str,
462    ) -> Result<usize, DtErr> {
463        let ymdhms = self.ymdhms_rich_with_tz(current, tz_name);
464        let mut internal_buf = [0u8; STRFTIME_SIZE];
465        let mut pos = 0usize;
466        ymdhms.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
467        let written = pos.min(dest.len());
468        if written > 0 {
469            dest[0..written].copy_from_slice(&internal_buf[0..written]);
470        }
471        Ok(written)
472    }
473
474    /// Returns `(is_negative, hours, minutes)`.
475    #[inline]
476    pub(crate) const fn sec_as_hhmm(seconds: i32) -> (bool, u8, u8) {
477        let total = seconds.saturating_abs();
478        let hours = (total / 3600) as u8;
479        let minutes = ((total % 3600) / 60) as u8;
480        (seconds < 0, hours, minutes)
481    }
482
483    /// Helper for creating an offset adjusted YmdHmsRich.
484    pub(crate) fn ymdhms_rich_with_offset(&self, current: Scale, secs: i32) -> YmdHmsRich {
485        let local_tp = if secs != 0 {
486            *self + Dt::new(secs as i64, 0)
487        } else {
488            *self
489        };
490        let mut ymdhms = local_tp.to_ymdhms_rich_on(current, current.to_utc());
491        ymdhms.set_offset(Some(secs));
492        ymdhms
493    }
494
495    /// Helper for creating a timezone-adjusted YmdHmsRich.
496    pub(crate) fn ymdhms_rich_with_tz(&self, current: Scale, tz_name: &str) -> YmdHmsRich {
497        // 1. Get the true UTC Unix timestamp
498        let utc_unix = self
499            .to(current, current.to_utc())
500            .to_diff_raw(Dt::UNIX_EPOCH);
501
502        // 2. Look up offset + abbrev at that exact UTC instant
503        let (offset_secs, abbrev) = match offset_info_at_utc(tz_name, utc_unix.sec) {
504            Some(info) => (info.offset, info.abbrev),
505            None => (0, "UTC"), // fallback for unknown timezone
506        };
507
508        // 3. Build local time = UTC + offset
509        let span = Dt::new(offset_secs as i64, 0);
510        let local_tp = *self + span;
511
512        let mut ymdhms = local_tp.to_ymdhms_rich_on(current, current.to_utc());
513        ymdhms.set_offset(Some(offset_secs));
514        ymdhms.set_tz(Some(tz_name));
515        ymdhms.set_tz_abbrev(Some(abbrev));
516        ymdhms
517    }
518}