Skip to content

Commit 293ddba

Browse files
committed
Move suggestion logic into its own class
1 parent c5ac0f9 commit 293ddba

File tree

2 files changed

+70
-3
lines changed

2 files changed

+70
-3
lines changed

src/Elastic.Markdown/Myst/FrontMatter/Products.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
44

5+
using System.Collections.Frozen;
56
using System.ComponentModel.DataAnnotations;
7+
using Elastic.Markdown.Suggestions;
68
using EnumFastToStringGenerated;
79
using YamlDotNet.Core;
810
using YamlDotNet.Core.Events;
@@ -255,14 +257,14 @@ public object ReadYaml(IParser parser, Type type, ObjectDeserializer rootDeseria
255257
public class InvalidProductException(string invalidValue)
256258
: Exception(
257259
$"Invalid products frontmatter value: \"{invalidValue}\"." +
258-
(!string.IsNullOrWhiteSpace(invalidValue) ? $" Did you mean \"{ProductExtensions.Suggestion(invalidValue)}\"?" : "") +
260+
(!string.IsNullOrWhiteSpace(invalidValue) ? " " + new Suggestion(ProductExtensions.GetProductIds(), invalidValue).GetSuggestionQuestion() : "") +
259261
"\nYou can find the full list at https://docs-v3-preview.elastic.dev/elastic/docs-builder/tree/main/syntax/frontmatter#products.");
260262

261263
public static class ProductExtensions
262264
{
263-
private static IReadOnlyCollection<string> GetProductIds() =>
265+
public static IReadOnlySet<string> GetProductIds() =>
264266
ProductEnumExtensions.GetValuesFast()
265-
.Select(p => p.ToDisplayFast()).ToList();
267+
.Select(p => p.ToDisplayFast()).ToFrozenSet();
266268

267269
public static string Suggestion(string input) =>
268270
GetProductIds()
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
namespace Elastic.Markdown.Suggestions;
6+
7+
public class Suggestion(IReadOnlySet<string> candidates, string input)
8+
{
9+
private IReadOnlyCollection<string> GetSuggestions() =>
10+
candidates
11+
.Select(source => (source, Distance: LevenshteinDistance(input, source)))
12+
.OrderBy(suggestion => suggestion.Distance)
13+
.Where(suggestion => suggestion.Distance <= 2)
14+
.Select(suggestion => suggestion.source)
15+
.Take(3)
16+
.ToList();
17+
18+
public string GetSuggestionQuestion()
19+
{
20+
var suggestions = GetSuggestions();
21+
if (suggestions.Count == 0)
22+
return string.Empty;
23+
24+
return "Did you mean " + string.Join(", ", suggestions.SkipLast(1).Select(s => $"\"{s}\"")) + (suggestions.Count > 1 ? " or " : "") + (suggestions.LastOrDefault() != null ? $"\"{suggestions.LastOrDefault()}\"" : "") + "?";
25+
}
26+
27+
private static int LevenshteinDistance(string source, string target)
28+
{
29+
if (string.IsNullOrEmpty(target))
30+
return int.MaxValue;
31+
32+
var sourceLength = source.Length;
33+
var targetLength = target.Length;
34+
35+
if (sourceLength == 0)
36+
return targetLength;
37+
38+
if (targetLength == 0)
39+
return sourceLength;
40+
41+
var distance = new int[sourceLength + 1, targetLength + 1];
42+
43+
for (var i = 0; i <= sourceLength; i++)
44+
distance[i, 0] = i;
45+
46+
for (var j = 0; j <= targetLength; j++)
47+
distance[0, j] = j;
48+
49+
for (var i = 1; i <= sourceLength; i++)
50+
{
51+
for (var j = 1; j <= targetLength; j++)
52+
{
53+
var cost = (source[i - 1] == target[j - 1]) ? 0 : 1;
54+
55+
distance[i, j] = Math.Min(
56+
Math.Min(
57+
distance[i - 1, j] + 1,
58+
distance[i, j - 1] + 1),
59+
distance[i - 1, j - 1] + cost);
60+
}
61+
}
62+
63+
return distance[sourceLength, targetLength];
64+
}
65+
}

0 commit comments

Comments
 (0)