Skip to content

Commit 5b7b8dc

Browse files
committed
✨ feat: implement @algorithm.ts/diff
1 parent 636a36e commit 5b7b8dc

File tree

8 files changed

+380
-3
lines changed

8 files changed

+380
-3
lines changed

packages/diff/README.md

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<header>
2+
<h1 align="center">
3+
<a href="https://github.com/guanghechen/algorithm.ts/tree/@algorithm.ts/[email protected]/packages/diff#readme">@algorithm.ts/diff</a>
4+
</h1>
5+
<div align="center">
6+
<a href="https://www.npmjs.com/package/@algorithm.ts/diff">
7+
<img
8+
alt="Npm Version"
9+
src="https://img.shields.io/npm/v/@algorithm.ts/diff.svg"
10+
/>
11+
</a>
12+
<a href="https://www.npmjs.com/package/@algorithm.ts/diff">
13+
<img
14+
alt="Npm Download"
15+
src="https://img.shields.io/npm/dm/@algorithm.ts/diff.svg"
16+
/>
17+
</a>
18+
<a href="https://www.npmjs.com/package/@algorithm.ts/diff">
19+
<img
20+
alt="Npm License"
21+
src="https://img.shields.io/npm/l/@algorithm.ts/diff.svg"
22+
/>
23+
</a>
24+
<a href="#install">
25+
<img
26+
alt="Module Formats: cjs, esm"
27+
src="https://img.shields.io/badge/module_formats-cjs%2C%20esm-green.svg"
28+
/>
29+
</a>
30+
<a href="https://github.com/nodejs/node">
31+
<img
32+
alt="Node.js Version"
33+
src="https://img.shields.io/node/v/@algorithm.ts/diff"
34+
/>
35+
</a>
36+
<a href="https://github.com/facebook/jest">
37+
<img
38+
alt="Tested with Jest"
39+
src="https://img.shields.io/badge/tested_with-jest-9c465e.svg"
40+
/>
41+
</a>
42+
<a href="https://github.com/prettier/prettier">
43+
<img
44+
alt="Code Style: prettier"
45+
src="https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square"
46+
/>
47+
</a>
48+
</div>
49+
</header>
50+
<br/>
51+
52+
A typescript implementation to find the **diff** of two sequences or strings.
53+
54+
## Install
55+
56+
- npm
57+
58+
```bash
59+
npm install --save @algorithm.ts/diff
60+
```
61+
62+
- yarn
63+
64+
```bash
65+
yarn add @algorithm.ts/diff
66+
```
67+
68+
## Usage
69+
70+
- Basic
71+
72+
```typescript
73+
import { diff, IDiffItem } from '@algorithm.ts/diff'
74+
75+
const results1: IDiffItem<string>[] = diff('abc', 'ac')
76+
const results2: IDiffItem<string>[] = diff('abc', 'ac', { lcs = "myers"})
77+
const results3: IDiffItem<string>[] = diff('abc', 'ac', { lcs = "myers_linear_space"})
78+
const results4: IDiffItem<string>[] = diff('abc', 'ac', { lcs = "dp"})
79+
const results5: IDiffItem<number[]>[] = diff([1, 2, 3], [3, 4], { lcs = "dp", equals: (a, b) => a === b })
80+
81+
// results1 => [ { type: "common", tokens: "a" }, { type: "delete", tokens: "b" }, { type: "common", tokens: "c" } ]
82+
// results5 => [ { type: "delete", tokens: [1, 2] }, { type: "common", tokens: [3] }, { type: "added", tokens: [4] } ]
83+
84+
// customized lcs
85+
import { lcs_myers_linear_space } from '@algorithm.ts/lcs'
86+
const results6: IDiffItem<number[]>[] = diff([1, 2, 3], [3, 4], { lcs = lcs_myers_linear_space })
87+
```
88+
89+
## Related
90+
91+
- [An O(ND) Difference Algorithm and Its Variations](https://mailserver.org/diff2.pdf).
92+
- [最长公共子序列(lcs) | 光和尘][lcs]
93+
- [Longest common subsequence problem | Wikipedia][wikipedia-lcs]
94+
- [@algorithm.ts/lcs][https://github.com/guanghechen/algorithm.ts/tree/@algorithm.ts/[email protected]/packages/lcs#readme]
95+
96+
[homepage]:
97+
https://github.com/guanghechen/algorithm.ts/tree/@algorithm.ts/[email protected]/packages/diff#readme

packages/diff/__test__/diff.spec.ts

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { IDiffItem, ILcsAlgorithm } from '../src'
2+
import { DiffType, diff } from '../src'
3+
4+
function test_diff_string(
5+
lcs: ILcsAlgorithm | undefined,
6+
equals?: ((x: string, y: string) => boolean) | undefined,
7+
): void {
8+
const test_case = (a: string, b: string, expected: number): void => {
9+
const actual: Array<IDiffItem<string>> = diff(a, b, { lcs, equals })
10+
const left: string = actual
11+
.filter(x => x.type !== DiffType.ADDED)
12+
.map(x => x.tokens)
13+
.join('')
14+
const right: string = actual
15+
.filter(x => x.type !== DiffType.REMOVED)
16+
.map(x => x.tokens)
17+
.join('')
18+
expect(left).toEqual(a)
19+
expect(right).toEqual(b)
20+
21+
const common: string = actual
22+
.filter(x => x.type === DiffType.COMMON)
23+
.map(x => x.tokens)
24+
.join('')
25+
expect(common.length).toEqual(expected)
26+
27+
let i: number = 0
28+
let j: number = 0
29+
for (; i < common.length && j < a.length; ++i, ++j) {
30+
while (i < common.length && j < a.length && common[i] !== a[j]) j += 1
31+
}
32+
expect(i).toEqual(common.length)
33+
}
34+
35+
test_case('f8d1d155-d14e-433f-88e1-07b54f184740', 'a00322f7-256e-46fe-ae91-8de835c57778', 12)
36+
test_case('abcde', 'ace', 3)
37+
test_case('ace', 'abcde', 3)
38+
test_case('abc', 'abc', 3)
39+
test_case('abc', 'abce', 3)
40+
test_case('', 'abce', 0)
41+
test_case('abce', '', 0)
42+
test_case('', '', 0)
43+
test_case('123466786', '2347982352', 5)
44+
test_case('abeep boop', 'beep boob blah', 8)
45+
test_case('1234', '21234', 4)
46+
test_case('1234', '23414', 3)
47+
test_case('1234', '231234', 4)
48+
test_case('1234', '23131234', 4)
49+
test_case('1234', '2313123434', 4)
50+
test_case('23131234', '1234', 4)
51+
}
52+
53+
describe('diff', () => {
54+
it('lcs_myers', async () => {
55+
test_diff_string('myers')
56+
57+
const { lcs_myers } = await import('@algorithm.ts/lcs')
58+
test_diff_string(lcs_myers)
59+
})
60+
61+
it('lcs_myers_linear_space', async () => {
62+
test_diff_string('myers_linear_space')
63+
64+
const { lcs_myers_linear_space } = await import('@algorithm.ts/lcs')
65+
test_diff_string(lcs_myers_linear_space)
66+
})
67+
68+
it('lcs_dp', async () => {
69+
test_diff_string('dp')
70+
71+
const { lcs_dp } = await import('@algorithm.ts/lcs')
72+
test_diff_string(lcs_dp)
73+
})
74+
75+
it('default', async () => {
76+
test_diff_string(undefined)
77+
test_diff_string(undefined, (x, y) => x === y)
78+
})
79+
})

packages/diff/package.json

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "@algorithm.ts/diff",
3+
"version": "1.0.0",
4+
"description": "Find the diff between of two sequence or string.",
5+
"author": {
6+
"name": "guanghechen",
7+
"url": "https://github.com/guanghechen/"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "https://github.com/guanghechen/algorithm.ts/tree/@algorithm.ts/[email protected]",
12+
"directory": "packages/diff"
13+
},
14+
"homepage": "https://github.com/guanghechen/algorithm.ts/tree/@algorithm.ts/[email protected]/packages/diff#readme",
15+
"keywords": [
16+
"algorithm",
17+
"diff"
18+
],
19+
"type": "module",
20+
"exports": {
21+
"types": "./lib/types/index.d.ts",
22+
"import": "./lib/esm/index.mjs",
23+
"require": "./lib/cjs/index.cjs"
24+
},
25+
"source": "./src/index.ts",
26+
"types": "./lib/types/index.d.ts",
27+
"main": "./lib/cjs/index.cjs",
28+
"module": "./lib/esm/index.mjs",
29+
"license": "MIT",
30+
"files": [
31+
"lib/",
32+
"!lib/**/*.map",
33+
"package.json",
34+
"CHANGELOG.md",
35+
"LICENSE",
36+
"README.md"
37+
],
38+
"dependencies": {
39+
"@algorithm.ts/lcs": "^4.0.2"
40+
}
41+
}

packages/diff/project.json

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
3+
"name": "diff",
4+
"sourceRoot": "packages/diff/src",
5+
"projectType": "library",
6+
"tags": [],
7+
"targets": {
8+
"clean": {
9+
"executor": "nx:run-commands",
10+
"options": {
11+
"cwd": "packages/diff",
12+
"parallel": false,
13+
"commands": ["rimraf lib"]
14+
}
15+
},
16+
"build": {
17+
"executor": "nx:run-commands",
18+
"dependsOn": ["clean", "^build"],
19+
"options": {
20+
"cwd": "packages/diff",
21+
"parallel": false,
22+
"sourceMap": true,
23+
"commands": ["cross-env ROLLUP_CONFIG_TYPE=lib rollup -c ../../rollup.config.mjs"]
24+
},
25+
"configurations": {
26+
"production": {
27+
"sourceMap": false,
28+
"env": {
29+
"NODE_ENV": "production"
30+
}
31+
}
32+
}
33+
},
34+
"watch": {
35+
"executor": "nx:run-commands",
36+
"options": {
37+
"cwd": "packages/diff",
38+
"parallel": false,
39+
"sourceMap": true,
40+
"commands": ["cross-env ROLLUP_CONFIG_TYPE=lib rollup -c ../../rollup.config.mjs -w"]
41+
}
42+
},
43+
"test": {
44+
"executor": "nx:run-commands",
45+
"options": {
46+
"cwd": "packages/diff",
47+
"commands": [
48+
"node --experimental-vm-modules ../../node_modules/.bin/jest --config ../../jest.config.mjs --rootDir ."
49+
]
50+
},
51+
"configurations": {
52+
"coverage": {
53+
"commands": [
54+
"node --experimental-vm-modules ../../node_modules/.bin/jest --config ../../jest.config.mjs --rootDir . --coverage"
55+
]
56+
},
57+
"update": {
58+
"commands": [
59+
"node --experimental-vm-modules ../../node_modules/.bin/jest --config ../../jest.config.mjs --rootDir . -u"
60+
]
61+
}
62+
}
63+
}
64+
}
65+
}

packages/diff/src/index.ts

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { lcs_dp, lcs_myers, lcs_myers_linear_space } from '@algorithm.ts/lcs'
2+
3+
type IEquals = (x: number, y: number) => boolean
4+
5+
export enum DiffType {
6+
ADDED = 'added',
7+
REMOVED = 'removed',
8+
COMMON = 'common',
9+
}
10+
11+
export interface IDiffItem<T extends string | unknown[]> {
12+
type: DiffType
13+
tokens: T
14+
}
15+
16+
export type ILcs = (
17+
N1: number,
18+
N2: number,
19+
equals: (x: number, y: number) => boolean,
20+
) => Array<[number, number]>
21+
export type ILcsAlgorithm = 'myers' | 'myers_linear_space' | 'dp' | ILcs
22+
23+
export interface IDiffOptions<T> {
24+
equals?: (x: T, y: T) => boolean
25+
lcs?: ILcsAlgorithm
26+
}
27+
28+
export function diff<T extends string | unknown[]>(
29+
a: T,
30+
b: T,
31+
options: IDiffOptions<T[number]> = {},
32+
): Array<IDiffItem<T>> {
33+
const N1: number = a.length
34+
const N2: number = b.length
35+
36+
const originalEquals = options?.equals
37+
const equals: IEquals = originalEquals
38+
? (x: number, y: number): boolean => originalEquals(a[x], b[y])
39+
: (x: number, y: number): boolean => a[x] === b[y]
40+
41+
const lcs: ILcsAlgorithm = options?.lcs ?? 'myers_linear_space'
42+
const points: ReadonlyArray<[number, number]> = calcMatchPoints(N1, N2, equals, lcs)
43+
const answers: Array<IDiffItem<T>> = []
44+
45+
let x: number = 0
46+
let y: number = 0
47+
for (let i = 0, j: number; i < points.length; i = j) {
48+
const [u, v] = points[i]
49+
if (x < u) answers.push({ type: DiffType.REMOVED, tokens: a.slice(x, u) as T })
50+
if (y < v) answers.push({ type: DiffType.ADDED, tokens: b.slice(y, v) as T })
51+
52+
x = u + 1
53+
y = v + 1
54+
for (j = i + 1; j < points.length; ++j, ++x, ++y) {
55+
if (points[j][0] !== x || points[j][1] !== y) break
56+
}
57+
answers.push({ type: DiffType.COMMON, tokens: a.slice(u, x) as T })
58+
}
59+
60+
if (x < N1) answers.push({ type: DiffType.REMOVED, tokens: a.slice(x, N1) as T })
61+
if (y < N2) answers.push({ type: DiffType.ADDED, tokens: b.slice(y, N2) as T })
62+
return answers
63+
}
64+
65+
function calcMatchPoints(
66+
N1: number,
67+
N2: number,
68+
equals: IEquals,
69+
lcs: ILcsAlgorithm,
70+
): ReadonlyArray<[number, number]> {
71+
switch (lcs) {
72+
case 'myers':
73+
return lcs_myers(N1, N2, equals)
74+
case 'myers_linear_space':
75+
return lcs_myers_linear_space(N1, N2, equals)
76+
case 'dp':
77+
return lcs_dp(N1, N2, equals)
78+
default:
79+
return lcs(N1, N2, equals)
80+
}
81+
}

packages/diff/tsconfig.lib.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "../../tsconfig.lib.json",
3+
"compilerOptions": {
4+
"rootDir": "src"
5+
},
6+
"include": ["src"]
7+
}

tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"@algorithm.ts/binary-search": ["packages/binary-search/src"],
1212
"@algorithm.ts/bipartite-matching": ["packages/bipartite-matching/src"],
1313
"@algorithm.ts/calculator": ["packages/calculator/src"],
14+
"@algorithm.ts/diff": ["packages/diff/src"],
1415
"@algorithm.ts/dijkstra": ["packages/dijkstra/src"],
1516
"@algorithm.ts/dinic": ["packages/dinic/src"],
1617
"@algorithm.ts/dlx": ["packages/dlx/src"],

0 commit comments

Comments
 (0)