pub(crate) const BRIEF: usize = 100;
pub(crate) fn listing(head: &str, rows: Vec<String>, empty: &str) -> String {
if rows.is_empty() {
return format!("{head}\n{}", indent(empty));
}
std::iter::once(head.to_owned())
.chain(rows.iter().map(|row| indent(row)))
.collect::<Vec<String>>()
.join("\n")
}
pub(crate) fn indent(text: &str) -> String {
text.lines()
.map(|line| format!(" {line}"))
.collect::<Vec<String>>()
.join("\n")
}
pub(crate) fn clause(label: &str, value: Option<&str>) -> Option<String> {
value.map(|said| format!("{label} {said}"))
}
pub(crate) fn when(flag: bool, word: &str) -> Option<String> {
flag.then(|| word.to_owned())
}
pub(crate) fn tally(count: u64, word: &str) -> Option<String> {
(count > 0).then(|| format!("{count} {word}"))
}
pub(crate) fn things(count: u64, singular: &str) -> Option<String> {
tally(
count,
&format!("{singular}{}", if count == 1 { "" } else { "s" }),
)
}
pub(crate) fn line(parts: Vec<Option<String>>) -> String {
parts
.into_iter()
.flatten()
.filter(|part| !part.is_empty())
.collect::<Vec<String>>()
.join(" ")
}
pub(crate) fn age(secs: i64) -> String {
for (bound, unit, per) in [(60, "s", 1), (3_600, "m", 60), (86_400, "h", 3_600)] {
if secs < bound {
return format!("{}{unit}", secs / per);
}
}
format!("{}d", secs / 86_400)
}
pub(crate) fn brief(text: &str) -> String {
let flat = text.split_whitespace().collect::<Vec<&str>>().join(" ");
if flat.chars().count() <= BRIEF {
return flat;
}
format!("{}…", flat.chars().take(BRIEF).collect::<String>())
}
pub(crate) fn quoted(text: &str) -> Option<String> {
let said = brief(text);
(!said.is_empty()).then(|| format!("{said:?}"))
}
pub(crate) fn line_over(head: &str, body: Option<String>) -> String {
match body.filter(|said| !said.is_empty()) {
None => head.to_owned(),
Some(said) => format!("{head}\n{}", indent(&said)),
}
}
#[cfg(test)]
mod tests;