1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

stat: fix precision when rendering mtime (%Y)

Support precision when rendering the time of last data modification
with `stat`. For example, after this commit

    $ stat --printf='%.1Y\n' f
    1668645806.7

Previously, the precision in the format specification was
ignored. This is implemented with a custom renderer because GNU `stat`
seems to truncate the number as opposed to rounding the number as
would happen when using `format!` with a specified number of digits of
precision.

Fixes #3233
This commit is contained in:
Jeffrey Finkelstein 2025-01-10 21:31:01 -05:00
parent cef9a2b960
commit 1d0dcb5962
2 changed files with 88 additions and 2 deletions

View file

@ -94,6 +94,7 @@ pub enum OutputType {
Unsigned(u64),
UnsignedHex(u64),
UnsignedOct(u32),
Float(f64),
Unknown,
}
@ -283,6 +284,9 @@ fn print_it(output: &OutputType, flags: Flags, width: usize, precision: Option<u
OutputType::UnsignedHex(num) => {
print_unsigned_hex(*num, &flags, width, precision, padding_char);
}
OutputType::Float(num) => {
print_float(*num, &flags, width, precision, padding_char);
}
OutputType::Unknown => print!("?"),
}
}
@ -444,6 +448,58 @@ fn print_integer(
pad_and_print(&extended, flags.left, width, padding_char);
}
/// Truncate a float to the given number of digits after the decimal point.
fn precision_trunc(num: f64, precision: usize) -> String {
// GNU `stat` doesn't round, it just seems to truncate to the
// given precision:
//
// $ stat -c "%.5Y" /dev/pts/ptmx
// 1736344012.76399
// $ stat -c "%.4Y" /dev/pts/ptmx
// 1736344012.7639
// $ stat -c "%.3Y" /dev/pts/ptmx
// 1736344012.763
//
// Contrast this with `printf`, which seems to round the
// numbers:
//
// $ printf "%.5f\n" 1736344012.76399
// 1736344012.76399
// $ printf "%.4f\n" 1736344012.76399
// 1736344012.7640
// $ printf "%.3f\n" 1736344012.76399
// 1736344012.764
//
let num_str = num.to_string();
let n = num_str.len();
match (num_str.find('.'), precision) {
(None, 0) => num_str,
(None, p) => format!("{num_str}.{zeros}", zeros = "0".repeat(p)),
(Some(i), 0) => num_str[..i].to_string(),
(Some(i), p) if p < n - i => num_str[..i + 1 + p].to_string(),
(Some(i), p) => format!("{num_str}{zeros}", zeros = "0".repeat(p - (n - i - 1))),
}
}
fn print_float(
num: f64,
flags: &Flags,
width: usize,
precision: Option<usize>,
padding_char: Padding,
) {
let prefix = if flags.sign {
"+"
} else if flags.space {
" "
} else {
""
};
let num_str = precision_trunc(num, precision.unwrap_or(0));
let extended = format!("{prefix}{num_str}");
pad_and_print(&extended, flags.left, width, padding_char)
}
/// Prints an unsigned integer value based on the provided flags, width, and precision.
///
/// # Arguments
@ -898,7 +954,16 @@ impl Stater {
// time of last data modification, human-readable
'y' => OutputType::Str(pretty_time(meta.mtime(), meta.mtime_nsec())),
// time of last data modification, seconds since Epoch
'Y' => OutputType::Integer(meta.mtime()),
'Y' => {
let sec = meta.mtime();
let nsec = meta.mtime_nsec();
let tm =
chrono::DateTime::from_timestamp(sec, nsec as u32).unwrap_or_default();
let tm: DateTime<Local> = tm.into();
let micros = tm.timestamp_micros();
let secs = micros as f64 / 1_000_000.0;
OutputType::Float(secs)
}
// time of last status change, human-readable
'z' => OutputType::Str(pretty_time(meta.ctime(), meta.ctime_nsec())),
// time of last status change, seconds since Epoch
@ -1107,7 +1172,7 @@ fn pretty_time(sec: i64, nsec: i64) -> String {
#[cfg(test)]
mod tests {
use super::{group_num, Flags, ScanUtil, Stater, Token};
use super::{group_num, precision_trunc, Flags, ScanUtil, Stater, Token};
#[test]
fn test_scanners() {
@ -1216,4 +1281,14 @@ mod tests {
];
assert_eq!(&expected, &Stater::generate_tokens(s, true).unwrap());
}
#[test]
fn test_precision_trunc() {
assert_eq!(precision_trunc(123.456, 0), "123");
assert_eq!(precision_trunc(123.456, 1), "123.4");
assert_eq!(precision_trunc(123.456, 2), "123.45");
assert_eq!(precision_trunc(123.456, 3), "123.456");
assert_eq!(precision_trunc(123.456, 4), "123.4560");
assert_eq!(precision_trunc(123.456, 5), "123.45600");
}
}

View file

@ -184,6 +184,17 @@ fn test_char() {
];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
eprintln!("{expected_stdout}");
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}
#[cfg(target_os = "linux")]
#[test]
fn test_printf_mtime_precision() {
let args = ["-c", "%.0Y %.1Y %.2Y %.3Y %.4Y", "/dev/pts/ptmx"];
let ts = TestScenario::new(util_name!());
let expected_stdout = unwrap_or_return!(expected_result(&ts, &args)).stdout_move_str();
eprintln!("{expected_stdout}");
ts.ucmd().args(&args).succeeds().stdout_is(expected_stdout);
}