diff --git a/ConvertApi/Helpers/Helper.cs b/ConvertApi/Helpers/Helper.cs new file mode 100644 index 0000000..9f8a481 --- /dev/null +++ b/ConvertApi/Helpers/Helper.cs @@ -0,0 +1,10 @@ +using System; + +namespace ConvertApiDotNet.Helpers +{ + public static class Helper + { + public static bool ContainsIgnoreCase(string source, string token) => + source?.IndexOf(token, StringComparison.OrdinalIgnoreCase) >= 0; + } +} \ No newline at end of file diff --git a/ConvertApi/Services/ConverterCatalog.cs b/ConvertApi/Services/ConverterCatalog.cs index d4d23b8..e66289e 100644 --- a/ConvertApi/Services/ConverterCatalog.cs +++ b/ConvertApi/Services/ConverterCatalog.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using ConvertApiDotNet.Constants; +using ConvertApiDotNet.Helpers; using ConvertApiDotNet.Interface; using ConvertApiDotNet.Model; using Microsoft.OpenApi.Any; @@ -70,14 +71,224 @@ public List GetConvertersByTags(List tags = null) public List SearchConverters(string[] terms) { EnsureLoaded(); - if (terms == null || terms.Length == 0) return _converters; - var keys = terms.Where(t => !string.IsNullOrWhiteSpace(t)) - .Select(t => t.Trim()) - .ToArray(); - if (keys.Length == 0) return _converters; - - return _converters.Where(c => - keys.All(k => Matches(c, k))).ToList(); + + if (terms == null || terms.Length == 0) + return new List(); + + var stop = new HashSet(StringComparer.OrdinalIgnoreCase) + { "to", "into", "in", "as", "from", "->", "-", "–", "—" }; + + var tokens = terms + .Where(t => !string.IsNullOrWhiteSpace(t)) + .Select(t => t.Trim().Trim('-', '>', '<', '.', ',', ';', ':', '/', '\\')) + .Where(t => t.Length > 0 && !stop.Contains(t)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (tokens.Length == 0) + return new List(); + + var allConverters = GetAllConverters(); + + var knownFormats = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var c in allConverters) + { + if (!string.IsNullOrEmpty(c.SourceFormat)) knownFormats.Add(c.SourceFormat); + if (!string.IsNullOrEmpty(c.DestinationFormat)) knownFormats.Add(c.DestinationFormat); + if (c.SourceExtensions != null) foreach (var e in c.SourceExtensions) knownFormats.Add(e.TrimStart('.')); + if (c.DestinationExtensions != null) foreach (var e in c.DestinationExtensions) knownFormats.Add(e.TrimStart('.')); + } + + var formatTokens = tokens.Where(t => knownFormats.Contains(t)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + + bool SupportsFormat(string format, List extensions, string token) + { + if (string.Equals(format, token, StringComparison.OrdinalIgnoreCase)) return true; + if (extensions != null) + { + return extensions.Any(e => string.Equals(e.TrimStart('.'), token, StringComparison.OrdinalIgnoreCase)); + } + return false; + } + + bool MatchesToken(ConverterDto c, string t) + { + return SupportsFormat(c.SourceFormat, c.SourceExtensions, t) || + SupportsFormat(c.DestinationFormat, c.DestinationExtensions, t) || + (c.Summary != null && Helper.ContainsIgnoreCase(c.Summary, t)) || + (c.Tags != null && c.Tags.Any(tag => Helper.ContainsIgnoreCase(tag, t))) || + (c.Description != null && Helper.ContainsIgnoreCase(c.Description, t)) || + (c.Overview != null && Helper.ContainsIgnoreCase(c.Overview, t)); + } + + // Prioritize items that match ALL search terms + var candidates = allConverters.Where(c => tokens.All(t => MatchesToken(c, t))).ToList(); + + if (formatTokens.Length >= 2) + { + var src = formatTokens[0]; + var dst = formatTokens[1]; + + var pairMatches = allConverters.Where(c => + { + bool supportsSrc = SupportsFormat(c.SourceFormat, c.SourceExtensions, src); + bool supportsDst = SupportsFormat(c.DestinationFormat, c.DestinationExtensions, dst); + if (supportsSrc && supportsDst) return true; + + bool supportsRevSrc = SupportsFormat(c.SourceFormat, c.SourceExtensions, dst); + bool supportsRevDst = SupportsFormat(c.DestinationFormat, c.DestinationExtensions, src); + return supportsRevSrc && supportsRevDst; + }); + + foreach (var pm in pairMatches) + { + if (!candidates.Contains(pm)) candidates.Add(pm); + } + } + + // Fallback: if no matches for all terms, return items matching ANY term + if (candidates.Count == 0) + { + candidates = allConverters.Where(c => tokens.Any(t => MatchesToken(c, t))).ToList(); + } + + if (formatTokens.Length >= 2) + { + var src = formatTokens[0]; + var dst = formatTokens[1]; + + var virtuals = new List(); + + foreach (var c in candidates) + { + // Forward check (src -> dst) + bool supportsSrc = SupportsFormat(c.SourceFormat, c.SourceExtensions, src); + bool supportsDst = SupportsFormat(c.DestinationFormat, c.DestinationExtensions, dst); + + if (supportsSrc && supportsDst) + { + if (!string.Equals(c.SourceFormat, src, StringComparison.OrdinalIgnoreCase) || !string.Equals(c.DestinationFormat, dst, StringComparison.OrdinalIgnoreCase)) + { + virtuals.Add(CreateVirtual(c, src, dst)); + } + } + + // Reverse check (dst -> src) + bool supportsRevSrc = SupportsFormat(c.SourceFormat, c.SourceExtensions, dst); + bool supportsRevDst = SupportsFormat(c.DestinationFormat, c.DestinationExtensions, src); + + if (supportsRevSrc && supportsRevDst) + { + if (!string.Equals(c.SourceFormat, dst, StringComparison.OrdinalIgnoreCase) || !string.Equals(c.DestinationFormat, src, StringComparison.OrdinalIgnoreCase)) + { + virtuals.Add(CreateVirtual(c, dst, src)); + } + } + } + candidates.AddRange(virtuals); + } + + int Score(ConverterDto c) + { + int s = 0; + + // 0. Individual Token Scoring with Positional Weighting + for (int i = 0; i < formatTokens.Length; i++) + { + var t = formatTokens[i]; + bool isSrc = SupportsFormat(c.SourceFormat, c.SourceExtensions, t); + bool isDst = SupportsFormat(c.DestinationFormat, c.DestinationExtensions, t); + + if (isSrc) s += 3000; + if (isDst) s += 3000; + + // Positional Bonuses + if (i == 0) // First format term: Prefer Source + { + if (isSrc) s += 2000; + } + else if (i == 1) // Second format term: Prefer Destination + { + if (isDst) s += 2000; + } + } + + if (formatTokens.Length >= 2) + { + var t1 = formatTokens[0]; + var t2 = formatTokens[1]; + + // 1. SourceFormat & DestinationFormat (Highest Priority) + bool fwdFmt = string.Equals(c.SourceFormat, t1, StringComparison.OrdinalIgnoreCase) && string.Equals(c.DestinationFormat, t2, StringComparison.OrdinalIgnoreCase); + bool revFmt = string.Equals(c.SourceFormat, t2, StringComparison.OrdinalIgnoreCase) && string.Equals(c.DestinationFormat, t1, StringComparison.OrdinalIgnoreCase); + + if (fwdFmt) s += 100000; + if (revFmt) s += 50000; + + // 3. SourceExtensions & DestinationExtensions (Third Priority) + if (!fwdFmt && !revFmt) + { + bool fwdExt = SupportsFormat(c.SourceFormat, c.SourceExtensions, t1) && SupportsFormat(c.DestinationFormat, c.DestinationExtensions, t2); + bool revExt = SupportsFormat(c.SourceFormat, c.SourceExtensions, t2) && SupportsFormat(c.DestinationFormat, c.DestinationExtensions, t1); + + if (fwdExt) s += 40000; + if (revExt) s += 20000; + } + } + + // 2. Summary (Second Priority) + if (c.Summary != null) + { + int summaryHits = tokens.Count(t => Helper.ContainsIgnoreCase(c.Summary, t)); + s += summaryHits * 1000; + } + + // 4. Tags (Low Priority) + if (c.Tags != null) + { + int tagHits = tokens.Count(t => c.Tags.Any(tag => Helper.ContainsIgnoreCase(tag, t))); + s += tagHits * 10; + } + + // 5. Description (Lower Priority) + if (c.Description != null) + { + int descHits = tokens.Count(t => Helper.ContainsIgnoreCase(c.Description, t)); + s += descHits * 5; + } + + // 6. Overview (Lowest Priority) + if (c.Overview != null) + { + int ovHits = tokens.Count(t => Helper.ContainsIgnoreCase(c.Overview, t)); + s += ovHits * 2; + } + + return s; + } + + return candidates + .OrderByDescending(Score) + .ThenBy(c => c.Summary) + .GroupBy(c => new { S = c.SourceFormat?.ToLowerInvariant(), D = c.DestinationFormat?.ToLowerInvariant() }) + .Select(g => g.First()) + .Where(x=> GetConverter(x.SourceFormat, x.DestinationFormat) != null) + .ToList(); + } + + private ConverterDto CreateVirtual(ConverterDto c, string s, string d) + { + return new ConverterDto + { + SourceFormat = s.ToLowerInvariant(), + DestinationFormat = d.ToLowerInvariant(), + SourceExtensions = c.SourceExtensions, + DestinationExtensions = c.DestinationExtensions, + Summary = $"{s.ToUpper()} to {d.ToUpper()} API", + Description = c.Description, + Tags = c.Tags, + Overview = c.Overview + }; } public List GetTags()