Skip to content

Commit

Permalink
df: fix rounding behavior in humanreadable mode
Browse files Browse the repository at this point in the history
Fixes #3422
  • Loading branch information
cakebaker committed May 22, 2022
1 parent 887f49c commit 2bf3dcf
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 131 deletions.
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion src/uu/df/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ path = "src/df.rs"

[dependencies]
clap = { version = "3.1", features = ["wrap_help", "cargo"] }
number_prefix = "0.4"
uucore = { version=">=0.0.11", package="uucore", path="../../uucore", features=["libc", "fsext"] }
unicode-width = "0.1.9"

Expand Down
207 changes: 105 additions & 102 deletions src/uu/df/src/blocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ const IEC_BASES: [u128; 10] = [
1_237_940_039_285_380_274_899_124_224,
];

/// Suffixes for the first nine multi-byte unit suffixes.
const SUFFIXES: [char; 9] = ['B', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];

/// The first ten powers of 1000.
const SI_BASES: [u128; 10] = [
1,
1_000,
Expand All @@ -42,84 +40,69 @@ const SI_BASES: [u128; 10] = [
1_000_000_000_000_000_000_000_000_000,
];

// we use "kB" instead of "KB" because of GNU df
const SI_SUFFIXES: [&str; 9] = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
/// A SuffixType determines whether the suffixes are 1000 or 1024 based, and whether they are
/// intended for HumanReadable mode or not.
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Copy)]
pub(crate) enum SuffixType {
IEC,
SI,
HumanReadableIEC,
HumanReadableSI,
}

