File: Utils\StringUtils.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.RegularExpressions;
 
namespace Aspire.Cli.Utils;
 
internal static partial class StringUtils
{
    public static string RemoveSpectreFormatting(this string input)
    {
        return RemoveSpectreFormattingRegex().Replace(input, string.Empty).Trim();
    }
 
    [GeneratedRegex(@"\[[^\]]+\]")]
    private static partial Regex RemoveSpectreFormattingRegex();
 
    /// <summary>
    /// Calculates a fuzzy match score between a search term and a target string.
    /// </summary>
    /// <param name="searchTerm">The search term.</param>
    /// <param name="target">The target string to match against.</param>
    /// <returns>
    /// A score between 0.0 and 1.0, where 1.0 is a perfect match and 0.0 is no match.
    /// Higher scores indicate better matches. The caller is responsible for filtering based on score thresholds.
    /// </returns>
    /// <remarks>
    /// The scoring prioritizes different match types:
    /// <list type="bullet">
    /// <item>1.0 - Exact match (case-insensitive)</item>
    /// <item>0.95 - Target starts with search term</item>
    /// <item>0.85 - Target contains search term</item>
    /// <item>0.0-0.75 - Fuzzy match based on Levenshtein distance</item>
    /// </list>
    /// </remarks>
    public static double CalculateFuzzyScore(string searchTerm, string target)
    {
        if (string.IsNullOrWhiteSpace(searchTerm) || string.IsNullOrWhiteSpace(target))
        {
            return 0.0;
        }
 
        var searchLower = searchTerm.ToLowerInvariant();
        var targetLower = target.ToLowerInvariant();
 
        // Exact match
        if (searchLower == targetLower)
        {
            return 1.0;
        }
 
        // Starts with (high priority)
        if (targetLower.StartsWith(searchLower))
        {
            return 0.95;
        }
 
        // Contains (medium priority)
        if (targetLower.Contains(searchLower))
        {
            return 0.85;
        }
 
        // Levenshtein distance for fuzzy matching (low priority)
        var distance = GetLevenshteinDistance(searchLower, targetLower);
        var maxLength = Math.Max(searchLower.Length, targetLower.Length);
 
        // Normalize score: closer to 0 distance = higher score
        return (1.0 - (double)distance / maxLength) * 0.75;
    }
 
    /// <summary>
    /// Calculates the Levenshtein distance between two strings.
    /// </summary>
    /// <param name="source">The source string.</param>
    /// <param name="target">The target string.</param>
    /// <returns>The minimum number of single-character edits required to change the source string into the target string.</returns>
    public static int GetLevenshteinDistance(string source, string target)
    {
        if (string.IsNullOrWhiteSpace(source))
        {
            return string.IsNullOrWhiteSpace(target) ? 0 : target.Length;
        }
 
        if (string.IsNullOrWhiteSpace(target))
        {
            return source.Length;
        }
 
        var sourceLength = source.Length;
        var targetLength = target.Length;
        var distance = new int[sourceLength + 1, targetLength + 1];
 
        for (var i = 0; i <= sourceLength; i++)
        {
            distance[i, 0] = i;
        }
 
        for (var j = 0; j <= targetLength; j++)
        {
            distance[0, j] = j;
        }
 
        for (var i = 1; i <= sourceLength; i++)
        {
            for (var j = 1; j <= targetLength; j++)
            {
                var cost = target[j - 1] == source[i - 1] ? 0 : 1;
                distance[i, j] = Math.Min(
                    Math.Min(distance[i - 1, j] + 1, distance[i, j - 1] + 1),
                    distance[i - 1, j - 1] + cost);
            }
        }
 
        return distance[sourceLength, targetLength];
    }
}