Skip to content

Commit f36f2ac

Browse files
committed
Merge pull request #2 from nighca/v2
V2
2 parents abdaab5 + ccfd3e6 commit f36f2ac

18 files changed

+597
-437
lines changed

README.md

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,56 @@
1-
diff-merge
1+
universal-diff
22
==========
33

44
diff & merge algorithm realized with Javascript
55

6-
There are two compare methods: simple/myers(used as default), while the latter performs better in most situations(O(ND)).
7-
86
### Usage
97

108
- nodejs
119

12-
var _ = require('diff-merge'),
10+
var _ = require('universal-diff'),
1311
compare = _.compare,
14-
merge = _.merge;
12+
merge = _.merge,
13+
compareStr = _.compareStr,
14+
mergeStr = _.mergeStr;
1515

1616
- browser
1717

18-
<script type="text/javascript" src="../dist/diff.min.js"></script>
18+
<script type="text/javascript" src="diff.min.js"></script>
1919
<script type="text/javascript">
2020
var _ = window.diff,
2121
compare = _.compare,
22-
merge = _.merge;
22+
merge = _.merge,
23+
compareStr = _.compareStr,
24+
mergeStr = _.mergeStr;
2325
</script>
2426

2527
### Compare
2628

29+
var seq1 = [1, 2, 'a', 'b'],
30+
seq2 = [1, 2, 'c', 'b'];
31+
32+
var seqResult = compare(seq1, seq2); // seqResult: [[2, 1, ['c']]
33+
2734
var s1 = 'abc',
2835
s2 = 'abcd',
2936
splitter = '';
3037

31-
var compareResult = compare(s1, s2, splitter);
38+
var strResult = compareStr(s1, s2, splitter); // strResult: { splitter: '', diff: [[3, 0, 'd']] }
3239

3340
### Merge
3441

35-
var s3 = merge(s1, compareResult);
42+
var seq3 = merge(seq1, seqResult); // seq3: [1, 2, 'b']
43+
44+
var s3 = mergeStr(s1, strResult); // s3: 'abcd'
3645

3746
### Test
3847

39-
test/test.html
48+
gulp test
4049

41-
### Algorithm
50+
### Build
51+
52+
gulp
4253

43-
SIMPLE: http://en.wikipedia.org/wiki/Levenshtein_distance
54+
### Algorithm
4455

4556
MYERS': https://neil.fraser.name/software/diff_match_patch/myers.pdf

