pub const MAX_LESSONS: usize = 10;
pub const MAX_LESSON_CHARS: usize = 240;
pub const MAX_BLOB_BYTES: usize = 2000;
pub const LESSONS_HEADER: &str = "=== Lessons (self-recorded) ===";
fn normalize(lesson: &str) -> String {
let collapsed: String = lesson
.split(['\n', '\r'])
.map(str::trim)
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
collapsed.chars().take(MAX_LESSON_CHARS).collect()
}
fn lines_of(blob: &str) -> Vec<&str> {
blob.lines()
.map(str::trim)
.filter(|l| !l.is_empty())
.collect()
}
pub fn merge_lesson(existing: &str, new_lesson: &str) -> String {
let lesson = normalize(new_lesson);
let mut lines = lines_of(existing);
if lesson.is_empty() || lines.iter().any(|l| *l == lesson) {
return existing.to_string();
}
lines.push(&lesson);
if lines.len() > MAX_LESSONS {
lines.drain(..lines.len() - MAX_LESSONS);
}
let joined_len =
|ls: &[&str]| ls.iter().map(|l| l.len()).sum::<usize>() + ls.len().saturating_sub(1);
while lines.len() > 1 && joined_len(&lines) > MAX_BLOB_BYTES {
lines.remove(0);
}
lines.join("\n")
}
pub fn replace_all(new_blob: &str) -> String {
let mut lines: Vec<String> = Vec::new();
for raw in new_blob.lines() {
let line = normalize(raw);
if line.is_empty() || lines.contains(&line) {
continue;
}
lines.push(line);
}
if lines.len() > MAX_LESSONS {
lines.drain(..lines.len() - MAX_LESSONS);
}
let joined_len =
|ls: &[String]| ls.iter().map(|l| l.len()).sum::<usize>() + ls.len().saturating_sub(1);
while lines.len() > 1 && joined_len(&lines) > MAX_BLOB_BYTES {
lines.remove(0);
}
lines.join("\n")
}
pub fn compose_section(lessons: &str) -> Option<String> {
let lines = lines_of(lessons);
if lines.is_empty() {
return None;
}
Some(format!("{LESSONS_HEADER}\n{}", lines.join("\n")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_appends_to_empty() {
assert_eq!(merge_lesson("", "always compile first"), "always compile first");
}
#[test]
fn merge_appends_after_existing() {
let blob = merge_lesson("lesson one", "lesson two");
assert_eq!(blob, "lesson one\nlesson two");
}
#[test]
fn merge_rejects_empty_and_whitespace() {
assert_eq!(merge_lesson("a", ""), "a");
assert_eq!(merge_lesson("a", " "), "a");
assert_eq!(merge_lesson("a", "\n\r\n \n"), "a");
assert_eq!(merge_lesson("", " "), "");
}
#[test]
fn merge_rejects_exact_duplicate() {
let existing = "lesson one\nlesson two";
assert_eq!(merge_lesson(existing, "lesson one"), existing);
assert_eq!(merge_lesson(existing, "lesson two"), existing);
assert_eq!(merge_lesson(existing, " lesson one "), existing);
assert_eq!(merge_lesson(existing, "lesson\none"), existing);
let messy = " lesson one \n\nlesson two";
assert_eq!(merge_lesson(messy, "lesson one"), messy);
}
#[test]
fn merge_collapses_internal_newlines() {
let blob = merge_lesson("", "first part\nsecond part\r\nthird part");
assert_eq!(blob, "first part second part third part");
let blob = merge_lesson("", "a\n\n\nb");
assert_eq!(blob, "a b");
}
#[test]
fn merge_caps_lesson_at_240_chars() {
let long = "x".repeat(500);
let blob = merge_lesson("", &long);
assert_eq!(blob.chars().count(), MAX_LESSON_CHARS);
let emoji = "é".repeat(500);
let blob = merge_lesson("", &emoji);
assert_eq!(blob.chars().count(), MAX_LESSON_CHARS);
}
#[test]
fn merge_truncation_then_duplicate_check() {
let a = format!("{}{}", "x".repeat(240), "AAA");
let b = format!("{}{}", "x".repeat(240), "BBB");
let blob = merge_lesson("", &a);
assert_eq!(merge_lesson(&blob, &b), blob);
}
#[test]
fn merge_keeps_only_last_10() {
let mut blob = String::new();
for i in 0..12 {
blob = merge_lesson(&blob, &format!("lesson {i}"));
}
let lines: Vec<&str> = blob.lines().collect();
assert_eq!(lines.len(), MAX_LESSONS);
assert_eq!(lines[0], "lesson 2");
assert_eq!(lines[9], "lesson 11");
}
#[test]
fn merge_caps_blob_at_2000_bytes_dropping_oldest() {
let mut blob = String::new();
for i in 0..10 {
blob = merge_lesson(&blob, &format!("{i}{}", "x".repeat(239)));
}
assert!(blob.len() <= MAX_BLOB_BYTES, "blob is {} bytes", blob.len());
let lines: Vec<&str> = blob.lines().collect();
assert_eq!(lines.len(), 8, "two oldest dropped to fit 2000 bytes");
assert!(lines[0].starts_with('2'), "oldest surviving lesson is #2");
assert!(lines[7].starts_with('9'), "newest lesson always survives");
}
#[test]
fn merge_never_drops_the_newest_lesson() {
let mut blob = String::new();
for i in 0..9 {
blob = merge_lesson(&blob, &format!("{i}{}", "y".repeat(239)));
}
let newest = "z".repeat(240);
let merged = merge_lesson(&blob, &newest);
assert!(merged.lines().any(|l| l == newest));
assert!(merged.len() <= MAX_BLOB_BYTES);
}
#[test]
fn merge_normalizes_messy_existing_blob() {
let merged = merge_lesson(" a \n\n\nb\n", "c");
assert_eq!(merged, "a\nb\nc");
}
#[test]
fn replace_all_empty_input_yields_empty() {
assert_eq!(replace_all(""), "");
assert_eq!(replace_all(" \n \r\n \n"), "");
}
#[test]
fn replace_all_trims_and_drops_blank_lines() {
assert_eq!(replace_all(" a \n\n\nb\n"), "a\nb");
}
#[test]
fn replace_all_drops_exact_duplicates_keeping_first() {
assert_eq!(replace_all("a\nb\na\n b \nc"), "a\nb\nc");
}
#[test]
fn replace_all_caps_each_line_at_240_chars() {
let long = "x".repeat(500);
let blob = replace_all(&long);
assert_eq!(blob.chars().count(), MAX_LESSON_CHARS);
let emoji = "é".repeat(500);
let blob = replace_all(&emoji);
assert_eq!(blob.chars().count(), MAX_LESSON_CHARS);
}
#[test]
fn replace_all_truncation_then_duplicate_check() {
let a = format!("{}{}", "x".repeat(240), "AAA");
let b = format!("{}{}", "x".repeat(240), "BBB");
let blob = replace_all(&format!("{a}\n{b}"));
assert_eq!(blob, "x".repeat(240));
}
#[test]
fn replace_all_keeps_only_last_10() {
let input: Vec<String> = (0..12).map(|i| format!("lesson {i}")).collect();
let blob = replace_all(&input.join("\n"));
let lines: Vec<&str> = blob.lines().collect();
assert_eq!(lines.len(), MAX_LESSONS);
assert_eq!(lines[0], "lesson 2");
assert_eq!(lines[9], "lesson 11");
}
#[test]
fn replace_all_caps_blob_at_2000_bytes_dropping_oldest() {
let input: Vec<String> = (0..10).map(|i| format!("{i}{}", "x".repeat(239))).collect();
let blob = replace_all(&input.join("\n"));
assert!(blob.len() <= MAX_BLOB_BYTES, "blob is {} bytes", blob.len());
let lines: Vec<&str> = blob.lines().collect();
assert_eq!(lines.len(), 8, "two oldest dropped to fit 2000 bytes");
assert!(lines[0].starts_with('2'), "oldest surviving line is #2");
assert!(lines[7].starts_with('9'), "newest line always survives");
}
#[test]
fn replace_all_never_drops_the_newest_line() {
let mut input: Vec<String> = (0..9).map(|i| format!("{i}{}", "y".repeat(239))).collect();
let newest = "z".repeat(240);
input.push(newest.clone());
let blob = replace_all(&input.join("\n"));
assert!(blob.lines().any(|l| l == newest));
assert!(blob.len() <= MAX_BLOB_BYTES);
}
#[test]
fn replace_all_is_idempotent_on_merge_output() {
let mut blob = String::new();
for i in 0..12 {
blob = merge_lesson(&blob, &format!("lesson {i}"));
}
assert_eq!(replace_all(&blob), blob);
}
#[test]
fn replace_all_then_compose_round_trip() {
let blob = replace_all("first\nsecond");
let section = compose_section(&blob).unwrap();
assert!(section.starts_with(LESSONS_HEADER));
assert!(section.contains("first"));
assert!(section.ends_with("second"));
}
#[test]
fn compose_section_none_when_empty() {
assert_eq!(compose_section(""), None);
assert_eq!(compose_section(" \n \n"), None);
}
#[test]
fn compose_section_renders_header_and_lines() {
let s = compose_section("lesson one\nlesson two").unwrap();
assert_eq!(s, "=== Lessons (self-recorded) ===\nlesson one\nlesson two");
let s = compose_section("\na\n\nb\n").unwrap();
assert_eq!(s, format!("{LESSONS_HEADER}\na\nb"));
}
#[test]
fn merge_then_compose_round_trip() {
let blob = merge_lesson(&merge_lesson("", "first"), "second");
let section = compose_section(&blob).unwrap();
assert!(section.starts_with(LESSONS_HEADER));
assert!(section.contains("first"));
assert!(section.ends_with("second"));
}
}