From 6e422b728e902c263107cec5af31d70eba991ac8 Mon Sep 17 00:00:00 2001 From: alexander Date: Thu, 16 Oct 2025 05:11:44 +0300 Subject: [PATCH] df: add tracing zero and rounding (#8685) --- src/uu/df/src/blocks.rs | 204 +++++++++++++++++++++++++++++++-------- src/uu/df/src/table.rs | 36 +++++-- tests/by-util/test_df.rs | 24 +++++ 3 files changed, 214 insertions(+), 50 deletions(-) diff --git a/src/uu/df/src/blocks.rs b/src/uu/df/src/blocks.rs index 57301ba5f..d6842f369 100644 --- a/src/uu/df/src/blocks.rs +++ b/src/uu/df/src/blocks.rs @@ -77,7 +77,13 @@ impl SuffixType { /// Convert a number into a magnitude and a multi-byte unit suffix. /// /// The returned string has a maximum length of 5 chars, for example: "1.1kB", "999kB", "1MB". -pub(crate) fn to_magnitude_and_suffix(n: u128, suffix_type: SuffixType) -> String { +/// `add_tracing_zero` allows to add tracing zero for values in 0 < x <= 9 +/// +pub(crate) fn to_magnitude_and_suffix( + n: u128, + suffix_type: SuffixType, + add_tracing_zero: bool, +) -> String { let bases = suffix_type.bases(); let suffixes = suffix_type.suffixes(); let mut i = 0; @@ -91,14 +97,25 @@ pub(crate) fn to_magnitude_and_suffix(n: u128, suffix_type: SuffixType) -> Strin let suffix = suffixes[i]; if rem == 0 { - format!("{quot}{suffix}") + if add_tracing_zero && !suffix.is_empty() && quot != 0 && quot <= 9 { + format!("{quot}.0{suffix}") + } else { + format!("{quot}{suffix}") + } } else { let tenths_place = rem / (bases[i] / 10); - if rem % (bases[i] / 10) == 0 { + if quot >= 100 && rem > 0 { + format!("{}{suffix}", quot + 1) + } else if rem % (bases[i] / 10) == 0 { format!("{quot}.{tenths_place}{suffix}") } else if tenths_place + 1 == 10 || quot >= 10 { - format!("{}{suffix}", quot + 1) + let quot = quot + 1; + if add_tracing_zero && !suffix.is_empty() && quot <= 9 { + format!("{quot}.0{suffix}") + } else { + format!("{quot}{suffix}") + } } else { format!("{quot}.{}{suffix}", tenths_place + 1) } @@ -150,6 +167,18 @@ impl BlockSize { Self::Bytes(n) => n, } } + + pub(crate) fn to_header(&self) -> String { + match self { + Self::Bytes(n) => { + if n % 1024 == 0 && n % 1000 != 0 { + to_magnitude_and_suffix(*n as u128, SuffixType::Iec, false) + } else { + to_magnitude_and_suffix(*n as u128, SuffixType::Si, false) + } + } + } + } } impl Default for BlockSize { @@ -196,9 +225,9 @@ impl fmt::Display for BlockSize { match self { Self::Bytes(n) => { let s = if n % 1024 == 0 && n % 1000 != 0 { - to_magnitude_and_suffix(*n as u128, SuffixType::Iec) + to_magnitude_and_suffix(*n as u128, SuffixType::Iec, true) } else { - to_magnitude_and_suffix(*n as u128, SuffixType::Si) + to_magnitude_and_suffix(*n as u128, SuffixType::Si, true) }; write!(f, "{s}") @@ -215,21 +244,73 @@ mod tests { use crate::blocks::{BlockSize, SuffixType, to_magnitude_and_suffix}; #[test] - fn test_to_magnitude_and_suffix_powers_of_1024() { - assert_eq!(to_magnitude_and_suffix(1024, SuffixType::Iec), "1K"); - assert_eq!(to_magnitude_and_suffix(2048, SuffixType::Iec), "2K"); - assert_eq!(to_magnitude_and_suffix(4096, SuffixType::Iec), "4K"); - assert_eq!(to_magnitude_and_suffix(1024 * 1024, SuffixType::Iec), "1M"); + fn test_to_magnitude_and_suffix_rounding() { assert_eq!( - to_magnitude_and_suffix(2 * 1024 * 1024, SuffixType::Iec), + to_magnitude_and_suffix(999_440, SuffixType::Si, true), + "1.0MB" + ); + assert_eq!( + to_magnitude_and_suffix(819_200, SuffixType::Si, true), + "820kB" + ); + assert_eq!( + to_magnitude_and_suffix(819_936, SuffixType::Si, true), + "820kB" + ); + assert_eq!( + to_magnitude_and_suffix(818_400, SuffixType::Si, true), + "819kB" + ); + assert_eq!( + to_magnitude_and_suffix(817_600, SuffixType::Si, true), + "818kB" + ); + assert_eq!( + to_magnitude_and_suffix(817_200, SuffixType::Si, true), + "818kB" + ); + } + + #[test] + fn test_to_magnitude_and_suffix_add_tracing_zero() { + assert_eq!(to_magnitude_and_suffix(1024, SuffixType::Iec, true), "1.0K"); + assert_eq!(to_magnitude_and_suffix(2048, SuffixType::Iec, true), "2.0K"); + assert_eq!(to_magnitude_and_suffix(10240, SuffixType::Iec, true), "10K"); + + assert_eq!(to_magnitude_and_suffix(1024, SuffixType::Iec, false), "1K"); + assert_eq!(to_magnitude_and_suffix(2048, SuffixType::Iec, false), "2K"); + assert_eq!( + to_magnitude_and_suffix(10240, SuffixType::Iec, false), + "10K" + ); + } + + #[test] + fn test_to_magnitude_and_suffix_powers_of_1024() { + assert_eq!(to_magnitude_and_suffix(1024, SuffixType::Iec, false), "1K"); + assert_eq!( + to_magnitude_and_suffix(10240, SuffixType::Iec, false), + "10K" + ); + assert_eq!(to_magnitude_and_suffix(2048, SuffixType::Iec, false), "2K"); + assert_eq!( + to_magnitude_and_suffix(1024 * 40, SuffixType::Iec, false), + "40K" + ); + assert_eq!( + to_magnitude_and_suffix(1024 * 1024, SuffixType::Iec, false), + "1M" + ); + assert_eq!( + to_magnitude_and_suffix(2 * 1024 * 1024, SuffixType::Iec, false), "2M" ); assert_eq!( - to_magnitude_and_suffix(1024 * 1024 * 1024, SuffixType::Iec), + to_magnitude_and_suffix(1024 * 1024 * 1024, SuffixType::Iec, false), "1G" ); assert_eq!( - to_magnitude_and_suffix(34 * 1024 * 1024 * 1024, SuffixType::Iec), + to_magnitude_and_suffix(34 * 1024 * 1024 * 1024, SuffixType::Iec, false), "34G" ); } @@ -237,54 +318,93 @@ mod tests { #[test] #[allow(clippy::cognitive_complexity)] fn test_to_magnitude_and_suffix_not_powers_of_1024() { - assert_eq!(to_magnitude_and_suffix(1, SuffixType::Si), "1B"); - assert_eq!(to_magnitude_and_suffix(999, SuffixType::Si), "999B"); + assert_eq!(to_magnitude_and_suffix(1, SuffixType::Si, true), "1.0B"); + assert_eq!(to_magnitude_and_suffix(999, SuffixType::Si, true), "999B"); - assert_eq!(to_magnitude_and_suffix(1000, SuffixType::Si), "1kB"); - assert_eq!(to_magnitude_and_suffix(1001, SuffixType::Si), "1.1kB"); - assert_eq!(to_magnitude_and_suffix(1023, SuffixType::Si), "1.1kB"); - assert_eq!(to_magnitude_and_suffix(1025, SuffixType::Si), "1.1kB"); - assert_eq!(to_magnitude_and_suffix(10_001, SuffixType::Si), "11kB"); - assert_eq!(to_magnitude_and_suffix(999_000, SuffixType::Si), "999kB"); - - assert_eq!(to_magnitude_and_suffix(999_001, SuffixType::Si), "1MB"); - assert_eq!(to_magnitude_and_suffix(999_999, SuffixType::Si), "1MB"); - assert_eq!(to_magnitude_and_suffix(1_000_000, SuffixType::Si), "1MB"); - assert_eq!(to_magnitude_and_suffix(1_000_001, SuffixType::Si), "1.1MB"); - assert_eq!(to_magnitude_and_suffix(1_100_000, SuffixType::Si), "1.1MB"); - assert_eq!(to_magnitude_and_suffix(1_100_001, SuffixType::Si), "1.2MB"); - assert_eq!(to_magnitude_and_suffix(1_900_000, SuffixType::Si), "1.9MB"); - assert_eq!(to_magnitude_and_suffix(1_900_001, SuffixType::Si), "2MB"); - assert_eq!(to_magnitude_and_suffix(9_900_000, SuffixType::Si), "9.9MB"); - assert_eq!(to_magnitude_and_suffix(9_900_001, SuffixType::Si), "10MB"); + assert_eq!(to_magnitude_and_suffix(1000, SuffixType::Si, true), "1.0kB"); + assert_eq!(to_magnitude_and_suffix(1001, SuffixType::Si, true), "1.1kB"); + assert_eq!(to_magnitude_and_suffix(1023, SuffixType::Si, true), "1.1kB"); + assert_eq!(to_magnitude_and_suffix(1025, SuffixType::Si, true), "1.1kB"); assert_eq!( - to_magnitude_and_suffix(999_000_000, SuffixType::Si), + to_magnitude_and_suffix(10_001, SuffixType::Si, true), + "11kB" + ); + assert_eq!( + to_magnitude_and_suffix(999_000, SuffixType::Si, true), + "999kB" + ); + + assert_eq!( + to_magnitude_and_suffix(999_001, SuffixType::Si, true), + "1.0MB" + ); + assert_eq!( + to_magnitude_and_suffix(999_999, SuffixType::Si, true), + "1.0MB" + ); + assert_eq!( + to_magnitude_and_suffix(1_000_000, SuffixType::Si, true), + "1.0MB" + ); + assert_eq!( + to_magnitude_and_suffix(1_000_001, SuffixType::Si, true), + "1.1MB" + ); + assert_eq!( + to_magnitude_and_suffix(1_100_000, SuffixType::Si, true), + "1.1MB" + ); + assert_eq!( + to_magnitude_and_suffix(1_100_001, SuffixType::Si, true), + "1.2MB" + ); + assert_eq!( + to_magnitude_and_suffix(1_900_000, SuffixType::Si, true), + "1.9MB" + ); + assert_eq!( + to_magnitude_and_suffix(1_900_001, SuffixType::Si, true), + "2.0MB" + ); + assert_eq!( + to_magnitude_and_suffix(9_900_000, SuffixType::Si, true), + "9.9MB" + ); + assert_eq!( + to_magnitude_and_suffix(9_900_001, SuffixType::Si, true), + "10MB" + ); + assert_eq!( + to_magnitude_and_suffix(999_000_000, SuffixType::Si, true), "999MB" ); - assert_eq!(to_magnitude_and_suffix(999_000_001, SuffixType::Si), "1GB"); assert_eq!( - to_magnitude_and_suffix(1_000_000_000, SuffixType::Si), - "1GB" + to_magnitude_and_suffix(999_000_001, SuffixType::Si, true), + "1.0GB" ); assert_eq!( - to_magnitude_and_suffix(1_000_000_001, SuffixType::Si), + to_magnitude_and_suffix(1_000_000_000, SuffixType::Si, true), + "1.0GB" + ); + assert_eq!( + to_magnitude_and_suffix(1_000_000_001, SuffixType::Si, true), "1.1GB" ); } #[test] fn test_block_size_display() { - assert_eq!(format!("{}", BlockSize::Bytes(1024)), "1K"); - assert_eq!(format!("{}", BlockSize::Bytes(2 * 1024)), "2K"); - assert_eq!(format!("{}", BlockSize::Bytes(3 * 1024 * 1024)), "3M"); + assert_eq!(format!("{}", BlockSize::Bytes(1024)), "1.0K"); + assert_eq!(format!("{}", BlockSize::Bytes(2 * 1024)), "2.0K"); + assert_eq!(format!("{}", BlockSize::Bytes(3 * 1024 * 1024)), "3.0M"); } #[test] fn test_block_size_display_multiples_of_1000_and_1024() { assert_eq!(format!("{}", BlockSize::Bytes(128_000)), "128kB"); assert_eq!(format!("{}", BlockSize::Bytes(1000 * 1024)), "1.1MB"); - assert_eq!(format!("{}", BlockSize::Bytes(1_000_000_000_000)), "1TB"); + assert_eq!(format!("{}", BlockSize::Bytes(1_000_000_000_000)), "1.0TB"); } #[test] diff --git a/src/uu/df/src/table.rs b/src/uu/df/src/table.rs index 041040ded..a50861758 100644 --- a/src/uu/df/src/table.rs +++ b/src/uu/df/src/table.rs @@ -264,7 +264,7 @@ impl<'a> RowFormatter<'a> { /// The scaling factor is defined in the `options` field. fn scaled_bytes(&self, size: u64) -> Cell { let s = if let Some(h) = self.options.human_readable { - to_magnitude_and_suffix(size.into(), SuffixType::HumanReadable(h)) + to_magnitude_and_suffix(size.into(), SuffixType::HumanReadable(h), true) } else { let BlockSize::Bytes(d) = self.options.block_size; (size as f64 / d as f64).ceil().to_string() @@ -277,7 +277,7 @@ impl<'a> RowFormatter<'a> { /// The scaling factor is defined in the `options` field. fn scaled_inodes(&self, size: u128) -> Cell { let s = if let Some(h) = self.options.human_readable { - to_magnitude_and_suffix(size, SuffixType::HumanReadable(h)) + to_magnitude_and_suffix(size, SuffixType::HumanReadable(h), true) } else { size.to_string() }; @@ -377,7 +377,11 @@ impl Header { translate!("df-blocks-suffix") ) } - _ => format!("{}{}", options.block_size, translate!("df-blocks-suffix")), + _ => format!( + "{}{}", + options.block_size.to_header(), + translate!("df-blocks-suffix") + ), }, Column::Used => translate!("df-header-used"), Column::Avail => match options.header_mode { @@ -822,17 +826,25 @@ mod tests { fs_type: "my_type".to_string(), fs_mount: "my_mount".into(), - bytes: 4000, + bytes: 40000, bytes_used: 1000, - bytes_avail: 3000, - bytes_usage: Some(0.25), + bytes_avail: 39000, + bytes_usage: Some(0.025), ..Default::default() }; let fmt = RowFormatter::new(&row, &options, false); assert!(compare_cell_content( fmt.get_cells(), - vec!("my_device", "my_type", "4k", "1k", "3k", "25%", "my_mount") + vec!( + "my_device", + "my_type", + "40k", + "1.0k", + "39k", + "3%", + "my_mount" + ) )); } @@ -859,7 +871,15 @@ mod tests { let fmt = RowFormatter::new(&row, &options, false); assert!(compare_cell_content( fmt.get_cells(), - vec!("my_device", "my_type", "4K", "1K", "3K", "25%", "my_mount") + vec!( + "my_device", + "my_type", + "4.0K", + "1.0K", + "3.0K", + "25%", + "my_mount" + ) )); } diff --git a/tests/by-util/test_df.rs b/tests/by-util/test_df.rs index 512ea11ab..b982e929e 100644 --- a/tests/by-util/test_df.rs +++ b/tests/by-util/test_df.rs @@ -139,6 +139,30 @@ fn test_df_follows_symlinks() { ); } +#[test] +fn test_df_trailing_zeros() { + use regex::Regex; + + new_ucmd!() + .arg("-h") + .arg("--output=size,used") + .arg("--total") + .succeeds() + .stdout_does_not_match(&Regex::new("\\s[1-9][A-Z]").unwrap()); +} + +#[test] +fn test_df_rounding() { + use regex::Regex; + + new_ucmd!() + .arg("-H") + .arg("--output=size,used") + .arg("--total") + .succeeds() + .stdout_does_not_match(&Regex::new("\\s\\d{3}\\.\\d[A-Z]").unwrap()); +} + #[test] fn test_df_output_overridden() { let expected = if cfg!(target_os = "macos") {