-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from nighca/v2
V2
- Loading branch information
Showing
18 changed files
with
597 additions
and
437 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,45 +1,56 @@ | ||
diff-merge | ||
universal-diff | ||
========== | ||
|
||
diff & merge algorithm realized with Javascript | ||
|
||
There are two compare methods: simple/myers(used as default), while the latter performs better in most situations(O(ND)). | ||
|
||
### Usage | ||
|
||
- nodejs | ||
|
||
var _ = require('diff-merge'), | ||
var _ = require('universal-diff'), | ||
compare = _.compare, | ||
merge = _.merge; | ||
merge = _.merge, | ||
compareStr = _.compareStr, | ||
mergeStr = _.mergeStr; | ||
|
||
- browser | ||
|
||
<script type="text/javascript" src="../dist/diff.min.js"></script> | ||
<script type="text/javascript" src="diff.min.js"></script> | ||
<script type="text/javascript"> | ||
var _ = window.diff, | ||
compare = _.compare, | ||
merge = _.merge; | ||
merge = _.merge, | ||
compareStr = _.compareStr, | ||
mergeStr = _.mergeStr; | ||
</script> | ||
|
||
### Compare | ||
|
||
var seq1 = [1, 2, 'a', 'b'], | ||
seq2 = [1, 2, 'c', 'b']; | ||
|
||
var seqResult = compare(seq1, seq2); // seqResult: [[2, 1, ['c']] | ||
|
||
var s1 = 'abc', | ||
s2 = 'abcd', | ||
splitter = ''; | ||
|
||
var compareResult = compare(s1, s2, splitter); | ||
var strResult = compareStr(s1, s2, splitter); // strResult: { splitter: '', diff: [[3, 0, 'd']] } | ||
|
||
### Merge | ||
|
||
var s3 = merge(s1, compareResult); | ||
var seq3 = merge(seq1, seqResult); // seq3: [1, 2, 'b'] | ||
|
||
var s3 = mergeStr(s1, strResult); // s3: 'abcd' | ||
|
||
### Test | ||
|
||
test/test.html | ||
gulp test | ||
|
||
### Algorithm | ||
### Build | ||
|
||
gulp | ||
|
||
SIMPLE: http://en.wikipedia.org/wiki/Levenshtein_distance | ||
### Algorithm | ||
|
||
MYERS': https://neil.fraser.name/software/diff_match_patch/myers.pdf |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
{ | ||
"name": "diff-merge", | ||
"name": "universal-diff", | ||
"main": "dist/index.js", | ||
"version": "1.0.1", | ||
"homepage": "https://github.com/nighca/diff-merge", | ||
"homepage": "https://github.com/nighca/universal-diff", | ||
"authors": [ | ||
"nighca <[email protected]>" | ||
], | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,48 +1,87 @@ | ||
/*! diff-merge v1.0.1 | nighca([email protected]) | Apache License(2.0) */ | ||
/*! universal-diff v2.0.0 | nighca([email protected]) | Apache License(2.0) */ | ||
|
||
(function(global, undefined){ | ||
|
||
|
||
var compare = function(cnt1, cnt2, splitter){ | ||
var SPLITTER = typeof splitter === 'string' ? splitter : '', | ||
// steps | ||
|
||
MARK_EMPTY = -1, | ||
MARK_SAME = 0, | ||
var STEP_NOCHANGE = 0, | ||
STEP_REPLACE = 1, | ||
STEP_REMOVE = 2, | ||
STEP_INSERT = 3; | ||
|
||
STEP_NOCHANGE = 0, | ||
STEP_REPLACE = 1, | ||
STEP_REMOVE = 2, | ||
STEP_INSERT = 3; | ||
|
||
// result object | ||
var diff = [], | ||
result = { | ||
splitter: SPLITTER, | ||
diff: diff | ||
}; | ||
// script marks | ||
|
||
// if string equal | ||
if(cnt1 === cnt2){ | ||
return result; | ||
} | ||
var MARK_EMPTY = -1, | ||
MARK_SAME = 0; | ||
|
||
|
||
var defaultEqual = function(a, b){ | ||
return a === b; | ||
}; | ||
|
||
// caculate min-edit-script (naive) | ||
var naiveCompare = function(seq1, seq2, eq){ | ||
|
||
// convert string to array | ||
var arr1, arr2; | ||
if(typeof splitter === 'function'){ | ||
arr1 = splitter(cnt1); | ||
arr2 = splitter(cnt2); | ||
}else{ | ||
arr1 = cnt1.split(SPLITTER); | ||
arr2 = cnt2.split(SPLITTER); | ||
var l1 = seq1.length, | ||
l2 = seq2.length, | ||
distMap = Array.apply(null, {length: l1 + 1}).map(function(){return [];}), | ||
stepMap = Array.apply(null, {length: l1 + 1}).map(function(){return [];}), | ||
i, j; | ||
|
||
eq = eq || defaultEqual; | ||
|
||
for(i = 0; i <= l1; i++){ | ||
for(j = 0; j <= l2; j++){ | ||
|
||
if(i === 0 || j === 0){ | ||
distMap[i][j] = i || j; | ||
stepMap[i][j] = i > 0 ? STEP_REMOVE : STEP_INSERT; | ||
|
||
}else{ | ||
var equal = eq(seq1[i-1], seq2[j-1]), | ||
|
||
removeDist = distMap[i-1][j] + 1, | ||
insertDist = distMap[i][j-1] + 1, | ||
replaceDist = distMap[i-1][j-1] + (equal ? 0 : 2), | ||
dist = Math.min(replaceDist, removeDist, insertDist); | ||
|
||
distMap[i][j] = dist; | ||
|
||
switch(dist){ | ||
|
||
case replaceDist: | ||
stepMap[i][j] = equal ? STEP_NOCHANGE : STEP_REPLACE; | ||
break; | ||
|
||
case removeDist: | ||
stepMap[i][j] = STEP_REMOVE; | ||
break; | ||
|
||
case insertDist: | ||
stepMap[i][j] = STEP_INSERT; | ||
|
||
} | ||
} | ||
} | ||
} | ||
|
||
var N = arr1.length, | ||
M = arr2.length, | ||
return stepMap; | ||
}; | ||
|
||
// caculate min-edit-script (myers) | ||
var myersCompare = function(seq1, seq2, eq){ | ||
|
||
var N = seq1.length, | ||
M = seq2.length, | ||
MAX = N + M, | ||
steps = Array.apply(null, {length: M+N+1}).map(function(){return [];}), | ||
stepMap = Array.apply(null, {length: M+N+1}).map(function(){return [];}), | ||
furthestReaching = [], | ||
dist = -1; | ||
|
||
eq = eq || defaultEqual; | ||
|
||
furthestReaching[MAX + 1] = 0; | ||
|
||
// caculate min distance & log each step | ||
|
@@ -57,12 +96,12 @@ var compare = function(cnt1, cnt2, splitter){ | |
} | ||
|
||
y = x - k; | ||
steps[x][y] = step; | ||
stepMap[x][y] = step; | ||
|
||
while(x < N && y < M && arr1[x] === arr2[y]){ | ||
while(x < N && y < M && eq(seq1[x], seq2[y])){ | ||
x++; | ||
y++; | ||
steps[x][y] = STEP_NOCHANGE; | ||
stepMap[x][y] = STEP_NOCHANGE; | ||
} | ||
|
||
furthestReaching[k + MAX] = x; | ||
|
@@ -73,47 +112,75 @@ var compare = function(cnt1, cnt2, splitter){ | |
} | ||
} | ||
|
||
return stepMap; | ||
}; | ||
|
||
// use myers as default | ||
var coreCompare = myersCompare; | ||
|
||
// stepMap to contrast array | ||
var transformStepMap = function(seq1, seq2, stepMap){ | ||
// get contrast arrays (src & target) by analyze step by step | ||
var src = [], target = []; | ||
var l1 = seq1.length, | ||
l2 = seq2.length, | ||
src = [], target = []; | ||
|
||
for(var i = N,j = M; i > 0 || j > 0;){ | ||
switch(steps[i][j]){ | ||
for(var i = l1,j = l2; i > 0 || j > 0;){ | ||
switch(stepMap[i][j]){ | ||
|
||
case STEP_NOCHANGE: | ||
src.unshift(arr1[i-1]); | ||
src.unshift(seq1[i-1]); | ||
target.unshift(MARK_SAME); | ||
i -= 1; | ||
j -= 1; | ||
break; | ||
|
||
case STEP_REPLACE: | ||
src.unshift(arr1[i-1]); | ||
target.unshift(arr2[j-1]); | ||
src.unshift(seq1[i-1]); | ||
target.unshift(seq2[j-1]); | ||
i -= 1; | ||
j -= 1; | ||
break; | ||
|
||
case STEP_REMOVE: | ||
src.unshift(arr1[i-1]); | ||
src.unshift(seq1[i-1]); | ||
target.unshift(MARK_EMPTY); | ||
i -= 1; | ||
j -= 0; | ||
break; | ||
|
||
case STEP_INSERT: | ||
src.unshift(MARK_EMPTY); | ||
target.unshift(arr2[j-1]); | ||
target.unshift(seq2[j-1]); | ||
i -= 0; | ||
j -= 1; | ||
break; | ||
|
||
} | ||
} | ||
|
||
// convert contrast arrays to diff array | ||
return { | ||
src: src, | ||
target: target | ||
}; | ||
}; | ||
|
||
// get edit script | ||
var compare = function(seq1, seq2, eq){ | ||
|
||
// do compare | ||
var stepMap = coreCompare(seq1, seq2, eq); | ||
|
||
// transform stepMap | ||
var contrast = transformStepMap(seq1, seq2, stepMap), | ||
src = contrast.src, | ||
target = contrast.target; | ||
|
||
// convert contrast arrays to edit script | ||
var l = target.length, | ||
start, len, to, | ||
notEmpty = function(s){return s !== MARK_EMPTY;}; | ||
notEmpty = function(s){return s !== MARK_EMPTY;}, | ||
script = []; | ||
|
||
for(i = l - 1; i >= 0;){ | ||
// join continuous diffs | ||
|
@@ -124,24 +191,57 @@ var compare = function(cnt1, cnt2, splitter){ | |
len = src.slice(j + 1, i + 1).filter(notEmpty).length; // length should be replaced (on src) | ||
to = target.slice(j + 1, i + 1).filter(notEmpty); // new content | ||
|
||
diff.unshift( | ||
script.unshift( | ||
to.length ? | ||
[start, len, to.join(SPLITTER)] : // replace | ||
[start, len, to] : // replace | ||
[start, len] // remove | ||
); | ||
} | ||
|
||
i = j - 1; | ||
} | ||
|
||
return script; | ||
}; | ||
|
||
// merge | ||
var merge = function(seq, script){ | ||
var result = seq.slice(); | ||
|
||
for(var i = script.length - 1, modify; i >= 0; i--){ | ||
modify = script[i]; | ||
var to = modify[2]; | ||
if(to){ | ||
modify = modify.slice(0, 2).concat(to); | ||
} | ||
result.splice.apply(result, modify); | ||
} | ||
|
||
return result; | ||
}; | ||
|
||
if(typeof module === "object" && typeof module.exports === "object"){ | ||
module.exports = compare; | ||
} | ||
// compare string (use splitter) | ||
var compareStr = function(str1, str2, splitter){ | ||
splitter = typeof splitter === 'string' ? splitter : ''; | ||
|
||
var seq1 = str1.split(splitter), | ||
seq2 = str2.split(splitter), | ||
script = compare(seq1, seq2); | ||
|
||
script.forEach(function(change){ | ||
if(change[2]){ | ||
change[2] = change[2].join(splitter); | ||
} | ||
}); | ||
|
||
return { | ||
splitter: splitter, | ||
diff: script | ||
}; | ||
}; | ||
|
||
var merge = function(cnt, compareResult){ | ||
// merge string (add spliter back) | ||
var mergeStr = function(cnt, compareResult){ | ||
var splitter = compareResult.splitter, | ||
diff = compareResult.diff, | ||
result = cnt.split(splitter); | ||
|
@@ -154,13 +254,25 @@ var merge = function(cnt, compareResult){ | |
return result.join(splitter); | ||
}; | ||
|
||
if(typeof module === "object" && typeof module.exports === "object"){ | ||
module.exports = merge; | ||
} | ||
|
||
global.diff = { | ||
var diff = { | ||
coreCompare: coreCompare, | ||
compare: compare, | ||
merge: merge | ||
merge: merge, | ||
compareStr: compareStr, | ||
mergeStr: mergeStr | ||
}; | ||
|
||
// RequireJS && SeaJS | ||
if(typeof define === 'function'){ | ||
define(function(){ | ||
return diff; | ||
}); | ||
|
||
// NodeJS | ||
}else if(typeof exports !== 'undefined'){ | ||
module.exports = diff; | ||
}else{ | ||
global.diff = diff; | ||
} | ||
|
||
})(this); |
Oops, something went wrong.