Skip to content

Commit

Permalink
Merge pull request #2 from nighca/v2
Browse files Browse the repository at this point in the history
V2
  • Loading branch information
nighca committed Sep 24, 2014
2 parents abdaab5 + ccfd3e6 commit f36f2ac
Show file tree
Hide file tree
Showing 18 changed files with 597 additions and 437 deletions.
35 changes: 23 additions & 12 deletions README.md
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
4 changes: 2 additions & 2 deletions bower.json
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]>"
],
Expand Down
222 changes: 167 additions & 55 deletions dist/diff.js
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
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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);
Loading

0 comments on commit f36f2ac

Please sign in to comment.