use regex::Regex;
use unicode_segmentation::UnicodeSegmentation;
const HEADER_RE: &str = r"^(####|##) \d+ \w+, \w+";
pub fn merge(s1: &str, s2: &str) -> String {
if s1.is_empty() {
return s2.to_string();
}
if s2.is_empty() {
return s1.to_string();
}
let lines1: Vec<&str> = s1.lines().collect();
let lines2: Vec<&str> = s2.lines().collect();
let mut lcs: Vec<Vec<usize>> = vec![vec![0; lines2.len() + 1]; lines1.len() + 1];
for i in 1..=lines1.len() {
for j in 1..=lines2.len() {
if lines1[i - 1] == lines2[j - 1] {
lcs[i][j] = lcs[i - 1][j - 1] + 1;
} else {
lcs[i][j] = lcs[i - 1][j].max(lcs[i][j - 1]);
}
}
}
let result = backtrack(&lines1, &lines2, &lcs, lines1.len(), lines2.len());
let result = merge_journal_headers(&result);
result.join("\n")
}
fn backtrack(
lines1: &[&str],
lines2: &[&str],
lcs: &[Vec<usize>],
i: usize,
j: usize,
) -> Vec<String> {
if i == 0 && j == 0 {
return vec![];
}
if i == 0 {
let mut r = backtrack(lines1, lines2, lcs, 0, j - 1);
r.push(lines2[j - 1].to_string());
return r;
}
if j == 0 {
let mut r = backtrack(lines1, lines2, lcs, i - 1, 0);
r.push(lines1[i - 1].to_string());
return r;
}
if lines1[i - 1] == lines2[j - 1] {
let mut r = backtrack(lines1, lines2, lcs, i - 1, j - 1);
r.push(lines1[i - 1].to_string());
r
} else if lcs[i - 1][j] > lcs[i][j - 1] {
let mut r = backtrack(lines1, lines2, lcs, i - 1, j);
r.push(lines1[i - 1].to_string());
r
} else {
let mut r = backtrack(lines1, lines2, lcs, i, j - 1);
r.push(lines2[j - 1].to_string());
r
}
}
fn merge_journal_headers(lines: &[String]) -> Vec<String> {
let re = Regex::new(HEADER_RE).unwrap();
let emoji_re = Regex::new(r" [^\w\s\p{P}]+$").unwrap();
let mut merged = Vec::new();
let groups = group_consecutive_headers(&re, lines);
for group in groups {
if group.len() == 1 {
merged.push(group[0].clone());
continue;
}
let date = emoji_re.replace_all(&group[0], "").to_string();
let prefix_same = group.iter().all(|line| {
let emojis = emoji_re.find(line).map(|m| m.as_str()).unwrap_or("");
date.clone() + emojis == *line
});
if !prefix_same {
merged.extend(group);
continue;
}
let mut found = String::new();
for line in &group {
if let Some(m) = emoji_re.find(line) {
found.push_str(m.as_str());
}
}
if !found.is_empty() {
found = format!(" {}", unique_graphemes(&found));
}
merged.push(date + &found);
}
merged
}
fn group_consecutive_headers(re: &Regex, lines: &[String]) -> Vec<Vec<String>> {
let mut groups: Vec<Vec<String>> = vec![];
let mut i = 0;
while i < lines.len() {
if re.is_match(&lines[i]) {
let mut group = vec![];
while i < lines.len() && re.is_match(&lines[i]) {
group.push(lines[i].clone());
i += 1;
}
groups.push(group);
} else {
groups.push(vec![lines[i].clone()]);
i += 1;
}
}
groups
}
fn unique_graphemes(s: &str) -> String {
let mut seen = String::new();
for g in s.graphemes(true) {
if !seen.contains(g) {
seen.push_str(g);
}
}
seen
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_merge_empty() {
assert_eq!(merge("", "hello"), "hello");
assert_eq!(merge("hello", ""), "hello");
}
#[test]
fn test_merge_identical() {
assert_eq!(merge("a\nb", "a\nb"), "a\nb");
}
#[test]
fn test_merge_different() {
let result = merge("a\nb\nc", "x\ny\nz");
assert!(result.contains("a"));
assert!(result.contains("x"));
}
#[test]
fn test_merge_preserves_common() {
let result = merge("header\na\nb\nfooter", "header\nc\nb\nfooter");
assert!(result.contains("header"));
assert!(result.contains("footer"));
assert!(result.contains("a"));
assert!(result.contains("c"));
assert!(result.contains("b"));
}
#[test]
fn test_unique_graphemes() {
assert_eq!(unique_graphemes("aab"), "ab");
}
}