df: add tracing zero and rounding (#8685)

This commit is contained in:
alexander
2025-10-15 22:11:44 -04:00
committed by GitHub
parent 517b5fb8b3
commit 6e422b728e
3 changed files with 214 additions and 50 deletions
+162 -42
View File
@@ -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]
+28 -8
View File
@@ -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"
)
));
}
+24
View File
@@ -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") {