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