Add new tests, fix bugs with Differ

This commit is contained in:
Dima Kudosh
2016-02-28 17:38:44 +03:00
parent 53553047df
commit 2d309c0ba4
3 changed files with 135 additions and 48 deletions
+42 -19
View File
@@ -16,18 +16,22 @@ impl Differ{
} }
} }
pub fn compare<T: ?Sized + Sequence>(&self, first_sequence: &T, second_sequence: &T) -> Vec<Vec<String>> { pub fn compare<T: ?Sized + Sequence>(&self, first_sequence: &T, second_sequence: &T) -> Vec<String> {
let mut matcher = SequenceMatcher::new(first_sequence, second_sequence); let mut matcher = SequenceMatcher::new(first_sequence, second_sequence);
let mut res = Vec::new(); let mut res = Vec::new();
for opcode in matcher.get_opcodes(){ for opcode in matcher.get_opcodes(){
let mut gen = Vec::new();
match opcode.tag.as_ref() { match opcode.tag.as_ref() {
"replace" => { res.push( self.fancy_replace(first_sequence, opcode.first_start, opcode.first_end, "replace" => { gen = self.fancy_replace(first_sequence, opcode.first_start, opcode.first_end,
second_sequence, opcode.second_start, opcode.second_end) ) }, second_sequence, opcode.second_start, opcode.second_end) },
"delete" => { res.push( self.dump("-", first_sequence, opcode.first_start, opcode.first_end) ) }, "delete" => { gen = self.dump("-", first_sequence, opcode.first_start, opcode.first_end) },
"insert" => { res.push( self.dump("+", second_sequence, opcode.second_start, opcode.second_end) ) }, "insert" => { gen = self.dump("+", second_sequence, opcode.second_start, opcode.second_end) },
"equal" => { res.push( self.dump(" ", first_sequence, opcode.first_start, opcode.first_end) ) }, "equal" => { gen = self.dump(" ", first_sequence, opcode.first_start, opcode.first_end) },
_ => {} _ => {}
} }
for i in gen {
res.push(i);
}
} }
res res
} }
@@ -57,19 +61,18 @@ impl Differ{
first = self.dump("-", first_sequence, first_start, first_end); first = self.dump("-", first_sequence, first_start, first_end);
second = self.dump("+", second_sequence, second_start, second_end); second = self.dump("+", second_sequence, second_start, second_end);
} }
for str in second{ for s in second{
first.push(str); first.push(s);
} }
first first
} }
fn fancy_replace<T: ?Sized + Sequence>(&self, first_sequence: &T, first_start: usize, first_end: usize, fn fancy_replace<T: ?Sized + Sequence>(&self, first_sequence: &T, first_start: usize, first_end: usize,
second_sequence: &T, second_start: usize, second_end: usize) -> Vec<String> { second_sequence: &T, second_start: usize, second_end: usize) -> Vec<String> {
let mut res = Vec::new(); let mut res = Vec::new();
let (mut best_ratio, cutoff) = (0.74, 0.75); let (mut best_ratio, cutoff) = (0.74, 0.75);
let (mut best_i, mut best_j) = (0, 0); let (mut best_i, mut best_j) = (0, 0);
let (mut second_sequence_str, mut first_sequence_str) = ("", ""); let (mut second_sequence_str, mut first_sequence_str) = ("", "");
//cruncher.charjunk = self.charjunk;
let mut eqi: Option<usize> = None; let mut eqi: Option<usize> = None;
let mut eqj: Option<usize> = None; let mut eqj: Option<usize> = None;
for j in second_start..second_end{ for j in second_start..second_end{
@@ -84,6 +87,7 @@ impl Differ{
continue; continue;
} }
let mut cruncher = SequenceMatcher::new(first_sequence_str, second_sequence_str); let mut cruncher = SequenceMatcher::new(first_sequence_str, second_sequence_str);
//cruncher.charjunk = self.charjunk;
if cruncher.ratio() > best_ratio{ if cruncher.ratio() > best_ratio{
best_ratio = cruncher.ratio(); best_ratio = cruncher.ratio();
best_i = i; best_i = i;
@@ -93,8 +97,8 @@ impl Differ{
} }
if best_ratio < cutoff{ if best_ratio < cutoff{
if eqi.is_none(){ if eqi.is_none(){
res.extend(self.plain_replace(first_sequence, first_start, first_end, second_sequence, second_start, second_end).iter().cloned()); //res.extend(self.plain_replace(first_sequence, first_start, first_end, second_sequence, second_start, second_end).iter().cloned());
return res return res
} }
best_i = eqi.unwrap(); best_i = eqi.unwrap();
best_j = eqj.unwrap(); best_j = eqj.unwrap();
@@ -103,10 +107,11 @@ impl Differ{
eqi = None; eqi = None;
} }
res.extend(self.fancy_helper(first_sequence, first_start, best_i, second_sequence, second_start, best_j).iter().cloned()); res.extend(self.fancy_helper(first_sequence, first_start, best_i, second_sequence, second_start, best_j).iter().cloned());
let (first_elt, second_elt) = (first_sequence_str.at_index(best_i).unwrap(), second_sequence_str.at_index(best_j).unwrap()); let (first_element, second_element) = (first_sequence.at_index(best_i).unwrap(), second_sequence.at_index(best_j).unwrap());
if eqi.is_none(){ if eqi.is_none(){
let (mut first_tag, mut second_tag) = (String::new(), String::new()); let (mut first_tag, mut second_tag) = (String::new(), String::new());
let mut cruncher = SequenceMatcher::new(first_elt, second_elt); let mut cruncher = SequenceMatcher::new(first_element, second_element);
//cruncher.charjunk = self.charjunk;
for opcode in &cruncher.get_opcodes(){ for opcode in &cruncher.get_opcodes(){
let (first_length, second_length) = (opcode.first_end - opcode.first_start, opcode.second_end - opcode.second_start); let (first_length, second_length) = (opcode.first_end - opcode.first_start, opcode.second_end - opcode.second_start);
match opcode.tag.as_ref() { match opcode.tag.as_ref() {
@@ -115,10 +120,10 @@ impl Differ{
second_tag.push_str(&str_with_similar_chars('^', second_length)); second_tag.push_str(&str_with_similar_chars('^', second_length));
}, },
"delete" => { "delete" => {
first_tag.push_str(&str_with_similar_chars('^', first_length)); first_tag.push_str(&str_with_similar_chars('-', first_length));
}, },
"insert" => { "insert" => {
second_tag.push_str(&str_with_similar_chars('^', second_length)); second_tag.push_str(&str_with_similar_chars('+', second_length));
}, },
"equal" => { "equal" => {
first_tag.push_str(&str_with_similar_chars(' ', first_length)); first_tag.push_str(&str_with_similar_chars(' ', first_length));
@@ -126,12 +131,12 @@ impl Differ{
}, },
_ => {} _ => {}
} }
res.extend(self.qformat(&first_elt, &second_elt, &first_tag, &second_tag).iter().cloned());
} }
res.extend(self.qformat(&first_element, &second_element, &first_tag, &second_tag).iter().cloned());
} else { } else {
let mut s = String::from(" "); let mut s = String::from(" ");
s.push_str(&first_elt); s.push_str(&first_element);
res.extend(vec![s].iter().cloned()); 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.extend(self.fancy_helper(first_sequence, best_i + 1, first_end, second_sequence, best_j + 1, second_end).iter().cloned());
res res
@@ -154,9 +159,13 @@ impl Differ{
fn qformat(&self, first_line: &str, second_line: &str, first_tags: &str, second_tags: &str) -> Vec<String> { fn qformat(&self, first_line: &str, second_line: &str, first_tags: &str, second_tags: &str) -> Vec<String> {
let mut res = Vec::new(); 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')); 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, ' '));
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 = String::from(format!("- {}", first_line)); let mut s = String::from(format!("- {}", first_line));
res.push(s); res.push(s);
if first_tags != ""{ if first_tags != ""{
@@ -173,3 +182,17 @@ impl Differ{
} }
} }
#[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"]);
}
+26 -27
View File
@@ -1,34 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::cmp::{Ordering, max, min}; use std::cmp::{Ordering, max, min};
use utils::calculate_ratio; use utils::calculate_ratio;
use std::fmt::Debug;
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord)]
pub struct Match{ pub struct Match{
pub first_start: usize, pub first_start: usize,
pub second_start: usize, pub second_start: usize,
pub size: usize pub size: usize
} }
impl Ord for Match {
fn cmp(&self, other: &Match) -> Ordering {
self.size.cmp(&other.size)
}
}
impl PartialOrd for Match {
fn partial_cmp(&self, other: &Match) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Match {
fn eq(&self, other: &Match) -> bool {
self.size == other.size
}
}
impl Eq for Match { }
impl Match{ impl Match{
fn new(first_start: usize, second_start: usize, size: usize) -> Match { fn new(first_start: usize, second_start: usize, size: usize) -> Match {
Match{ Match{
@@ -39,7 +20,7 @@ impl Match{
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub struct Opcode{ pub struct Opcode{
pub tag: String, pub tag: String,
pub first_start: usize, pub first_start: usize,
@@ -60,7 +41,7 @@ impl Opcode{
} }
} }
pub trait Sequence { pub trait Sequence: Debug {
fn len(&self) -> usize; fn len(&self) -> usize;
fn at_index(&self, index: usize) -> Option<&str>; fn at_index(&self, index: usize) -> Option<&str>;
fn slice(&self, start: usize, end: usize) -> Option<&str>; fn slice(&self, start: usize, end: usize) -> Option<&str>;
@@ -204,7 +185,6 @@ impl<'a, T: ?Sized + Sequence> SequenceMatcher<'a, T>{
} }
size += 1; size += 1;
new_j2len.insert(j, size); new_j2len.insert(j, size);
println!("{:?} {}", i, size);
if size > best_size { if size > best_size {
best_i = i + 1 - size; best_i = i + 1 - size;
best_j = j + 1 - size; best_j = j + 1 - size;
@@ -254,8 +234,7 @@ impl<'a, T: ?Sized + Sequence> SequenceMatcher<'a, T>{
}, },
} }
} }
println!("{:?}", matches); matches.sort_by(|a, b| a.cmp(b));
matches.sort_by(|a, b| b.cmp(a));
let (mut first_start, mut second_start, mut size) = (0, 0, 0); let (mut first_start, mut second_start, mut size) = (0, 0, 0);
let mut non_adjacent = Vec::new(); let mut non_adjacent = Vec::new();
for m in &matches{ for m in &matches{
@@ -278,7 +257,26 @@ impl<'a, T: ?Sized + Sequence> SequenceMatcher<'a, T>{
self.matching_blocks = Some(non_adjacent); self.matching_blocks = Some(non_adjacent);
self.matching_blocks.as_ref().unwrap().clone() self.matching_blocks.as_ref().unwrap().clone()
} }
/*
if self.opcodes is not None:
return self.opcodes
i = j = 0
self.opcodes = answer = []
for ai, bj, size in self.get_matching_blocks():
tag = ''
if i < ai and j < bj:
tag = 'replace'
elif i < ai:
tag = 'delete'
elif j < bj:
tag = 'insert'
if tag:
answer.append( (tag, i, ai, j, bj) )
i, j = ai+size, bj+size
if size:
answer.append( ('equal', ai, i, bj, j) )
return answer
*/
pub fn get_opcodes(&mut self) -> Vec<Opcode>{ pub fn get_opcodes(&mut self) -> Vec<Opcode>{
if self.opcodes.as_ref().is_some(){ if self.opcodes.as_ref().is_some(){
return self.opcodes.as_ref().unwrap().clone() return self.opcodes.as_ref().unwrap().clone()
@@ -315,6 +313,7 @@ impl<'a, T: ?Sized + Sequence> SequenceMatcher<'a, T>{
if codes.is_empty() { if codes.is_empty() {
codes.push(Opcode::new("equal".to_string(), 0, 1, 0, 1)); codes.push(Opcode::new("equal".to_string(), 0, 1, 0, 1));
} }
if codes.first().unwrap().tag == "equal" { if codes.first().unwrap().tag == "equal" {
let mut opcode = codes.first_mut().unwrap(); let mut opcode = codes.first_mut().unwrap();
opcode.first_start = max(opcode.first_start, opcode.first_end - n); opcode.first_start = max(opcode.first_start, opcode.first_end - n);
+67 -2
View File
@@ -1,7 +1,7 @@
extern crate difflib; extern crate difflib;
use difflib::sequencematcher::{SequenceMatcher, Match}; use difflib::sequencematcher::{SequenceMatcher, Match, Opcode};
use difflib::differ::Differ;
#[test] #[test]
fn test_longest_match() fn test_longest_match()
@@ -35,4 +35,69 @@ fn test_all_matches()
size: 0 size: 0
}); });
assert_eq!(result, expected_result); assert_eq!(result, expected_result);
}
#[test]
fn test_get_opcodes() {
let mut matcher = SequenceMatcher::new("qabxcd", "abycdf");
let result = matcher.get_opcodes();
let mut expected_result = Vec::new();
expected_result.push(Opcode{
tag: "delete".to_string(),
first_start: 0,
first_end: 1,
second_start: 0,
second_end: 0
});
expected_result.push(Opcode{
tag: "equal".to_string(),
first_start: 1,
first_end: 3,
second_start: 0,
second_end: 2
});
expected_result.push(Opcode{
tag: "replace".to_string(),
first_start: 3,
first_end: 4,
second_start: 2,
second_end: 3
});
expected_result.push(Opcode{
tag: "equal".to_string(),
first_start: 4,
first_end: 6,
second_start: 3,
second_end: 5
});
expected_result.push(Opcode{
tag: "insert".to_string(),
first_start: 6,
first_end: 6,
second_start: 5,
second_end: 6
});
assert_eq!(result, expected_result);
}
#[test]
fn test_ratio() {
let mut matcher = SequenceMatcher::new("abcd", "bcde");
assert_eq!(matcher.ratio(), 0.75);
}
#[test]
fn test_get_close_matches() {
let words = vec!["ape", "apple", "peach", "puppy"];
let result = difflib::get_close_matches("appel", words, 3, 0.6);
assert_eq!(result, vec!["apple", "ape"]);
}
#[test]
fn test_differ_compare() {
let first_text = vec!["one\n", "two\n", "three\n"];
let second_text = vec!["ore\n", "tree\n", "emu\n"];
let differ = Differ::new();
let result = differ.compare(&first_text, &second_text).join("");
assert_eq!(result, "- one\n? ^\n+ ore\n? ^\n- two\n- three\n? -\n+ tree\n+ emu\n");
} }