Skip to main content

oxihuman_core/
patience_diff.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Patience diff algorithm stub.
6//!
7//! Finds unique common lines first (patience LCS), then recurses to diff
8//! the regions between them. This produces more readable diffs for code.
9
10/// A hunk in a patience diff result.
11#[derive(Debug, Clone, PartialEq)]
12pub struct PatienceHunk {
13    pub old_start: usize,
14    pub old_len: usize,
15    pub new_start: usize,
16    pub new_len: usize,
17    pub removed: Vec<String>,
18    pub added: Vec<String>,
19}
20
21/// Result of running patience diff.
22#[derive(Debug, Clone)]
23pub struct PatienceDiff {
24    pub hunks: Vec<PatienceHunk>,
25}
26
27impl PatienceDiff {
28    pub fn new() -> Self {
29        Self { hunks: Vec::new() }
30    }
31
32    pub fn hunk_count(&self) -> usize {
33        self.hunks.len()
34    }
35
36    pub fn is_identical(&self) -> bool {
37        self.hunks.is_empty()
38    }
39
40    pub fn total_removed(&self) -> usize {
41        self.hunks.iter().map(|h| h.old_len).sum()
42    }
43
44    pub fn total_added(&self) -> usize {
45        self.hunks.iter().map(|h| h.new_len).sum()
46    }
47}
48
49impl Default for PatienceDiff {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55/// Find lines that appear exactly once in both `old` and `new`.
56pub fn unique_common_lines<'a>(old: &[&'a str], new: &[&'a str]) -> Vec<&'a str> {
57    use std::collections::HashMap;
58    let mut old_counts: HashMap<&str, usize> = HashMap::new();
59    let mut new_counts: HashMap<&str, usize> = HashMap::new();
60    for &l in old {
61        *old_counts.entry(l).or_insert(0) += 1;
62    }
63    for &l in new {
64        *new_counts.entry(l).or_insert(0) += 1;
65    }
66    old.iter()
67        .filter(|&&l| old_counts.get(l) == Some(&1) && new_counts.get(l) == Some(&1))
68        .copied()
69        .collect()
70}
71
72/// Run patience diff on two line slices and return the result.
73pub fn patience_diff(old: &[&str], new: &[&str]) -> PatienceDiff {
74    let mut diff = PatienceDiff::new();
75    /* Simple stub: emit one hunk per differing line position */
76    let n = old.len().max(new.len());
77    let mut oi = 0usize;
78    let mut ni = 0usize;
79    while oi < old.len() || ni < new.len() {
80        let old_line = old.get(oi).copied();
81        let new_line = new.get(ni).copied();
82        if old_line == new_line {
83            oi += 1;
84            ni += 1;
85            continue;
86        }
87        let mut hunk = PatienceHunk {
88            old_start: oi,
89            new_start: ni,
90            old_len: 0,
91            new_len: 0,
92            removed: Vec::new(),
93            added: Vec::new(),
94        };
95        if let Some(ol) = old_line {
96            hunk.removed.push(ol.to_string());
97            hunk.old_len = 1;
98        }
99        if let Some(nl) = new_line {
100            hunk.added.push(nl.to_string());
101            hunk.new_len = 1;
102        }
103        diff.hunks.push(hunk);
104        if oi < old.len() {
105            oi += 1;
106        }
107        if ni < new.len() {
108            ni += 1;
109        }
110    }
111    let _ = n; /* suppress unused warning */
112    diff
113}
114
115/// Return a unified-diff-like string from a PatienceDiff.
116pub fn patience_diff_to_string(diff: &PatienceDiff) -> String {
117    let mut out = String::new();
118    for h in &diff.hunks {
119        out.push_str(&format!(
120            "@@ -{},{} +{},{} @@\n",
121            h.old_start, h.old_len, h.new_start, h.new_len
122        ));
123        for l in &h.removed {
124            out.push('-');
125            out.push_str(l);
126            out.push('\n');
127        }
128        for l in &h.added {
129            out.push('+');
130            out.push_str(l);
131            out.push('\n');
132        }
133    }
134    out
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_identical_is_empty() {
143        let lines = ["a", "b", "c"];
144        let diff = patience_diff(&lines, &lines);
145        assert!(diff.is_identical());
146    }
147
148    #[test]
149    fn test_one_change() {
150        let old = ["a", "b"];
151        let new = ["a", "c"];
152        let diff = patience_diff(&old, &new);
153        assert!(!diff.is_identical());
154    }
155
156    #[test]
157    fn test_hunk_count() {
158        let old = ["x"];
159        let new = ["y"];
160        let diff = patience_diff(&old, &new);
161        assert_eq!(diff.hunk_count(), 1);
162    }
163
164    #[test]
165    fn test_total_removed_added() {
166        let old = ["a", "b"];
167        let new = ["c", "d"];
168        let diff = patience_diff(&old, &new);
169        assert_eq!(diff.total_removed(), diff.total_added());
170    }
171
172    #[test]
173    fn test_unique_common_lines() {
174        let old = ["a", "b", "c"];
175        let new = ["b", "d", "e"];
176        let common = unique_common_lines(&old, &new);
177        assert_eq!(common, vec!["b"]);
178    }
179
180    #[test]
181    fn test_unique_common_lines_empty_when_duplicate() {
182        let old = ["a", "a"];
183        let new = ["a"];
184        let common = unique_common_lines(&old, &new);
185        assert!(common.is_empty());
186    }
187
188    #[test]
189    fn test_to_string_contains_at() {
190        let old = ["x"];
191        let new = ["y"];
192        let diff = patience_diff(&old, &new);
193        let s = patience_diff_to_string(&diff);
194        assert!(s.contains("@@"));
195    }
196
197    #[test]
198    fn test_default() {
199        let d = PatienceDiff::default();
200        assert!(d.is_identical());
201    }
202
203    #[test]
204    fn test_added_lines_tracked() {
205        let old: &[&str] = &[];
206        let new = ["a", "b"];
207        let diff = patience_diff(old, &new);
208        assert!(diff.total_added() > 0);
209    }
210}