oxihuman_core/
edit_script.rs1#![allow(dead_code)]
4
5#[derive(Debug, Clone, PartialEq)]
13pub enum EditOp {
14 Keep(String),
15 Delete(String),
16 Insert(String),
17}
18
19#[derive(Debug, Clone)]
21pub struct EditScript {
22 pub ops: Vec<EditOp>,
23}
24
25impl EditScript {
26 pub fn new() -> Self {
27 Self { ops: Vec::new() }
28 }
29
30 pub fn push(&mut self, op: EditOp) {
31 self.ops.push(op);
32 }
33
34 pub fn len(&self) -> usize {
35 self.ops.len()
36 }
37
38 pub fn is_empty(&self) -> bool {
39 self.ops.is_empty()
40 }
41
42 pub fn keep_count(&self) -> usize {
43 self.ops
44 .iter()
45 .filter(|o| matches!(o, EditOp::Keep(_)))
46 .count()
47 }
48
49 pub fn delete_count(&self) -> usize {
50 self.ops
51 .iter()
52 .filter(|o| matches!(o, EditOp::Delete(_)))
53 .count()
54 }
55
56 pub fn insert_count(&self) -> usize {
57 self.ops
58 .iter()
59 .filter(|o| matches!(o, EditOp::Insert(_)))
60 .count()
61 }
62}
63
64impl Default for EditScript {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70pub fn build_edit_script(old: &[&str], new: &[&str]) -> EditScript {
74 let mut script = EditScript::new();
75 let n = old.len();
76 let m = new.len();
77 let mut i = 0;
78 let mut j = 0;
79 while i < n && j < m {
80 if old[i] == new[j] {
81 script.push(EditOp::Keep(old[i].to_string()));
82 i += 1;
83 j += 1;
84 } else {
85 script.push(EditOp::Delete(old[i].to_string()));
86 script.push(EditOp::Insert(new[j].to_string()));
87 i += 1;
88 j += 1;
89 }
90 }
91 while i < n {
92 script.push(EditOp::Delete(old[i].to_string()));
93 i += 1;
94 }
95 while j < m {
96 script.push(EditOp::Insert(new[j].to_string()));
97 j += 1;
98 }
99 script
100}
101
102pub fn apply_edit_script(script: &EditScript) -> Vec<String> {
104 script
105 .ops
106 .iter()
107 .filter_map(|op| match op {
108 EditOp::Keep(s) | EditOp::Insert(s) => Some(s.clone()),
109 EditOp::Delete(_) => None,
110 })
111 .collect()
112}
113
114pub fn edit_distance_from_script(script: &EditScript) -> usize {
116 script
117 .ops
118 .iter()
119 .filter(|o| !matches!(o, EditOp::Keep(_)))
120 .count()
121}
122
123pub fn script_to_diff_string(script: &EditScript) -> String {
125 let mut out = String::new();
126 for op in &script.ops {
127 match op {
128 EditOp::Keep(s) => {
129 out.push(' ');
130 out.push_str(s);
131 out.push('\n');
132 }
133 EditOp::Delete(s) => {
134 out.push('-');
135 out.push_str(s);
136 out.push('\n');
137 }
138 EditOp::Insert(s) => {
139 out.push('+');
140 out.push_str(s);
141 out.push('\n');
142 }
143 }
144 }
145 out
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_empty_inputs() {
154 let script = build_edit_script(&[], &[]);
155 assert!(script.is_empty());
156 }
157
158 #[test]
159 fn test_identical_lines_all_keep() {
160 let old = ["a", "b", "c"];
161 let script = build_edit_script(&old, &old);
162 assert_eq!(script.keep_count(), 3);
163 assert_eq!(script.delete_count(), 0);
164 assert_eq!(script.insert_count(), 0);
165 }
166
167 #[test]
168 fn test_all_deleted() {
169 let old = ["x", "y"];
170 let script = build_edit_script(&old, &[]);
171 assert_eq!(script.delete_count(), 2);
172 assert_eq!(script.insert_count(), 0);
173 }
174
175 #[test]
176 fn test_all_inserted() {
177 let new = ["x", "y"];
178 let script = build_edit_script(&[], &new);
179 assert_eq!(script.insert_count(), 2);
180 assert_eq!(script.delete_count(), 0);
181 }
182
183 #[test]
184 fn test_apply_gives_new_lines() {
185 let old = ["a", "b"];
186 let new = ["a", "c"];
187 let script = build_edit_script(&old, &new);
188 let result = apply_edit_script(&script);
189 assert_eq!(result, vec!["a", "c"]);
190 }
191
192 #[test]
193 fn test_edit_distance_nonzero() {
194 let old = ["a"];
195 let new = ["b"];
196 let script = build_edit_script(&old, &new);
197 assert!(edit_distance_from_script(&script) > 0);
198 }
199
200 #[test]
201 fn test_script_to_diff_string_contains_plus_minus() {
202 let old = ["hello"];
203 let new = ["world"];
204 let script = build_edit_script(&old, &new);
205 let diff = script_to_diff_string(&script);
206 assert!(diff.contains('-'));
207 assert!(diff.contains('+'));
208 }
209
210 #[test]
211 fn test_len_and_is_empty() {
212 let mut s = EditScript::new();
213 assert!(s.is_empty());
214 s.push(EditOp::Keep("x".into()));
215 assert_eq!(s.len(), 1);
216 }
217
218 #[test]
219 fn test_default() {
220 let s = EditScript::default();
221 assert!(s.is_empty());
222 }
223}