use crate::entry::Entry;
use std::io::Write;
use time::OffsetDateTime;
use time::macros::format_description;
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
const GB: u64 = 1024 * MB;
const TB: u64 = 1024 * GB;
pub fn format_size(bytes: u64, is_dir: bool) -> String {
if is_dir {
return "-".to_owned();
}
match bytes {
0 => "0".to_owned(),
b if b < KB => format!("{b}"),
b if b < MB => format!("{:.1}K", b as f64 / KB as f64),
b if b < GB => format!("{:.1}M", b as f64 / MB as f64),
b if b < TB => format!("{:.1}G", b as f64 / GB as f64),
b => format!("{:.1}T", b as f64 / TB as f64),
}
}
pub fn write_size(w: &mut impl Write, bytes: u64, is_dir: bool) -> std::io::Result<()> {
if is_dir {
return w.write_all(b"-");
}
match bytes {
0 => w.write_all(b"0"),
b if b < KB => write!(w, "{b}"),
b if b < MB => write!(w, "{:.1}K", b as f64 / KB as f64),
b if b < GB => write!(w, "{:.1}M", b as f64 / MB as f64),
b if b < TB => write!(w, "{:.1}G", b as f64 / GB as f64),
b => write!(w, "{:.1}T", b as f64 / TB as f64),
}
}
pub fn format_size_width(bytes: u64, is_dir: bool) -> usize {
if is_dir {
return 1; }
match bytes {
0 => 1, b if b < KB => digit_count(b as usize),
b => {
let divisor = if b < MB {
KB
} else if b < GB {
MB
} else if b < TB {
GB
} else {
TB
};
let int_part = (b as f64 / divisor as f64 + 0.05) as usize;
digit_count(int_part.max(1)) + 3
}
}
}
fn digit_count(n: usize) -> usize {
if n == 0 {
return 1;
}
let mut count = 0;
let mut v = n;
while v > 0 {
count += 1;
v /= 10;
}
count
}
const SIX_MONTHS_SECS: i64 = 15_778_476;
pub fn format_date(timestamp: i64, now_secs: i64) -> String {
let Ok(dt) = OffsetDateTime::from_unix_timestamp(timestamp) else {
return "-".to_owned();
};
let age = now_secs - timestamp;
let recent = age >= 0 && age < SIX_MONTHS_SECS;
if recent {
let fmt = format_description!("[month repr:short] [day padding:space] [hour]:[minute]");
dt.format(&fmt).unwrap_or_else(|_| "-".to_owned())
} else {
let fmt = format_description!("[month repr:short] [day padding:space] [year]");
dt.format(&fmt).unwrap_or_else(|_| "-".to_owned())
}
}
pub fn write_date(w: &mut impl Write, timestamp: i64, now_secs: i64) -> std::io::Result<()> {
static MONTHS: [&[u8; 3]; 12] = [
b"Jan", b"Feb", b"Mar", b"Apr", b"May", b"Jun", b"Jul", b"Aug", b"Sep", b"Oct", b"Nov",
b"Dec",
];
let Ok(dt) = OffsetDateTime::from_unix_timestamp(timestamp) else {
return w.write_all(b" -");
};
let month_idx = dt.month() as usize - 1;
let day = dt.day();
let age = now_secs - timestamp;
let recent = age >= 0 && age < SIX_MONTHS_SECS;
w.write_all(MONTHS[month_idx])?;
if recent {
let hour = dt.hour();
let minute = dt.minute();
write!(w, " {day:2} {hour:02}:{minute:02}")
} else {
let year = dt.year();
write!(w, " {day:2} {year}")
}
}
pub fn classify_suffix(entry: &Entry) -> &'static str {
if entry.is_dir {
"/"
} else if entry.is_symlink {
"@"
} else {
""
}
}
pub fn write_name(
w: &mut impl Write,
entry: &Entry,
opts: &super::DisplayOptions,
) -> std::io::Result<()> {
let colored = if opts.color_enabled {
super::color::write_entry_color(w, &opts.ls_colors, entry)?
} else {
false
};
w.write_all(entry.name().as_bytes())?;
if opts.classify {
w.write_all(classify_suffix(entry).as_bytes())?;
}
if colored {
w.write_all(super::color::RESET.as_bytes())?;
}
Ok(())
}
pub fn name_width(entry: &Entry, classify: bool) -> usize {
let name = entry.name();
let name_w = if name.is_ascii() {
name.len()
} else {
unicode_width::UnicodeWidthStr::width(name)
};
let suffix_len = if classify {
classify_suffix(entry).len()
} else {
0
};
name_w + suffix_len
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn size_dir() {
assert_eq!(format_size(0, true), "-");
}
#[test]
fn size_zero_file() {
assert_eq!(format_size(0, false), "0");
}
#[test]
fn size_bytes() {
assert_eq!(format_size(512, false), "512");
assert_eq!(format_size(1, false), "1");
assert_eq!(format_size(1023, false), "1023");
}
#[test]
fn size_kilobytes() {
assert_eq!(format_size(1024, false), "1.0K");
assert_eq!(format_size(1536, false), "1.5K");
assert_eq!(format_size(10240, false), "10.0K");
}
#[test]
fn size_megabytes() {
assert_eq!(format_size(1024 * 1024, false), "1.0M");
assert_eq!(format_size(1024 * 1024 * 5 + 1024 * 512, false), "5.5M");
}
#[test]
fn size_gigabytes() {
assert_eq!(format_size(1024 * 1024 * 1024, false), "1.0G");
}
#[test]
fn size_terabytes() {
assert_eq!(format_size(1024u64 * 1024 * 1024 * 1024, false), "1.0T");
}
fn now() -> i64 {
OffsetDateTime::now_utc().unix_timestamp()
}
#[test]
fn date_epoch() {
let formatted = format_date(0, now());
assert!(
formatted.contains("1970"),
"expected year 1970, got: {formatted}"
);
}
#[test]
fn date_recent() {
let n = now();
let formatted = format_date(n - 3600, n);
assert!(
formatted.contains(':'),
"expected time format, got: {formatted}"
);
}
#[test]
fn date_old() {
let n = now();
let formatted = format_date(n - 2 * 365 * 24 * 3600, n);
assert!(
!formatted.contains(':'),
"expected year format, got: {formatted}"
);
}
#[test]
fn date_future() {
let n = now();
let formatted = format_date(n + 365 * 24 * 3600, n);
assert!(
!formatted.contains(':'),
"expected year format for future, got: {formatted}"
);
}
#[test]
fn size_width_matches_format_size() {
let test_values: Vec<u64> = {
let mut vals = vec![0u64, 1, 9, 10, 99, 100, 999, 1000, 1023];
for unit in [KB, MB, GB, TB] {
for offset in [0, 1, 2, 10, 100] {
if unit > offset {
vals.push(unit - offset);
}
vals.push(unit + offset);
}
for mult in [10u64, 100, 1000] {
let base = unit.saturating_mul(mult);
for offset in [0, 1, 2, 10, 100] {
if base > offset {
vals.push(base - offset);
}
vals.push(base + offset);
}
}
}
vals.sort();
vals.dedup();
vals
};
for &b in &test_values {
let actual_len = format_size(b, false).len();
let predicted = format_size_width(b, false);
assert_eq!(
predicted,
actual_len,
"format_size_width({b}) = {predicted}, but format_size({b}) = {:?} (len={actual_len})",
format_size(b, false)
);
}
assert_eq!(format_size_width(0, true), format_size(0, true).len());
assert_eq!(format_size_width(1000, true), format_size(1000, true).len());
}
#[test]
fn write_date_matches_format_date_for_valid_timestamps() {
let n = now();
for ts in [0i64, n - 3600, n - 200 * 24 * 3600, n + 365 * 24 * 3600] {
let expected = format_date(ts, n);
let mut buf = Vec::new();
write_date(&mut buf, ts, n).unwrap();
let actual = String::from_utf8(buf).unwrap();
assert_eq!(actual, expected, "write_date mismatch for ts={ts}");
}
}
#[test]
fn write_date_invalid_timestamp_emits_fixed_width_placeholder() {
let mut buf = Vec::new();
write_date(&mut buf, i64::MAX, 0).unwrap();
let s = String::from_utf8(buf).unwrap();
assert_eq!(s, " -");
assert_eq!(s.len(), 12, "placeholder must match valid date width");
}
#[test]
fn write_size_matches_format_size() {
for &(b, is_dir) in &[
(0u64, false),
(1, false),
(512, false),
(1023, false),
(KB, false),
(KB + 512, false),
(10 * KB, false),
(MB, false),
(GB, false),
(TB, false),
(0, true),
(1000, true),
] {
let expected = format_size(b, is_dir);
let mut buf = Vec::new();
write_size(&mut buf, b, is_dir).unwrap();
let actual = String::from_utf8(buf).unwrap();
assert_eq!(actual, expected, "write_size({b}, {is_dir}) mismatch");
}
}
}