Jaro Similarity

P
using System;

namespace Algorithms.Strings.Similarity;

/// <summary>
///     <para>
///         Jaro Similarity measures how similar two strings are.
///         Result is between 0 and 1 where 0 represnts that there is no similarity between strings and 1 represents equal strings.
///         Time complexity is O(a*b) where a is the length of the first string and b is the length of the second string.
///     </para>
///     <para>
///         Wikipedia: https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance#Jaro_similarity.
///     </para>
/// </summary>
public static class JaroSimilarity
{
    /// <summary>
    ///     Calculates Jaro Similarity between two strings.
    /// </summary>
    /// <param name="s1">First string.</param>
    /// <param name="s2">Second string.</param>
    public static double Calculate(string s1, string s2)
    {
        if (s1 == s2)
        {
            return 1;
        }

        var longerString = s1.Length > s2.Length ? s1 : s2;
        var shorterString = s1.Length < s2.Length ? s1 : s2;

        // will look for matching characters in this range
        var matchingCharacterRange = Math.Max((longerString.Length / 2) - 1, 0);
        var matches = 0d;

        // true if i-th index of s1 was matched in s2
        var s1MatchedIndeces = new bool[s1.Length];

        // true if i-th index of s2 was matched in s1
        var s2MatchedIndeces = new bool[s2.Length];

        for (var i = 0; i < longerString.Length; i++)
        {
            var startIndex = Math.Max(i - matchingCharacterRange, 0);
            var endIndex = Math.Min(i + matchingCharacterRange, shorterString.Length - 1);
            for (var j = startIndex; j <= endIndex; j++)
            {
                if (s1[i] == s2[j] && !s2MatchedIndeces[j])
                {
                    matches++;
                    s1MatchedIndeces[i] = true;
                    s2MatchedIndeces[j] = true;
                    break;
                }
            }
        }

        if (matches == 0)
        {
            return 0;
        }

        var transpositions = CalculateTranspositions(s1, s2, s1MatchedIndeces, s2MatchedIndeces);

        return ((matches / s1.Length) + (matches / s2.Length) + ((matches - transpositions) / matches)) / 3;
    }

    /// <summary>
    ///     Calculates number of matched characters that are not in the right order.
    /// </summary>
    private static int CalculateTranspositions(string s1, string s2, bool[] s1MatchedIndeces, bool[] s2MatchedIndeces)
    {
        var transpositions = 0;
        var s2Index = 0;
        for (var s1Index = 0; s1Index < s1.Length; s1Index++)
        {
            if (s1MatchedIndeces[s1Index])
            {
                while (!s2MatchedIndeces[s2Index])
                {
                    s2Index++;
                }

                if (s1[s1Index] != s2[s2Index])
                {
                    transpositions++;
                }

                s2Index++;
            }
        }

        transpositions /= 2;
        return transpositions;
    }
}