use sequencematcher::SequenceMatcher; use std::cmp; use utils::{count_leading, str_with_similar_chars}; #[derive(Default)] pub struct Differ { pub line_junk: Option bool>, pub char_junk: Option bool>, } impl Differ { pub fn new() -> Differ { Differ { line_junk: None, char_junk: None, } } pub fn compare(&self, first_sequence: &[&str], second_sequence: &[&str]) -> Vec { let mut matcher = SequenceMatcher::new(first_sequence, second_sequence); matcher.set_is_junk(self.line_junk); let mut res = Vec::new(); for opcode in matcher.get_opcodes() { let mut gen = Vec::new(); match opcode.tag.as_ref() { "replace" => { gen = self.fancy_replace( first_sequence, opcode.first_start, opcode.first_end, second_sequence, opcode.second_start, opcode.second_end, ) } "delete" => { gen = self.dump("-", first_sequence, opcode.first_start, opcode.first_end) } "insert" => { gen = self.dump("+", second_sequence, opcode.second_start, opcode.second_end) } "equal" => { gen = self.dump(" ", first_sequence, opcode.first_start, opcode.first_end) } _ => {} } for i in gen { res.push(i); } } res } fn dump(&self, tag: &str, sequence: &[&str], start: usize, end: usize) -> Vec { let mut res = Vec::new(); for i in start..end { if let Some(s) = sequence.get(i) { res.push(format!("{} {}", tag, s)) } } res } fn plain_replace( &self, first_sequence: &[&str], first_start: usize, first_end: usize, second_sequence: &[&str], second_start: usize, second_end: usize, ) -> Vec { if !(first_start < first_end && second_start < second_end) { return Vec::new(); } let (mut first, second) = if second_end - second_start < first_end - first_start { ( self.dump("+", second_sequence, second_start, second_end), self.dump("-", first_sequence, first_start, first_end), ) } else { ( self.dump("-", first_sequence, first_start, first_end), self.dump("+", second_sequence, second_start, second_end), ) }; for s in second { first.push(s); } first } fn fancy_replace( &self, first_sequence: &[&str], first_start: usize, first_end: usize, second_sequence: &[&str], second_start: usize, second_end: usize, ) -> Vec { let mut res = Vec::new(); let (mut best_ratio, cutoff) = (0.74, 0.75); let (mut best_i, mut best_j) = (0, 0); let mut eqi: Option = None; let mut eqj: Option = None; for (j, second_sequence_str) in second_sequence .iter() .enumerate() .take(second_end) .skip(second_start) { for (i, first_sequence_str) in first_sequence .iter() .enumerate() .take(second_end) .skip(second_start) { if first_sequence_str == second_sequence_str { if eqi.is_none() { eqi = Some(i); eqj = Some(j); } continue; } let (first_sequence_chars, second_sequence_chars) = ( first_sequence_str.chars().collect::>(), second_sequence_str.chars().collect::>(), ); let mut cruncher = SequenceMatcher::new(&first_sequence_chars, &second_sequence_chars); cruncher.set_is_junk(self.char_junk); if cruncher.ratio() > best_ratio { best_ratio = cruncher.ratio(); best_i = i; best_j = j; } } } if best_ratio < cutoff { if eqi.is_none() { res.extend( self.plain_replace( first_sequence, first_start, first_end, second_sequence, second_start, second_end, ).iter() .cloned(), ); return res; } best_i = eqi.unwrap(); best_j = eqj.unwrap(); } else { eqi = None; } res.extend( self.fancy_helper( first_sequence, first_start, best_i, second_sequence, second_start, best_j, ).iter() .cloned(), ); let first_element = &first_sequence[best_i]; let second_element = &second_sequence[best_j]; if eqi.is_none() { let (mut first_tag, mut second_tag) = (String::new(), String::new()); let first_element_chars: Vec = first_element.chars().collect(); let second_element_chars: Vec = second_element.chars().collect(); let mut cruncher = SequenceMatcher::new(&first_element_chars, &second_element_chars); cruncher.set_is_junk(self.char_junk); for opcode in &cruncher.get_opcodes() { let (first_length, second_length) = ( opcode.first_end - opcode.first_start, opcode.second_end - opcode.second_start, ); match opcode.tag.as_ref() { "replace" => { first_tag.push_str(&str_with_similar_chars('^', first_length)); second_tag.push_str(&str_with_similar_chars('^', second_length)); } "delete" => { first_tag.push_str(&str_with_similar_chars('-', first_length)); } "insert" => { second_tag.push_str(&str_with_similar_chars('+', second_length)); } "equal" => { first_tag.push_str(&str_with_similar_chars(' ', first_length)); second_tag.push_str(&str_with_similar_chars(' ', second_length)); } _ => {} } } res.extend( self.qformat(&first_element, &second_element, &first_tag, &second_tag) .iter() .cloned(), ); } else { let mut s = String::from(" "); s.push_str(&first_element); res.push(s); } res.extend( self.fancy_helper( first_sequence, best_i + 1, first_end, second_sequence, best_j + 1, second_end, ).iter() .cloned(), ); res } fn fancy_helper( &self, first_sequence: &[&str], first_start: usize, first_end: usize, second_sequence: &[&str], second_start: usize, second_end: usize, ) -> Vec { let mut res = Vec::new(); if first_start < first_end { if second_start < second_end { res = self.fancy_replace( first_sequence, first_start, first_end, second_sequence, second_start, second_end, ); } else { res = self.dump("-", first_sequence, first_start, first_end); } } else if second_start < second_end { res = self.dump("+", second_sequence, second_start, second_end); } res } fn qformat( &self, first_line: &str, second_line: &str, first_tags: &str, second_tags: &str, ) -> Vec { let mut res = Vec::new(); let mut first_tags = first_tags; let mut second_tags = second_tags; let mut common = cmp::min( count_leading(first_line, '\t'), count_leading(second_line, '\t'), ); common = cmp::min(common, count_leading(first_tags.split_at(common).0, ' ')); common = cmp::min(common, count_leading(first_tags.split_at(common).0, ' ')); first_tags = first_tags.split_at(common).1.trim_right(); second_tags = second_tags.split_at(common).1.trim_right(); let mut s = format!("- {}", first_line); res.push(s); if first_tags != "" { s = format!("? {}{}\n", str_with_similar_chars('\t', common), first_tags); res.push(s); } s = format!("+ {}", second_line); res.push(s); if second_tags != "" { s = format!( "? {}{}\n", str_with_similar_chars('\t', common), second_tags ); res.push(s); } res } pub fn restore(delta: &[String], which: usize) -> Vec { if !(which == 1 || which == 2) { panic!("Second parameter must be 1 or 2"); } let mut res = Vec::new(); let tag = if which == 1 { "- " } else { "+ " }.to_string(); let prefixes = vec![tag, " ".to_string()]; for line in delta { for prefix in &prefixes { if line.starts_with(prefix) { res.push(line.split_at(2).1.to_string()); } } } res } } #[test] fn test_fancy_replace() { let differ = Differ::new(); let result = differ .fancy_replace(&vec!["abcDefghiJkl\n"], 0, 1, &vec!["abcdefGhijkl\n"], 0, 1) .join(""); assert_eq!( result, "- abcDefghiJkl\n? ^ ^ ^\n+ abcdefGhijkl\n? ^ ^ ^\n" ); } #[test] fn test_qformat() { let differ = Differ::new(); let result = differ.qformat( "\tabcDefghiJkl\n", "\tabcdefGhijkl\n", " ^ ^ ^ ", " ^ ^ ^ ", ); assert_eq!( result, vec![ "- \tabcDefghiJkl\n", "? \t ^ ^ ^\n", "+ \tabcdefGhijkl\n", "? \t ^ ^ ^\n", ] ); }