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 Spectre.Console;
 
namespace Aspire.Cli.Utils;
 
internal static class StringUtils
{
    public static string RemoveMarkup(string input)
    {
        if (string.IsNullOrEmpty(input))
        {
            return input;
        }
 
        try
        {
            return Markup.Remove(input);
        }
        catch (InvalidOperationException)
        {
            // Backchannel payloads can contain plain text with literal '[' or ']' from
            // user/project output (for example, compiler diagnostics). Treat malformed
            // markup as plain text so error reporting never throws while logging another error.
            return input;
        }
    }
 
    /// <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, StringComparison.Ordinal))
        {
            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];
    }
}