From 95b53debba49dfbdc89e2cf0564c2694525f1d60 Mon Sep 17 00:00:00 2001 From: Osama Ansar Date: Tue, 13 Oct 2026 22:15:42 +0500 Subject: [PATCH 1/2] fix: JaroWinklerDistance of single character strings that differ in case For strings of length 1 the match window floor(1 / 2) - 1 is negative, so no character matched and the Jaro distance was 0. The prefix loop also ran past the end of the strings, so JaroWinklerDistance('A', 'a', { ignoreCase: true }) returned 0.4 instead of 1. Clamp the match window to 0 and bound the prefix loop by the lengths of the strings. --- lib/natural/distance/jaro-winkler_distance.js | 5 +++-- spec/jaro-winkler_spec.ts | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/natural/distance/jaro-winkler_distance.js b/lib/natural/distance/jaro-winkler_distance.js index a2318a96a..2f9d70d24 100644 --- a/lib/natural/distance/jaro-winkler_distance.js +++ b/lib/natural/distance/jaro-winkler_distance.js @@ -37,7 +37,8 @@ function distance (s1, s2) { return 0 } - const matchWindow = (Math.floor(Math.max(s1.length, s2.length) / 2.0)) - 1 + // The window is negative for strings of length 1, so clamp it to 0 + const matchWindow = Math.max(0, (Math.floor(Math.max(s1.length, s2.length) / 2.0)) - 1) const matches1 = new Array(s1.length) const matches2 = new Array(s2.length) let m = 0 // number of matches @@ -115,7 +116,7 @@ function JaroWinklerDistance (s1, s2, options = {}) { const jaro = (options.dj === undefined) ? distance(s1, s2) : options.dj const p = 0.1 // default scaling factor let l = 0 // length of the matching prefix - while (s1[l] === s2[l] && l < 4) { + while (l < 4 && l < s1.length && l < s2.length && s1[l] === s2[l]) { l++ } diff --git a/spec/jaro-winkler_spec.ts b/spec/jaro-winkler_spec.ts index a52ca14c5..c0eaa1f0b 100644 --- a/spec/jaro-winkler_spec.ts +++ b/spec/jaro-winkler_spec.ts @@ -73,6 +73,20 @@ describe('jaro-winkler', function () { expect(approxEql(JaroWinklerDistance('MARTHA', 'MARHTA', { ignoreCase: true }), 0.96111)).toBeTruthy() expect(JaroWinklerDistance('abcd', 'ABCD', { ignoreCase: true })).toBe(1) }) + + it('should handle strings of length 1', function () { + expect(JaroWinklerDistance('a', 'a', {})).toBe(1) + expect(JaroWinklerDistance('a', 'b', {})).toBe(0) + expect(JaroWinklerDistance('A', 'a', {})).toBe(0) + expect(JaroWinklerDistance('A', 'a', { ignoreCase: true })).toBe(1) + expect(JaroWinklerDistance('a', 'A', { ignoreCase: true })).toBe(1) + expect(JaroWinklerDistance('A', 'b', { ignoreCase: true })).toBe(0) + }) + + it('should not count a matching prefix longer than the strings', function () { + // Both strings are 2 characters long, so the matching prefix is at most 2: 0.5 + 2 * 0.1 * (1 - 0.5) + expect(approxEql(JaroWinklerDistance('Ab', 'aB', { ignoreCase: true, dj: 0.5 }), 0.6)).toBeTruthy() + }) it('should not fail while passing no options', function () { expect(approxEql(JaroWinklerDistance('check', 'chakk'), 0.78666)).toBeTruthy() }) From 70638ab2424e09dbb2ec17570fb370036e0b44ac Mon Sep 17 00:00:00 2001 From: "Hugo W.L. ter Doest" Date: Mon, 26 Oct 2026 08:11:52 +0100 Subject: [PATCH 2/2] chore: trigger CI