├── .gitignore ├── scripts └── update-changelog.sh ├── package.json ├── README.md ├── LICENSE ├── CHANGELOG.md ├── index.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | config.cson 4 | build/ 5 | data/ 6 | wiki/ 7 | npm-debug.log 8 | *.sublime-workspace 9 | spare-parts 10 | .DS_Store 11 | .idea/ 12 | -------------------------------------------------------------------------------- /scripts/update-changelog.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # To make it work on OSX (provided gnu-sed in installed) 6 | if type gsed 2> /dev/null 7 | then 8 | alias sed=gsed 9 | fi 10 | 11 | 12 | version=$(jq --raw-output ' .version ' "package.json") 13 | date=$(date +%Y-%m-%d) 14 | 15 | sed \ 16 | --regexp-extended \ 17 | --in-place="" \ 18 | "s$^## \[Unreleased\]$\## [Unreleased\]\n\n\n## [${version}] - ${date}$" \ 19 | CHANGELOG.md 20 | 21 | git add CHANGELOG.md 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "damerau-levenshtein", 3 | "version": "1.0.8", 4 | "description": "Damerau - Levenshtein distance by The Spanish Inquisition + relative distance", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha --use_strict", 8 | "version": "scripts/update-changelog.sh" 9 | }, 10 | "keywords": [ 11 | "Damerau-Levenshtein", 12 | "Damerau", 13 | "Levenshtein", 14 | "distance", 15 | "compare", 16 | "relative" 17 | ], 18 | "author": "The Spanish Inquisition", 19 | "contributors": [ 20 | "Tadeusz Łazurski (https://tad-lispy.com/)", 21 | "Gustavo Marques Adolph", 22 | "Ivan Gilchrist (http://jumpingfishes.com)", 23 | "Boris Yakubchik (http://dev.yboris.com/)" 24 | ], 25 | "license": "BSD-2-Clause", 26 | "devDependencies": { 27 | "mocha": "^9.1.3" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/tad-lispy/node-damerau-levenshtein.git" 32 | }, 33 | "bugs": { 34 | "url": "https://github.com/tad-lispy/node-damerau-levenshtein/issues" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![NPM](https://nodei.co/npm/damerau-levenshtein.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/damerau-levenshtein/) 2 | 3 | It provides a function that takes two string arguments and returns a hash like this: 4 | 5 | ``` javascript 6 | { 7 | steps: 5, // Levenstein demerau distance 8 | relative: 0.7, // steps / length of the longer string 9 | similarity: 0.3 // 1 - relative 10 | } 11 | ``` 12 | 13 | ## Install 14 | 15 | ```sh 16 | npm install damerau-levenshtein 17 | ``` 18 | 19 | ## Use with ES6 modules 20 | 21 | ```js 22 | import * as levenshtein from 'damerau-levenshtein'; 23 | 24 | const lev = levenshtein('hello world', 'Hello World!'); 25 | // { steps: 4, relative: 0.3076923076923077, similarity: 0.6923076923076923 } 26 | ``` 27 | 28 | Please see [tests](./test/test.js) for more insights. 29 | 30 | ## Use with TypeScript 31 | 32 | ```ts 33 | import * as levenshtein from 'damerau-levenshtein'; 34 | 35 | interface LevenshteinResponse { 36 | steps: number; 37 | relative: number; 38 | similarity: number; 39 | } 40 | 41 | const lev: LevenshteinResponse = levenshtein('hello world', 'Hello World!'); 42 | 43 | console.log(lev.steps); 44 | // 2 45 | console.log(lev.foo); 46 | // TypeScript Error: Property 'foo' does not exist on type 'LevenshteinResponse'. 47 | ``` 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2018, Tadeusz Łazurski 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/). This project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [Unreleased] 6 | 7 | 8 | ## [1.0.8] - 2021-12-20 9 | 10 | Security: 11 | - Upgrade mocha to > 9.0.0 (fixes three security vulnerabilities brought by transitive dependencies) 12 | 13 | ## [1.0.7] - 2021-05-05 14 | 15 | Fixed: 16 | - The scripts/update-changelog.sh wouldn't work without gsed in path 17 | 18 | Security: 19 | - Upgrade lodash (transitive development dependency) #17 20 | - Upgrade yargs/y18n (transitive development dependency) #18 21 | 22 | 23 | ## [1.0.6] - 2020-01-27 24 | 25 | Changed: 26 | - Upgrade lodash to version 4.17.15 #16 27 | 28 | ## [1.0.5] - 2019-05-09 29 | 30 | Changed: 31 | - Upgrade Mocha to version 6.1.4 #12 by @whyboris 32 | - Example use in README.md by @whyboris 33 | 34 | ## [1.0.4] - 2017-03-24 35 | 36 | Fixed: 37 | - Fails in strict mode #7 by @gilly3 38 | 39 | ## [1.0.3] - 2016-09-26 40 | 41 | Fixed: 42 | - A title of this document :P 43 | 44 | Added: 45 | - List of contributors 46 | - Bugs URL 47 | - Git repository URL 48 | 49 | ## [1.0.2] - 2016-09-26 50 | 51 | Fixed: 52 | - Similarity 0 returned for equal strings #4 by @tad-lispy 53 | 54 | ## [1.0.1] - 2016-09-12 55 | 56 | Fixed: 57 | - Wrong results for transposition #2 by @g-adolph 58 | 59 | Added: 60 | - First unit test by @g-adolph 61 | - A Change Log :) by @tad-lispy 62 | 63 | ## [1.0.0] - 2016-02-23 64 | 65 | Fixed: 66 | - Update README to match the actual output by @gilly3 67 | 68 | ## [0.1.3] - 2013-09-02 69 | 70 | Fixed: 71 | - Clear matrix on each call @tad-lispy 72 | - Always return an object @tad-lispy 73 | 74 | ## [0.1.2] - 2013-08-29 75 | 76 | Added: 77 | - ReadMe 78 | 79 | ## [0.1.1] - 2013-08-28 80 | 81 | Added: 82 | - Initial working release @tad-lispy 83 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // TheSpanishInquisition 2 | 3 | // Cache the matrix. Note that if you not pass a limit this implementation will use a dynamically calculate one. 4 | 5 | module.exports = function(__this, that, limit) { 6 | 7 | var thisLength = __this.length, 8 | thatLength = that.length, 9 | matrix = []; 10 | 11 | // If the limit is not defined it will be calculate from this and that args. 12 | limit = (limit || ((thatLength > thisLength ? thatLength : thisLength)))+1; 13 | 14 | for (var i = 0; i < limit; i++) { 15 | matrix[i] = [i]; 16 | matrix[i].length = limit; 17 | } 18 | for (i = 0; i < limit; i++) { 19 | matrix[0][i] = i; 20 | } 21 | 22 | if (Math.abs(thisLength - thatLength) > (limit || 100)){ 23 | return prepare (limit || 100); 24 | } 25 | if (thisLength === 0){ 26 | return prepare (thatLength); 27 | } 28 | if (thatLength === 0){ 29 | return prepare (thisLength); 30 | } 31 | 32 | // Calculate matrix. 33 | var j, this_i, that_j, cost, min, t; 34 | for (i = 1; i <= thisLength; ++i) { 35 | this_i = __this[i-1]; 36 | 37 | // Step 4 38 | for (j = 1; j <= thatLength; ++j) { 39 | // Check the jagged ld total so far 40 | if (i === j && matrix[i][j] > 4) return prepare (thisLength); 41 | 42 | that_j = that[j-1]; 43 | cost = (this_i === that_j) ? 0 : 1; // Step 5 44 | // Calculate the minimum (much faster than Math.min(...)). 45 | min = matrix[i - 1][j ] + 1; // Deletion. 46 | if ((t = matrix[i ][j - 1] + 1 ) < min) min = t; // Insertion. 47 | if ((t = matrix[i - 1][j - 1] + cost) < min) min = t; // Substitution. 48 | 49 | // Update matrix. 50 | matrix[i][j] = (i > 1 && j > 1 && this_i === that[j-2] && __this[i-2] === that_j && (t = matrix[i-2][j-2]+cost) < min) ? t : min; // Transposition. 51 | } 52 | } 53 | 54 | return prepare (matrix[thisLength][thatLength]); 55 | 56 | /** 57 | * 58 | */ 59 | function prepare(steps) { 60 | var length = Math.max(thisLength, thatLength) 61 | var relative = length === 0 62 | ? 0 63 | : (steps / length); 64 | var similarity = 1 - relative 65 | return { 66 | steps: steps, 67 | relative: relative, 68 | similarity: similarity 69 | }; 70 | } 71 | 72 | }; 73 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var levenshtien = require("./../index"); 2 | 3 | var assert = require("assert"); 4 | 5 | describe("Damerau - Levenshtein", function() { 6 | describe("Equality", function() { 7 | it("returns 0 steps for equal strings", function() { 8 | assert.deepEqual(levenshtien("test", "test"), { 9 | steps: 0, 10 | relative: 0, 11 | similarity: 1 12 | }); 13 | }); 14 | }); 15 | 16 | describe("Additions", function() { 17 | it("returns 1 step when appending one char", function() { 18 | assert.deepEqual(levenshtien("test", "tests"), { 19 | steps: 1, 20 | relative: 1 / 5, 21 | similarity: 1 - 1 / 5 22 | }); 23 | }); 24 | 25 | it("returns 1 step when prepending one char", function() { 26 | assert.deepEqual(levenshtien("test", "stest"), { 27 | steps: 1, 28 | relative: 1 / 5, 29 | similarity: 1 - 1 / 5 30 | }); 31 | }); 32 | 33 | it("returns 2 steps when appending two char", function() { 34 | assert.deepEqual(levenshtien("test", "mytest"), { 35 | steps: 2, 36 | relative: 2 / 6, 37 | similarity: 1 - 2 / 6 38 | }); 39 | }); 40 | 41 | it("returns 7 steps when appending seven char", function() { 42 | assert.deepEqual(levenshtien("test", "mycrazytest"), { 43 | steps: 7, 44 | relative: 7 / 11, 45 | similarity: 1 - 7 / 11 46 | }); 47 | }); 48 | 49 | it("returns 9 steps when prepend two chars and append seven chars", function() { 50 | assert.deepEqual(levenshtien("test", "mytestiscrazy"), { 51 | steps: 9, 52 | relative: 9 / 13, 53 | similarity: 1 - 9 / 13 54 | }); 55 | }); 56 | }); 57 | 58 | 59 | describe("Addition of repeated chars", function() { 60 | it("returns 1 step when repeating a character", function() { 61 | assert.deepEqual(levenshtien("test", "teest"), { 62 | steps: 1, 63 | relative: 1 / 5, 64 | similarity: 1 - 1 / 5 65 | }); 66 | }); 67 | 68 | it("returns 2 step when repeating a character twice", function() { 69 | assert.deepEqual(levenshtien("test", "teeest"), { 70 | steps: 2, 71 | relative: 2 / 6, 72 | similarity: 1 - 2 / 6 73 | }); 74 | }); 75 | }); 76 | 77 | 78 | describe("#Deletion", function() { 79 | it("returns 1 step when removing one char", function() { 80 | assert.deepEqual(levenshtien("test", "tst"), { 81 | steps: 1, 82 | relative: 1 / 4, 83 | similarity: 1 - 1 / 4 84 | }); 85 | }); 86 | }); 87 | 88 | 89 | describe("Transposition", function() { 90 | it("returns 1 step when transposing one char", function() { 91 | assert.deepEqual(levenshtien("test", "tset"), { 92 | steps: 1, 93 | relative: 1 / 4, 94 | similarity: 1 - 1 / 4 95 | }); 96 | }); 97 | }); 98 | 99 | 100 | describe("Addition with transposition", function() { 101 | it("returns 2 step when transposing one char and append another", function() { 102 | assert.deepEqual(levenshtien("test", "tsets"), { 103 | steps: 2, 104 | relative: 2 / 5, 105 | similarity: 1 - 2 / 5 106 | }); 107 | }); 108 | it("returns 2 step when transposing a char and repeating it", function() { 109 | assert.deepEqual(levenshtien("test", "tsset"), { 110 | steps: 2, 111 | relative: 2 / 5, 112 | similarity: 1 - 2 / 5 113 | }); 114 | }); 115 | }); 116 | 117 | describe("Transposition of multiple chars", function() { 118 | it("returns 1 step when transposing two neighbouring characters", function() { 119 | assert.deepEqual(levenshtien("banana", "banaan"), { 120 | steps: 1, 121 | relative: 1 / 6, 122 | similarity: 1 - 1 / 6 123 | }); 124 | }); 125 | 126 | it("returns 2 step when transposing two neighbouring characters by two places", function() { 127 | assert.deepEqual(levenshtien("banana", "nabana"), { 128 | steps: 2, 129 | relative: 2 / 6, 130 | similarity: 1 - 2 / 6 131 | }); 132 | }); 133 | 134 | it("returns 2 step when transposing two pairs of characters", function() { 135 | assert.deepEqual(levenshtien("banana", "abnaan"), { 136 | steps: 2, 137 | relative: 2 / 6, 138 | similarity: 1 - 2 / 6 139 | }); 140 | }); 141 | }); 142 | 143 | describe("Empty strings", function() { 144 | it("returns 0 step and 0 relative when both are empty", function() { 145 | assert.deepEqual(levenshtien("", ""), { 146 | steps: 0, 147 | relative: 0, 148 | similarity: 1 149 | }); 150 | }); 151 | 152 | it("returns steps equal to first string lenght when second string is empty", function() { 153 | assert.deepEqual(levenshtien("test", ""), { 154 | steps: 4, 155 | relative: 4 / 4, 156 | similarity: 0 157 | }); 158 | }); 159 | 160 | it("returns steps equal to second string lenght when first string is empty", function() { 161 | assert.deepEqual(levenshtien("", "test"), { 162 | steps: 4, 163 | relative: 1, 164 | similarity: 0 165 | }); 166 | }); 167 | }); 168 | }); 169 | --------------------------------------------------------------------------------