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}