deep_time/ymdhms/mod.rs
1use crate::{ATTOS_PER_SEC_I128, Dt, LiteStr, STRTIME_SIZE, Scale};
2use core::fmt::Write;
3
4#[cfg(any(feature = "jiff-tz-bundle", feature = "jiff-tz"))]
5use crate::{DtErr, DtErrKind, an_err};
6#[cfg(any(feature = "jiff-tz-bundle", feature = "jiff-tz"))]
7use jiff::civil;
8
9/// Combined date + time object.
10///
11/// Has calendar aware and, when the `jiff-tz` feature is enabled,
12/// timezone aware math functions.
13///
14/// ## Examples
15///
16/// **Creating a** [`YmdHms`].
17///
18/// ```rust
19/// use deep_time::{Dt, Scale};
20///
21/// // clamped to 29
22/// let x = Dt::from_ymd(2000, 2, 30, Scale::UTC, 0, 0, 0, 0).to_ymd();
23///
24/// assert_eq!(x.day(), 29);
25/// ```
26///
27/// **Adding a year.** 2000 is a leap year and Feb. 29th is possible, but
28/// 2001 isn't a leap year so the day is clamped to the 28th.
29///
30/// ```rust
31/// use deep_time::{Dt, Scale};
32///
33/// let x = Dt::from_ymd(2000, 2, 29, Scale::UTC, 0, 0, 0, 0).to_ymd();
34/// let x = x.add_years(1);
35///
36/// assert_eq!(x.day(), 28);
37/// ```
38#[derive(Clone, Debug, PartialEq, Eq, Hash)]
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
41#[cfg_attr(feature = "defmt", derive(defmt::Format))]
42pub struct YmdHms {
43 pub(crate) yr: i64,
44 pub(crate) mo: u8,
45 pub(crate) day: u8,
46 pub(crate) hr: u8,
47 pub(crate) min: u8,
48 pub(crate) sec: u8, // 0–60 (60 only during leap seconds)
49 pub(crate) attos: u64, // attoseconds (0 ≤ subsec < 10¹⁸)
50 pub(crate) dt: Dt,
51}
52
53impl YmdHms {
54 /// Create a new [`YmdHms`], wrapper around
55 /// [`Dt::from_ymd`](../struct.Dt.html#method.from_ymd).
56 #[inline(always)]
57 pub const fn new(
58 yr: i64,
59 mo: u8,
60 day: u8,
61 scale: Scale,
62 hr: u8,
63 min: u8,
64 sec: u8,
65 attos: u64,
66 ) -> YmdHms {
67 Dt::from_ymd(yr, mo, day, scale, hr, min, sec, attos).to_ymd()
68 }
69
70 /// Returns a copy of the [`YmdHms`] object. Const fn version of
71 /// [`Clone::clone`](core::clone::Clone::clone).
72 #[inline(always)]
73 pub const fn clone_const(&self) -> Self {
74 YmdHms {
75 yr: self.yr,
76 mo: self.mo,
77 day: self.day,
78 hr: self.hr,
79 min: self.min,
80 sec: self.sec,
81 attos: self.attos,
82 dt: self.dt,
83 }
84 }
85
86 /// Returns the [`Dt`] that was used to make this [`YmdHms`] object.
87 #[inline(always)]
88 pub const fn to_dt(&self) -> Dt {
89 self.dt
90 }
91
92 /// Internal helper that round-trips through [`Dt`] to obtain a normalized
93 /// `YmdHms` (handles clamping, leap seconds, etc.).
94 #[inline(always)]
95 const fn reconstruct(
96 &self,
97 yr: i64,
98 mo: u8,
99 day: u8,
100 hr: u8,
101 min: u8,
102 sec: u8,
103 attos: u64,
104 ) -> Self {
105 let mut ymd = Dt::from_ymd(yr, mo, day, self.dt.target, hr, min, sec, attos).to_ymd();
106 ymd.dt.scale = self.dt.scale;
107 ymd
108 }
109
110 /// Adds (or subtracts) whole years, preserving month and day-of-month.
111 /// - Uses standard last-day-of-month clamping.
112 /// - Negative values subtract.
113 pub const fn add_years(&self, n: i64) -> Self {
114 if n == 0 {
115 return self.clone_const();
116 }
117 let new_yr = self.yr.saturating_add(n);
118 let max_day = Dt::days_in_month(new_yr, self.mo);
119 let new_day = Dt::clamp_u8(self.day, 1, max_day);
120 self.reconstruct(
121 new_yr, self.mo, new_day, self.hr, self.min, self.sec, self.attos,
122 )
123 }
124
125 /// Adds (or subtracts) calendar months. Negative values subtract.
126 pub const fn add_months(&self, n: i64) -> Self {
127 if n == 0 {
128 return self.clone_const();
129 }
130
131 let yr = self.yr as i128;
132 let mo = self.mo as i128;
133 let delta = n as i128;
134
135 let total_months = yr * 12 + (mo - 1) + delta;
136
137 let new_yr = Dt::to_i64(total_months.div_euclid(12));
138 let new_mo = Dt::clamp_u8((total_months.rem_euclid(12) + 1) as u8, 1, 12);
139
140 let max_day = Dt::days_in_month(new_yr, new_mo);
141 let new_day = Dt::clamp_u8(self.day, 1, max_day);
142
143 self.reconstruct(
144 new_yr, new_mo, new_day, self.hr, self.min, self.sec, self.attos,
145 )
146 }
147
148 /// Adds (or subtracts) calendar weeks. Negative values subtract.
149 #[inline(always)]
150 pub const fn add_weeks(&self, n: i64) -> Self {
151 self.add_days(n.saturating_mul(7))
152 }
153
154 /// Adds (or subtracts) calendar days. Negative values subtract.
155 pub const fn add_days(&self, n: i64) -> Self {
156 if n == 0 {
157 return self.clone_const();
158 }
159 let jd = Dt::ymd_to_jd(self.yr, self.mo, self.day);
160 let new_jd = jd.saturating_add(n);
161 let (new_yr, new_mo, new_day) = Dt::jd_to_ymd(new_jd);
162 self.reconstruct(
163 new_yr, new_mo, new_day, self.hr, self.min, self.sec, self.attos,
164 )
165 }
166
167 /// Internal implementation detail for all sub-day / physical-time additions.
168 /// Creates a temporary [`Dt`], performs the addition, then converts back to `YmdHms`.
169 const fn _add_attos(&self, attos_delta: i128) -> Self {
170 let tai = Dt::from_ymd(
171 self.yr,
172 self.mo,
173 self.day,
174 self.dt.target,
175 self.hr,
176 self.min,
177 self.sec,
178 self.attos,
179 );
180 let mut ymd = tai.add(Dt::span(attos_delta)).to_ymd();
181 ymd.dt.scale = self.dt.scale;
182 ymd
183 }
184
185 /// Adds (or subtracts) attoseconds. Negative values subtract.
186 #[inline(always)]
187 pub const fn add_attos(&self, n: i128) -> Self {
188 self._add_attos(n)
189 }
190
191 /// Adds (or subtracts) whole seconds. Negative values subtract.
192 #[inline(always)]
193 pub const fn add_sec(&self, n: i64) -> Self {
194 self._add_attos((n as i128).saturating_mul(ATTOS_PER_SEC_I128))
195 }
196
197 /// Adds (or subtracts) whole minutes. Negative values subtract.
198 #[inline]
199 pub const fn add_mins(&self, n: i64) -> Self {
200 let delta = (n as i128)
201 .saturating_mul(60)
202 .saturating_mul(ATTOS_PER_SEC_I128);
203 self._add_attos(delta)
204 }
205
206 /// Adds (or subtracts) whole hours. Negative values subtract.
207 #[inline]
208 pub const fn add_hours(&self, n: i64) -> Self {
209 let delta = (n as i128)
210 .saturating_mul(3600)
211 .saturating_mul(ATTOS_PER_SEC_I128);
212 self._add_attos(delta)
213 }
214
215 /// Returns the year component.
216 #[inline(always)]
217 pub const fn yr(&self) -> i64 {
218 self.yr
219 }
220
221 /// Returns the month component (1–12).
222 #[inline(always)]
223 pub const fn mo(&self) -> u8 {
224 self.mo
225 }
226
227 /// Returns the day-of-month component (1–31, depending on month/year).
228 #[inline(always)]
229 pub const fn day(&self) -> u8 {
230 self.day
231 }
232
233 /// Returns the hour component (0–23).
234 #[inline(always)]
235 pub const fn hr(&self) -> u8 {
236 self.hr
237 }
238
239 /// Returns the minute component (0–59).
240 #[inline(always)]
241 pub const fn min(&self) -> u8 {
242 self.min
243 }
244
245 /// Returns the second component (0–60). The value 60 only occurs during
246 /// a positive leap second on `Scale::UTC` / `UtcSpice` / `UtcHist`.
247 #[inline(always)]
248 pub const fn sec(&self) -> u8 {
249 self.sec
250 }
251
252 /// Returns the attosecond (sub-second) component (0 ≤ attos < 10¹⁸).
253 #[inline(always)]
254 pub const fn attos(&self) -> u64 {
255 self.attos
256 }
257
258 /// The time scale that the object was created on.
259 #[inline(always)]
260 pub const fn time_scale(&self) -> Scale {
261 self.dt.target
262 }
263
264 /// Returns the **ISO week year** (can differ from the calendar year near
265 /// January 1 / December 31).
266 #[inline(always)]
267 pub const fn iso_yr(&self) -> i64 {
268 let (iso_yr, _, _) = Dt::_to_iso_wk_date(self.yr, self.mo, self.day);
269 iso_yr
270 }
271
272 /// Returns the **ISO week number** (1–53). Weeks start on Monday; week 1
273 /// is the week containing the first Thursday of the year.
274 #[inline(always)]
275 pub const fn iso_wk(&self) -> u8 {
276 let (_, iso_wk, _) = Dt::_to_iso_wk_date(self.yr, self.mo, self.day);
277 iso_wk
278 }
279
280 /// Returns the **day of the year** (ordinal date), 1-based (Jan 1 = 1,
281 /// Dec 31 = 365 or 366 in leap years).
282 #[inline(always)]
283 pub const fn day_of_yr(&self) -> u16 {
284 Dt::_day_of_yr(self.yr, self.mo, self.day)
285 }
286
287 /// Returns the **weekday** number according to [`Dt::jd_to_wkday`]
288 /// (0 = Sunday … 6 = Saturday).
289 #[inline(always)]
290 pub const fn wkday(&self) -> u8 {
291 let jd = Dt::ymd_to_jd(self.yr, self.mo, self.day);
292 Dt::jd_to_wkday(jd)
293 }
294
295 /// Returns the **week of year** number when weeks are considered to start
296 /// on Sunday (US-style numbering).
297 #[inline(always)]
298 pub const fn wk_of_yr_sun(&self) -> u8 {
299 Dt::_wk_sun(self.yr, self.day_of_yr())
300 }
301
302 /// Returns the **week of year** number when weeks are considered to start
303 /// on Monday.
304 #[inline(always)]
305 pub const fn wk_of_yr_mon(&self) -> u8 {
306 Dt::_wk_mon(self.yr, self.day_of_yr())
307 }
308
309 /// Formats this value the same way as [`Display`](core::fmt::Display),
310 /// but into a [`LiteStr`](../struct.LiteStr.html).
311 ///
312 /// ## Example
313 ///
314 /// ```rust
315 /// use deep_time::{Dt, Scale};
316 ///
317 /// let ymd = Dt::from_ymd(2000, 1, 2, Scale::UTC, 3, 4, 5, 0).to_ymd();
318 /// assert_eq!(ymd.to_str_lite().as_str(), "2000-01-02T03:04:05 UTC");
319 /// ```
320 #[inline]
321 pub fn to_str_lite(&self) -> LiteStr<STRTIME_SIZE> {
322 let mut s = LiteStr::<STRTIME_SIZE>::default();
323 let _ = write!(s, "{}", self);
324 s
325 }
326}
327
328#[cfg(any(feature = "jiff-tz-bundle", feature = "jiff-tz"))]
329impl YmdHms {
330 /// Adds the given number of years in the specified IANA timezone,
331 /// respecting timezone rules (including DST) and proper calendar arithmetic.
332 ///
333 /// ## Errors
334 ///
335 /// - [`DtErrKind::YearOutOfRange`] if the year of the date is outside the
336 /// `-9999..=9999` range (checked before involving Jiff).
337 /// - Specific errors for invalid time components when preparing values for Jiff:
338 /// [`DtErrKind::InvalidHour`], [`DtErrKind::InvalidMinute`],
339 /// [`DtErrKind::InvalidSecond`], [`DtErrKind::InvalidMonth`], or [`DtErrKind::InvalidDay`].
340 /// - [`DtErrKind::InvalidTimeZone`] if Jiff cannot find/resolve the IANA timezone name.
341 /// - [`DtErrKind::OutOfRange`] if the result of the calendar arithmetic operation
342 /// would be outside the range supported by Jiff (the checked_add fails).
343 pub fn add_years_tz(&self, n: i64, tz: &str) -> Result<Self, DtErr> {
344 let zoned = self
345 .to_jiff_zoned(tz)?
346 .checked_add(jiff::Span::new().years(n))
347 .map_err(|e| an_err!(DtErrKind::OutOfRange, "{}", e))?;
348 Ok(self.from_jiff_zoned(zoned))
349 }
350
351 /// Adds the given number of months in the specified IANA timezone,
352 /// respecting timezone rules and calendar month-end clamping.
353 ///
354 /// ## Errors
355 ///
356 /// - [`DtErrKind::YearOutOfRange`] if the year of the date is outside the
357 /// `-9999..=9999` range (checked before involving Jiff).
358 /// - Specific errors for invalid time components when preparing values for Jiff:
359 /// [`DtErrKind::InvalidHour`], [`DtErrKind::InvalidMinute`],
360 /// [`DtErrKind::InvalidSecond`], [`DtErrKind::InvalidMonth`], or [`DtErrKind::InvalidDay`].
361 /// - [`DtErrKind::InvalidTimeZone`] if Jiff cannot find/resolve the IANA timezone name.
362 /// - [`DtErrKind::OutOfRange`] if the result of the calendar arithmetic operation
363 /// would be outside the range supported by Jiff (the checked_add fails).
364 pub fn add_months_tz(&self, n: i64, tz: &str) -> Result<Self, DtErr> {
365 let zoned = self
366 .to_jiff_zoned(tz)?
367 .checked_add(jiff::Span::new().months(n))
368 .map_err(|e| an_err!(DtErrKind::OutOfRange, "{}", e))?;
369 Ok(self.from_jiff_zoned(zoned))
370 }
371
372 /// Adds the given number of weeks in the specified IANA timezone.
373 ///
374 /// ## Errors
375 ///
376 /// - [`DtErrKind::YearOutOfRange`] if the year of the date is outside the
377 /// `-9999..=9999` range (checked before involving Jiff).
378 /// - Specific errors for invalid time components when preparing values for Jiff:
379 /// [`DtErrKind::InvalidHour`], [`DtErrKind::InvalidMinute`],
380 /// [`DtErrKind::InvalidSecond`], [`DtErrKind::InvalidMonth`], or [`DtErrKind::InvalidDay`].
381 /// - [`DtErrKind::InvalidTimeZone`] if Jiff cannot find/resolve the IANA timezone name.
382 /// - [`DtErrKind::OutOfRange`] if the result of the calendar arithmetic operation
383 /// would be outside the range supported by Jiff (the checked_add fails).
384 #[inline(always)]
385 pub fn add_weeks_tz(&self, n: i64, tz: &str) -> Result<Self, DtErr> {
386 self.add_days_tz(n.saturating_mul(7), tz)
387 }
388
389 /// Adds the given number of calendar days in the specified IANA timezone.
390 ///
391 /// ## Errors
392 ///
393 /// - [`DtErrKind::YearOutOfRange`] if the year of the date is outside the
394 /// `-9999..=9999` range (checked before involving Jiff).
395 /// - Specific errors for invalid time components when preparing values for Jiff:
396 /// [`DtErrKind::InvalidHour`], [`DtErrKind::InvalidMinute`],
397 /// [`DtErrKind::InvalidSecond`], [`DtErrKind::InvalidMonth`], or [`DtErrKind::InvalidDay`].
398 /// - [`DtErrKind::InvalidTimeZone`] if Jiff cannot find/resolve the IANA timezone name.
399 /// - [`DtErrKind::OutOfRange`] if the result of the calendar arithmetic operation
400 /// would be outside the range supported by Jiff (the checked_add fails).
401 pub fn add_days_tz(&self, n: i64, tz: &str) -> Result<Self, DtErr> {
402 let zoned = self
403 .to_jiff_zoned(tz)?
404 .checked_add(jiff::Span::new().days(n))
405 .map_err(|e| an_err!(DtErrKind::OutOfRange, "{}", e))?;
406 Ok(self.from_jiff_zoned(zoned))
407 }
408
409 /// Adds the given number of hours in the specified IANA timezone,
410 /// respecting timezone rules (including DST).
411 ///
412 /// ## Errors
413 ///
414 /// - [`DtErrKind::YearOutOfRange`] if the year of the date is outside the
415 /// `-9999..=9999` range (checked before involving Jiff).
416 /// - Specific errors for invalid time components when preparing values for Jiff:
417 /// [`DtErrKind::InvalidHour`], [`DtErrKind::InvalidMinute`],
418 /// [`DtErrKind::InvalidSecond`], [`DtErrKind::InvalidMonth`], or [`DtErrKind::InvalidDay`].
419 /// - [`DtErrKind::InvalidTimeZone`] if Jiff cannot find/resolve the IANA timezone name.
420 /// - [`DtErrKind::OutOfRange`] if the result of the calendar arithmetic operation
421 /// would be outside the range supported by Jiff (the checked_add fails).
422 pub fn add_hours_tz(&self, n: i64, tz: &str) -> Result<Self, DtErr> {
423 let new_zoned = self
424 .to_jiff_zoned(tz)?
425 .checked_add(jiff::Span::new().hours(n))
426 .map_err(|e| an_err!(DtErrKind::OutOfRange, "{}", e))?;
427 Ok(self.from_jiff_zoned(new_zoned))
428 }
429
430 /// Adds the given number of minutes in the specified IANA timezone,
431 /// respecting timezone rules (including DST).
432 ///
433 /// ## Errors
434 ///
435 /// - [`DtErrKind::YearOutOfRange`] if the year of the date is outside the
436 /// `-9999..=9999` range (checked before involving Jiff).
437 /// - Specific errors for invalid time components when preparing values for Jiff:
438 /// [`DtErrKind::InvalidHour`], [`DtErrKind::InvalidMinute`],
439 /// [`DtErrKind::InvalidSecond`], [`DtErrKind::InvalidMonth`], or [`DtErrKind::InvalidDay`].
440 /// - [`DtErrKind::InvalidTimeZone`] if Jiff cannot find/resolve the IANA timezone name.
441 /// - [`DtErrKind::OutOfRange`] if the result of the calendar arithmetic operation
442 /// would be outside the range supported by Jiff (the checked_add fails).
443 pub fn add_mins_tz(&self, n: i64, tz: &str) -> Result<Self, DtErr> {
444 let zoned = self
445 .to_jiff_zoned(tz)?
446 .checked_add(jiff::Span::new().minutes(n))
447 .map_err(|e| an_err!(DtErrKind::OutOfRange, "{}", e))?;
448 Ok(self.from_jiff_zoned(zoned))
449 }
450
451 /// Adds the given number of seconds in the specified IANA timezone.
452 ///
453 /// ## Errors
454 ///
455 /// - [`DtErrKind::YearOutOfRange`] if the year of the date is outside the
456 /// `-9999..=9999` range (checked before involving Jiff).
457 /// - Specific errors for invalid time components when preparing values for Jiff:
458 /// [`DtErrKind::InvalidHour`], [`DtErrKind::InvalidMinute`],
459 /// [`DtErrKind::InvalidSecond`], [`DtErrKind::InvalidMonth`], or [`DtErrKind::InvalidDay`].
460 /// - [`DtErrKind::InvalidTimeZone`] if Jiff cannot find/resolve the IANA timezone name.
461 /// - [`DtErrKind::OutOfRange`] if the result of the calendar arithmetic operation
462 /// would be outside the range supported by Jiff (the checked_add fails).
463 pub fn add_sec_tz(&self, n: i64, tz: &str) -> Result<Self, DtErr> {
464 let zoned = self
465 .to_jiff_zoned(tz)?
466 .checked_add(jiff::Span::new().seconds(n))
467 .map_err(|e| an_err!(DtErrKind::OutOfRange, "{}", e))?;
468 Ok(self.from_jiff_zoned(zoned))
469 }
470
471 // helpers
472
473 fn to_jiff_zoned(&self, tz: &str) -> Result<jiff::Zoned, DtErr> {
474 if !(-9999..=9999).contains(&self.yr) {
475 return Err(an_err!(DtErrKind::YearOutOfRange));
476 }
477
478 let hr: i8 = self
479 .hr
480 .try_into()
481 .map_err(|_| an_err!(DtErrKind::InvalidHour))?;
482 let min: i8 = self
483 .min
484 .try_into()
485 .map_err(|_| an_err!(DtErrKind::InvalidMinute))?;
486
487 let sec_for_jiff: i8 = if self.sec == 60 {
488 59
489 } else {
490 self.sec
491 .try_into()
492 .map_err(|_| an_err!(DtErrKind::InvalidSecond))?
493 };
494
495 let mo: i8 = self
496 .mo
497 .try_into()
498 .map_err(|_| an_err!(DtErrKind::InvalidMonth))?;
499 let day: i8 = self
500 .day
501 .try_into()
502 .map_err(|_| an_err!(DtErrKind::InvalidDay))?;
503
504 let civil_time = civil::date(self.yr as i16, mo, day).at(hr, min, sec_for_jiff, 0);
505
506 civil_time
507 .in_tz(tz)
508 .map_err(|e| an_err!(DtErrKind::InvalidTimeZone, "{}", e))
509 }
510
511 fn from_jiff_zoned(&self, zoned: jiff::Zoned) -> Self {
512 let civil = zoned.datetime();
513
514 self.reconstruct(
515 civil.year() as i64,
516 civil.month() as u8,
517 civil.day() as u8,
518 civil.hour() as u8,
519 civil.minute() as u8,
520 civil.second() as u8,
521 self.attos,
522 )
523 }
524}
525
526impl core::fmt::Display for YmdHms {
527 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
528 // Year: 4-digit padded when |yr| < 10000, natural width otherwise
529 if self.yr >= 0 {
530 if self.yr < 10000 {
531 core::write!(f, "{:04}", self.yr)?;
532 } else {
533 core::write!(f, "{}", self.yr)?;
534 }
535 } else {
536 let abs = (-self.yr) as u64;
537 if abs < 10000 {
538 core::write!(f, "-{:04}", abs)?;
539 } else {
540 core::write!(f, "-{}", abs)?;
541 }
542 }
543
544 // Month (pad only if < 10)
545 if self.mo < 10 {
546 core::write!(f, "-0{}", self.mo)?;
547 } else {
548 core::write!(f, "-{}", self.mo)?;
549 }
550
551 // Day (pad only if < 10)
552 if self.day < 10 {
553 core::write!(f, "-0{}", self.day)?;
554 } else {
555 core::write!(f, "-{}", self.day)?;
556 }
557
558 core::write!(f, "T")?;
559
560 // Hour (pad only if < 10)
561 if self.hr < 10 {
562 core::write!(f, "0{}", self.hr)?;
563 } else {
564 core::write!(f, "{}", self.hr)?;
565 }
566
567 core::write!(f, ":")?;
568
569 // Minute (pad only if < 10)
570 if self.min < 10 {
571 core::write!(f, "0{}", self.min)?;
572 } else {
573 core::write!(f, "{}", self.min)?;
574 }
575
576 core::write!(f, ":")?;
577
578 // Second (pad only if < 10) — 60 is still fine
579 if self.sec < 10 {
580 core::write!(f, "0{}", self.sec)?;
581 } else {
582 core::write!(f, "{}", self.sec)?;
583 }
584
585 // Fractional attoseconds
586 if self.attos != 0 {
587 let mut buf = [0u8; 18];
588 let mut n = self.attos;
589 for i in (0..18).rev() {
590 buf[i] = (n % 10) as u8 + b'0';
591 n /= 10;
592 }
593 let mut end = 18;
594 while end > 0 && buf[end - 1] == b'0' {
595 end -= 1;
596 }
597
598 core::write!(f, ".")?;
599 for &byte in &buf[..end] {
600 core::write!(f, "{}", byte as char)?;
601 }
602 }
603
604 // Scale abbreviation at the end
605 core::write!(f, " {}", self.dt.target.abbrev())
606 }
607}