/// Convert a multiple of 1024 into a string like "12K" or "34M".
///
/// # Examples
///
/// Powers of 1024 become "1K", "1M", "1G", etc.
///
/// ```rust,ignore
/// assert_eq!(to_magnitude_and_suffix_1024(1024).unwrap(), "1K");
/// assert_eq!(to_magnitude_and_suffix_1024(1024 * 1024).unwrap(), "1M");
/// assert_eq!(to_magnitude_and_suffix_1024(1024 * 1024 * 1024).unwrap(), "1G");
/// ```
///
/// Multiples of those powers affect the magnitude part of the
/// returned string:
///
/// ```rust,ignore
/// assert_eq!(to_magnitude_and_suffix_1024(123 * 1024).unwrap(), "123K");
/// assert_eq!(to_magnitude_and_suffix_1024(456 * 1024 * 1024).unwrap(), "456M");
/// assert_eq!(to_magnitude_and_suffix_1024(789 * 1024 * 1024 * 1024).unwrap(), "789G");
/// ```
fn to_magnitude_and_suffix_1024(n: u128) -> Result<String, ()> {
// Find the smallest power of 1024 that is larger than `n`. That
// number indicates which units and suffix to use.
for i in 0..IEC_BASES.len() - 1 {
if n < IEC_BASES[i + 1] {
return Ok(format!("{}{}", n / IEC_BASES[i], SUFFIXES[i]));
impl SuffixType {
/// The first ten powers of 1024 and 1000, respectively.
fn bases(&self) -> [u128; 10] {
match self {
Self::IEC | Self::HumanReadableIEC => IEC_BASES,
Self::SI | Self::HumanReadableSI => SI_BASES,
}
}

/// Suffixes for the first nine multi-byte unit suffixes.
fn suffixes(&self) -> [&'static str; 9] {
match self {
// we use "kB" instead of "KB", same as GNU df
Self::SI => ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
Self::IEC => ["B", "K", "M", "G", "T", "P", "E", "Z", "Y"],
Self::HumanReadableSI => ["", "k", "M", "G", "T", "P", "E", "Z", "Y"],
Self::HumanReadableIEC => ["", "K", "M", "G", "T", "P", "E", "Z", "Y"],
}
}
Err(())
}

/// Convert a number into a string like "12kB" or "34MB".
///
/// Powers of 1000 become "1kB", "1MB", "1GB", etc.
/// 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".
fn to_magnitude_and_suffix_not_powers_of_1024(n: u128) -> Result<String, ()> {
pub(crate) fn to_magnitude_and_suffix(n: u128, suffix_type: SuffixType) -> String {
let bases = suffix_type.bases();
let suffixes = suffix_type.suffixes();
let mut i = 0;

while SI_BASES[i + 1] - SI_BASES[i] < n && i < SI_SUFFIXES.len() {
while bases[i + 1] - bases[i] < n && i < suffixes.len() {
i += 1;
}

let quot = n / SI_BASES[i];
let rem = n % SI_BASES[i];
let suffix = SI_SUFFIXES[i];
let quot = n / bases[i];
let rem = n % bases[i];
let suffix = suffixes[i];

if rem == 0 {
Ok(format!("{}{}", quot, suffix))
format!("{}{}", quot, suffix)
} else {
let tenths_place = rem / (SI_BASES[i] / 10);
let tenths_place = rem / (bases[i] / 10);

if rem % (SI_BASES[i] / 10) == 0 {
Ok(format!("{}.{}{}", quot, tenths_place, suffix))
if rem % (bases[i] / 10) == 0 {
format!("{}.{}{}", quot, tenths_place, suffix)
} else if tenths_place + 1 == 10 || quot >= 10 {
Ok(format!("{}{}", quot + 1, suffix))
format!("{}{}", quot + 1, suffix)
} else {
Ok(format!("{}.{}{}", quot, tenths_place + 1, suffix))
format!("{}.{}{}", quot, tenths_place + 1, suffix)
}
}
}

/// Convert a number into a magnitude and a multi-byte unit suffix.
///
/// # Errors
///
/// If the number is too large to represent.
fn to_magnitude_and_suffix(n: u128) -> Result<String, ()> {
if n % 1024 == 0 && n % 1000 != 0 {
to_magnitude_and_suffix_1024(n)
} else {
to_magnitude_and_suffix_not_powers_of_1024(n)
}
}

/// A mode to use in condensing the display of a large number of bytes.
pub(crate) enum SizeFormat {
HumanReadable(HumanReadable),
Expand Down Expand Up @@ -207,10 +190,15 @@ pub(crate) fn block_size_from_matches(matches: &ArgMatches) -> Result<BlockSize,
impl fmt::Display for BlockSize {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Bytes(n) => match to_magnitude_and_suffix(*n as u128) {
Ok(s) => write!(f, "{}", s),
Err(_) => Err(fmt::Error),
},
Self::Bytes(n) => {
let s = if n % 1024 == 0 && n % 1000 != 0 {
to_magnitude_and_suffix(*n as u128, SuffixType::IEC)
} else {
to_magnitude_and_suffix(*n as u128, SuffixType::SI)
};

write!(f, "{}", s)
}
}
}
}
Expand All @@ -220,56 +208,64 @@ mod tests {

use std::env;

use crate::blocks::{to_magnitude_and_suffix, BlockSize};
use crate::blocks::{to_magnitude_and_suffix, BlockSize, SuffixType};

#[test]
fn test_to_magnitude_and_suffix_powers_of_1024() {
assert_eq!(to_magnitude_and_suffix(1024).unwrap(), "1K");
assert_eq!(to_magnitude_and_suffix(2048).unwrap(), "2K");
assert_eq!(to_magnitude_and_suffix(4096).unwrap(), "4K");
assert_eq!(to_magnitude_and_suffix(1024 * 1024).unwrap(), "1M");
assert_eq!(to_magnitude_and_suffix(2 * 1024 * 1024).unwrap(), "2M");
assert_eq!(to_magnitude_and_suffix(1024 * 1024 * 1024).unwrap(), "1G");
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");
assert_eq!(
to_magnitude_and_suffix(2 * 1024 * 1024, SuffixType::IEC),
"2M"
);
assert_eq!(
to_magnitude_and_suffix(34 * 1024 * 1024 * 1024).unwrap(),
to_magnitude_and_suffix(1024 * 1024 * 1024, SuffixType::IEC),
"1G"
);
assert_eq!(
to_magnitude_and_suffix(34 * 1024 * 1024 * 1024, SuffixType::IEC),
"34G"
);
}

#[test]
fn test_to_magnitude_and_suffix_not_powers_of_1024() {
assert_eq!(to_magnitude_and_suffix(1).unwrap(), "1B");
assert_eq!(to_magnitude_and_suffix(999).unwrap(), "999B");

assert_eq!(to_magnitude_and_suffix(1000).unwrap(), "1kB");
assert_eq!(to_magnitude_and_suffix(1001).unwrap(), "1.1kB");
assert_eq!(to_magnitude_and_suffix(1023).unwrap(), "1.1kB");
assert_eq!(to_magnitude_and_suffix(1025).unwrap(), "1.1kB");
assert_eq!(to_magnitude_and_suffix(10_001).unwrap(), "11kB");
assert_eq!(to_magnitude_and_suffix(999_000).unwrap(), "999kB");

assert_eq!(to_magnitude_and_suffix(999_001).unwrap(), "1MB");
assert_eq!(to_magnitude_and_suffix(999_999).unwrap(), "1MB");
assert_eq!(to_magnitude_and_suffix(1_000_000).unwrap(), "1MB");
assert_eq!(to_magnitude_and_suffix(1_000_001).unwrap(), "1.1MB");
assert_eq!(to_magnitude_and_suffix(1_100_000).unwrap(), "1.1MB");
assert_eq!(to_magnitude_and_suffix(1_100_001).unwrap(), "1.2MB");
assert_eq!(to_magnitude_and_suffix(1_900_000).unwrap(), "1.9MB");
assert_eq!(to_magnitude_and_suffix(1_900_001).unwrap(), "2MB");
assert_eq!(to_magnitude_and_suffix(9_900_000).unwrap(), "9.9MB");
assert_eq!(to_magnitude_and_suffix(9_900_001).unwrap(), "10MB");
assert_eq!(to_magnitude_and_suffix(999_000_000).unwrap(), "999MB");

assert_eq!(to_magnitude_and_suffix(999_000_001).unwrap(), "1GB");
assert_eq!(to_magnitude_and_suffix(1_000_000_000).unwrap(), "1GB");
assert_eq!(to_magnitude_and_suffix(1_000_000_001).unwrap(), "1.1GB");
}
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(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(999_000_000, SuffixType::SI),
"999MB"
);

#[test]
fn test_to_magnitude_and_suffix_multiples_of_1000_and_1024() {
assert_eq!(to_magnitude_and_suffix(128_000).unwrap(), "128kB");
assert_eq!(to_magnitude_and_suffix(1000 * 1024).unwrap(), "1.1MB");
assert_eq!(to_magnitude_and_suffix(1_000_000_000_000).unwrap(), "1TB");
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"
);
assert_eq!(
to_magnitude_and_suffix(1_000_000_001, SuffixType::SI),
"1.1GB"
);
}

#[test]
Expand All @@ -279,6 +275,13 @@ mod tests {
assert_eq!(format!("{}", BlockSize::Bytes(3 * 1024 * 1024)), "3M");
}

#[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");
}

#[test]
fn test_default_block_size() {
assert_eq!(BlockSize::Bytes(1024), BlockSize::default());
Expand Down
37 changes: 10 additions & 27 deletions src/uu/df/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
//!
//! A table ([`Table`]) comprises a header row ([`Header`]) and a
//! collection of data rows ([`Row`]), one per filesystem.
use number_prefix::NumberPrefix;
use unicode_width::UnicodeWidthStr;

use crate::blocks::{HumanReadable, SizeFormat};
use crate::blocks::{to_magnitude_and_suffix, HumanReadable, SizeFormat, SuffixType};
use crate::columns::{Alignment, Column};
use crate::filesystem::Filesystem;
use crate::{BlockSize, Options};
Expand Down Expand Up @@ -215,13 +214,13 @@ impl<'a> RowFormatter<'a> {

/// Get a human readable string giving the scaled version of the input number.
fn scaled_human_readable(&self, size: u64, human_readable: HumanReadable) -> String {
let number_prefix = match human_readable {
HumanReadable::Decimal => NumberPrefix::decimal(size as f64),
HumanReadable::Binary => NumberPrefix::binary(size as f64),
};
match number_prefix {
NumberPrefix::Standalone(bytes) => bytes.to_string(),
NumberPrefix::Prefixed(prefix, bytes) => format!("{:.1}{}", bytes, prefix.symbol()),
match human_readable {
HumanReadable::Decimal => {
to_magnitude_and_suffix(size.into(), SuffixType::HumanReadableSI)
}
HumanReadable::Binary => {
to_magnitude_and_suffix(size.into(), SuffixType::HumanReadableIEC)
}
}
}

Expand Down Expand Up @@ -734,15 +733,7 @@ mod tests {
let fmt = RowFormatter::new(&row, &options);
assert_eq!(
fmt.get_values(),
vec!(
"my_device",
"my_type",
"4.0k",
"1.0k",
"3.0k",
"25%",
"my_mount"
)
vec!("my_device", "my_type", "4k", "1k", "3k", "25%", "my_mount")
);
}

Expand All @@ -768,15 +759,7 @@ mod tests {
let fmt = RowFormatter::new(&row, &options);
assert_eq!(
fmt.get_values(),
vec!(
"my_device",
"my_type",
"4.0Ki",
"1.0Ki",
"3.0Ki",
"25%",
"my_mount"
)
vec!("my_device", "my_type", "4K", "1K", "3K", "25%", "my_mount")
);
}

Expand Down

0 comments on commit 2bf3dcf

Please sign in to comment.