pub fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}
pub fn progress_bar(done: u64, total: u64, width: usize) -> String {
let width = width.max(1);
let total = total.max(1);
let filled = ((done as f64 / total as f64) * width as f64).round() as usize;
let filled = filled.min(width);
let empty = width - filled;
let mut s = String::with_capacity(width * 3);
s.push_str(&"\u{25B0}".repeat(filled));
s.push_str(&"\u{25B1}".repeat(empty));
s
}
pub fn split_chunks(text: &str, limit: usize) -> Vec<String> {
let limit = limit.max(1);
if text.chars().count() <= limit {
return if text.is_empty() {
Vec::new()
} else {
vec![text.to_owned()]
};
}
let mut chunks = Vec::new();
let mut current = String::new();
for line in text.split_inclusive('\n') {
if line.chars().count() > limit {
if !current.is_empty() {
chunks.push(std::mem::take(&mut current));
}
for piece in hard_wrap(line, limit) {
chunks.push(piece);
}
continue;
}
if current.chars().count() + line.chars().count() > limit {
chunks.push(std::mem::take(&mut current));
}
current.push_str(line);
}
if !current.is_empty() {
chunks.push(current);
}
chunks
.into_iter()
.map(|c| c.trim_end_matches('\n').to_owned())
.filter(|c| !c.is_empty())
.collect()
}
pub fn truncate_chunk(text: &str, limit: usize) -> String {
split_chunks(text, limit)
.into_iter()
.next()
.unwrap_or_default()
}
fn hard_wrap(line: &str, limit: usize) -> Vec<String> {
let mut out = Vec::new();
let mut current = String::new();
for word in line.split(' ') {
if word.chars().count() > limit {
if !current.is_empty() {
out.push(std::mem::take(&mut current));
}
let mut buf = String::new();
for ch in word.chars() {
if buf.chars().count() == limit {
out.push(std::mem::take(&mut buf));
}
buf.push(ch);
}
if !buf.is_empty() {
current = buf;
}
continue;
}
let extra = if current.is_empty() { 0 } else { 1 };
if current.chars().count() + extra + word.chars().count() > limit {
out.push(std::mem::take(&mut current));
}
if !current.is_empty() {
current.push(' ');
}
current.push_str(word);
}
if !current.is_empty() {
out.push(current);
}
out
}
pub fn urlencode(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for byte in s.as_bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(*byte as char);
}
_ => out.push_str(&format!("%{:02X}", byte)),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capitalize_ascii() {
assert_eq!(capitalize("telegram"), "Telegram");
assert_eq!(capitalize(""), "");
}
#[test]
fn capitalize_unicode() {
assert_eq!(capitalize("ёлка"), "Ёлка");
}
#[test]
fn progress_bar_edges() {
assert_eq!(progress_bar(0, 10, 4), "▱▱▱▱");
assert_eq!(progress_bar(10, 10, 4), "▰▰▰▰");
assert_eq!(progress_bar(5, 0, 4), "▰▰▰▰"); }
#[test]
fn split_short_text_stays_whole() {
assert_eq!(split_chunks("hi", 100), vec!["hi".to_owned()]);
assert!(split_chunks("", 100).is_empty());
}
#[test]
fn split_breaks_on_lines() {
let text = "aaaa\nbbbb\ncccc";
let chunks = split_chunks(text, 9);
assert!(chunks.iter().all(|c| c.chars().count() <= 9));
assert_eq!(chunks.join("\n"), text);
}
#[test]
fn split_hard_wraps_a_long_line() {
let text = "a".repeat(25);
let chunks = split_chunks(&text, 10);
assert_eq!(chunks.len(), 3);
assert!(chunks.iter().all(|c| c.chars().count() <= 10));
assert_eq!(chunks.concat(), text);
}
#[test]
fn split_prefers_word_boundaries() {
let chunks = split_chunks("one two three four", 9);
assert!(chunks.iter().all(|c| c.chars().count() <= 9));
assert!(chunks.iter().all(|c| !c.starts_with(' ')));
}
#[test]
fn truncate_chunk_short_text_is_unchanged() {
assert_eq!(truncate_chunk("hi", 100), "hi");
assert_eq!(truncate_chunk("", 100), "");
}
#[test]
fn truncate_chunk_cuts_to_first_chunk() {
let text = "a".repeat(25);
let cut = truncate_chunk(&text, 10);
assert_eq!(cut.chars().count(), 10);
assert!(text.starts_with(&cut));
}
#[test]
fn truncate_chunk_prefers_line_boundary() {
assert_eq!(truncate_chunk("aaaa\nbbbb\ncccc", 9), "aaaa");
assert_eq!(truncate_chunk("aa\nbb\ncccc", 6), "aa\nbb");
}
#[test]
fn urlencode_basic() {
assert_eq!(urlencode("hello"), "hello");
assert_eq!(urlencode("hello world"), "hello%20world");
assert_eq!(urlencode("a&b=c"), "a%26b%3Dc");
}
}