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 /// ```rust
167 /// use deep_time::{Dt, Scale};
168 ///
169 /// let x = Dt::from_ymd(2000, 1, 1);
170 /// let s = x.to_str_with_tz(Scale::TAI, "%F", "America/New_York").unwrap();
171 ///
172 /// println!("{}", s);
173 /// ```
174 ///
175 /// ## Errors
176 ///
177 /// Returns [`DtErr`] if the format string contains invalid specifiers
178 /// or if the internal formatting buffer overflows (extremely unlikely
179 /// with [`STRFTIME_SIZE`]).
180 ///
181 /// ## See also
182 ///
183 /// - [`Dt::to_str`](../struct.Dt.html#method.to_str)
184 /// - [`Dt::to_str_with_offset`](../struct.Dt.html#method.to_str_with_offset)
185 #[inline]
186 pub fn to_str_with_tz(
187 &self,
188 current: Scale,
189 fmt: &str,
190 tz_name: &str,
191 ) -> Result<alloc::string::String, DtErr> {
192 let mut buf = [0u8; STRFTIME_SIZE];
193 let n = self._to_u8_with_tz(current, fmt, &mut buf, tz_name)?;
194 Ok(alloc::string::String::from_utf8_lossy(&buf[0..n]).into_owned())
195 }
196}
197
198impl Dt {
199 /// Formats this [`Dt`] into a fixed-size binary string.
200 ///
201 /// - It is first converted from the `current` [`Scale`] into
202 /// the `UTC` time scale.
203 /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice`
204 /// are preserved. See [`Scale`] for more info on time scales.
205 ///
206 /// ## Example
207 ///
208 /// ```rust
209 /// use deep_time::{Dt, Scale};
210 ///
211 /// let x = Dt::from_ymd(2000, 1, 1);
212 /// let b = x.to_str_bin(Scale::TAI, "%F").unwrap();
213 /// let s = b.as_str().unwrap();
214 ///
215 /// println!("{}", s);
216 /// ```
217 ///
218 /// ## Errors
219 ///
220 /// Returns [`DtErr`] if the format string contains invalid specifiers
221 /// or if the internal formatting buffer overflows (extremely unlikely
222 /// with [`STRFTIME_SIZE`]).
223 ///
224 /// ## See also
225 ///
226 /// - [`Dt::to_str_bin_with_offset`](../struct.Dt.html#method.to_str_bin_with_offset)
227 /// - [`Dt::to_str_bin_with_tz`](../struct.Dt.html#method.to_str_bin_with_tz)
228 pub fn to_str_bin(&self, current: Scale, fmt: &str) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
229 let mut gt = self.to_ymdhms_rich_on(current, current.to_utc());
230 gt.set_offset(Some(0)).set_tz_abbrev(None);
231 let mut buf = [0u8; STRFTIME_SIZE];
232 let mut pos = 0usize;
233 gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
234 Ok(LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))?)
235 }
236
237 /// Formats this [`Dt`] into a fixed-size binary string, applying a fixed UTC offset.
238 ///
239 /// - A copy of the [`Dt`] is adjusted by the given `secs` offset **before**
240 /// formatting, and the offset is stored so that `%z` / `%:z` format directives
241 /// will reflect it.
242 /// - Then it's converted from the `current` [`Scale`] into the
243 /// `UTC` time scale.
244 /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice` are preserved.
245 /// See [`Scale`] for more info on time scales.
246 /// - No IANA timezone name or abbreviation is set.
247 ///
248 /// ## Example
249 ///
250 /// ```rust
251 /// use deep_time::{Dt, Scale};
252 ///
253 /// let x = Dt::from_ymd(2000, 1, 1);
254 ///
255 /// // offset of minus one hour
256 /// let b = x.to_str_bin_with_offset(Scale::TAI, "%F", -3600).unwrap();
257 /// let s = b.as_str().unwrap();
258 ///
259 /// println!("{}", s);
260 /// ```
261 ///
262 /// ## Errors
263 ///
264 /// Returns [`DtErr`] if the format string contains invalid specifiers
265 /// or if the internal formatting buffer overflows (extremely unlikely
266 /// with [`STRFTIME_SIZE`]).
267 ///
268 /// ## See also
269 ///
270 /// - [`Dt::to_str_bin`](../struct.Dt.html#method.to_str_bin)
271 /// - [`Dt::to_str_bin_with_tz`](../struct.Dt.html#method.to_str_bin_with_tz)
272 pub fn to_str_bin_with_offset(
273 &self,
274 current: Scale,
275 fmt: &str,
276 secs: i32,
277 ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
278 let gt = self.ymdhms_rich_with_offset(current, secs);
279 let mut buf = [0u8; STRFTIME_SIZE];
280 let mut pos = 0usize;
281 gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
282 Ok(LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))?)
283 }
284
285 /// Formats this [`Dt`] into a fixed-size binary string, time adjusted to the given
286 /// IANA timezone.
287 ///
288 /// Use this method when you want full IANA-aware formatting (`%Q`, `%Z`, `%z`, etc.).
289 ///
290 /// - A copy of the [`Dt`] is adjusted by the offset at the [`Dt`]s time for the given
291 /// IANA timezone. This is so that the formatter will have:
292 /// - Accurate wall time for the timezone.
293 /// - Correct numeric offset (for `%z` / `%:z`).
294 /// - Timezone abbreviation (for `%Z`). These **do not** round-trip.
295 /// - Full IANA timezone name (for `%Q` / `%:Q`).
296 /// - Then it's converted from the `current` [`Scale`] into the
297 /// `UTC` time scale.
298 /// - Historical UTC time scales such as `UTCSofa` and `UTCSpice` are preserved.
299 /// See [`Scale`] for more info on time scales.
300 /// - No IANA timezone name or abbreviation is set.
301 ///
302 /// ## Example
303 ///
304 /// ```rust
305 /// use deep_time::{Dt, Scale};
306 ///
307 /// let x = Dt::from_ymd(2000, 1, 1);
308 ///
309 /// let b = x.to_str_bin_with_tz(Scale::TAI, "%F", "America/New_York").unwrap();
310 /// let s = b.as_str().unwrap();
311 ///
312 /// println!("{}", s);
313 /// ```
314 ///
315 /// ## Errors
316 ///
317 /// Returns [`DtErr`] if the format string contains invalid specifiers
318 /// or if the internal formatting buffer overflows (extremely unlikely
319 /// with [`STRFTIME_SIZE`]).
320 ///
321 /// ## See also
322 ///
323 /// - [`Dt::to_str_bin`](../struct.Dt.html#method.to_str_bin)
324 /// - [`Dt::to_str_bin_with_offset`](../struct.Dt.html#method.to_str_bin_with_offset)
325 pub fn to_str_bin_with_tz(
326 &self,
327 current: Scale,
328 fmt: &str,
329 tz_name: &str,
330 ) -> Result<LiteStr<STRFTIME_SIZE>, DtErr> {
331 let gt = self.ymdhms_rich_with_tz(current, tz_name);
332 let mut buf = [0u8; STRFTIME_SIZE];
333 let mut pos = 0usize;
334 gt.format_to_buffer(fmt.as_bytes(), &mut buf, &mut pos)?;
335 Ok(LiteStr::from_bytes(&buf).map_err(|_| an_err!(DtErrKind::InvalidBytes))?)
336 }
337
338 /// Low-level no-alloc formatter that writes into a caller-provided slice,
339 /// using a fixed UTC offset.
340 ///
341 /// Same logic as [`Self::to_str_bin_with_offset`], but writes directly into
342 /// `dest` (truncated to `dest.len()`) and returns the number of bytes written.
343 pub fn _to_u8_with_offset(
344 &self,
345 current: Scale,
346 fmt: &str,
347 dest: &mut [u8],
348 secs: i32,
349 ) -> Result<usize, DtErr> {
350 let gt = self.ymdhms_rich_with_offset(current, secs);
351 let mut internal_buf = [0u8; STRFTIME_SIZE];
352 let mut pos = 0usize;
353 gt.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
354 let written = pos.min(dest.len());
355 if written > 0 {
356 dest[0..written].copy_from_slice(&internal_buf[0..written]);
357 }
358 Ok(written)
359 }
360
361 /// Low-level no-alloc formatter that writes into a caller-provided slice,
362 /// using a full IANA timezone.
363 ///
364 /// Same logic as [`Self::to_str_bin_with_tz`], but writes directly into
365 /// `dest` (truncated to `dest.len()`) and returns the number of bytes written.
366 pub fn _to_u8_with_tz(
367 &self,
368 current: Scale,
369 fmt: &str,
370 dest: &mut [u8],
371 tz_name: &str,
372 ) -> Result<usize, DtErr> {
373 let gt = self.ymdhms_rich_with_tz(current, tz_name);
374 let mut internal_buf = [0u8; STRFTIME_SIZE];
375 let mut pos = 0usize;
376 gt.format_to_buffer(fmt.as_bytes(), &mut internal_buf, &mut pos)?;
377 let written = pos.min(dest.len());
378 if written > 0 {
379 dest[0..written].copy_from_slice(&internal_buf[0..written]);
380 }
381 Ok(written)
382 }
383
384 /// Returns `(is_negative, hours, minutes)`.
385 #[inline]
386 pub(crate) const fn sec_as_hhmm(seconds: i32) -> (bool, u8, u8) {
387 let total = seconds.saturating_abs();
388 let hours = (total / 3600) as u8;
389 let minutes = ((total % 3600) / 60) as u8;
390 (seconds < 0, hours, minutes)
391 }
392
393 /// Helper for creating an offset adjusted YmdHmsRich.
394 pub(crate) fn ymdhms_rich_with_offset(&self, current: Scale, secs: i32) -> YmdHmsRich {
395 let local_tp = if secs != 0 {
396 *self + Dt::new(secs as i64, 0)
397 } else {
398 *self
399 };
400 let mut gt = local_tp.to_ymdhms_rich_on(current, current.to_utc());
401 gt.set_offset(Some(secs));
402 gt
403 }
404
405 /// Helper for creating a timezone-adjusted YmdHmsRich.
406 ///
407 /// Always converts to UTC first, then does a correct UTC-based lookup
408 /// in the IANA transition table. This avoids the previous bug where
409 /// a non-UTC `unix_ts` was being passed to `offset_info_at_local`.
410 pub(crate) fn ymdhms_rich_with_tz(&self, current: Scale, tz_name: &str) -> YmdHmsRich {
411 // 1. Get the true UTC Unix timestamp (this is what we search with)
412 let utc_unix = self
413 .to(current, current.to_utc())
414 .to_diff_raw(Dt::UNIX_EPOCH);
415
416 // 2. Look up offset + abbrev at that exact UTC instant
417 let (offset_secs, abbrev) = match offset_info_at_utc(tz_name, utc_unix.sec) {
418 Some(info) => (info.offset, info.abbrev),
419 None => (0, "UTC"), // fallback for unknown timezone
420 };
421
422 // 3. Build local time = UTC + offset
423 let span = Dt::new(offset_secs as i64, 0);
424 let local_tp = *self + span;
425
426 let mut gt = local_tp.to_ymdhms_rich_on(current, current.to_utc());
427 gt.set_offset(Some(offset_secs));
428 gt.set_tz(Some(tz_name));
429 gt.set_tz_abbrev(Some(abbrev));
430 gt
431 }
432}