bower.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
{
2-
"name": "diff-merge",
2+
"name": "universal-diff",
33
"main": "dist/index.js",
44
"version": "1.0.1",
5-
"homepage": "https://github.com/nighca/diff-merge",
5+
"homepage": "https://github.com/nighca/universal-diff",
66
"authors": [
77
"nighca <[email protected]>"
88
],

dist/diff.js

Lines changed: 167 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,87 @@
1-
/*! diff-merge v1.0.1 | nighca([email protected]) | Apache License(2.0) */
1+
/*! universal-diff v2.0.0 | nighca([email protected]) | Apache License(2.0) */
22

33
(function(global, undefined){
44

55

6-
var compare = function(cnt1, cnt2, splitter){
7-
var SPLITTER = typeof splitter === 'string' ? splitter : '',
6+
// steps
87

9-
MARK_EMPTY = -1,
10-
MARK_SAME = 0,
8+
var STEP_NOCHANGE = 0,
9+
STEP_REPLACE = 1,
10+
STEP_REMOVE = 2,
11+
STEP_INSERT = 3;
1112

12-
STEP_NOCHANGE = 0,
13-
STEP_REPLACE = 1,
14-
STEP_REMOVE = 2,
15-
STEP_INSERT = 3;
1613

17-
// result object
18-
var diff = [],
19-
result = {
20-
splitter: SPLITTER,
21-
diff: diff
22-
};
14+
// script marks
2315

24-
// if string equal
25-
if(cnt1 === cnt2){
26-
return result;
27-
}
16+
var MARK_EMPTY = -1,
17+
MARK_SAME = 0;
18+
19+
20+
var defaultEqual = function(a, b){
21+
return a === b;
22+
};
23+
24+
// caculate min-edit-script (naive)
25+
var naiveCompare = function(seq1, seq2, eq){
2826

29-
// convert string to array
30-
var arr1, arr2;
31-
if(typeof splitter === 'function'){
32-
arr1 = splitter(cnt1);
33-
arr2 = splitter(cnt2);
34-
}else{
35-
arr1 = cnt1.split(SPLITTER);
36-
arr2 = cnt2.split(SPLITTER);
27+
var l1 = seq1.length,
28+
l2 = seq2.length,
29+
distMap = Array.apply(null, {length: l1 + 1}).map(function(){return [];}),
30+
stepMap = Array.apply(null, {length: l1 + 1}).map(function(){return [];}),
31+
i, j;
32+
33+
eq = eq || defaultEqual;
34+
35+
for(i = 0; i <= l1; i++){
36+
for(j = 0; j <= l2; j++){
37+
38+
if(i === 0 || j === 0){
39+
distMap[i][j] = i || j;
40+
stepMap[i][j] = i > 0 ? STEP_REMOVE : STEP_INSERT;
41+
42+
}else{
43+
var equal = eq(seq1[i-1], seq2[j-1]),
44+
45+
removeDist = distMap[i-1][j] + 1,
46+
insertDist = distMap[i][j-1] + 1,
47+
replaceDist = distMap[i-1][j-1] + (equal ? 0 : 2),
48+
dist = Math.min(replaceDist, removeDist, insertDist);
49+
50+
distMap[i][j] = dist;
51+
52+
switch(dist){
53+
54+
case replaceDist:
55+
stepMap[i][j] = equal ? STEP_NOCHANGE : STEP_REPLACE;
56+
break;
57+
58+
case removeDist:
59+
stepMap[i][j] = STEP_REMOVE;
60+
break;
61+
62+
case insertDist:
63+
stepMap[i][j] = STEP_INSERT;
64+
65+
}
66+
}
67+
}
3768
}
3869

39-
var N = arr1.length,
40-
M = arr2.length,
70+
return stepMap;
71+
};
72+
73+
// caculate min-edit-script (myers)
74+
var myersCompare = function(seq1, seq2, eq){
75+
76+
var N = seq1.length,
77+
M = seq2.length,
4178
MAX = N + M,
42-
steps = Array.apply(null, {length: M+N+1}).map(function(){return [];}),
79+
stepMap = Array.apply(null, {length: M+N+1}).map(function(){return [];}),
4380
furthestReaching = [],
4481
dist = -1;
4582

83+
eq = eq || defaultEqual;
84+
4685
furthestReaching[MAX + 1] = 0;
4786

4887
// caculate min distance & log each step
@@ -57,12 +96,12 @@ var compare = function(cnt1, cnt2, splitter){
5796
}
5897

5998
y = x - k;
60-
steps[x][y] = step;
99+
stepMap[x][y] = step;
61100

62-
while(x < N && y < M && arr1[x] === arr2[y]){
101+
while(x < N && y < M && eq(seq1[x], seq2[y])){
63102
x++;
64103
y++;
65-
steps[x][y] = STEP_NOCHANGE;
104+
stepMap[x][y] = STEP_NOCHANGE;
66105
}
67106

68107
furthestReaching[k + MAX] = x;
@@ -73,47 +112,75 @@ var compare = function(cnt1, cnt2, splitter){
73112
}
74113
}
75114

115+
return stepMap;
116+
};
117+
118+
// use myers as default
119+
var coreCompare = myersCompare;
120+
121+
// stepMap to contrast array
122+
var transformStepMap = function(seq1, seq2, stepMap){
76123
// get contrast arrays (src & target) by analyze step by step
77-
var src = [], target = [];
124+
var l1 = seq1.length,
125+
l2 = seq2.length,
126+
src = [], target = [];
78127

79-
for(var i = N,j = M; i > 0 || j > 0;){
80-
switch(steps[i][j]){
128+
for(var i = l1,j = l2; i > 0 || j > 0;){
129+
switch(stepMap[i][j]){
81130

82131
case STEP_NOCHANGE:
83-
src.unshift(arr1[i-1]);
132+
src.unshift(seq1[i-1]);
84133
target.unshift(MARK_SAME);
85134
i -= 1;
86135
j -= 1;
87136
break;
88137

89138
case STEP_REPLACE:
90-
src.unshift(arr1[i-1]);
91-
target.unshift(arr2[j-1]);
139+
src.unshift(seq1[i-1]);
140+
target.unshift(seq2[j-1]);
92141
i -= 1;
93142
j -= 1;
94143
break;
95144

96145
case STEP_REMOVE:
97-
src.unshift(arr1[i-1]);
146+
src.unshift(seq1[i-1]);
98147
target.unshift(MARK_EMPTY);
99148
i -= 1;
100149
j -= 0;
101150
break;
102151

103152
case STEP_INSERT:
104153
src.unshift(MARK_EMPTY);
105-
target.unshift(arr2[j-1]);
154+
target.unshift(seq2[j-1]);
106155
i -= 0;
107156
j -= 1;
108157
break;
109158

110159
}
111160
}
112161

113-
// convert contrast arrays to diff array
162+
return {
163+
src: src,
164+
target: target
165+
};
166+
};
167+
168+
// get edit script
169+
var compare = function(seq1, seq2, eq){
170+
171+
// do compare
172+
var stepMap = coreCompare(seq1, seq2, eq);
173+
174+
// transform stepMap
175+
var contrast = transformStepMap(seq1, seq2, stepMap),
176+
src = contrast.src,
177+
target = contrast.target;
178+
179+
// convert contrast arrays to edit script
114180
var l = target.length,
115181
start, len, to,
116-
notEmpty = function(s){return s !== MARK_EMPTY;};
182+
notEmpty = function(s){return s !== MARK_EMPTY;},
183+
script = [];
117184

118185
for(i = l - 1; i >= 0;){
119186
// join continuous diffs
@@ -124,24 +191,57 @@ var compare = function(cnt1, cnt2, splitter){
124191
len = src.slice(j + 1, i + 1).filter(notEmpty).length; // length should be replaced (on src)
125192
to = target.slice(j + 1, i + 1).filter(notEmpty); // new content
126193

127-
diff.unshift(
194+
script.unshift(
128195
to.length ?
129-
[start, len, to.join(SPLITTER)] : // replace
196+
[start, len, to] : // replace
130197
[start, len] // remove
131198
);
132199
}
133200

134201
i = j - 1;
135202
}
136203

204+
return script;
205+
};
206+
207+
// merge
208+
var merge = function(seq, script){
209+
var result = seq.slice();
210+
211+
for(var i = script.length - 1, modify; i >= 0; i--){
212+
modify = script[i];
213+
var to = modify[2];
214+
if(to){
215+
modify = modify.slice(0, 2).concat(to);
216+
}
217+
result.splice.apply(result, modify);
218+
}
219+
137220
return result;
138221
};
139222

140-
if(typeof module === "object" && typeof module.exports === "object"){
141-
module.exports = compare;
142-
}
223+
// compare string (use splitter)
224+
var compareStr = function(str1, str2, splitter){
225+
splitter = typeof splitter === 'string' ? splitter : '';
226+
227+
var seq1 = str1.split(splitter),
228+
seq2 = str2.split(splitter),
229+
script = compare(seq1, seq2);
230+
231+
script.forEach(function(change){
232+
if(change[2]){
233+
change[2] = change[2].join(splitter);
234+
}
235+
});
236+
237+
return {
238+
splitter: splitter,
239+
diff: script
240+
};
241+
};
143242

144-
var merge = function(cnt, compareResult){
243+
// merge string (add spliter back)
244+
var mergeStr = function(cnt, compareResult){
145245
var splitter = compareResult.splitter,
146246
diff = compareResult.diff,
147247
result = cnt.split(splitter);
@@ -154,13 +254,25 @@ var merge = function(cnt, compareResult){
154254
return result.join(splitter);
155255
};
156256

157-
if(typeof module === "object" && typeof module.exports === "object"){
158-
module.exports = merge;
159-
}
160-
161-
global.diff = {
257+
var diff = {
258+
coreCompare: coreCompare,
162259
compare: compare,
163-
merge: merge
260+
merge: merge,
261+
compareStr: compareStr,
262+
mergeStr: mergeStr
164263
};
165264

265+
// RequireJS && SeaJS
266+
if(typeof define === 'function'){
267+
define(function(){
268+
return diff;
269+
});
270+
271+
// NodeJS
272+
}else if(typeof exports !== 'undefined'){
273+
module.exports = diff;
274+
}else{
275+
global.diff = diff;
276+
}
277+
166278
})(this);

0 commit comments

Comments
 (0)