use crate::constants::WEB_CONTENT_MAX_CHARS;
pub fn truncate_content(content: &str, max_chars: usize) -> String {
if content.len() <= max_chars {
return content.to_string();
}
if let Some((byte_end, _)) = content.char_indices().nth(max_chars) {
format!("{}...[truncated]", &content[..byte_end])
} else {
content.to_string()
}
}
pub fn truncate_middle(content: &str, max_chars: usize) -> String {
if content.len() <= max_chars {
return content.to_string();
}
let total_chars = content.chars().count();
if total_chars <= max_chars {
return content.to_string();
}
let head_chars = max_chars / 2;
let tail_chars = max_chars - head_chars;
let elided = total_chars - head_chars - tail_chars;
let head_end = content
.char_indices()
.nth(head_chars)
.map(|(i, _)| i)
.unwrap_or(content.len());
let tail_start = content
.char_indices()
.nth(total_chars - tail_chars)
.map(|(i, _)| i)
.unwrap_or(content.len());
format!(
"{}\n…[{elided} chars elided]…\n{}",
&content[..head_end],
&content[tail_start..]
)
}
pub fn truncate_web_content(content: &str) -> String {
truncate_middle(content, WEB_CONTENT_MAX_CHARS)
}
const CONTINUATION_OVERLAP_WINDOW_BYTES: usize = 400;
const CONTINUATION_OVERLAP_MIN_BYTES: usize = 16;
pub fn continuation_overlap(prev: &str, continuation: &str) -> usize {
let mut window_start = prev.len().saturating_sub(CONTINUATION_OVERLAP_WINDOW_BYTES);
while window_start < prev.len() && !prev.is_char_boundary(window_start) {
window_start += 1;
}
let tail = &prev[window_start..];
let max_len = tail.len().min(continuation.len());
if max_len < CONTINUATION_OVERLAP_MIN_BYTES {
return 0;
}
for len in (CONTINUATION_OVERLAP_MIN_BYTES..=max_len).rev() {
if !continuation.is_char_boundary(len) {
continue;
}
let head = &continuation[..len];
if tail.ends_with(head) {
return len;
}
}
0
}
pub fn format_duration(total_secs: f64) -> String {
let secs = total_secs as u64;
if secs < 60 {
return format!("{:.1}s", total_secs);
}
let days = secs / 86400;
let hours = (secs % 86400) / 3600;
let mins = (secs % 3600) / 60;
let remainder = secs % 60;
if days > 0 {
format!("{}d {}h {}m {}s", days, hours, mins, remainder)
} else if hours > 0 {
format!("{}h {}m {}s", hours, mins, remainder)
} else {
format!("{}m {}s", mins, remainder)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_duration_sub_minute() {
assert_eq!(format_duration(0.0), "0.0s");
assert_eq!(format_duration(12.3), "12.3s");
assert_eq!(format_duration(59.9), "59.9s");
}
#[test]
fn test_format_duration_minutes_and_above() {
assert_eq!(format_duration(60.0), "1m 0s");
assert_eq!(format_duration(107.0), "1m 47s");
assert_eq!(format_duration(3600.0), "1h 0m 0s");
assert_eq!(format_duration(86400.0), "1d 0h 0m 0s");
assert_eq!(format_duration(90061.0), "1d 1h 1m 1s");
}
#[test]
fn continuation_overlap_trims_a_resume_echo() {
let prev = "The resolver clamps the budget to the window room";
let cont = "to the window room, then omits the field entirely.";
assert_eq!(continuation_overlap(prev, cont), "to the window room".len());
}
#[test]
fn continuation_overlap_keeps_short_ambiguous_matches() {
assert_eq!(
continuation_overlap("…and then the ", "the answer is 42"),
0
);
assert_eq!(continuation_overlap("first half", "second half"), 0);
assert_eq!(continuation_overlap("", "anything"), 0);
assert_eq!(continuation_overlap("anything", ""), 0);
}
#[test]
fn continuation_overlap_prefers_the_longest_echo() {
let prev = "It hit the cap. It hit the cap. ";
let cont = "It hit the cap. Continuing now.";
assert_eq!(continuation_overlap(prev, cont), "It hit the cap. ".len());
}
#[test]
fn continuation_overlap_is_window_bounded() {
let echo = "a distinctive sentence that repeats";
let prev = format!("{echo}{}", "x".repeat(500));
assert_eq!(continuation_overlap(&prev, echo), 0);
}
#[test]
fn continuation_overlap_respects_char_boundaries() {
let prev = "código con acentuación específica";
let cont = "acentuación específica y más contenido";
let n = continuation_overlap(prev, cont);
assert_eq!(&cont[..n], "acentuación específica");
let _ = &cont[n..]; }
#[test]
fn truncate_middle_keeps_head_and_tail() {
let short = "hello";
assert_eq!(truncate_middle(short, 100), "hello");
let long = format!("{}TAIL_ERROR", "H".repeat(200));
let truncated = truncate_middle(&long, 50);
assert!(
truncated.starts_with("HHHH"),
"head must survive: {truncated}"
);
assert!(
truncated.ends_with("TAIL_ERROR"),
"tail must survive: {truncated}"
);
assert!(
truncated.contains("elided"),
"must mark elision: {truncated}"
);
assert!(truncated.chars().count() < long.chars().count());
}
}