Skip to main content

deep_time/dt/
to_str.rs

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