功能说明
jaroSimilarity 计算两个字符串的 Jaro 相似度,jaroWinkler 在此基础上对相同公共前缀给予额外加分。分数范围为 0 到 1,越大越相似。该算法对换位和短字符串较友好,常用于姓名去重、拼写纠错候选、客户主数据匹配和搜索建议。相比编辑距离,它更关注字符匹配与顺序,而不是插入删除的具体代价。
关键代码
匹配窗口取较长字符串长度的一半减一,每个字符只在窗口内寻找尚未匹配的对应字符。先统计匹配数量,再按匹配顺序排列并统计位置不一致的换位次数,最后套用 Jaro 公式。Winkler 版本最多取前四个公共前缀字符,乘以默认 0.1 的缩放系数与剩余差异,前缀权重上限为 0.25,防止分数虚高。
使用注意
算法区分大小写与重音,匹配前通常应统一大小写并做 Unicode 规范化。短字符串的分数波动大,设置阈值时应结合业务样本评估,不要直接使用 0.8 之类的通用值。前缀加分对同名前缀友好,但也可能让无关的长前缀词获得高分。该函数不处理分词或拼音,中文姓名匹配需要先转拼音或使用专门算法。
验证方式
测试完全相同、完全不同、空字符串、单字符与典型换位,断言分数在 0 到 1 之间。用 MARTHA 与 MARHTA 验证经典 Jaro 结果,用 DWAYNE 与 DUANE 验证 Winkler 前缀加分。测试大小写差异、前缀长度为四以上、prefixScale 越界以及超长字符串的性能,确认不出现 NaN。
代码示例
export function jaroSimilarity(a: string, b: string): number {
if (a === b) return 1;
if (a.length === 0 || b.length === 0) return 0;
const window = Math.max(0, Math.floor(Math.max(a.length, b.length) / 2) - 1);
const aMatches = new Array<boolean>(a.length).fill(false);
const bMatches = new Array<boolean>(b.length).fill(false);
let matches = 0;
for (let i = 0; i < a.length; i += 1) {
const start = Math.max(0, i - window);
const end = Math.min(i + window + 1, b.length);
for (let j = start; j < end; j += 1) {
if (bMatches[j] || a[i] !== b[j]) continue;
aMatches[i] = true;
bMatches[j] = true;
matches += 1;
break;
}
}
if (matches === 0) return 0;
let transpositions = 0;
let cursor = 0;
for (let i = 0; i < a.length; i += 1) {
if (!aMatches[i]) continue;
while (!bMatches[cursor]) cursor += 1;
if (a[i] !== b[cursor]) transpositions += 1;
cursor += 1;
}
return (matches / a.length + matches / b.length + (matches - transpositions / 2) / matches) / 3;
}
export function jaroWinkler(a: string, b: string, prefixScale = 0.1): number {
if (prefixScale < 0 || prefixScale > 0.25) {
throw new RangeError("prefixScale must be between 0 and 0.25");
}
const jaro = jaroSimilarity(a, b);
let prefix = 0;
const limit = Math.min(4, a.length, b.length);
while (prefix < limit && a[prefix] === b[prefix]) prefix += 1;
return jaro + prefix * prefixScale * (1 - jaro);
}