use std::iter;
use std::mem;
use std::time::{SystemTime, UNIX_EPOCH};
use unicode_width::UnicodeWidthChar;
pub fn human_size(bytes: u64) -> String {
const UNITS: [&str; 7] = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"];
if bytes < 1024 {
return format!("{bytes} B");
}
let mut value = bytes as f64;
let mut unit = 0;
while value >= 1024.0 && unit < UNITS.len() - 1 {
value /= 1024.0;
unit += 1;
}
if value < 10.0 { format!("{value:.1} {}", UNITS[unit]) } else { format!("{value:.0} {}", UNITS[unit]) }
}
pub fn visible_char(c: char) -> char {
match c {
'\0'..='\x1f' => char::from_u32(0x2400 + u32::from(c)).unwrap_or('\u{fffd}'),
'\x7f' => '\u{2421}',
'\u{80}'..='\u{9f}' => '\u{fffd}',
c => c,
}
}
pub fn sanitize(text: &str) -> String {
text.chars().map(visible_char).collect()
}
pub fn sanitize_lines(text: &str) -> String {
text.chars().map(|c| if c == '\n' { c } else { visible_char(c) }).collect()
}
pub fn columns(text: &str) -> usize {
text.chars().map(|c| c.width().unwrap_or(0)).sum()
}
pub fn truncate(text: &str, width: usize) -> String {
if columns(text) <= width {
return text.to_owned();
}
if width == 0 {
return String::new();
}
let mut used = 0;
let kept = text.chars().take_while(|c| {
used += c.width().unwrap_or(0);
used < width
});
kept.chain(iter::once('…')).collect()
}
pub fn truncate_start(text: &str, width: usize) -> String {
if columns(text) <= width {
return text.to_owned();
}
if width == 0 {
return String::new();
}
let mut used = 0;
let mut kept: Vec<char> = text
.chars()
.rev()
.take_while(|c| {
used += c.width().unwrap_or(0);
used < width
})
.collect();
kept.push('…');
kept.into_iter().rev().collect()
}
pub fn first_line(text: &str, width: usize) -> String {
let first = truncate(text.lines().next().unwrap_or("").trim(), width);
if text.trim().lines().count() > 1 { format!("{first} …") } else { first }
}
pub fn pad_start(text: &str, width: usize) -> String {
let text = truncate(text, width);
format!("{}{text}", " ".repeat(width.saturating_sub(columns(&text))))
}
pub fn wrap(text: &str, width: usize) -> Vec<String> {
let width = width.max(1);
let mut lines = Vec::new();
for source in text.lines() {
let mut line = String::new();
let mut used = 0;
for word in source.split(' ').filter(|word| !word.is_empty()) {
let word_width = columns(word);
if used > 0 && used + 1 + word_width <= width {
line.push(' ');
used += 1;
} else if used > 0 {
lines.push(mem::take(&mut line));
used = 0;
}
for c in word.chars() {
let w = c.width().unwrap_or(0);
if used + w > width && used > 0 {
lines.push(mem::take(&mut line));
used = 0;
}
line.push(c);
used += w;
}
}
lines.push(line);
}
lines
}
pub fn fnv64(text: &str) -> u64 {
text.bytes().fold(0xcbf2_9ce4_8422_2325, |h: u64, b| (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3))
}
pub fn unix_seconds(time: SystemTime) -> i64 {
match time.duration_since(UNIX_EPOCH) {
Ok(after) => i64::try_from(after.as_secs()).unwrap_or(i64::MAX),
Err(before) => i64::try_from(before.duration().as_secs()).map_or(i64::MIN, |secs| -secs),
}
}
fn local_tm(secs: i64) -> Option<libc::tm> {
#[allow(
deprecated,
reason = "on musl, the libc crate warns that time_t will become 64-bit: it has been since musl 1.2"
)]
let t = libc::time_t::try_from(secs).ok()?;
let mut tm: libc::tm = unsafe { std::mem::zeroed() };
let ok = unsafe { !libc::localtime_r(&t, &mut tm).is_null() };
ok.then_some(tm)
}
pub fn local_datetime(secs: i64) -> String {
match local_tm(secs) {
Some(tm) => format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
tm.tm_year + 1900,
tm.tm_mon + 1,
tm.tm_mday,
tm.tm_hour,
tm.tm_min,
tm.tm_sec
),
None => secs.to_string(),
}
}
pub fn local_minutes(secs: i64) -> String {
let mut s = local_datetime(secs);
if s.len() == 19 {
s.truncate(16);
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_wall_clock_time_read_as_local_shows_as_itself() {
let wall = 1_709_168_399;
let offset = local_tm(wall).map_or(0, |tm| tm.tm_gmtoff);
assert_eq!(local_minutes(wall - offset), "2024-02-29 00:59");
}
#[test]
fn a_first_line_is_cut_to_columns() {
assert_eq!(first_line(" make ", 80), "make");
assert_eq!(first_line("make\nmake install\n", 80), "make …");
let wide = "漢字".repeat(10);
assert_eq!(first_line(&wide, 7), "漢字漢…");
assert_eq!(first_line(&format!("{wide}\nmore"), 7), "漢字漢… …");
}
}