@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
132 lines (108 loc) • 3.85 kB
JavaScript
import { assert } from "../../assert.js";
import { BitSet } from "../../binary/BitSet.js";
import { max2 } from "../../math/max2.js";
import { min2 } from "../../math/min2.js";
/**
* Calculate Jaro distance between two strings, this is a measure of string similarity.
* Higher value means more similarity.
* @param {string} first
* @param {string} second
* @param {number} first_length
* @param {number} second_length
* @return {number}
*/
export function string_jaro_distance(
first, second,
first_length, second_length
) {
assert.isString(first,'first');
assert.isString(second,'second');
// TODO can do better for short strings under 32 characters by using int32 bitmask. This would avoid allocation entirely
const matches1 = BitSet.fixedSize(first_length);
const matches2 = BitSet.fixedSize(second_length);
const matches = getMatching(first, second, matches1, matches2);
if (matches <= 0) {
return 0;
}
const transpositions = getTranspositions(first, second, matches1, matches2);
return (matches / first_length + matches / second_length + (matches - transpositions) / matches) / 3;
}
/**
* Find matching characters in both strings according to Jaro algorithm
* @param {string} a1
* @param {string} a2
* @param {BitSet} matches1
* @param {BitSet} matches2
* @return {number}
*/
function getMatching(a1, a2, matches1, matches2) {
const a1_length = a1.length;
const a2_length = a2.length;
// Window is modified to work with string of length 1
const matchWindow = max2(
0,
Math.floor(max2(a1_length, a2_length) * 0.5) - 1
);
let matches = 0;
// Loop to find matched characters:
for (let index1 = 0; index1 < a1_length; index1++) {
// Use the highest of the window diff and the min of the window and string 2 length:
const start = max2(0, index1 - matchWindow);
const end = min2(index1 + matchWindow + 1, a2_length);
// Iterate second string index:
for (let index2 = start; index2 < end; index2++) {
// If second string character already matched, skip:
if (matches2.get(index2)) {
continue;
}
// If the characters don't match, skip:
if (a1.charAt(index1) !== a2.charAt(index2)) {
continue;
}
// Assume match if the above 2 checks don't continue:
matches1.set(index1, true);
matches2.set(index2, true);
// Add matches by 1, break inner loop:
++matches;
break;
}
}
return matches;
}
/**
* Calculate the number of transpositions between the two words
* @param {string} a1 The first string to compare
* @param {string} a2 The second string to compare
* @param {BitSet} matches1
* @param {BitSet} matches2
* @returns {number}
*/
function getTranspositions(
a1, a2,
matches1, matches2
) {
let transpositions = 0;
// Loop to find transpositions:
const a1_length = a1.length;
const a2_length = a2.length;
for (let i1 = 0, i2 = 0; i1 < a1_length; i1++) {
// If a non-matching character was found, skip:
if (matches1.get(i1) === false) {
continue;
}
// Move i2 index to the next match:
while (
i2 < a2_length
&& matches2.get(i2) === false
) {
i2++;
}
// If the characters don't match, increase transposition:
if (a1.charAt(i1) !== a2.charAt(i2)) {
transpositions++;
}
// Iterate i2 index normally:
i2++;
}
return Math.floor(transpositions * 0.5);
}