From 796f93f7f45c7dc387ce49305154d29a4f511110 Mon Sep 17 00:00:00 2001 From: "Jan Alexander Steffens (heftig)" Date: Thu, 28 Jan 2021 18:02:21 +0100 Subject: [PATCH] gstreamer/clock_time: Improve Display formatting - Add support for precision, padding and alignment. - Format the invalid time using dashes (`--:--:--.---------`) instead of a technically valid `99:99:99.999999999` The latter is something C's `GST_TIME_FORMAT` can't do, though something obviously invalid like `-1:-1:-1.000000000` would be possible. --- gstreamer/Cargo.toml | 1 + gstreamer/src/clock_time.rs | 283 ++++++++++++++++++++++++++++++++---- 2 files changed, 252 insertions(+), 32 deletions(-) diff --git a/gstreamer/Cargo.toml b/gstreamer/Cargo.toml index f13daf78f..5dd5e2b00 100644 --- a/gstreamer/Cargo.toml +++ b/gstreamer/Cargo.toml @@ -19,6 +19,7 @@ cfg-if = "1.0" libc = "0.2" ffi = { package = "gstreamer-sys", path = "sys", features = ["v1_8"] } glib = { git = "https://github.com/gtk-rs/gtk-rs" } +num-integer = { version = "0.1", default-features = false, features = [] } num-rational = { version = "0.3", default-features = false, features = [] } once_cell = "1.0" futures-core = "0.3" diff --git a/gstreamer/src/clock_time.rs b/gstreamer/src/clock_time.rs index e2c14d7bc..42415155c 100644 --- a/gstreamer/src/clock_time.rs +++ b/gstreamer/src/clock_time.rs @@ -1,8 +1,10 @@ // Take a look at the license at the top of the repository in the LICENSE file. use glib::translate::*; +use num_integer::div_rem; +use std::io::{self, prelude::*}; use std::time::Duration; -use std::{cmp, convert, fmt}; +use std::{cmp, convert, fmt, str}; #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, Default)] pub struct ClockTime(pub Option); @@ -125,44 +127,157 @@ macro_rules! impl_common_ops_for_opt_int( impl_common_ops_for_opt_int!(ClockTime); +/// Tell [`pad_clocktime`] what kind of time we're formatting +enum Sign { + /// An invalid time (`None`) + Invalid, + + /// A non-negative time (zero or greater) + NonNegative, + + // For a future ClockTimeDiff formatting + #[allow(dead_code)] + /// A negative time (below zero) + Negative, +} + +// Derived from libcore `Formatter::pad_integral` (same APACHE v2 + MIT licenses) +// +// TODO: Would be useful for formatting ClockTimeDiff +// if it was a new type instead of an alias for i64 +// +/// Performs the correct padding for a clock time which has already been +/// emitted into a str, as by [`write_clocktime`]. The str should *not* +/// contain the sign; that will be added by this method. +fn pad_clocktime(f: &mut fmt::Formatter<'_>, sign: Sign, buf: &str) -> fmt::Result { + skip_assert_initialized!(); + use self::Sign::*; + use std::fmt::{Alignment, Write}; + + // Start by determining how we're padding, gathering + // settings from the Formatter and the Sign + + // Choose the fill character + let sign_aware_zero_pad = f.sign_aware_zero_pad(); + let fill_char = match sign { + Invalid if sign_aware_zero_pad => '-', // Zero-padding an invalid time + _ if sign_aware_zero_pad => '0', // Zero-padding a valid time + _ => f.fill(), // Otherwise, pad with the user-chosen character + }; + + // Choose the sign character + let sign_plus = f.sign_plus(); + let sign_char = match sign { + Invalid if sign_plus => Some(fill_char), // User requested sign, time is invalid + NonNegative if sign_plus => Some('+'), // User requested sign, time is zero or above + Negative => Some('-'), // Time is below zero + _ => None, // Otherwise, add no sign + }; + + // Our minimum width is the value's width, plus 1 for the sign if present + let width = buf.len() + sign_char.map_or(0, |_| 1); + + // Subtract the minimum width from the requested width to get the padding, + // taking care not to allow wrapping due to underflow + let padding = f.width().unwrap_or(0).saturating_sub(width); + + // Split the required padding into the three possible parts + let align = f.align().unwrap_or(Alignment::Right); + let (pre_padding, zero_padding, post_padding) = match align { + _ if sign_aware_zero_pad => (0, padding, 0), // Zero-padding: Pad between sign and value + Alignment::Left => (0, 0, padding), // Align left: Pad on the right side + Alignment::Right => (padding, 0, 0), // Align right: Pad on the left side + + // Align center: Split equally between left and right side + // If the required padding is odd, the right side gets one more char + Alignment::Center => (padding / 2, 0, (padding + 1) / 2), + }; + + // And now for the actual writing + + for _ in 0..pre_padding { + f.write_char(fill_char)?; // Left padding + } + if let Some(c) = sign_char { + f.write_char(c)?; // ------- Sign character + } + for _ in 0..zero_padding { + f.write_char(fill_char)?; // Padding between sign and value + } + f.write_str(buf)?; // ---------- Value + for _ in 0..post_padding { + f.write_char(fill_char)?; // Right padding + } + + Ok(()) +} + +/// Writes an unpadded, signless clocktime string with the given precision +fn write_clocktime( + mut writer: W, + clocktime: Option, + precision: usize, +) -> io::Result<()> { + skip_assert_initialized!(); + let precision = cmp::min(9, precision); + + if let Some(ns) = clocktime { + // Valid time + + // Split the time into parts + let (s, ns) = div_rem(ns, 1_000_000_000); + let (m, s) = div_rem(s, 60); + let (h, m) = div_rem(m, 60); + + // Write HH:MM:SS + write!(writer, "{}:{:02}:{:02}", h, m, s)?; + + if precision > 0 { + // Format the nanoseconds into a stack-allocated string + // The value is zero-padded so always 9 digits long + let mut buf = [0u8; 9]; + write!(&mut buf[..], "{:09}", ns).unwrap(); + let buf_str = str::from_utf8(&buf[..]).unwrap(); + + // Write decimal point and a prefix of the nanoseconds for more precision + write!(writer, ".{:.p$}", buf_str, p = precision)?; + } + } else { + // Invalid time + + // Write HH:MM:SS, but invalid + write!(writer, "--:--:--")?; + + if precision > 0 { + // Write decimal point and dashes for more precision + write!(writer, ".{:->p$}", "", p = precision)?; + } + } + + Ok(()) +} + impl fmt::Display for ClockTime { - #[allow(clippy::many_single_char_names)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let precision = f.precision().unwrap_or(9); - // TODO: Could also check width and pad the hours as needed - let (h, m, s, ns) = match self.0 { - Some(v) => { - let mut s = v / 1_000_000_000; - let mut m = s / 60; - let h = m / 60; - s %= 60; - m %= 60; - let ns = v % 1_000_000_000; + // What the maximum time (u64::MAX - 1) would format to + const MAX_SIZE: usize = "5124095:34:33.709551614".len(); - (h, m, s, ns) - } - None => (99, 99, 99, 999_999_999), + // Write the unpadded clocktime value into a stack-allocated string + let mut buf = [0u8; MAX_SIZE]; + let mut cursor = io::Cursor::new(&mut buf[..]); + write_clocktime(&mut cursor, self.0, precision).unwrap(); + let pos = cursor.position() as usize; + let buf_str = str::from_utf8(&buf[..pos]).unwrap(); + + let sign = if self.0.is_some() { + Sign::NonNegative + } else { + Sign::Invalid }; - if precision == 0 { - f.write_fmt(format_args!("{:02}:{:02}:{:02}", h, m, s)) - } else { - let mut divisor = 1; - let precision = cmp::min(precision, 9); - for _ in 0..(9 - precision) { - divisor *= 10; - } - - f.write_fmt(format_args!( - "{:02}:{:02}:{:02}.{:0width$}", - h, - m, - s, - ns / divisor, - width = precision - )) - } + pad_clocktime(f, sign, buf_str) } } @@ -396,4 +511,108 @@ mod tests { assert!(ct_none.max(ct_10).is_none()); assert!(ct_20.max(ct_none).is_none()); } + + #[test] + fn display() { + let none = ClockTime::none(); + let some = ClockTime::from_nseconds(45834908569837); + let lots = ClockTime::from_nseconds(std::u64::MAX - 1); + + // Simple + + assert_eq!(format!("{:.0}", none), "--:--:--"); + assert_eq!(format!("{:.3}", none), "--:--:--.---"); + assert_eq!(format!("{}", none), "--:--:--.---------"); + + assert_eq!(format!("{:.0}", some), "12:43:54"); + assert_eq!(format!("{:.3}", some), "12:43:54.908"); + assert_eq!(format!("{}", some), "12:43:54.908569837"); + + assert_eq!(format!("{:.0}", lots), "5124095:34:33"); + assert_eq!(format!("{:.3}", lots), "5124095:34:33.709"); + assert_eq!(format!("{}", lots), "5124095:34:33.709551614"); + + // Precision caps at 9 + assert_eq!(format!("{:.10}", none), "--:--:--.---------"); + assert_eq!(format!("{:.10}", some), "12:43:54.908569837"); + assert_eq!(format!("{:.10}", lots), "5124095:34:33.709551614"); + + // Short width + + assert_eq!(format!("{:4.0}", none), "--:--:--"); + assert_eq!(format!("{:4.3}", none), "--:--:--.---"); + assert_eq!(format!("{:4}", none), "--:--:--.---------"); + + assert_eq!(format!("{:4.0}", some), "12:43:54"); + assert_eq!(format!("{:4.3}", some), "12:43:54.908"); + assert_eq!(format!("{:4}", some), "12:43:54.908569837"); + + assert_eq!(format!("{:4.0}", lots), "5124095:34:33"); + assert_eq!(format!("{:4.3}", lots), "5124095:34:33.709"); + assert_eq!(format!("{:4}", lots), "5124095:34:33.709551614"); + + // Simple padding + + assert_eq!(format!("{:>9.0}", none), " --:--:--"); + assert_eq!(format!("{:<9.0}", none), "--:--:-- "); + assert_eq!(format!("{:^10.0}", none), " --:--:-- "); + assert_eq!(format!("{:>13.3}", none), " --:--:--.---"); + assert_eq!(format!("{:<13.3}", none), "--:--:--.--- "); + assert_eq!(format!("{:^14.3}", none), " --:--:--.--- "); + assert_eq!(format!("{:>19}", none), " --:--:--.---------"); + assert_eq!(format!("{:<19}", none), "--:--:--.--------- "); + assert_eq!(format!("{:^20}", none), " --:--:--.--------- "); + + assert_eq!(format!("{:>9.0}", some), " 12:43:54"); + assert_eq!(format!("{:<9.0}", some), "12:43:54 "); + assert_eq!(format!("{:^10.0}", some), " 12:43:54 "); + assert_eq!(format!("{:>13.3}", some), " 12:43:54.908"); + assert_eq!(format!("{:<13.3}", some), "12:43:54.908 "); + assert_eq!(format!("{:^14.3}", some), " 12:43:54.908 "); + assert_eq!(format!("{:>19}", some), " 12:43:54.908569837"); + assert_eq!(format!("{:<19}", some), "12:43:54.908569837 "); + assert_eq!(format!("{:^20}", some), " 12:43:54.908569837 "); + + assert_eq!(format!("{:>14.0}", lots), " 5124095:34:33"); + assert_eq!(format!("{:<14.0}", lots), "5124095:34:33 "); + assert_eq!(format!("{:^15.0}", lots), " 5124095:34:33 "); + assert_eq!(format!("{:>18.3}", lots), " 5124095:34:33.709"); + assert_eq!(format!("{:<18.3}", lots), "5124095:34:33.709 "); + assert_eq!(format!("{:^19.3}", lots), " 5124095:34:33.709 "); + assert_eq!(format!("{:>24}", lots), " 5124095:34:33.709551614"); + assert_eq!(format!("{:<24}", lots), "5124095:34:33.709551614 "); + assert_eq!(format!("{:^25}", lots), " 5124095:34:33.709551614 "); + + // Padding with sign or zero-extension + + assert_eq!(format!("{:+11.0}", none), " --:--:--"); + assert_eq!(format!("{:011.0}", none), "-----:--:--"); + assert_eq!(format!("{:+011.0}", none), "-----:--:--"); + assert_eq!(format!("{:+15.3}", none), " --:--:--.---"); + assert_eq!(format!("{:015.3}", none), "-----:--:--.---"); + assert_eq!(format!("{:+015.3}", none), "-----:--:--.---"); + assert_eq!(format!("{:+21}", none), " --:--:--.---------"); + assert_eq!(format!("{:021}", none), "-----:--:--.---------"); + assert_eq!(format!("{:+021}", none), "-----:--:--.---------"); + + assert_eq!(format!("{:+11.0}", some), " +12:43:54"); + assert_eq!(format!("{:011.0}", some), "00012:43:54"); + assert_eq!(format!("{:+011.0}", some), "+0012:43:54"); + assert_eq!(format!("{:+15.3}", some), " +12:43:54.908"); + assert_eq!(format!("{:015.3}", some), "00012:43:54.908"); + assert_eq!(format!("{:+015.3}", some), "+0012:43:54.908"); + assert_eq!(format!("{:+21}", some), " +12:43:54.908569837"); + assert_eq!(format!("{:021}", some), "00012:43:54.908569837"); + assert_eq!(format!("{:+021}", some), "+0012:43:54.908569837"); + + assert_eq!(format!("{:+16.0}", lots), " +5124095:34:33"); + assert_eq!(format!("{:016.0}", lots), "0005124095:34:33"); + assert_eq!(format!("{:+016.0}", lots), "+005124095:34:33"); + assert_eq!(format!("{:+20.3}", lots), " +5124095:34:33.709"); + assert_eq!(format!("{:020.3}", lots), "0005124095:34:33.709"); + assert_eq!(format!("{:+020.3}", lots), "+005124095:34:33.709"); + assert_eq!(format!("{:+26}", lots), " +5124095:34:33.709551614"); + assert_eq!(format!("{:026}", lots), "0005124095:34:33.709551614"); + assert_eq!(format!("{:+026}", lots), "+005124095:34:33.709551614"); + } }