pub fn floor_char_boundary(s: &str, max: usize) -> usize {
let mut end = max.min(s.len());
while !s.is_char_boundary(end) {
end -= 1;
}
end
}
pub fn substring(s: &str, start: usize, end: usize) -> &str {
let end = floor_char_boundary(s, end);
let start = floor_char_boundary(s, start.min(end));
s.get(start..end).unwrap_or("")
}
pub fn split_at_boundary(s: &str, mid: usize) -> (&str, &str) {
let mid = floor_char_boundary(s, mid);
(substring(s, 0, mid), substring(s, mid, s.len()))
}
pub fn truncate_at_boundary(s: &str, max: usize) -> &str {
substring(s, 0, max)
}
pub fn ceil_char_boundary(s: &str, min: usize) -> usize {
let mut start = min.min(s.len());
while !s.is_char_boundary(start) {
start += 1;
}
start
}
pub fn snippet_around(s: &str, at: usize, radius: usize) -> String {
let at = at.min(s.len());
let start = floor_char_boundary(s, at.saturating_sub(radius));
let end = ceil_char_boundary(s, (at + radius).min(s.len()));
let mut out = String::new();
if start > 0 {
out.push('…');
}
out.push_str(substring(s, start, end));
if end < s.len() {
out.push('…');
}
out
}
pub fn estimate_tokens(s: &str) -> usize {
s.len().div_ceil(4)
}
pub fn interpolate(template: &str, vars: &[(&str, &str)]) -> String {
let mut out = template.to_string();
for (name, value) in vars {
out = out.replace(&format!("{{{name}}}"), value);
}
out
}
#[cfg(test)]
mod snippet_tests {
use super::*;
const HAY: &str = "the quick brown fox jumps over the lazy dog";
#[test]
fn a_window_in_the_middle_is_elided_at_both_ends() {
let at = HAY.find("fox").unwrap();
let out = snippet_around(HAY, at, 6);
assert!(out.starts_with('…'));
assert!(out.ends_with('…'));
assert!(out.contains("fox"));
}
#[test]
fn a_window_at_the_edges_is_not_elided_there() {
assert!(!snippet_around(HAY, 0, 5).starts_with('…'));
assert!(!snippet_around(HAY, HAY.len(), 5).ends_with('…'));
}
#[test]
fn a_radius_covering_everything_returns_the_whole_string_unmarked() {
assert_eq!(snippet_around(HAY, 10, 1000), HAY);
}
#[test]
fn multi_byte_characters_are_never_split() {
let hay = "aaa🇯🇵🎉bbb needle ccc🚀ddd";
let at = hay.find("needle").unwrap();
for radius in 0..hay.len() + 4 {
let out = snippet_around(hay, at, radius);
assert!(out.chars().all(|c| c != '\u{FFFD}'));
if radius >= "needle".len() {
assert!(out.contains("needle"));
}
}
}
#[test]
fn an_offset_past_the_end_clamps_instead_of_panicking() {
let out = snippet_around(HAY, HAY.len() + 500, 4);
assert!(out.starts_with('…'));
assert!(!out.ends_with('…'));
}
#[test]
fn an_empty_haystack_yields_an_empty_snippet() {
assert_eq!(snippet_around("", 0, 10), "");
assert_eq!(snippet_around("", 7, 10), "");
}
#[test]
fn ceil_char_boundary_walks_forward_and_clamps() {
let s = "a🎉b";
assert_eq!(ceil_char_boundary(s, 0), 0);
assert_eq!(ceil_char_boundary(s, 2), 5);
assert_eq!(ceil_char_boundary(s, 900), s.len());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn estimate_tokens_rounds_up_and_never_zero_for_nonempty() {
assert_eq!(estimate_tokens(""), 0);
assert_eq!(estimate_tokens("abc"), 1);
assert_eq!(estimate_tokens("abcd"), 1);
assert_eq!(estimate_tokens("abcde"), 2);
assert_eq!(estimate_tokens("\u{65e5}"), 1);
}
#[test]
fn interpolate_replaces_known_placeholders_and_keeps_the_rest() {
assert_eq!(
interpolate(
"populate {region} - yes, {region} - in stage {stage} {unknown}",
&[("region", "plan"), ("stage", "design")]
),
"populate plan - yes, plan - in stage design {unknown}"
);
assert_eq!(interpolate("no placeholders", &[]), "no placeholders");
}
#[test]
fn floor_char_boundary_clamps_and_walks_back() {
assert_eq!(floor_char_boundary("abc", 99), 3);
assert_eq!(floor_char_boundary("abc", 2), 2);
assert_eq!(floor_char_boundary("日本語", 0), 0);
let s = "abc🎉";
assert_eq!(floor_char_boundary(s, 4), 3);
assert_eq!(floor_char_boundary(s, 6), 3);
assert_eq!(floor_char_boundary(s, 7), 7);
assert_eq!(floor_char_boundary("🎉abc", 2), 0);
assert_eq!(floor_char_boundary("", 5), 0);
}
#[test]
fn truncate_at_boundary_never_splits_a_character() {
assert_eq!(truncate_at_boundary("abc", 99), "abc");
assert_eq!(truncate_at_boundary("abcdef", 3), "abc");
assert_eq!(truncate_at_boundary("abc🎉def", 5), "abc");
assert_eq!(truncate_at_boundary("🎉abc", 2), "");
assert_eq!(truncate_at_boundary("", 5), "");
}
#[test]
fn substring_is_total() {
assert_eq!(substring("abcdef", 2, 4), "cd");
assert_eq!(substring("abcdef", 0, 6), "abcdef");
assert_eq!(substring("a🎉b", 1, 4), "");
assert_eq!(substring("a🎉b", 0, 3), "a");
assert_eq!(substring("a🎉b", 1, 5), "🎉");
assert_eq!(substring("abc", 1, 99), "bc");
assert_eq!(substring("abc", 99, 99), "");
assert_eq!(substring("abc", 2, 1), "");
assert_eq!(substring("abc", usize::MAX, usize::MAX), "");
assert_eq!(substring("", 3, 9), "");
}
#[test]
fn split_at_boundary_halves_rejoin_to_the_input() {
assert_eq!(split_at_boundary("abcdef", 2), ("ab", "cdef"));
assert_eq!(split_at_boundary("abc", 0), ("", "abc"));
assert_eq!(split_at_boundary("a🎉b", 3), ("a", "🎉b"));
assert_eq!(split_at_boundary("abc", 99), ("abc", ""));
assert_eq!(split_at_boundary("", 4), ("", ""));
for mid in 0..=10 {
let (head, tail) = split_at_boundary("a🎉bc", mid);
assert_eq!(format!("{head}{tail}"), "a🎉bc", "lost text at mid={mid}");
}
}
}