From 4cfee1af743fcdcb23ceadb1bd21423d7a89b82f Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Thu, 6 Oct 2022 17:08:13 +0200 Subject: [PATCH] Use Custom Formatting for Integer Parts of Times Going through the standard library's formatter is usually quite slow. And usually we want to show either minutes or seconds that are in the range from 0 to 59, or even tenths from 0 to 99. For those we can just define a lookup table where we look up the formatted strings and directly call `write_str` on the formatter instead of going through the `write!` macro, which would go through a lot more setup. Additionally we already have `itoa` as an indirect dependency which we now directly use for formatting integers that are not bound that way. `itoa` generally does the same algorithm as `std` but does so without going through any formatting machinery and is thus a lot faster, but also much less customizable. Overall this and #576 together result in a `~3.86x` performance improvement when formatting a time. | When | Time | |-------------|------------:| | Both PRs | `59.205 ns` | | Previous PR | `154.63 ns` | | Before | `228.47 ns` | --- Cargo.toml | 1 + src/timing/formatter/accuracy.rs | 106 +++++++++++++++++++++++++-- src/timing/formatter/complete.rs | 28 +++++-- src/timing/formatter/days.rs | 23 ++++-- src/timing/formatter/delta.rs | 30 +++++--- src/timing/formatter/mod.rs | 32 +++++++- src/timing/formatter/none_wrapper.rs | 4 +- src/timing/formatter/regular.rs | 28 +++---- src/timing/formatter/segment_time.rs | 38 +++++----- src/timing/formatter/timer.rs | 43 ++++++++--- 10 files changed, 257 insertions(+), 76 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 99a48cf01..97ab1b25f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ members = ["capi", "capi/bind_gen", "crates/*"] base64 = { version = "0.13.0", default-features = false, features = ["alloc"] } bytemuck = { version = "1.9.1", default-features = false, features = ["derive"] } cfg-if = "1.0.0" +itoa = { version = "1.0.3", default-features = false } time = { version = "0.3.3", default-features = false } hashbrown = "0.12.0" libm = "0.2.1" diff --git a/src/timing/formatter/accuracy.rs b/src/timing/formatter/accuracy.rs index bdd92b6d7..74b974353 100644 --- a/src/timing/formatter/accuracy.rs +++ b/src/timing/formatter/accuracy.rs @@ -16,29 +16,119 @@ pub enum Accuracy { impl Accuracy { /// Formats the nanoseconds provided with the chosen accuracy. - pub const fn format_nanoseconds(self, nanoseconds: u32) -> FormattedSeconds { - FormattedSeconds { + pub const fn format_nanoseconds(self, nanoseconds: u32) -> FractionalPart { + FractionalPart { accuracy: self, nanoseconds, } } } -use core::fmt::{Display, Formatter, Result}; +use core::{ + fmt::{Display, Formatter, Result}, + str, +}; + +use crate::timing::formatter::{format_padded, NANOS_PER_MILLI, NANOS_PER_TENTH}; + +use super::NANOS_PER_HUNDREDTH; #[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub struct FormattedSeconds { +pub struct FractionalPart { accuracy: Accuracy, nanoseconds: u32, } -impl Display for FormattedSeconds { +impl Display for FractionalPart { fn fmt(&self, f: &mut Formatter<'_>) -> Result { match self.accuracy { Accuracy::Seconds => Ok(()), - Accuracy::Tenths => write!(f, ".{}", self.nanoseconds / 100_000_000), - Accuracy::Hundredths => write!(f, ".{:02}", self.nanoseconds / 10_000_000), - Accuracy::Milliseconds => write!(f, ".{:03}", self.nanoseconds / 1_000_000), + Accuracy::Tenths => { + f.write_str(".")?; + let v = (self.nanoseconds / NANOS_PER_TENTH) as u8; + assert!(v < 10); + // SAFETY: We ensured the value is between 0 and 10, so adding + // that on top of ASCII '0' ensures us that we get an ASCII + // digit. + unsafe { f.write_str(str::from_utf8_unchecked(&[v + b'0'])) } + } + Accuracy::Hundredths => { + f.write_str(".")?; + f.write_str(format_padded( + (self.nanoseconds / NANOS_PER_HUNDREDTH) as u8, + )) + } + Accuracy::Milliseconds => { + f.write_str(".")?; + let first = (self.nanoseconds / NANOS_PER_TENTH) as u8; + let second_and_third = + ((self.nanoseconds % NANOS_PER_TENTH) / NANOS_PER_MILLI) as u8; + assert!(first < 10); + // SAFETY: We ensured the value is between 0 and 10, so adding + // that on top of ASCII '0' ensures us that we get an ASCII + // digit. + unsafe { + f.write_str(str::from_utf8_unchecked(&[first + b'0']))?; + } + f.write_str(format_padded(second_and_third)) + } } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn format_seconds() { + let acc = Accuracy::Seconds; + assert_eq!(acc.format_nanoseconds(0).to_string(), ""); + assert_eq!(acc.format_nanoseconds(1).to_string(), ""); + assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), ""); + assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), ""); + assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), ""); + assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), ""); + assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), ""); + assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), ""); + } + + #[test] + fn format_tenths() { + let acc = Accuracy::Tenths; + assert_eq!(acc.format_nanoseconds(0).to_string(), ".0"); + assert_eq!(acc.format_nanoseconds(1).to_string(), ".0"); + assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), ".7"); + assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), ".0"); + assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), ".0"); + assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), ".7"); + assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), ".1"); + assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), ".9"); + } + + #[test] + fn format_hundredths() { + let acc = Accuracy::Hundredths; + assert_eq!(acc.format_nanoseconds(0).to_string(), ".00"); + assert_eq!(acc.format_nanoseconds(1).to_string(), ".00"); + assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), ".78"); + assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), ".00"); + assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), ".07"); + assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), ".70"); + assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), ".10"); + assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), ".99"); + } + + #[test] + fn format_milliseconds() { + let acc = Accuracy::Milliseconds; + assert_eq!(acc.format_nanoseconds(0).to_string(), ".000"); + assert_eq!(acc.format_nanoseconds(1).to_string(), ".000"); + assert_eq!(acc.format_nanoseconds(789_654_321).to_string(), ".789"); + assert_eq!(acc.format_nanoseconds(7_654_321).to_string(), ".007"); + assert_eq!(acc.format_nanoseconds(70_654_321).to_string(), ".070"); + assert_eq!(acc.format_nanoseconds(700_654_321).to_string(), ".700"); + assert_eq!(acc.format_nanoseconds(109_654_321).to_string(), ".109"); + assert_eq!(acc.format_nanoseconds(999_999_999).to_string(), ".999"); + } +} diff --git a/src/timing/formatter/complete.rs b/src/timing/formatter/complete.rs index 2fc8640b8..429b1389a 100644 --- a/src/timing/formatter/complete.rs +++ b/src/timing/formatter/complete.rs @@ -1,4 +1,7 @@ -use super::{TimeFormatter, ASCII_MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE}; +use super::{ + format_padded, TimeFormatter, ASCII_MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR, + SECONDS_PER_MINUTE, +}; use crate::TimeSpan; use core::fmt::{Display, Formatter, Result}; @@ -57,14 +60,27 @@ impl Display for Inner { // calculate all of them in parallel. On top of that they are // integer divisions of known constants, which get turned into // multiplies and shifts, which is very fast. - let seconds = total_seconds % SECONDS_PER_MINUTE; - let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE; - let hours = (total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR; + let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8; + let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8; + let hours = ((total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR) as u8; let days = total_seconds / SECONDS_PER_DAY; + + let mut buffer = itoa::Buffer::new(); + if days > 0 { - write!(f, "{days}.")?; + f.write_str(buffer.format(days))?; + f.write_str(".")?; } - write!(f, "{hours:02}:{minutes:02}:{seconds:02}.{nanoseconds:09}") + + f.write_str(format_padded(hours))?; + f.write_str(":")?; + f.write_str(format_padded(minutes))?; + f.write_str(":")?; + f.write_str(format_padded(seconds))?; + f.write_str(".")?; + let nanoseconds = buffer.format(nanoseconds); + f.write_str(&"000000000"[nanoseconds.len()..])?; + f.write_str(nanoseconds) } else { f.write_str("00:00:00.000000000") } diff --git a/src/timing/formatter/days.rs b/src/timing/formatter/days.rs index d74212023..e863f2c39 100644 --- a/src/timing/formatter/days.rs +++ b/src/timing/formatter/days.rs @@ -1,4 +1,6 @@ -use super::{TimeFormatter, MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE}; +use super::{ + format_padded, TimeFormatter, MINUS, SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE, +}; use crate::TimeSpan; use core::fmt::{Display, Formatter, Result}; @@ -54,20 +56,27 @@ impl Display for Inner { // calculate all of them in parallel. On top of that they are // integer divisions of known constants, which get turned into // multiplies and shifts, which is very fast. - let seconds = total_seconds % SECONDS_PER_MINUTE; - let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE; - let hours = (total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR; + let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8; + let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8; + let hours = ((total_seconds % SECONDS_PER_DAY) / SECONDS_PER_HOUR) as u8; let days = total_seconds / SECONDS_PER_DAY; + let mut buffer = itoa::Buffer::new(); + if days > 0 { - write!(f, "{days}d ")?; + f.write_str(buffer.format(days))?; + f.write_str("d ")?; } if days > 0 || hours > 0 { - write!(f, "{hours}:{minutes:02}:{seconds:02}") + f.write_str(buffer.format(hours))?; + f.write_str(":")?; + f.write_str(format_padded(minutes))?; } else { - write!(f, "{minutes}:{seconds:02}") + f.write_str(buffer.format(minutes))?; } + f.write_str(":")?; + f.write_str(format_padded(seconds)) } else { f.write_str("0:00") } diff --git a/src/timing/formatter/delta.rs b/src/timing/formatter/delta.rs index b46b1b33e..5c60b306c 100644 --- a/src/timing/formatter/delta.rs +++ b/src/timing/formatter/delta.rs @@ -1,4 +1,6 @@ -use super::{Accuracy, TimeFormatter, DASH, MINUS, PLUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE}; +use super::{ + format_padded, Accuracy, TimeFormatter, DASH, MINUS, PLUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE, +}; use crate::TimeSpan; use core::fmt::{Display, Formatter, Result}; @@ -83,22 +85,28 @@ impl Display for Inner { // calculate all of them in parallel. On top of that they are // integer divisions of known constants, which get turned into // multiplies and shifts, which is very fast. - let seconds = total_seconds % SECONDS_PER_MINUTE; - let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE; + let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8; + let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8; let hours = total_seconds / SECONDS_PER_HOUR; + + let mut buffer = itoa::Buffer::new(); + if hours > 0 { - write!(f, "{hours}:{minutes:02}:{seconds:02}")?; + f.write_str(buffer.format(hours))?; + f.write_str(":")?; + f.write_str(format_padded(minutes))?; + f.write_str(":")?; + f.write_str(format_padded(seconds))?; } else if minutes > 0 { - write!(f, "{minutes}:{seconds:02}")?; + f.write_str(buffer.format(minutes))?; + f.write_str(":")?; + f.write_str(format_padded(seconds))?; } else { - return write!( - f, - "{seconds}{}", - self.accuracy.format_nanoseconds(nanoseconds) - ); + f.write_str(buffer.format(seconds))?; + return self.accuracy.format_nanoseconds(nanoseconds).fmt(f); } if !self.drop_decimals { - write!(f, "{}", self.accuracy.format_nanoseconds(nanoseconds)) + self.accuracy.format_nanoseconds(nanoseconds).fmt(f) } else { Ok(()) } diff --git a/src/timing/formatter/mod.rs b/src/timing/formatter/mod.rs index 8a34c8e9c..7dab34cbd 100644 --- a/src/timing/formatter/mod.rs +++ b/src/timing/formatter/mod.rs @@ -36,7 +36,7 @@ pub use self::{ }; use crate::TimeSpan; -use core::fmt::Display; +use core::{fmt::Display, str}; /// Time Formatters can be used to format optional Time Spans in various ways. pub trait TimeFormatter<'a> { @@ -63,3 +63,33 @@ pub const PLUS: &str = "+"; const SECONDS_PER_MINUTE: u64 = 60; const SECONDS_PER_HOUR: u64 = 60 * SECONDS_PER_MINUTE; const SECONDS_PER_DAY: u64 = 24 * SECONDS_PER_HOUR; + +const NANOS_PER_MILLI: u32 = 1_000_000; +const NANOS_PER_HUNDREDTH: u32 = 10_000_000; +const NANOS_PER_TENTH: u32 = 100_000_000; + +#[rustfmt::skip] +static LOOKUP: [[u8; 2]; 100] = [ + *b"00", *b"01", *b"02", *b"03", *b"04", *b"05", *b"06", *b"07", *b"08", *b"09", + *b"10", *b"11", *b"12", *b"13", *b"14", *b"15", *b"16", *b"17", *b"18", *b"19", + *b"20", *b"21", *b"22", *b"23", *b"24", *b"25", *b"26", *b"27", *b"28", *b"29", + *b"30", *b"31", *b"32", *b"33", *b"34", *b"35", *b"36", *b"37", *b"38", *b"39", + *b"40", *b"41", *b"42", *b"43", *b"44", *b"45", *b"46", *b"47", *b"48", *b"49", + *b"50", *b"51", *b"52", *b"53", *b"54", *b"55", *b"56", *b"57", *b"58", *b"59", + *b"60", *b"61", *b"62", *b"63", *b"64", *b"65", *b"66", *b"67", *b"68", *b"69", + *b"70", *b"71", *b"72", *b"73", *b"74", *b"75", *b"76", *b"77", *b"78", *b"79", + *b"80", *b"81", *b"82", *b"83", *b"84", *b"85", *b"86", *b"87", *b"88", *b"89", + *b"90", *b"91", *b"92", *b"93", *b"94", *b"95", *b"96", *b"97", *b"98", *b"99", +]; + +#[inline(always)] +fn format_padded(x: u8) -> &'static str { + // SAFETY: The lookup table is always initialized with valid UTF-8. + unsafe { str::from_utf8_unchecked(&LOOKUP[x as usize]) } +} + +#[inline(always)] +fn format_unpadded(x: u8) -> &'static str { + // SAFETY: The lookup table is always initialized with valid UTF-8. + unsafe { str::from_utf8_unchecked(&LOOKUP[x as usize][(x < 10) as usize..]) } +} diff --git a/src/timing/formatter/none_wrapper.rs b/src/timing/formatter/none_wrapper.rs index 3db4b8836..41d44034f 100644 --- a/src/timing/formatter/none_wrapper.rs +++ b/src/timing/formatter/none_wrapper.rs @@ -65,9 +65,9 @@ impl<'a, F: 'a + TimeFormatter<'a>, S: 'a + AsRef> TimeFormatter<'a> for No impl<'a, F: TimeFormatter<'a>, S: 'a + AsRef> Display for Inner<'a, F, S> { fn fmt(&self, f: &mut Formatter<'_>) -> Result { if self.time.is_none() { - write!(f, "{}", self.wrapper.1.as_ref()) + self.wrapper.1.as_ref().fmt(f) } else { - write!(f, "{}", self.wrapper.0.format(self.time)) + self.wrapper.0.format(self.time).fmt(f) } } } diff --git a/src/timing/formatter/regular.rs b/src/timing/formatter/regular.rs index b5c6aaa6e..b329442f8 100644 --- a/src/timing/formatter/regular.rs +++ b/src/timing/formatter/regular.rs @@ -1,4 +1,6 @@ -use super::{Accuracy, TimeFormatter, DASH, MINUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE}; +use super::{ + format_padded, Accuracy, TimeFormatter, DASH, MINUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE, +}; use crate::TimeSpan; use core::fmt::{Display, Formatter, Result}; @@ -76,22 +78,22 @@ impl Display for Inner { // calculate all of them in parallel. On top of that they are // integer divisions of known constants, which get turned into // multiplies and shifts, which is very fast. - let seconds = total_seconds % SECONDS_PER_MINUTE; - let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE; + let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8; + let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8; let hours = total_seconds / SECONDS_PER_HOUR; + + let mut buffer = itoa::Buffer::new(); + if hours > 0 { - write!( - f, - "{hours}:{minutes:02}:{seconds:02}{}", - self.accuracy.format_nanoseconds(nanoseconds) - ) + f.write_str(buffer.format(hours))?; + f.write_str(":")?; + f.write_str(format_padded(minutes))?; } else { - write!( - f, - "{minutes}:{seconds:02}{}", - self.accuracy.format_nanoseconds(nanoseconds) - ) + f.write_str(buffer.format(minutes))?; } + f.write_str(":")?; + f.write_str(format_padded(seconds))?; + self.accuracy.format_nanoseconds(nanoseconds).fmt(f) } else { f.write_str(DASH) } diff --git a/src/timing/formatter/segment_time.rs b/src/timing/formatter/segment_time.rs index 47f8902e4..d7f1980be 100644 --- a/src/timing/formatter/segment_time.rs +++ b/src/timing/formatter/segment_time.rs @@ -1,4 +1,6 @@ -use super::{Accuracy, TimeFormatter, DASH, MINUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE}; +use super::{ + format_padded, Accuracy, TimeFormatter, DASH, MINUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE, +}; use crate::TimeSpan; use core::fmt::{Display, Formatter, Result}; @@ -74,28 +76,26 @@ impl Display for Inner { // calculate all of them in parallel. On top of that they are // integer divisions of known constants, which get turned into // multiplies and shifts, which is very fast. - let seconds = total_seconds % SECONDS_PER_MINUTE; - let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE; + let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8; + let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8; let hours = total_seconds / SECONDS_PER_HOUR; + + let mut buffer = itoa::Buffer::new(); + if hours > 0 { - write!( - f, - "{hours}:{minutes:02}:{seconds:02}{}", - self.accuracy.format_nanoseconds(nanoseconds) - ) + f.write_str(buffer.format(hours))?; + f.write_str(":")?; + f.write_str(format_padded(minutes))?; + f.write_str(":")?; + f.write_str(format_padded(seconds))?; } else if minutes > 0 { - write!( - f, - "{minutes}:{seconds:02}{}", - self.accuracy.format_nanoseconds(nanoseconds) - ) + f.write_str(buffer.format(minutes))?; + f.write_str(":")?; + f.write_str(format_padded(seconds))?; } else { - write!( - f, - "{seconds}{}", - self.accuracy.format_nanoseconds(nanoseconds) - ) + f.write_str(buffer.format(seconds))?; } + self.accuracy.format_nanoseconds(nanoseconds).fmt(f) } else { f.write_str(DASH) } @@ -106,6 +106,8 @@ impl Display for Inner { fn test() { let time = "4:20.69".parse::().unwrap(); let formatted = SegmentTime::new().format(time).to_string(); + // FIXME: The parser should use integer parsing as well, and then this + // doesn't need to specialize bad floating point behavior anymore. assert!( // Modern processors formatted == "4:20.69" || diff --git a/src/timing/formatter/timer.rs b/src/timing/formatter/timer.rs index f71c587e8..5993a121b 100644 --- a/src/timing/formatter/timer.rs +++ b/src/timing/formatter/timer.rs @@ -3,7 +3,8 @@ //! is the Time Formatter pair used by the Timer Component. use super::{ - Accuracy, DigitsFormat, TimeFormatter, DASH, MINUS, SECONDS_PER_HOUR, SECONDS_PER_MINUTE, + format_padded, format_unpadded, Accuracy, DigitsFormat, TimeFormatter, DASH, MINUS, + SECONDS_PER_HOUR, SECONDS_PER_MINUTE, }; use crate::TimeSpan; use core::fmt::{Display, Formatter, Result}; @@ -75,21 +76,43 @@ impl Display for TimeInner { } else { total_seconds as u64 }; - let seconds = total_seconds % SECONDS_PER_MINUTE; - let minutes = (total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE; + // These are intentionally not data dependent, such that the CPU can + // calculate all of them in parallel. On top of that they are + // integer divisions of known constants, which get turned into + // multiplies and shifts, which is very fast. + let seconds = (total_seconds % SECONDS_PER_MINUTE) as u8; + let minutes = ((total_seconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE) as u8; let hours = total_seconds / SECONDS_PER_HOUR; + if self.digits_format == DigitsFormat::DoubleDigitHours { - write!(f, "{hours:02}:{minutes:02}:{seconds:02}") + let mut buffer = itoa::Buffer::new(); + let hours = buffer.format(hours); + if hours.len() < 2 { + f.write_str("0")?; + } + f.write_str(hours)?; + f.write_str(":")?; + f.write_str(format_padded(minutes))?; + f.write_str(":")?; + f.write_str(format_padded(seconds)) } else if hours > 0 || self.digits_format == DigitsFormat::SingleDigitHours { - write!(f, "{hours}:{minutes:02}:{seconds:02}") + f.write_str(itoa::Buffer::new().format(hours))?; + f.write_str(":")?; + f.write_str(format_padded(minutes))?; + f.write_str(":")?; + f.write_str(format_padded(seconds)) } else if self.digits_format == DigitsFormat::DoubleDigitMinutes { - write!(f, "{minutes:02}:{seconds:02}") + f.write_str(format_padded(minutes))?; + f.write_str(":")?; + f.write_str(format_padded(seconds)) } else if minutes > 0 || self.digits_format == DigitsFormat::SingleDigitMinutes { - write!(f, "{minutes}:{seconds:02}") + f.write_str(format_unpadded(minutes))?; + f.write_str(":")?; + f.write_str(format_padded(seconds)) } else if self.digits_format == DigitsFormat::DoubleDigitSeconds { - write!(f, "{seconds:02}") + f.write_str(format_padded(seconds)) } else { - write!(f, "{seconds}") + f.write_str(format_unpadded(seconds)) } } else { f.write_str(DASH) @@ -159,7 +182,7 @@ impl Display for FractionInner { fn fmt(&self, f: &mut Formatter<'_>) -> Result { if let Some(time) = self.time { let nanoseconds = time.to_duration().subsec_nanoseconds().unsigned_abs(); - write!(f, "{}", self.accuracy.format_nanoseconds(nanoseconds)) + self.accuracy.format_nanoseconds(nanoseconds).fmt(f) } else { Ok(()) }