Repository: BurntSushi/jiff (language: rust)

Issue:
Add an option to specify a unit for the zero value in the SpanPrinter
For my use case I'm pretty printing spans rounded up to the minute. However when a zero span is printed it shows up in seconds instead and the only way that I see to influence that is to mark a unit as fractional.

In an ideal scenario a rounded span could propagate its lower limit to the printer to avoid having to repeat the lower bound multiple times. But I understand that this would require to store the information in the span to handle the zero case, which is probably not desirable.

There could be a method to the printer to specify the zero value unit. But this could potentially lead to mismatches between the rounded span's lowest unit and the zero value unit and it would also require the user to specify the lowest unit twice, once for the rounding and once for the printer.

Maybe a method could be added to the printer to apply the rounding to the span and print it. That would allow the zero value to be printed in the same unit as the rounding.

What are your thoughts on this? Are there better solutions for this use case?


Discussion / hints:
I definitely don't want to be coupling rounding with the printer. It was very intentional that rounding is kept separate from the printer. And especially since in a lot of cases, rounding is done as part of datetime arithmetic anyway, e.g., `Zoned::until` (which is introducing its own form of coupling borrowed from the Temporal API design).

And yeah, attaching more metadata to `Span` is a no-no I think. I suspect it wouldn't compose well. And it increases the size of a type that is already huge.

If that coupling is off the table, then I don't think I see a way to do this without repeating information. Adding a knob to the printer is easy enough to do I think. I would be fine with that. But yeah, it does require some careful attention to the zero span to get this right.

Another option aside from a printer knob is to just special case it yourself. The friendly format is really easy to format to in an ad hoc way (unless you're writing a highly configurable printer like that found in Jiff, lol):

```rust
let span = <insert rounding code>;
if span.is_zero() {
    write!(f, "0m")
} else {
    write!(f, "{span:#}")
}
```
>Another option aside from a printer knob is to just special case it yourself.

That's fair :slightly_smiling_face: It would remove a little bit of friction if jiff could do it for us, but special casing it on the user side is easy enough. Thanks!

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 631d3589..c31f25c7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,16 @@
 # CHANGELOG
 
+0.1.21 (TBD)
+============
+TODO
+
+Enhancements:
+
+* [#192](https://github.com/BurntSushi/jiff/issues/192):
+Add option to the friendly printer for setting the unit when writing a
+zero-length duration.
+
+
 0.1.20 (2025-01-03)
 ===================
 This release inclues a new type, `Pieces`, in the `jiff::fmt::temporal`
diff --git a/src/fmt/friendly/printer.rs b/src/fmt/friendly/printer.rs
index 8a4b4cdd..330ddfc1 100644
--- a/src/fmt/friendly/printer.rs
+++ b/src/fmt/friendly/printer.rs
@@ -462,6 +462,7 @@ pub struct SpanPrinter {
     hms: bool,
     padding: Option<u8>,
     precision: Option<u8>,
+    zero_unit: Unit,
 }
 
 impl SpanPrinter {
@@ -497,6 +498,7 @@ impl SpanPrinter {
             hms: false,
             padding: None,
             precision: None,
+            zero_unit: Unit::Second,
         }
     }
 
@@ -907,6 +909,65 @@ impl SpanPrinter {
         SpanPrinter { precision, ..self }
     }
 
+    /// Sets the unit to use when printing a duration that is zero.
+    ///
+    /// When [`SpanPrinter::fractional`] is set, then this setting is ignored
+    /// and the zero unit corresponds to the fractional unit specified.
+    ///
+    /// This defaults to [`Unit::Second`].
+    ///
+    /// # Example
+    ///
+    /// ```
+    /// use jiff::{fmt::friendly::{FractionalUnit, SpanPrinter}, ToSpan, Unit};
+    ///
+    /// // The default just always uses seconds.
+    /// let printer = SpanPrinter::new();
+    /// assert_eq!(printer.span_to_string(&0.years()), "0s");
+    ///
+    /// // We can set our own unit.
+    /// let printer = SpanPrinter::new().zero_unit(Unit::Year);
+    /// assert_eq!(printer.span_to_string(&0.years()), "0y");
+    ///
+    /// // But it's overridden if fractional units are set.
+    /// let printer = SpanPrinter::new()
+    ///     .zero_unit(Unit::Year)
+    ///     .fractional(Some(FractionalUnit::Minute));
+    /// assert_eq!(printer.span_to_string(&0.years()), "0m");
+    ///
+    /// // One use case for this option is if you're rounding
+    /// // spans and want the zero unit to reflect the smallest
+    /// // unit you're using.
+    /// let printer = SpanPrinter::new().zero_unit(Unit::Minute);
+    /// let span = 5.hours().minutes(30).seconds(59);
+    /// let rounded = span.round(Unit::Minute)?;
+    /// assert_eq!(printer.span_to_string(&rounded), "5h 31m");
+    ///
+    /// let span = 5.seconds();
+    /// let rounded = span.round(Unit::Minute)?;
+    /// assert_eq!(printer.span_to_string(&rounded), "0m");
+    ///
+    /// # Ok::<(), Box<dyn std::error::Error>>(())
+    /// ```
+    ///
+    /// The same applies for `SignedDuration`:
+    ///
+    /// ```
+    /// use jiff::{fmt::friendly::SpanPrinter, SignedDuration, Unit};
+    ///
+    /// // The default just always uses seconds.
+    /// let printer = SpanPrinter::new();
+    /// assert_eq!(printer.duration_to_string(&SignedDuration::ZERO), "0s");
+    ///
+    /// // We can set our own unit.
+    /// let printer = SpanPrinter::new().zero_unit(Unit::Minute);
+    /// assert_eq!(printer.duration_to_string(&SignedDuration::ZERO), "0m");
+    /// ```
+    #[inline]
+    pub const fn zero_unit(self, unit: Unit) -> SpanPrinter {
+        SpanPrinter { zero_unit: unit, ..self }
+    }
+
     /// Format a `Span` into a string using the "friendly" format.
     ///
     /// This is a convenience routine for [`SpanPrinter::print_span`] with a
@@ -1462,8 +1523,11 @@ impl<'p, 'w, W: Write> DesignatorWriter<'p, 'w, W> {
         }
         // If a fractional unit is set, then we should use that unit
         // specifically to express "zero."
-        let unit =
-            self.printer.fractional.map(Unit::from).unwrap_or(Unit::Second);
+        let unit = self
+            .printer
+            .fractional
+            .map(Unit::from)
+            .unwrap_or(self.printer.zero_unit);
         self.wtr.write_int(&self.fmtint, 0)?;
         self.wtr
             .write_str(self.printer.spacing.between_units_and_designators())?;


Test patch: