├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .npmignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── package-lock.json ├── package.json ├── release.config.js ├── src ├── diff │ ├── apply.ts │ ├── diff.ts │ ├── lcs.ts │ ├── patch.ts │ └── same.ts ├── index.ts └── test │ ├── apply.spec.ts │ ├── diff.spec.ts │ ├── index.spec.ts │ ├── patch.spec.ts │ └── same.spec.ts ├── tsconfig-esm.json └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 3 | "plugins": ["prettier"], 4 | "env": { "browser": true, "es6": true, "node": true }, 5 | "rules": { 6 | "prettier/prettier": [ 7 | "error", 8 | { 9 | "singleQuote": true, 10 | "printWidth": 100, 11 | "trailingComma": "all" 12 | } 13 | ] 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2018 17 | }, 18 | "globals": { 19 | "chrome": "readonly" 20 | }, 21 | 22 | "overrides": [ 23 | { 24 | "files": ["**/*.ts", "**/*.tsx"], 25 | "env": { "browser": true, "es6": true, "node": true }, 26 | "extends": [ 27 | "eslint:recommended", 28 | "plugin:@typescript-eslint/eslint-recommended", 29 | "plugin:@typescript-eslint/recommended", 30 | "prettier" 31 | ], 32 | "parser": "@typescript-eslint/parser", 33 | "parserOptions": { 34 | "ecmaVersion": 2022, 35 | "sourceType": "module" 36 | }, 37 | "plugins": ["@typescript-eslint", "prettier"], 38 | "rules": { 39 | "prettier/prettier": [ 40 | "error", 41 | { "singleQuote": true, "printWidth": 100, "trailingComma": "all" } 42 | ], 43 | "@typescript-eslint/explicit-function-return-type": 0, 44 | "@typescript-eslint/explicit-member-accessibility": 0, 45 | "@typescript-eslint/no-empty-function": 0 46 | }, 47 | "globals": { 48 | "Atomics": "readonly", 49 | "SharedArrayBuffer": "readonly", 50 | "chrome": "readonly" 51 | } 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | open-pull-requests-limit: 15 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Test and publish 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Setup Node.js environment 19 | uses: actions/setup-node@v3 20 | with: 21 | cache: "npm" 22 | node-version: 18.x 23 | - run: npm ci 24 | - run: npm run lint 25 | - run: npm run build 26 | - run: npm run coverage 27 | - run: npx remap-istanbul -i coverage/coverage.json -t lcovonly > ./coverage/ts.info 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@master 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | path-to-lcov: ./coverage/ts.info 33 | - name: Archive production artifacts 34 | uses: actions/upload-artifact@v3 35 | with: 36 | name: dist 37 | path: | 38 | dist 39 | esm 40 | 41 | publish-npm: 42 | if: ${{ github.ref == 'refs/heads/master' }} 43 | needs: build 44 | runs-on: ubuntu-latest 45 | steps: 46 | - uses: actions/checkout@v3 47 | - name: Setup Node.js environment 48 | uses: actions/setup-node@v3 49 | with: 50 | cache: "npm" 51 | node-version: 18.x 52 | - run: npm ci 53 | - name: Install semantic-release extra plugins 54 | run: npm install --save-dev @semantic-release/changelog @semantic-release/git 55 | - name: Download the build artifacts 56 | uses: actions/download-artifact@v3 57 | with: 58 | name: dist 59 | - name: Release 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 63 | run: HUSKY=0 npx semantic-release 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Coverage directory used by tools like istanbul 12 | coverage 13 | 14 | # nyc test coverage 15 | .nyc_output 16 | 17 | # Dependency directories 18 | node_modules 19 | jspm_packages 20 | 21 | # Optional npm cache directory 22 | .npm 23 | 24 | # Optional REPL history 25 | .node_repl_history 26 | 27 | dist* 28 | esm 29 | .vscode/launch.json 30 | .npmrc 31 | umd 32 | 33 | .DS_Store 34 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | coverage 4 | dist/test 5 | esm/test 6 | dist_map 7 | *.log 8 | tsconfig*.json 9 | .eslintrc.json 10 | .npmrc 11 | .travis.yml 12 | .gitignore 13 | .vscode 14 | release.config.js 15 | .husky 16 | .github 17 | commitlint.config.js 18 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "dbaeumer.vscode-eslint", 8 | "streetsidesoftware.code-spell-checker", 9 | "esbenp.prettier-vscode" 10 | ], 11 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 12 | "unwantedRecommendations": ["HookyQR.beautify", "taichi.react-beautify"] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": ["javascript", "typescript"], 3 | "editor.rulers": [100], 4 | "editor.trimAutoWhitespace": true, 5 | "editor.formatOnSave": true, 6 | 7 | "files.insertFinalNewline": true, 8 | "files.trimTrailingWhitespace": true, 9 | 10 | "[typescript]": { 11 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 12 | }, 13 | "[javascript]": { 14 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 15 | }, 16 | "typescript.implementationsCodeLens.enabled": true, 17 | "debug.javascript.usePreview": false, 18 | "eslint.format.enable": true 19 | } 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.1.0](https://github.com/YuJianrong/fast-array-diff/compare/v1.0.1...v1.1.0) (2023-03-21) 2 | 3 | 4 | ### Features 5 | 6 | * expand types to allow differently typed arrays ([#156](https://github.com/YuJianrong/fast-array-diff/issues/156)) ([9da10de](https://github.com/YuJianrong/fast-array-diff/commit/9da10dedc86bcdc30b635c75473fd81f3ea6bf45)) 7 | 8 | - 1.0.1: 9 | 10 | - Fix for Security Vulnerability on dependencies 11 | 12 | - 1.0.0: 13 | 14 | - Update Typescript to 4.x 15 | - Remove `browsersify` 16 | - Export both esm and commonjs packages 17 | 18 | - 0.2.0: 19 | 20 | - Change the function `editScript` to `getPatch` 21 | - Add function `applyPatch` 22 | - Change the algorithm to get a better patch, but slower than the old implementation. 23 | 24 | - 0.1.6: 25 | 26 | - Add `editScript` function 27 | - Fix a bug on lcs function which casue the solution not the best one. 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Yu Jianrong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-array-diff 2 | 3 | [![Build Status](https://travis-ci.org/YuJianrong/fast-array-diff.svg?branch=master)](https://travis-ci.org/YuJianrong/fast-array-diff) 4 | [![Coverage Status](https://coveralls.io/repos/github/YuJianrong/fast-array-diff/badge.svg?branch=master)](https://coveralls.io/github/YuJianrong/fast-array-diff?branch=master) 5 | [![npm version](https://badge.fury.io/js/fast-array-diff.svg)](https://badge.fury.io/js/fast-array-diff) 6 | [![MIT Licence](https://badges.frapsoft.com/os/mit/mit.svg?v=103)](https://opensource.org/licenses/mit-license.php) 7 | [![TypeScript](https://badges.frapsoft.com/typescript/code/typescript.svg?v=101)](https://www.typescriptlang.org/) 8 | 9 | `fast-array-diff` is a npm module to find the common or different parts of two array, it based on the solution of LCS (Longest common subsequence) problems, widely used in diff/patch of two arrays (like diff/patch feature in git). 10 | 11 | The algorithm of this module is implemented based on the paper "An O(ND) Difference Algorithm and its Variations" by Eugene Myers, Algorithm Vol. 1 No. 2, 1986, pp. 251-266. The difference of this implementation to the implementation of npm module [diff](https://www.npmjs.com/package/diff) is: the space complexity of this implementation is O(N), while the implementation of `diff` is O(ND), so this implementation will cost less memory on large data set. Note: although the time complexity of the implementations are both O(ND), this implementation run slower than the `diff`. 12 | 13 | ## Installation 14 | 15 | You can install the module via `npm`: 16 | 17 | ```bash 18 | npm install fast-array-diff 19 | ``` 20 | 21 | ## API 22 | 23 | - `same(arrayOld, arrayNew, compareFunc?)` - Get the LCS of the two arrays. 24 | 25 | Return a list of the common subsequence. Like: `[1,2,3]` 26 | 27 | _Note: The parameter `compareFunc` is optional, `===` will be used if no compare function supplied._ 28 | 29 | - `diff(arrayOld, arrayNew, compareFunc?)` - Get the difference the two array. 30 | 31 | Return an object of the difference. Like this: 32 | 33 | ```js 34 | { 35 | removed: [1,2,3], 36 | added: [2,3,4] 37 | } 38 | ``` 39 | 40 | - `getPatch(arrayOld, arrayNew, compareFunc?)` - Get the patch array which transform from old array to the new. 41 | 42 | Return an array of edit action. Like this: 43 | 44 | ```js 45 | [ 46 | { type: "remove", oldPos: 0, newPos: 0, items: [1] }, 47 | { type: "add", oldPos: 3, newPos: 2, items: [4] }, 48 | ]; 49 | ``` 50 | 51 | - `applyPatch(arrayOld, patchArray)` - Thansform the old array to the new from the input patch array 52 | 53 | Return the new Array. The input value format can be same of return value of `getPatch`, and for the `remove` type, 54 | the `items` can be replaced to `length` value which is number. 55 | 56 | ```js 57 | [ 58 | { type: "remove", oldPos: 0, newPos: 0, items: [1] }, 59 | { type: "add", oldPos: 3, newPos: 2, items: [4] }, 60 | { type: "remove", oldPos: 5, newPos: 3, length: 3 }, 61 | ]; 62 | ``` 63 | 64 | ## Examples 65 | 66 | Example for `same` on array of number: 67 | 68 | ```js 69 | var diff = require("fast-array-diff"); 70 | 71 | console.log(diff.same([1, 2, 3, 4], [2, 1, 4])); 72 | // Output: [2, 4] 73 | ``` 74 | 75 | Example for `diff` on array of Object with a compare function 76 | 77 | ```js 78 | function compare(personA, personB) { 79 | return ( 80 | personA.firstName === personB.firstName && 81 | personA.lastName === personB.lastName 82 | ); 83 | } 84 | 85 | var result = diff.diff( 86 | [ 87 | { firstName: "Foo", lastName: "Bar" }, 88 | { firstName: "Apple", lastName: "Banana" }, 89 | { firstName: "Foo", lastName: "Bar" }, 90 | ], 91 | [ 92 | { firstName: "Apple", lastName: "Banana" }, 93 | { firstName: "Square", lastName: "Triangle" }, 94 | ], 95 | compare 96 | ); 97 | 98 | // Result is : 99 | // { 100 | // removed:[ 101 | // { firstName: 'Foo', lastName: 'Bar' }, 102 | // { firstName: 'Foo', lastName: 'Bar' } 103 | // ], 104 | // added: [ { firstName: 'Square', lastName: 'Triangle' } ] 105 | // } 106 | ``` 107 | 108 | Example for `getPatch` on array of number: 109 | 110 | ```js 111 | var es = diff.getPatch([1, 2, 3], [2, 3, 4]); 112 | 113 | // Result is: 114 | // [ 115 | // { type: "remove", oldPos: 0, newPos: 0, items: [1] }, 116 | // { type: "add", oldPos: 3, newPos: 2, items: [4] }, 117 | // ] 118 | ``` 119 | 120 | Example for `applyPatch`: 121 | 122 | ```js 123 | var arr = diff.applyPatch( 124 | [1, 2, 3], 125 | [ 126 | { type: "remove", oldPos: 0, newPos: 0, length: 1 }, 127 | { type: "add", oldPos: 3, newPos: 2, items: [4] }, 128 | ] 129 | ); 130 | 131 | // Result is: 132 | // [2, 3, 4] 133 | ``` 134 | 135 | ## TypeScript 136 | 137 | This module is written in [TypeScript](https://www.typescriptlang.org/), you can import it directly in TypeScript and get the benefit of static type checking and auto-complete of IDE. 138 | 139 | ```typescript 140 | import * as diff from "fast-array-diff"; 141 | 142 | console.log(diff.same([1, 2, 3], [2, 3, 4])); 143 | 144 | let result: diff.DiffData = diff.diff([1, 2], [2, 3]); 145 | // Note: DiffData is the interface of the difference result. 146 | ``` 147 | 148 | ## License 149 | 150 | This module is licensed under MIT. 151 | 152 | ### [Changelog](https://github.com/YuJianrong/fast-array-diff/blob/master/CHANGELOG.md) 153 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-array-diff", 3 | "version": "1.1.0", 4 | "description": "Implementation of paper 'An O(ND) Difference Algorithm and Its Variations' on array", 5 | "homepage": "https://github.com/YuJianrong/fast-array-diff", 6 | "bugs": { 7 | "url": "https://github.com/YuJianrong/fast-array-diff/issues" 8 | }, 9 | "repository": "YuJianrong/fast-array-diff", 10 | "main": "./dist/index.js", 11 | "module": "./esm/index.js", 12 | "scripts": { 13 | "clean": "rm -rf dist esm; rm -rf coverage; rm -rf dist_map", 14 | "clean-test": "rm -rf dist/test esm/test dist_map", 15 | "commit": "cz", 16 | "prebuild": "npm run clean", 17 | "build": "tsc && tsc -p tsconfig-esm.json", 18 | "test": "mocha dist/test/*.spec.js", 19 | "coverage": "tsc --sourceMap --outDir dist_map && istanbul cover node_modules/.bin/_mocha -- dist_map/test/*.spec.js", 20 | "debug": "npm run build && npm run test -- --debug-brk", 21 | "lint": "eslint './src/**/*.ts' --max-warnings 0", 22 | "prepare": "husky install" 23 | }, 24 | "keywords": [ 25 | "array", 26 | "diff" 27 | ], 28 | "author": "Jianrong Yu ", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "@commitlint/cli": "^19.0.3", 32 | "@commitlint/config-conventional": "^19.0.3", 33 | "@semantic-release/changelog": "^6.0.3", 34 | "@semantic-release/git": "^10.0.1", 35 | "@types/mocha": "^10.0.6", 36 | "@types/node": "^20.11.24", 37 | "@typescript-eslint/eslint-plugin": "^7.1.0", 38 | "@typescript-eslint/parser": "^7.1.0", 39 | "chai": "^5.1.0", 40 | "commitizen": "^4.3.0", 41 | "coveralls": "^3.1.1", 42 | "cz-conventional-changelog": "^3.3.0", 43 | "eslint": "^8.57.0", 44 | "eslint-config-prettier": "^9.1.0", 45 | "eslint-plugin-prettier": "^5.1.3", 46 | "husky": "^9.0.11", 47 | "istanbul": "^0.4.5", 48 | "lint-staged": "^15.2.2", 49 | "mocha": "^10.3.0", 50 | "prettier": "^3.2.5", 51 | "remap-istanbul": "^0.13.0", 52 | "typescript": "^5.3.3" 53 | }, 54 | "lint-staged": { 55 | "*.{json,md,less,yaml,yml}": [ 56 | "prettier --write" 57 | ], 58 | "*.{ts,tsx}": [ 59 | "eslint --fix --max-warnings 0" 60 | ] 61 | }, 62 | "config": { 63 | "commitizen": { 64 | "path": "./node_modules/cz-conventional-changelog" 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /release.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | branches: ['master', { name: 'beta', prerelease: true }], 3 | plugins: [ 4 | '@semantic-release/commit-analyzer', 5 | '@semantic-release/release-notes-generator', 6 | '@semantic-release/changelog', 7 | '@semantic-release/npm', 8 | '@semantic-release/github', 9 | '@semantic-release/git', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /src/diff/apply.ts: -------------------------------------------------------------------------------- 1 | import * as patch from './patch'; 2 | 3 | export type ApplyItem = 4 | | patch.PatchItem 5 | | { 6 | type: 'remove'; 7 | oldPos: number; 8 | newPos: number; 9 | length: number; 10 | }; 11 | 12 | export type Apply = ApplyItem[]; 13 | 14 | export function applyPatch(a: T[], patch: Apply): T[] { 15 | const segments: T[][] = []; 16 | 17 | let sameStart = 0; 18 | 19 | for (let i = 0; i < patch.length; ++i) { 20 | const patchItem = patch[i]; 21 | sameStart !== patchItem.oldPos && segments.push(a.slice(sameStart, patchItem.oldPos)); 22 | if (patchItem.type === 'add') { 23 | segments.push(patchItem.items); 24 | sameStart = patchItem.oldPos; 25 | } else if ((>patchItem).items) { 26 | sameStart = patchItem.oldPos + (>patchItem).items.length; 27 | } else { 28 | sameStart = patchItem.oldPos + (<{ length: number }>patchItem).length; 29 | } 30 | } 31 | sameStart !== a.length && segments.push(a.slice(sameStart)); 32 | 33 | return ([] as T[]).concat(...segments); 34 | } 35 | -------------------------------------------------------------------------------- /src/diff/diff.ts: -------------------------------------------------------------------------------- 1 | import bestSubSequence from './lcs'; 2 | 3 | export interface DiffData { 4 | removed: T[]; 5 | added: U[]; 6 | } 7 | 8 | export function diff( 9 | a: T[], 10 | b: U[], 11 | compareFunc: (ia: T, ib: U) => boolean = (ia: T, ib: U) => ia === (ib as unknown as T), 12 | ): DiffData { 13 | const ret: DiffData = { 14 | removed: [], 15 | added: [], 16 | }; 17 | bestSubSequence(a, b, compareFunc, (type, oldArr, oldStart, oldEnd, newArr, newStart, newEnd) => { 18 | if (type === 'add') { 19 | for (let i = newStart; i < newEnd; ++i) { 20 | ret.added.push(newArr[i]); 21 | } 22 | } else if (type === 'remove') { 23 | for (let i = oldStart; i < oldEnd; ++i) { 24 | ret.removed.push(oldArr[i]); 25 | } 26 | } 27 | }); 28 | return ret; 29 | } 30 | -------------------------------------------------------------------------------- /src/diff/lcs.ts: -------------------------------------------------------------------------------- 1 | function lcs(a: T[], b: U[], compareFunc: (a: T, b: U) => boolean): number { 2 | const M = a.length, 3 | N = b.length; 4 | const MAX = M + N; 5 | 6 | interface LongestPosition { 7 | [index: number]: number; 8 | } 9 | const v: LongestPosition = { 1: 0 }; 10 | 11 | for (let d = 0; d <= MAX; ++d) { 12 | for (let k = -d; k <= d; k += 2) { 13 | let x: number; 14 | if (k === -d || (k !== d && v[k - 1] + 1 < v[k + 1])) { 15 | x = v[k + 1]; 16 | } else { 17 | x = v[k - 1] + 1; 18 | } 19 | let y = x - k; 20 | while (x < M && y < N && compareFunc(a[x], b[y])) { 21 | x++; 22 | y++; 23 | } 24 | if (x === M && y === N) { 25 | return d; 26 | } 27 | v[k] = x; 28 | } 29 | } 30 | /* istanbul ignore next */ 31 | return -1; // never reach 32 | } 33 | 34 | enum Direct { 35 | none = 0, 36 | horizontal = 1, 37 | vertical = 1 << 1, 38 | diagonal = 1 << 2, 39 | all = horizontal | vertical | diagonal, 40 | } 41 | 42 | function getSolution( 43 | a: T[], 44 | aStart: number, 45 | aEnd: number, 46 | b: U[], 47 | bStart: number, 48 | bEnd: number, 49 | d: number, 50 | startDirect: Direct, 51 | endDirect: Direct, 52 | compareFunc: (a: T, b: U) => boolean, 53 | elementsChanged: ( 54 | type: 'add' | 'remove' | 'same', 55 | a: T[], 56 | aStart: number, 57 | aEnd: number, 58 | b: U[], 59 | bStart: number, 60 | bEnd: number, 61 | ) => void, 62 | ): void { 63 | if (d === 0) { 64 | elementsChanged('same', a, aStart, aEnd, b, bStart, bEnd); 65 | return; 66 | } else if (d === aEnd - aStart + (bEnd - bStart)) { 67 | const removeFirst = 68 | (startDirect & Direct.horizontal ? 1 : 0) + (endDirect & Direct.vertical ? 1 : 0); 69 | const addFirst = 70 | (startDirect & Direct.vertical ? 1 : 0) + (endDirect & Direct.horizontal ? 1 : 0); 71 | if (removeFirst >= addFirst) { 72 | aStart !== aEnd && elementsChanged('remove', a, aStart, aEnd, b, bStart, bStart); 73 | bStart !== bEnd && elementsChanged('add', a, aEnd, aEnd, b, bStart, bEnd); 74 | } else { 75 | bStart !== bEnd && elementsChanged('add', a, aStart, aStart, b, bStart, bEnd); 76 | aStart !== aEnd && elementsChanged('remove', a, aStart, aEnd, b, bEnd, bEnd); 77 | } 78 | return; 79 | } 80 | 81 | const M = aEnd - aStart; 82 | const N = bEnd - bStart; 83 | let HALF = Math.floor(N / 2); 84 | 85 | interface PointInfo { 86 | d: number; 87 | segments: number; 88 | direct: Direct; 89 | } 90 | 91 | interface PointInfoMap { 92 | [index: number]: PointInfo; 93 | } 94 | 95 | let now: PointInfoMap = {}; 96 | for (let k = -d - 1; k <= d + 1; ++k) { 97 | now[k] = { d: Infinity, segments: 0, direct: Direct.none }; 98 | } 99 | let preview: PointInfoMap = { 100 | [-d - 1]: { d: Infinity, segments: 0, direct: Direct.none }, 101 | [d + 1]: { d: Infinity, segments: 0, direct: Direct.none }, 102 | }; 103 | 104 | for (let y = 0; y <= HALF; ++y) { 105 | [now, preview] = [preview, now]; 106 | for (let k = -d; k <= d; ++k) { 107 | const x = y + k; 108 | 109 | if (y === 0 && x === 0) { 110 | now[k] = { 111 | d: 0, 112 | segments: 0, 113 | direct: startDirect, 114 | }; 115 | continue; 116 | } 117 | 118 | const currentPoints: PointInfo[] = [ 119 | { 120 | direct: Direct.horizontal, 121 | d: now[k - 1].d + 1, 122 | segments: now[k - 1].segments + (now[k - 1].direct & Direct.horizontal ? 0 : 1), 123 | }, 124 | { 125 | direct: Direct.vertical, 126 | d: preview[k + 1].d + 1, 127 | segments: preview[k + 1].segments + (preview[k + 1].direct & Direct.vertical ? 0 : 1), 128 | }, 129 | ]; 130 | 131 | if (x > 0 && x <= M && y > 0 && y <= N && compareFunc(a[aStart + x - 1], b[bStart + y - 1])) { 132 | currentPoints.push({ 133 | direct: Direct.diagonal, 134 | d: preview[k].d, 135 | segments: preview[k].segments + (preview[k].direct & Direct.diagonal ? 0 : 1), 136 | }); 137 | } 138 | 139 | const bestValue = currentPoints.reduce((best, info) => { 140 | if (best.d > info.d) { 141 | return info; 142 | } else if (best.d === info.d && best.segments > info.segments) { 143 | return info; 144 | } 145 | return best; 146 | }); 147 | 148 | currentPoints.forEach((info) => { 149 | if (bestValue.d === info.d && bestValue.segments === info.segments) { 150 | bestValue.direct |= info.direct; 151 | } 152 | }); 153 | now[k] = bestValue; 154 | } 155 | } 156 | 157 | let now2: PointInfoMap = {}; 158 | for (let k = -d - 1; k <= d + 1; ++k) { 159 | now2[k] = { d: Infinity, segments: 0, direct: Direct.none }; 160 | } 161 | let preview2: PointInfoMap = { 162 | [-d - 1]: { d: Infinity, segments: 0, direct: Direct.none }, 163 | [d + 1]: { d: Infinity, segments: 0, direct: Direct.none }, 164 | }; 165 | 166 | for (let y = N; y >= HALF; --y) { 167 | [now2, preview2] = [preview2, now2]; 168 | for (let k = d; k >= -d; --k) { 169 | const x = y + k; 170 | 171 | if (y === N && x === M) { 172 | now2[k] = { 173 | d: 0, 174 | segments: 0, 175 | direct: endDirect, 176 | }; 177 | continue; 178 | } 179 | 180 | const currentPoints: PointInfo[] = [ 181 | { 182 | direct: Direct.horizontal, 183 | d: now2[k + 1].d + 1, 184 | segments: now2[k + 1].segments + (now2[k + 1].direct & Direct.horizontal ? 0 : 1), 185 | }, 186 | { 187 | direct: Direct.vertical, 188 | d: preview2[k - 1].d + 1, 189 | segments: preview2[k - 1].segments + (preview2[k - 1].direct & Direct.vertical ? 0 : 1), 190 | }, 191 | ]; 192 | 193 | if (x >= 0 && x < M && y >= 0 && y < N && compareFunc(a[aStart + x], b[bStart + y])) { 194 | currentPoints.push({ 195 | direct: Direct.diagonal, 196 | d: preview2[k].d, 197 | segments: preview2[k].segments + (preview2[k].direct & Direct.diagonal ? 0 : 1), 198 | }); 199 | } 200 | 201 | const bestValue = currentPoints.reduce((best, info) => { 202 | if (best.d > info.d) { 203 | return info; 204 | } else if (best.d === info.d && best.segments > info.segments) { 205 | return info; 206 | } 207 | return best; 208 | }); 209 | 210 | currentPoints.forEach((info) => { 211 | if (bestValue.d === info.d && bestValue.segments === info.segments) { 212 | bestValue.direct |= info.direct; 213 | } 214 | }); 215 | now2[k] = bestValue; 216 | } 217 | } 218 | const best = { 219 | k: -1, 220 | d: Infinity, 221 | segments: 0, 222 | direct: Direct.none, 223 | }; 224 | 225 | for (let k = -d; k <= d; ++k) { 226 | const dSum = now[k].d + now2[k].d; 227 | if (dSum < best.d) { 228 | best.k = k; 229 | best.d = dSum; 230 | best.segments = 231 | now[k].segments + now2[k].segments + (now[k].segments & now2[k].segments ? 0 : 1); 232 | best.direct = now2[k].direct; 233 | } else if (dSum === best.d) { 234 | const segments = 235 | now[k].segments + now2[k].segments + (now[k].segments & now2[k].segments ? 0 : 1); 236 | if (segments < best.segments) { 237 | best.k = k; 238 | best.d = dSum; 239 | best.segments = segments; 240 | best.direct = now2[k].direct; 241 | } else if ( 242 | segments === best.segments && 243 | !(best.direct & Direct.diagonal) && 244 | now2[k].direct & Direct.diagonal 245 | ) { 246 | best.k = k; 247 | best.d = dSum; 248 | best.segments = segments; 249 | best.direct = now2[k].direct; 250 | } 251 | } 252 | } 253 | 254 | if (HALF + best.k === 0 && HALF === 0) { 255 | HALF++; 256 | now[best.k].direct = now2[best.k].direct; 257 | now2[best.k].direct = preview2[best.k].direct; 258 | } 259 | 260 | getSolution( 261 | a, 262 | aStart, 263 | aStart + HALF + best.k, 264 | b, 265 | bStart, 266 | bStart + HALF, 267 | now[best.k].d, 268 | startDirect, 269 | now2[best.k].direct, 270 | compareFunc, 271 | elementsChanged, 272 | ); 273 | getSolution( 274 | a, 275 | aStart + HALF + best.k, 276 | aEnd, 277 | b, 278 | bStart + HALF, 279 | bEnd, 280 | now2[best.k].d, 281 | now[best.k].direct, 282 | endDirect, 283 | compareFunc, 284 | elementsChanged, 285 | ); 286 | } 287 | 288 | export default function bestSubSequence( 289 | a: T[], 290 | b: U[], 291 | compareFunc: (a: T, b: U) => boolean, 292 | elementsChanged: ( 293 | type: 'add' | 'remove' | 'same', 294 | a: T[], 295 | aStart: number, 296 | aEnd: number, 297 | b: U[], 298 | bStart: number, 299 | bEnd: number, 300 | ) => void, 301 | ): void { 302 | const d = lcs(a, b, compareFunc); 303 | getSolution( 304 | a, 305 | 0, 306 | a.length, 307 | b, 308 | 0, 309 | b.length, 310 | d, 311 | Direct.diagonal, 312 | Direct.all, 313 | compareFunc, 314 | elementsChanged, 315 | ); 316 | } 317 | -------------------------------------------------------------------------------- /src/diff/patch.ts: -------------------------------------------------------------------------------- 1 | import bestSubSequence from './lcs'; 2 | 3 | export interface PatchItem { 4 | type: 'add' | 'remove'; 5 | oldPos: number; 6 | newPos: number; 7 | items: T[]; 8 | } 9 | 10 | export type Patch = PatchItem[]; 11 | 12 | export function getPatch( 13 | a: T[], 14 | b: T[], 15 | compareFunc: (ia: T, ib: T) => boolean = (ia: T, ib: T) => ia === ib, 16 | ): Patch { 17 | const patch: Patch = []; 18 | let lastAdd: PatchItem | null = null; 19 | let lastRemove: PatchItem | null = null; 20 | 21 | function pushChange( 22 | type: 'add' | 'remove' | 'same', 23 | oldArr: T[], 24 | oldStart: number, 25 | oldEnd: number, 26 | newArr: T[], 27 | newStart: number, 28 | newEnd: number, 29 | ) { 30 | if (type === 'same') { 31 | if (lastRemove) { 32 | patch.push(lastRemove); 33 | } 34 | if (lastAdd) { 35 | patch.push(lastAdd); 36 | } 37 | lastRemove = null; 38 | lastAdd = null; 39 | } else if (type === 'remove') { 40 | if (!lastRemove) { 41 | lastRemove = { 42 | type: 'remove', 43 | oldPos: oldStart as number, 44 | newPos: newStart as number, 45 | items: [], 46 | }; 47 | } 48 | for (let i = oldStart; i < oldEnd; ++i) { 49 | lastRemove.items.push(oldArr[i]); 50 | } 51 | if (lastAdd) { 52 | lastAdd.oldPos += oldEnd - oldStart; 53 | if (lastRemove.oldPos === oldStart) { 54 | lastRemove.newPos -= oldEnd - oldStart; 55 | } 56 | } 57 | } else if (type === 'add') { 58 | if (!lastAdd) { 59 | lastAdd = { 60 | type: 'add', 61 | oldPos: oldStart, 62 | newPos: newStart, 63 | items: [], 64 | }; 65 | } 66 | for (let i = newStart; i < newEnd; ++i) { 67 | lastAdd.items.push(newArr[i]); 68 | } 69 | } 70 | } 71 | 72 | bestSubSequence(a, b, compareFunc, pushChange); 73 | 74 | pushChange('same', [], 0, 0, [], 0, 0); 75 | 76 | return patch; 77 | } 78 | -------------------------------------------------------------------------------- /src/diff/same.ts: -------------------------------------------------------------------------------- 1 | import bestSubSequence from './lcs'; 2 | 3 | export default function ( 4 | a: T[], 5 | b: U[], 6 | compareFunc: (ia: T, ib: U) => boolean = (ia: T, ib: U) => ia === (ib as unknown as T), 7 | ): T[] { 8 | const ret: T[] = []; 9 | bestSubSequence(a, b, compareFunc, (type, oldArr, oldStart, oldEnd) => { 10 | if (type === 'same') { 11 | for (let i = oldStart; i < oldEnd; ++i) { 12 | ret.push(oldArr[i]); 13 | } 14 | } 15 | }); 16 | return ret; 17 | } 18 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import bestSubSequence from './diff/lcs'; 2 | import same from './diff/same'; 3 | export { bestSubSequence, same }; 4 | 5 | export * from './diff/diff'; 6 | 7 | export * from './diff/patch'; 8 | 9 | export * from './diff/apply'; 10 | -------------------------------------------------------------------------------- /src/test/apply.spec.ts: -------------------------------------------------------------------------------- 1 | import * as diff from '../diff/apply'; 2 | import * as assert from 'assert'; 3 | 4 | /** 5 | * Test for same function 6 | */ 7 | describe('Apply Patch', () => { 8 | it('Array not modified by function', () => { 9 | const a: number[] = [1, 2, 3]; 10 | diff.applyPatch(a, [ 11 | { type: 'remove', oldPos: 0, newPos: 0, items: [1] }, 12 | { type: 'add', oldPos: 3, newPos: 2, items: [4] }, 13 | ]); 14 | assert.deepStrictEqual(a, [1, 2, 3], 'input array changed!'); 15 | }); 16 | 17 | it('Functional test', () => { 18 | function add(oldPos: number, newPos: number, str: string): diff.ApplyItem { 19 | return { 20 | type: 'add', 21 | oldPos, 22 | newPos, 23 | items: str.split(''), 24 | }; 25 | } 26 | function remove(oldPos: number, newPos: number, str: string): diff.ApplyItem { 27 | return { 28 | type: 'remove', 29 | oldPos, 30 | newPos, 31 | items: str.split(''), 32 | }; 33 | } 34 | function apply_str(a: string, b: string, script: diff.Apply, msg?: string) { 35 | assert.deepStrictEqual(diff.applyPatch(a.split(''), script), b.split(''), msg); 36 | } 37 | 38 | apply_str('', '', [], 'empty'); 39 | apply_str('a', '', [remove(0, 0, 'a')], 'remove a'); 40 | apply_str('', 'b', [add(0, 0, 'b')]), 'add b'; 41 | apply_str('abcd', 'e', [remove(0, 0, 'abcd'), add(4, 0, 'e')], 'for abcd-e'); 42 | apply_str('abc', 'abc', [], 'same abc'); 43 | apply_str( 44 | 'abcd', 45 | 'obce', 46 | [remove(0, 0, 'a'), add(1, 0, 'o'), remove(3, 3, 'd'), add(4, 3, 'e')], 47 | 'abcd->obce', 48 | ); 49 | apply_str('abc', 'ab', [remove(2, 2, 'c')], 'abc->ac'); 50 | apply_str('cab', 'ab', [remove(0, 0, 'c')], 'cab->ab'); 51 | apply_str( 52 | 'abcde', 53 | 'zbodf', 54 | [ 55 | remove(0, 0, 'a'), 56 | add(1, 0, 'z'), 57 | remove(2, 2, 'c'), 58 | add(3, 2, 'o'), 59 | remove(4, 4, 'e'), 60 | add(5, 4, 'f'), 61 | ], 62 | 'abcde->cbodf', 63 | ); 64 | apply_str('bcd', 'bod', [remove(1, 1, 'c'), add(2, 1, 'o')], 'bcd->bod'); 65 | apply_str('a', 'aa', [add(1, 1, 'a')], 'a -> aa'); 66 | apply_str('aa', 'aaaa', [add(2, 2, 'aa')], 'aa -> aaaa'); 67 | apply_str('aaaa', 'aa', [remove(2, 2, 'aa')], 'aaaa -> aa'); 68 | apply_str('TGGT', 'GG', [remove(0, 0, 'T'), remove(3, 2, 'T')], 'TGGT -> GG'); 69 | // debugger; 70 | apply_str('G', 'AGG', [add(0, 0, 'AG')]); 71 | 72 | apply_str( 73 | 'GTCGTTCGGAATGCCGTTGCTCTGTAAA', 74 | 'ACCGGTCGAGTGCGCGGAAGCCGGCCGAA', 75 | [ 76 | add(0, 0, 'ACCG'), 77 | add(3, 7, 'GA'), 78 | remove(5, 13, 'T'), 79 | add(6, 11, 'GCG'), 80 | remove(11, 19, 'T'), 81 | remove(16, 23, 'TT'), 82 | remove(20, 25, 'T'), 83 | remove(22, 26, 'T'), 84 | remove(24, 27, 'TA'), 85 | ], 86 | 'GTCGTTCGGAATGCCGTTGCTCTGTAAA', 87 | ); 88 | 89 | // apply_str( 90 | // "GTCGTTCGGAATGCCGTTGCTCTGTAAA", "ACCGGTCGAGTGCGCGGAAGCCGGCCGAA", [ 91 | // add(0, 0, "ACCG"), 92 | // add(4, 8, "AG"), 93 | // remove(5, 13, "T"), 94 | // add(6, 11, "GCG"), 95 | // remove(11, 19, "T"), 96 | // remove(16, 23, "TT"), 97 | // remove(20, 25, "T"), 98 | // remove(22, 26, "T"), 99 | // remove(24, 27, "TA"), 100 | // ], "GTCGTTCGGAATGCCGTTGCTCTGTAAA"); 101 | apply_str( 102 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 103 | 'ABCDEFGHIJKL12345678901234567890MNOPQRSTUVWXYZ', 104 | [add(12, 12, '12345678901234567890')], 105 | 'remove 12345678901234567890', 106 | ); 107 | }); 108 | 109 | it('Functional test on different input style', () => { 110 | function add(oldPos: number, newPos: number, str: string): diff.ApplyItem { 111 | return { 112 | type: 'add', 113 | oldPos, 114 | newPos, 115 | items: str.split(''), 116 | }; 117 | } 118 | function remove(oldPos: number, newPos: number, str: string): diff.ApplyItem { 119 | return { 120 | type: 'remove', 121 | oldPos, 122 | newPos, 123 | length: str.length, 124 | }; 125 | } 126 | function apply_str(a: string, b: string, script: diff.Apply, msg?: string) { 127 | assert.deepStrictEqual(diff.applyPatch(a.split(''), script), b.split(''), msg); 128 | } 129 | 130 | apply_str('', '', [], 'empty'); 131 | apply_str('a', '', [remove(0, 0, 'a')], 'remove a'); 132 | apply_str('', 'b', [add(0, 0, 'b')]), 'add b'; 133 | apply_str('abcd', 'e', [remove(0, 0, 'abcd'), add(4, 0, 'e')], 'for abcd-e'); 134 | apply_str('abc', 'abc', [], 'same abc'); 135 | apply_str( 136 | 'abcd', 137 | 'obce', 138 | [remove(0, 0, 'a'), add(1, 0, 'o'), remove(3, 3, 'd'), add(4, 3, 'e')], 139 | 'abcd->obce', 140 | ); 141 | apply_str('abc', 'ab', [remove(2, 2, 'c')], 'abc->ac'); 142 | apply_str('cab', 'ab', [remove(0, 0, 'c')], 'cab->ab'); 143 | apply_str( 144 | 'abcde', 145 | 'zbodf', 146 | [ 147 | remove(0, 0, 'a'), 148 | add(1, 0, 'z'), 149 | remove(2, 2, 'c'), 150 | add(3, 2, 'o'), 151 | remove(4, 4, 'e'), 152 | add(5, 4, 'f'), 153 | ], 154 | 'abcde->cbodf', 155 | ); 156 | apply_str('bcd', 'bod', [remove(1, 1, 'c'), add(2, 1, 'o')], 'bcd->bod'); 157 | apply_str('a', 'aa', [add(1, 1, 'a')], 'a -> aa'); 158 | apply_str('aa', 'aaaa', [add(2, 2, 'aa')], 'aa -> aaaa'); 159 | apply_str('aaaa', 'aa', [remove(2, 2, 'aa')], 'aaaa -> aa'); 160 | apply_str('TGGT', 'GG', [remove(0, 0, 'T'), remove(3, 2, 'T')], 'TGGT -> GG'); 161 | // debugger; 162 | apply_str('G', 'AGG', [add(0, 0, 'AG')]); 163 | 164 | apply_str( 165 | 'GTCGTTCGGAATGCCGTTGCTCTGTAAA', 166 | 'ACCGGTCGAGTGCGCGGAAGCCGGCCGAA', 167 | [ 168 | add(0, 0, 'ACCG'), 169 | add(3, 7, 'GA'), 170 | remove(5, 13, 'T'), 171 | add(6, 11, 'GCG'), 172 | remove(11, 19, 'T'), 173 | remove(16, 23, 'TT'), 174 | remove(20, 25, 'T'), 175 | remove(22, 26, 'T'), 176 | remove(24, 27, 'TA'), 177 | ], 178 | 'GTCGTTCGGAATGCCGTTGCTCTGTAAA', 179 | ); 180 | 181 | // apply_str( 182 | // "GTCGTTCGGAATGCCGTTGCTCTGTAAA", "ACCGGTCGAGTGCGCGGAAGCCGGCCGAA", [ 183 | // add(0, 0, "ACCG"), 184 | // add(4, 8, "AG"), 185 | // remove(5, 13, "T"), 186 | // add(6, 11, "GCG"), 187 | // remove(11, 19, "T"), 188 | // remove(16, 23, "TT"), 189 | // remove(20, 25, "T"), 190 | // remove(22, 26, "T"), 191 | // remove(24, 27, "TA"), 192 | // ], "GTCGTTCGGAATGCCGTTGCTCTGTAAA"); 193 | apply_str( 194 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 195 | 'ABCDEFGHIJKL12345678901234567890MNOPQRSTUVWXYZ', 196 | [add(12, 12, '12345678901234567890')], 197 | 'remove 12345678901234567890', 198 | ); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /src/test/diff.spec.ts: -------------------------------------------------------------------------------- 1 | import * as diff from '../diff/diff'; 2 | import * as assert from 'assert'; 3 | 4 | /** 5 | * Test for diff function 6 | */ 7 | describe('Diff', () => { 8 | it('Array should not modified by function', () => { 9 | const a: number[] = [1, 2, 3], 10 | b: number[] = [2, 3, 4]; 11 | diff.diff(a, b); 12 | assert.deepStrictEqual(a, [1, 2, 3], 'input array changed!'); 13 | assert.deepStrictEqual(b, [2, 3, 4], 'input array changed!'); 14 | }); 15 | 16 | it('Functional test', () => { 17 | function diff_str(a: string, b: string, added: string, removed: string) { 18 | assert.deepStrictEqual(diff.diff(a.split(''), b.split('')), { 19 | added: added.split(''), 20 | removed: removed.split(''), 21 | }); 22 | } 23 | 24 | diff_str('', '', '', ''); 25 | diff_str('a', '', '', 'a'); 26 | diff_str('', 'b', 'b', ''); 27 | diff_str( 28 | '@@@abcdefxzxzxzxzxz9090909090909090990', 29 | '#abcdef###xzxzxzxzxz9090909090909090990', 30 | '####', 31 | '@@@', 32 | ); 33 | diff_str( 34 | '#12345###xzxzxzxzxz9090909090909090990', 35 | '@@@12345xzxzxzxzxz9090909090909090990', 36 | '@@@', 37 | '####', 38 | ); 39 | diff_str('abcd', 'e', 'e', 'abcd'); 40 | diff_str('abced', 'e', '', 'abcd'); 41 | diff_str('abc', 'abc', '', ''); 42 | diff_str('abcd', 'obce', 'oe', 'ad'); 43 | diff_str('abc', 'ab', '', 'c'); 44 | diff_str('cab', 'ab', '', 'c'); 45 | diff_str('abc', 'bc', '', 'a'); 46 | diff_str('12345abcdefg', '6789abc', '6789', '12345defg'); 47 | diff_str('12345abc', '6789abcdefg', '6789defg', '12345'); 48 | diff_str('abcde', 'zbodf', 'zof', 'ace'); 49 | diff_str('bcd', 'bod', 'o', 'c'); 50 | diff_str('aa', 'aaaa', 'aa', ''); 51 | diff_str('aaaa', 'aa', '', 'aa'); 52 | diff_str('TGGT', 'GG', '', 'TT'); 53 | diff_str( 54 | 'GTCGTTCGGAATGCCGTTGCTCTGTAAA', 55 | 'ACCGGTCGAGTGCGCGGAAGCCGGCCGAA', 56 | 'ACCGGAGCG', 57 | 'TTTTTTTA', 58 | ); 59 | diff_str( 60 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 61 | 'ABCDEFGHIJKL12345678901234567890MNOPQRSTUVWXYZ', 62 | '12345678901234567890', 63 | '', 64 | ); 65 | }); 66 | 67 | it('Functional test on arrays of different types', () => { 68 | assert.deepStrictEqual( 69 | diff.diff([1, 2, 3], ['2', '3', '4'], (l, r) => { 70 | assert.equal(typeof l, 'number'); 71 | assert.equal(typeof r, 'string'); 72 | 73 | return l.toString() === r; 74 | }), 75 | { added: ['4'], removed: [1] }, 76 | ); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/test/index.spec.ts: -------------------------------------------------------------------------------- 1 | import * as diff from '../index'; 2 | import * as assert from 'assert'; 3 | 4 | /** 5 | * Test for index interface 6 | */ 7 | describe('Index', () => { 8 | it('same function in index', () => { 9 | assert.deepStrictEqual(diff.same([1, 2, 3], [2, 3, 4]), [2, 3]); 10 | }); 11 | 12 | it('bestSubSequence function in index', () => { 13 | const changes: ( 14 | | { type: 'same' | 'remove'; values: number[] } 15 | | { type: 'add'; values: string[] } 16 | )[] = []; 17 | 18 | diff.bestSubSequence( 19 | [1, 2, 3], 20 | ['2', '3', '4'], 21 | (l, r) => { 22 | assert.equal(typeof l, 'number'); 23 | assert.equal(typeof r, 'string'); 24 | 25 | return l.toString() === r; 26 | }, 27 | (type, a, aStart, aEnd, b, bStart, bEnd) => { 28 | assert.equal(typeof a[0], 'number'); 29 | assert.equal(typeof b[0], 'string'); 30 | 31 | if (type === 'add') { 32 | changes.push({ type, values: b.slice(bStart, bEnd) }); 33 | } else { 34 | changes.push({ type, values: a.slice(aStart, aEnd) }); 35 | } 36 | }, 37 | ); 38 | 39 | assert.deepStrictEqual(changes, [ 40 | { type: 'remove', values: [1] }, 41 | { type: 'same', values: [2] }, 42 | { type: 'same', values: [3] }, 43 | { type: 'add', values: ['4'] }, 44 | ]); 45 | }); 46 | 47 | it('diff data and function in index', () => { 48 | const result: diff.DiffData = { 49 | added: [1, 2], 50 | removed: [3, 4], 51 | }; 52 | assert.deepStrictEqual(diff.diff([3, 4, 5, 6], [1, 2, 5, 6]), result); 53 | }); 54 | 55 | it('getPatch function in index', () => { 56 | assert.deepStrictEqual(diff.getPatch([1, 2, 3], [2, 3, 4]), [ 57 | { type: 'remove', oldPos: 0, newPos: 0, items: [1] }, 58 | { type: 'add', oldPos: 3, newPos: 2, items: [4] }, 59 | ]); 60 | }); 61 | 62 | it('applyPatch function in index', () => { 63 | assert.deepStrictEqual( 64 | diff.applyPatch( 65 | [1, 2, 3], 66 | [ 67 | { type: 'remove', oldPos: 0, newPos: 0, items: [1] }, 68 | { type: 'add', oldPos: 3, newPos: 2, items: [4] }, 69 | ], 70 | ), 71 | [2, 3, 4], 72 | ); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/test/patch.spec.ts: -------------------------------------------------------------------------------- 1 | import * as es from '../diff/patch'; 2 | import * as assert from 'assert'; 3 | 4 | /** 5 | * Test for same function 6 | */ 7 | describe('Get Patch', () => { 8 | it('Array not modified by function', () => { 9 | const a: number[] = [1, 2, 3], 10 | b: number[] = [2, 3, 4]; 11 | es.getPatch(a, b); 12 | assert.deepStrictEqual(a, [1, 2, 3], 'input array changed!'); 13 | assert.deepStrictEqual(b, [2, 3, 4], 'input array changed!'); 14 | }); 15 | 16 | it('Functional test', () => { 17 | function add(oldPos: number, newPos: number, str: string): es.PatchItem { 18 | return { 19 | type: 'add', 20 | oldPos, 21 | newPos, 22 | items: [...str], 23 | }; 24 | } 25 | function remove(oldPos: number, newPos: number, str: string): es.PatchItem { 26 | return { 27 | type: 'remove', 28 | oldPos, 29 | newPos, 30 | items: [...str], 31 | }; 32 | } 33 | function es_str(a: string, b: string, script: es.Patch, msg?: string) { 34 | assert.deepStrictEqual(es.getPatch([...a], [...b]), script, msg); 35 | } 36 | 37 | es_str('', '', [], 'empty'); 38 | es_str('a', '', [remove(0, 0, 'a')], 'remove a'); 39 | es_str('', 'b', [add(0, 0, 'b')]), 'add b'; 40 | es_str('abcd', 'e', [remove(0, 0, 'abcd'), add(4, 0, 'e')], 'for abcd-e'); 41 | es_str('abc', 'abc', [], 'same abc'); 42 | es_str( 43 | 'abcd', 44 | 'obce', 45 | [remove(0, 0, 'a'), add(1, 0, 'o'), remove(3, 3, 'd'), add(4, 3, 'e')], 46 | 'abcd->obce', 47 | ); 48 | es_str('abc', 'ab', [remove(2, 2, 'c')], 'abc->ac'); 49 | es_str('cab', 'ab', [remove(0, 0, 'c')], 'cab->ab'); 50 | es_str( 51 | 'abcde', 52 | 'zbodf', 53 | [ 54 | remove(0, 0, 'a'), 55 | add(1, 0, 'z'), 56 | remove(2, 2, 'c'), 57 | add(3, 2, 'o'), 58 | remove(4, 4, 'e'), 59 | add(5, 4, 'f'), 60 | ], 61 | 'abcde->cbodf', 62 | ); 63 | es_str('bcd', 'bod', [remove(1, 1, 'c'), add(2, 1, 'o')], 'bcd->bod'); 64 | es_str('a', 'aa', [add(1, 1, 'a')], 'a -> aa'); 65 | es_str('aa', 'aaaa', [add(2, 2, 'aa')], 'aa -> aaaa'); 66 | es_str('aaaa', 'aa', [remove(2, 2, 'aa')], 'aaaa -> aa'); 67 | es_str('TGGT', 'GG', [remove(0, 0, 'T'), remove(3, 2, 'T')], 'TGGT -> GG'); 68 | // debugger; 69 | es_str('G', 'AGG', [add(0, 0, 'AG')]); 70 | 71 | es_str( 72 | 'GTCGTTCGGAATGCCGTTGCTCTGTAAA', 73 | 'ACCGGTCGAGTGCGCGGAAGCCGGCCGAA', 74 | [ 75 | add(0, 0, 'ACCG'), 76 | add(3, 7, 'GA'), 77 | remove(5, 13, 'T'), 78 | add(6, 11, 'GCG'), 79 | remove(11, 19, 'T'), 80 | remove(16, 23, 'TT'), 81 | remove(20, 25, 'T'), 82 | remove(22, 26, 'T'), 83 | remove(24, 27, 'TA'), 84 | ], 85 | 'GTCGTTCGGAATGCCGTTGCTCTGTAAA', 86 | ); 87 | 88 | // es_str( 89 | // "GTCGTTCGGAATGCCGTTGCTCTGTAAA", "ACCGGTCGAGTGCGCGGAAGCCGGCCGAA", [ 90 | // add(0, 0, "ACCG"), 91 | // add(4, 8, "AG"), 92 | // remove(5, 13, "T"), 93 | // add(6, 11, "GCG"), 94 | // remove(11, 19, "T"), 95 | // remove(16, 23, "TT"), 96 | // remove(20, 25, "T"), 97 | // remove(22, 26, "T"), 98 | // remove(24, 27, "TA"), 99 | // ], "GTCGTTCGGAATGCCGTTGCTCTGTAAA"); 100 | es_str( 101 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 102 | 'ABCDEFGHIJKL12345678901234567890MNOPQRSTUVWXYZ', 103 | [add(12, 12, '12345678901234567890')], 104 | 'remove 12345678901234567890', 105 | ); 106 | 107 | es_str('💢💩💧', '💢💫💧', [remove(1, 1, '💩'), add(2, 1, '💫')], 'works with unicodes'); 108 | }); 109 | }); 110 | -------------------------------------------------------------------------------- /src/test/same.spec.ts: -------------------------------------------------------------------------------- 1 | import same from '../diff/same'; 2 | import * as assert from 'assert'; 3 | 4 | /** 5 | * Test for same function 6 | */ 7 | describe('Same', () => { 8 | it('Array not modified by function', () => { 9 | const a: number[] = [1, 2, 3], 10 | b: number[] = [2, 3, 4]; 11 | same(a, b); 12 | assert.deepStrictEqual(a, [1, 2, 3], 'input array changed!'); 13 | assert.deepStrictEqual(b, [2, 3, 4], 'input array changed!'); 14 | }); 15 | 16 | it('Different Type Check', () => { 17 | assert.deepStrictEqual(same([1, 2, 3], [2, 3, 4]), [2, 3]); 18 | assert.deepStrictEqual(same(['1', '2', '3'], ['2', '3', '4']), ['2', '3']); 19 | assert.deepStrictEqual(same([true, false], [false, false]), [false]); 20 | }); 21 | 22 | it.skip('Random Check', function () { 23 | this.timeout(100 * 1000); 24 | 25 | function lcs(a: number[], b: number[]): number[] { 26 | const s = Array(a.length + 1); 27 | for (let i = 0; i <= a.length; ++i) { 28 | s[i] = Array(b.length + 1); 29 | s[i][0] = { len: 0 }; 30 | } 31 | for (let i = 0; i <= b.length; ++i) { 32 | s[0][i] = { len: 0 }; 33 | } 34 | for (let i = 1; i <= a.length; ++i) { 35 | for (let j = 1; j <= b.length; ++j) { 36 | if (a[i - 1] === b[j - 1]) { 37 | const v = s[i - 1][j - 1].len + 1; 38 | s[i][j] = { len: v, direct: [-1, -1] }; 39 | } else { 40 | const v1 = s[i - 1][j].len; 41 | const v2 = s[i][j - 1].len; 42 | if (v1 > v2) { 43 | s[i][j] = { len: v1, direct: [-1, 0] }; 44 | } else { 45 | s[i][j] = { len: v2, direct: [0, -1] }; 46 | } 47 | } 48 | } 49 | } 50 | let n = a.length, 51 | m = b.length; 52 | const ret: number[] = []; 53 | while (s[n][m].len !== 0) { 54 | const node = s[n][m]; 55 | if (node.direct[0] === node.direct[1]) { 56 | ret.push(a[n - 1]); 57 | } 58 | n += node.direct[0]; 59 | m += node.direct[1]; 60 | } 61 | return ret.reverse(); 62 | } 63 | 64 | function getRandom(): number[] { 65 | const length = Math.floor(Math.random() * 20 + 2); 66 | return Array(length) 67 | .fill(0) 68 | .map(() => Math.floor(Math.random() * 10)); 69 | } 70 | 71 | function isSubSeq(main: number[], sub: number[]): boolean { 72 | let i = 0; 73 | main.forEach((n) => (i += n === sub[i] ? 1 : 0)); 74 | return i === sub.length; 75 | } 76 | 77 | for (let i = 0; i < 5000; ++i) { 78 | const arr1 = getRandom(), 79 | arr2 = getRandom(); 80 | const lcsResult = lcs(arr1, arr2), 81 | sameResult = same(arr1, arr2); 82 | assert.strictEqual( 83 | lcsResult.length, 84 | sameResult.length, 85 | `[${arr1}] <=> [${arr2}], correct: [${lcsResult}], incorrect: [${sameResult}]`, 86 | ); 87 | assert.strictEqual(isSubSeq(arr1, sameResult) && isSubSeq(arr2, sameResult), true); 88 | } 89 | }); 90 | 91 | it('Functional Check', () => { 92 | function same_str(a: string, b: string): string { 93 | return same(a.split(''), b.split('')).join(''); 94 | } 95 | 96 | assert.deepStrictEqual(same_str('846709', '2798'), '79'); 97 | assert.deepStrictEqual(same_str('5561279', '597142'), '512'); 98 | 99 | assert.deepStrictEqual(same_str('', ''), ''); 100 | assert.deepStrictEqual(same_str('a', ''), ''); 101 | assert.deepStrictEqual(same_str('', 'b'), ''); 102 | assert.deepStrictEqual(same_str('abcd', 'e'), ''); 103 | assert.deepStrictEqual(same_str('abc', 'abc'), 'abc'); 104 | assert.deepStrictEqual(same_str('abcd', 'obce'), 'bc'); 105 | assert.deepStrictEqual(same_str('abc', 'ab'), 'ab'); 106 | assert.deepStrictEqual(same_str('cab', 'ab'), 'ab'); 107 | assert.deepStrictEqual(same_str('abc', 'bc'), 'bc'); 108 | assert.deepStrictEqual(same_str('abcde', 'zbodf'), 'bd'); 109 | assert.deepStrictEqual(same_str('bcd', 'bod'), 'bd'); 110 | assert.deepStrictEqual(same_str('aa', 'aaaa'), 'aa'); 111 | assert.deepStrictEqual(same_str('aaaa', 'aa'), 'aa'); 112 | assert.deepStrictEqual(same_str('TGGT', 'GG'), 'GG'); 113 | assert.deepStrictEqual( 114 | same_str('GTCGTTCGGAATGCCGTTGCTCTGTAAA', 'ACCGGTCGAGTGCGCGGAAGCCGGCCGAA'), 115 | 'GTCGTCGGAAGCCGGCCGAA', 116 | ); 117 | assert.deepStrictEqual( 118 | same_str('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'ABCDEFGHIJKL12345678901234567890MNOPQRSTUVWXYZ'), 119 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 120 | ); 121 | }); 122 | 123 | it('Customize compare function', () => { 124 | interface CustomObj { 125 | name: string; 126 | age: number; 127 | } 128 | function compare(a: CustomObj, b: CustomObj) { 129 | return a.name === b.name && a.age === b.age; 130 | } 131 | const a: CustomObj[] = [ 132 | { name: 'Mike', age: 10 }, 133 | { name: 'Apple', age: 13 }, 134 | { name: 'Jack', age: 15 }, 135 | ], 136 | b: CustomObj[] = [ 137 | { name: 'Apple', age: 13 }, 138 | { name: 'Mimi', age: 0 }, 139 | { name: 'Jack', age: 15 }, 140 | ], 141 | result: CustomObj[] = [ 142 | { name: 'Apple', age: 13 }, 143 | { name: 'Jack', age: 15 }, 144 | ]; 145 | assert.deepStrictEqual(same(a, b, compare), result); 146 | }); 147 | 148 | it('Functional test on arrays of different types', () => { 149 | assert.deepStrictEqual( 150 | same([1, 2, 3], ['2', '3', '4'], (l, r) => { 151 | assert.equal(typeof l, 'number'); 152 | assert.equal(typeof r, 'string'); 153 | 154 | return l.toString() === r; 155 | }), 156 | [2, 3], 157 | ); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /tsconfig-esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "outDir": "./esm" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts"], 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "lib": ["dom", "es2015.core"], 8 | "declaration": true, 9 | "outDir": "./dist", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "downlevelIteration": true 13 | } 14 | } 15 | --------------------------------------------------------------------------------