oxihuman_export/
diff_export.rs1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct DiffEntry {
11 pub line_number: usize,
12 pub operation: DiffOp,
13 pub content: String,
14}
15
16#[allow(dead_code)]
18#[derive(Debug, Clone, PartialEq)]
19pub enum DiffOp {
20 Add,
21 Remove,
22 Context,
23}
24
25impl DiffOp {
26 pub fn symbol(&self) -> char {
27 match self {
28 DiffOp::Add => '+',
29 DiffOp::Remove => '-',
30 DiffOp::Context => ' ',
31 }
32 }
33}
34
35#[allow(dead_code)]
37pub struct DiffExport {
38 pub source_name: String,
39 pub target_name: String,
40 pub entries: Vec<DiffEntry>,
41}
42
43impl DiffExport {
44 #[allow(dead_code)]
45 pub fn new(source: &str, target: &str) -> Self {
46 Self {
47 source_name: source.to_string(),
48 target_name: target.to_string(),
49 entries: Vec::new(),
50 }
51 }
52}
53
54#[allow(dead_code)]
56pub fn compute_diff(source: &str, target: &str) -> DiffExport {
57 let mut diff = DiffExport::new(source, target);
58 let src_lines: Vec<&str> = source.lines().collect();
59 let tgt_lines: Vec<&str> = target.lines().collect();
60 let max = src_lines.len().max(tgt_lines.len());
61 for i in 0..max {
62 let src = src_lines.get(i).copied();
63 let tgt = tgt_lines.get(i).copied();
64 match (src, tgt) {
65 (Some(s), Some(t)) if s == t => {
66 diff.entries.push(DiffEntry {
67 line_number: i + 1,
68 operation: DiffOp::Context,
69 content: s.to_string(),
70 });
71 }
72 (Some(s), Some(t)) => {
73 diff.entries.push(DiffEntry {
74 line_number: i + 1,
75 operation: DiffOp::Remove,
76 content: s.to_string(),
77 });
78 diff.entries.push(DiffEntry {
79 line_number: i + 1,
80 operation: DiffOp::Add,
81 content: t.to_string(),
82 });
83 }
84 (Some(s), None) => {
85 diff.entries.push(DiffEntry {
86 line_number: i + 1,
87 operation: DiffOp::Remove,
88 content: s.to_string(),
89 });
90 }
91 (None, Some(t)) => {
92 diff.entries.push(DiffEntry {
93 line_number: i + 1,
94 operation: DiffOp::Add,
95 content: t.to_string(),
96 });
97 }
98 (None, None) => {}
99 }
100 }
101 diff
102}
103
104#[allow(dead_code)]
106pub fn export_diff_unified(diff: &DiffExport) -> String {
107 let mut out = format!("--- {}\n+++ {}\n", diff.source_name, diff.target_name);
108 for e in &diff.entries {
109 out.push(e.operation.symbol());
110 out.push_str(&e.content);
111 out.push('\n');
112 }
113 out
114}
115
116#[allow(dead_code)]
118pub fn addition_count(diff: &DiffExport) -> usize {
119 diff.entries
120 .iter()
121 .filter(|e| e.operation == DiffOp::Add)
122 .count()
123}
124
125#[allow(dead_code)]
127pub fn removal_count(diff: &DiffExport) -> usize {
128 diff.entries
129 .iter()
130 .filter(|e| e.operation == DiffOp::Remove)
131 .count()
132}
133
134#[allow(dead_code)]
136pub fn is_identical(diff: &DiffExport) -> bool {
137 !diff.entries.iter().any(|e| e.operation != DiffOp::Context)
138}
139
140#[allow(dead_code)]
142pub fn changed_line_count(diff: &DiffExport) -> usize {
143 addition_count(diff) + removal_count(diff)
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn identical_texts_no_changes() {
152 let diff = compute_diff("hello\nworld", "hello\nworld");
153 assert!(is_identical(&diff));
154 }
155
156 #[test]
157 fn changed_line_count_one_change() {
158 let diff = compute_diff("a\nb\nc", "a\nX\nc");
159 assert_eq!(changed_line_count(&diff), 2);
160 }
161
162 #[test]
163 fn addition_count_correct() {
164 let diff = compute_diff("a\nb", "a\nb\nc");
165 assert_eq!(addition_count(&diff), 1);
166 }
167
168 #[test]
169 fn removal_count_correct() {
170 let diff = compute_diff("a\nb\nc", "a\nb");
171 assert_eq!(removal_count(&diff), 1);
172 }
173
174 #[test]
175 fn unified_diff_starts_with_header() {
176 let diff = compute_diff("old", "new");
177 let out = export_diff_unified(&diff);
178 assert!(out.starts_with("---"));
179 }
180
181 #[test]
182 fn unified_diff_contains_plus() {
183 let diff = compute_diff("a", "b");
184 let out = export_diff_unified(&diff);
185 assert!(out.contains('+'));
186 }
187
188 #[test]
189 fn diff_op_symbol_correct() {
190 assert_eq!(DiffOp::Add.symbol(), '+');
191 assert_eq!(DiffOp::Remove.symbol(), '-');
192 assert_eq!(DiffOp::Context.symbol(), ' ');
193 }
194
195 #[test]
196 fn empty_strings_no_entries() {
197 let diff = compute_diff("", "");
198 assert_eq!(diff.entries.len(), 0);
199 }
200
201 #[test]
202 fn source_target_names_stored() {
203 let diff = DiffExport::new("old.cfg", "new.cfg");
204 assert_eq!(diff.source_name, "old.cfg");
205 assert_eq!(diff.target_name, "new.cfg");
206 }
207
208 #[test]
209 fn context_lines_counted() {
210 let diff = compute_diff("a\nb\nc", "a\nX\nc");
211 let ctx = diff
212 .entries
213 .iter()
214 .filter(|e| e.operation == DiffOp::Context)
215 .count();
216 assert_eq!(ctx, 2);
217 }
218}