diff --git a/.github/workflows/build-nuget-packages.yml b/.github/workflows/build-nuget-packages.yml new file mode 100644 index 0000000..9386cb9 --- /dev/null +++ b/.github/workflows/build-nuget-packages.yml @@ -0,0 +1,44 @@ +name: Build, Test, and Package + +on: + push: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: Build, test, and pack + runs-on: windows-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Restore + run: dotnet restore FuzzySharp.slnx + + - name: Build + run: dotnet build FuzzySharp.slnx --configuration Release --no-restore -p:GeneratePackageOnBuild=false + + - name: Test + run: dotnet test FuzzySharp.Test/FuzzySharp.Test.csproj --configuration Release --no-restore --no-build + + - name: Pack + run: dotnet pack FuzzySharp/FuzzySharp.csproj --configuration Release --no-restore --no-build --output artifacts -p:GeneratePackageOnBuild=false -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg + + - name: Upload NuGet packages + uses: actions/upload-artifact@v4 + with: + name: nuget-packages + path: | + artifacts/*.nupkg + artifacts/*.snupkg + if-no-files-found: error diff --git a/.github/workflows/development_package.yml b/.github/workflows/development_package.yml deleted file mode 100644 index 89a98b7..0000000 --- a/.github/workflows/development_package.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: CI Build - -on: - push: - branches: - - development - -jobs: - build: - runs-on: windows-latest - name: Build - steps: - - name: Checkout repository - uses: actions/checkout@v1 - - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 3.0.100 - - - name: Build with dotnet - run: dotnet build --configuration Release - - - name: Test with dotnet - run: dotnet test - deploy: - needs: [Build] - name: Package - runs-on: [windows-latest] - steps: - - uses: actions/checkout@v1 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 3.0.100 - - - name: Build with dotnet - run: dotnet build --configuration Release - - - name: Pack nuget package - run: dotnet pack --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg diff --git a/.github/workflows/master_package_and_publish.yml b/.github/workflows/master_package_and_publish.yml deleted file mode 100644 index a1896bb..0000000 --- a/.github/workflows/master_package_and_publish.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Nuget Package Deploy - -on: - push: - branches: - - master - -jobs: - build: - runs-on: windows-latest - name: Build - steps: - - name: Checkout repository - uses: actions/checkout@v1 - - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 3.0.100 - - - name: Build with dotnet - run: dotnet build --configuration Release - - - name: Test with dotnet - run: dotnet test - deploy: - needs: [Build] - name: Package and Publish - runs-on: [windows-latest] - steps: - - uses: actions/checkout@v1 - - name: Setup .NET Core - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 3.0.100 - - - name: Build with dotnet - run: dotnet build --configuration Release - - - name: Pack nuget package - run: dotnet pack --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg - - - name: Push package to NuGet - run: dotnet nuget push **/*.nupkg - --skip-duplicate - --api-key ${{ secrets.NUGET_DEPLOY_KEY }} - --source https://api.nuget.org/v3/index.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 82fff59..bb0e047 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v6.0.0 + +*Double-precision scoring and simplified distance APIs* + +- **Breaking:** Removed `scoreCutoff` from the Indel, Levenshtein, LCS, and partial-ratio APIs. +- **Breaking:** Scorers, scoring strategies, `Fuzz` methods, and extracted-result scores now use `double` and preserve fractional similarity values. +- Extraction `cutoff` parameters now accept `double` values. +- Optimized pattern-match-vector construction with bulk character population, a dedicated single-block fast path, lazy non-ASCII storage, and direct dense-mask lookup for ASCII characters. + ## v5.0.3 *Extractor selection performance and allocation improvements* diff --git a/FuzzySharp.Benchmarks/ExtractSelectionBenchmarks.cs b/FuzzySharp.Benchmarks/ExtractSelectionBenchmarks.cs index 195909c..86428db 100644 --- a/FuzzySharp.Benchmarks/ExtractSelectionBenchmarks.cs +++ b/FuzzySharp.Benchmarks/ExtractSelectionBenchmarks.cs @@ -203,12 +203,12 @@ internal static IEnumerable> LegacyParallelCachedExtract private sealed class CheapScorer : IRatioScorer { - public int Score(string input1, string input2) + public double Score(string input1, string input2) { return ScoreFromChoice(input2); } - public int Score(string input1, string input2, Func preprocessor) + public double Score(string input1, string input2, Func preprocessor) { return Score(preprocessor(input1), preprocessor(input2)); } @@ -216,7 +216,7 @@ public int Score(string input1, string input2, Func preprocessor private sealed class CheapCachedScorer : ICachedRatioScorer { - public int Score(string input2) + public double Score(string input2) { return ScoreFromChoice(input2); } @@ -335,12 +335,12 @@ public List> Legacy_ParallelCached_ExtractTop() private sealed class CheapScorer : IRatioScorer { - public int Score(string input1, string input2) + public double Score(string input1, string input2) { return ScoreFromChoice(input2); } - public int Score(string input1, string input2, Func preprocessor) + public double Score(string input1, string input2, Func preprocessor) { return Score(preprocessor(input1), preprocessor(input2)); } @@ -348,7 +348,7 @@ public int Score(string input1, string input2, Func preprocessor private sealed class CheapCachedScorer : ICachedRatioScorer { - public int Score(string input2) + public double Score(string input2) { return ScoreFromChoice(input2); } @@ -497,12 +497,12 @@ public List> Legacy_ParallelCached_ExtractTop() private sealed class CheapScorer : IRatioScorer { - public int Score(string input1, string input2) + public double Score(string input1, string input2) { return ScoreFromChoice(input2); } - public int Score(string input1, string input2, Func preprocessor) + public double Score(string input1, string input2, Func preprocessor) { return Score(preprocessor(input1), preprocessor(input2)); } @@ -510,7 +510,7 @@ public int Score(string input1, string input2, Func preprocessor private sealed class CheapCachedScorer : ICachedRatioScorer { - public int Score(string input2) + public double Score(string input2) { return ScoreFromChoice(input2); } diff --git a/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinLarge.cs b/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinLarge.cs index 29896d6..7d81e95 100644 --- a/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinLarge.cs +++ b/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinLarge.cs @@ -31,7 +31,7 @@ public void NaiveDp() } [Benchmark] - public void FuzzySharpClassic() + public void FuzzySharp() { for (var i = 0; i < _words.Length; i++) { @@ -67,8 +67,8 @@ public void Quickenshtein() } } - [Benchmark] - public void FuzzySharp() + [Benchmark(Description = "Raffinert.FuzzySharp")] + public void ThisLibrary() { for (var i = 0; i < _words.Length; i++) { diff --git a/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinNormal.cs b/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinNormal.cs index 53c1a9b..9194830 100644 --- a/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinNormal.cs +++ b/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinNormal.cs @@ -32,7 +32,7 @@ public void NaiveDp() } [Benchmark] - public void FuzzySharpClassic() + public void FuzzySharp() { for (var i = 0; i < _words.Length; i++) { @@ -68,8 +68,8 @@ public void Quickenshtein() } } - [Benchmark] - public void FuzzySharp() + [Benchmark(Description = "Raffinert.FuzzySharp")] + public void ThisLibrary() { for (var i = 0; i < _words.Length; i++) { diff --git a/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinSmall.cs b/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinSmall.cs index af92f28..8045749 100644 --- a/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinSmall.cs +++ b/FuzzySharp.Benchmarks/LevenshteinDistance/LevenshteinSmall.cs @@ -31,7 +31,7 @@ public void NaiveDp() } [Benchmark] - public void FuzzySharpClassic() + public void FuzzySharp() { for (var i = 0; i < _words.Length; i++) { @@ -67,8 +67,8 @@ public void Quickenshtein() } } - [Benchmark] - public void FuzzySharp() + [Benchmark(Description = "Raffinert.FuzzySharp")] + public void ThisLibrary() { for (var i = 0; i < _words.Length; i++) { diff --git a/FuzzySharp.Benchmarks/PartialRatioBenchmarks.cs b/FuzzySharp.Benchmarks/PartialRatioBenchmarks.cs index a6c554d..7076786 100644 --- a/FuzzySharp.Benchmarks/PartialRatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/PartialRatioBenchmarks.cs @@ -8,13 +8,13 @@ namespace Raffinert.FuzzySharp.Benchmarks; public class PartialRatioBenchmarks { [Benchmark] - public int PartialRatio() + public double PartialRatio() { return Fuzz.PartialRatio("similar", "somewhresimlrbetweenthisstring"); } [Benchmark] - public int PartialRatioClassic() + public double PartialRatioClassic() { return Classic.Fuzz.PartialRatio("similar", "somewhresimlrbetweenthisstring"); } diff --git a/FuzzySharp.Benchmarks/PartialRatioLongBenchmarks.cs b/FuzzySharp.Benchmarks/PartialRatioLongBenchmarks.cs index d383df5..9325240 100644 --- a/FuzzySharp.Benchmarks/PartialRatioLongBenchmarks.cs +++ b/FuzzySharp.Benchmarks/PartialRatioLongBenchmarks.cs @@ -32,13 +32,13 @@ private static string GenerateString(int length, Random rnd) } [Benchmark] - public int PartialRatio() + public double PartialRatio() { return Fuzz.PartialRatio(_s1, _s2); } [Benchmark] - public int PartialRatioClassic() + public double PartialRatioClassic() { return Classic.Fuzz.PartialRatio(_s1, _s2); } diff --git a/FuzzySharp.Benchmarks/PartialTokenAbbreviationRatioBenchmarks.cs b/FuzzySharp.Benchmarks/PartialTokenAbbreviationRatioBenchmarks.cs index 8fb6a78..2731fd0 100644 --- a/FuzzySharp.Benchmarks/PartialTokenAbbreviationRatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/PartialTokenAbbreviationRatioBenchmarks.cs @@ -9,13 +9,13 @@ namespace Raffinert.FuzzySharp.Benchmarks; public class PartialTokenAbbreviationRatioBenchmarks { [Benchmark] - public int PartialTokenAbbreviationRatio() + public double PartialTokenAbbreviationRatio() { return Fuzz.PartialTokenAbbreviationRatio("bl 420", "Baseline section 420", StringPreprocessor.Full); } [Benchmark] - public int PartialTokenAbbreviationRatioClassic() + public double PartialTokenAbbreviationRatioClassic() { return Classic.Fuzz.PartialTokenAbbreviationRatio("bl 420", "Baseline section 420", Classic.PreProcess.PreprocessMode.Full); } diff --git a/FuzzySharp.Benchmarks/PartialTokenInitialismRatioBenchmarks.cs b/FuzzySharp.Benchmarks/PartialTokenInitialismRatioBenchmarks.cs index 363dbc5..6287672 100644 --- a/FuzzySharp.Benchmarks/PartialTokenInitialismRatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/PartialTokenInitialismRatioBenchmarks.cs @@ -8,13 +8,13 @@ namespace Raffinert.FuzzySharp.Benchmarks; public class PartialTokenInitialismRatioBenchmarks { [Benchmark] - public int PartialTokenInitialismRatio() + public double PartialTokenInitialismRatio() { return Fuzz.PartialTokenInitialismRatio("NASA", "National Aeronautics Space Administration, Kennedy Space Center, Cape Canaveral, Florida 32899"); } [Benchmark] - public int PartialTokenInitialismRatioClassic() + public double PartialTokenInitialismRatioClassic() { return Classic.Fuzz.PartialTokenInitialismRatio("NASA", "National Aeronautics Space Administration, Kennedy Space Center, Cape Canaveral, Florida 32899"); } diff --git a/FuzzySharp.Benchmarks/PartialTokenSetRatioBenchmarks.cs b/FuzzySharp.Benchmarks/PartialTokenSetRatioBenchmarks.cs index 944c8a1..a07c85c 100644 --- a/FuzzySharp.Benchmarks/PartialTokenSetRatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/PartialTokenSetRatioBenchmarks.cs @@ -18,25 +18,25 @@ public void GlobalSetup() } [Benchmark] - public int PartialTokenSetRatio() + public double PartialTokenSetRatio() { return Fuzz.PartialTokenSetRatio("fuzzy was a bear", "fuzzy fuzzy fuzzy bear"); } [Benchmark] - public int PartialTokenSetRatioClassic() + public double PartialTokenSetRatioClassic() { return Classic.Fuzz.PartialTokenSetRatio("fuzzy was a bear", "fuzzy fuzzy fuzzy bear"); } [Benchmark] - public int PartialTokenSetRatioCached() + public double PartialTokenSetRatioCached() { return new CachedPartialTokenSetScorer("fuzzy was a bear").Score("fuzzy fuzzy fuzzy bear"); } [Benchmark] - public int PartialTokenSetRatioAcrossRunsCached() + public double PartialTokenSetRatioAcrossRunsCached() { return _cachedPartialTokenSetScorer.Score("fuzzy fuzzy fuzzy bear"); } diff --git a/FuzzySharp.Benchmarks/PartialTokenSortRatioBenchmarks.cs b/FuzzySharp.Benchmarks/PartialTokenSortRatioBenchmarks.cs index 580b4b4..82d0e8f 100644 --- a/FuzzySharp.Benchmarks/PartialTokenSortRatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/PartialTokenSortRatioBenchmarks.cs @@ -8,13 +8,13 @@ namespace Raffinert.FuzzySharp.Benchmarks; public class PartialTokenSortRatioBenchmarks { [Benchmark] - public int PartialTokenSortRatio() + public double PartialTokenSortRatio() { return Fuzz.PartialTokenSortRatio("order words out of", " words out of order"); } [Benchmark] - public int PartialTokenSortRatioClassic() + public double PartialTokenSortRatioClassic() { return Classic.Fuzz.PartialTokenSortRatio("order words out of", " words out of order"); } diff --git a/FuzzySharp.Benchmarks/RatioBenchmarks.cs b/FuzzySharp.Benchmarks/RatioBenchmarks.cs index e772b5d..43a6508 100644 --- a/FuzzySharp.Benchmarks/RatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/RatioBenchmarks.cs @@ -18,26 +18,26 @@ public void GlobalSetup() } [Benchmark] - public int Ratio() + public double Ratio() { return Fuzz.Ratio("mysmilarstring", "myawfullysimilarstirng"); } [Benchmark] - public int RatioClassic() + public double RatioClassic() { return Classic.Fuzz.Ratio("mysmilarstring", "myawfullysimilarstirng"); } [Benchmark] - public int RatioCached() + public double RatioCached() { using var scorer = new CachedDefaultRatioScorer("mysmilarstring"); return scorer.Score("myawfullysimilarstirng"); } [Benchmark] - public int RatioAcrossRunsCached() + public double RatioAcrossRunsCached() { return _cachedRatioScorer.Score("myawfullysimilarstirng"); } diff --git a/FuzzySharp.Benchmarks/TokenAbbreviationRatioBenchmarks.cs b/FuzzySharp.Benchmarks/TokenAbbreviationRatioBenchmarks.cs index 14e2632..e6e98c2 100644 --- a/FuzzySharp.Benchmarks/TokenAbbreviationRatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/TokenAbbreviationRatioBenchmarks.cs @@ -9,13 +9,13 @@ namespace Raffinert.FuzzySharp.Benchmarks; public class TokenAbbreviationRatioBenchmarks { [Benchmark] - public int TokenAbbreviationRatio() + public double TokenAbbreviationRatio() { return Fuzz.TokenAbbreviationRatio("bl 420", "Baseline section 420", StringPreprocessor.Full); } [Benchmark] - public int TokenAbbreviationRatioClassic() + public double TokenAbbreviationRatioClassic() { return Classic.Fuzz.TokenAbbreviationRatio("bl 420", "Baseline section 420", Classic.PreProcess.PreprocessMode.Full); } diff --git a/FuzzySharp.Benchmarks/TokenInitialismRatioBenchmarks.cs b/FuzzySharp.Benchmarks/TokenInitialismRatioBenchmarks.cs index fd4b84b..7907061 100644 --- a/FuzzySharp.Benchmarks/TokenInitialismRatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/TokenInitialismRatioBenchmarks.cs @@ -8,37 +8,37 @@ namespace Raffinert.FuzzySharp.Benchmarks; public class TokenInitialismRatioBenchmarks { [Benchmark] - public int TokenInitialismRatio1() + public double TokenInitialismRatio1() { return Fuzz.TokenInitialismRatio("NASA", "National Aeronautics and Space Administration"); } [Benchmark] - public int TokenInitialismRatio1Classic() + public double TokenInitialismRatio1Classic() { return Classic.Fuzz.TokenInitialismRatio("NASA", "National Aeronautics and Space Administration"); } [Benchmark] - public int TokenInitialismRatio2() + public double TokenInitialismRatio2() { return Fuzz.TokenInitialismRatio("NASA", "National Aeronautics Space Administration"); } [Benchmark] - public int TokenInitialismRatio2Classic() + public double TokenInitialismRatio2Classic() { return Classic.Fuzz.TokenInitialismRatio("NASA", "National Aeronautics Space Administration"); } [Benchmark] - public int TokenInitialismRatio3() + public double TokenInitialismRatio3() { return Fuzz.TokenInitialismRatio("NASA", "National Aeronautics Space Administration, Kennedy Space Center, Cape Canaveral, Florida 32899"); } [Benchmark] - public int TokenInitialismRatio3Classic() + public double TokenInitialismRatio3Classic() { return Classic.Fuzz.TokenInitialismRatio("NASA", "National Aeronautics Space Administration, Kennedy Space Center, Cape Canaveral, Florida 32899"); } diff --git a/FuzzySharp.Benchmarks/TokenSetRatioBenchmarks.cs b/FuzzySharp.Benchmarks/TokenSetRatioBenchmarks.cs index cd012f5..54f4531 100644 --- a/FuzzySharp.Benchmarks/TokenSetRatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/TokenSetRatioBenchmarks.cs @@ -18,25 +18,25 @@ public void GlobalSetup() } [Benchmark] - public int TokenSetRatio() + public double TokenSetRatio() { return Fuzz.TokenSetRatio("fuzzy was a bear", "fuzzy fuzzy fuzzy bear"); } [Benchmark] - public int TokenSetRatioClassic() + public double TokenSetRatioClassic() { return Classic.Fuzz.TokenSetRatio("fuzzy was a bear", "fuzzy fuzzy fuzzy bear"); } [Benchmark] - public int TokenSetRatioCached() + public double TokenSetRatioCached() { return new CachedTokenSetScorer("fuzzy was a bear").Score("fuzzy fuzzy fuzzy bear"); } [Benchmark] - public int TokenSetRatioAcrossRunsCached() + public double TokenSetRatioAcrossRunsCached() { return _cachedTokenSetScorer.Score("fuzzy fuzzy fuzzy bear"); } diff --git a/FuzzySharp.Benchmarks/TokenSortRatioBenchmarks.cs b/FuzzySharp.Benchmarks/TokenSortRatioBenchmarks.cs index 7b878f0..0c4ce2e 100644 --- a/FuzzySharp.Benchmarks/TokenSortRatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/TokenSortRatioBenchmarks.cs @@ -18,25 +18,25 @@ public void GlobalSetup() } [Benchmark] - public int TokenSortRatio() + public double TokenSortRatio() { return Fuzz.TokenSortRatio("order words out of", " words out of order"); } [Benchmark] - public int TokenSortRatioClassic() + public double TokenSortRatioClassic() { return Classic.Fuzz.TokenSortRatio("order words out of", " words out of order"); } [Benchmark] - public int TokenSortRatioCached() + public double TokenSortRatioCached() { return new CachedTokenSortScorer("order words out of").Score(" words out of order"); } [Benchmark] - public int TokenSortRatioAcrossRunsCached() + public double TokenSortRatioAcrossRunsCached() { return _cachedTokenSortScorer.Score(" words out of order"); } diff --git a/FuzzySharp.Benchmarks/WeightedRatioBenchmarks.cs b/FuzzySharp.Benchmarks/WeightedRatioBenchmarks.cs index d5c64f5..c5d6876 100644 --- a/FuzzySharp.Benchmarks/WeightedRatioBenchmarks.cs +++ b/FuzzySharp.Benchmarks/WeightedRatioBenchmarks.cs @@ -18,25 +18,25 @@ public void GlobalSetup() } [Benchmark] - public int WeightedRatio() + public double WeightedRatio() { return Fuzz.WeightedRatio("The quick brown fox jimps ofver the small lazy dog", "the quick brown fox jumps over the small lazy dog"); } [Benchmark] - public int WeightedRatioClassic() + public double WeightedRatioClassic() { return Classic.Fuzz.WeightedRatio("The quick brown fox jimps ofver the small lazy dog", "the quick brown fox jumps over the small lazy dog"); } [Benchmark] - public int WeightedRatioCached() + public double WeightedRatioCached() { return new CachedWeightedRatioScorer("The quick brown fox jimps ofver the small lazy dog").Score("the quick brown fox jumps over the small lazy dog"); } [Benchmark] - public int WeightedRatioAcrossRunsCached() + public double WeightedRatioAcrossRunsCached() { return _cachedWeightedScorer.Score("the quick brown fox jumps over the small lazy dog"); } diff --git a/FuzzySharp.Test/FuzzyTests/ExtractorSelectionTests.cs b/FuzzySharp.Test/FuzzyTests/ExtractorSelectionTests.cs index 6edc537..c38e8b3 100644 --- a/FuzzySharp.Test/FuzzyTests/ExtractorSelectionTests.cs +++ b/FuzzySharp.Test/FuzzyTests/ExtractorSelectionTests.cs @@ -43,6 +43,21 @@ public void ExtractOne_SelectsBest_UsesCutoffInclusively_AndKeepsOriginalIndex() AssertResult(result, "winner", 80, 1); } + [Fact] + public void ExtractOne_PreservesFractionalScoresAndCutoff() + { + var scorer = new MapScorer(("below", 80.24), ("winner", 80.26)); + + var result = Process.ExtractOne( + "query", + ["below", "winner"], + IdentityProcessor, + scorer, + cutoff: 80.25); + + AssertResult(result, "winner", 80.26, 1); + } + [Fact] public void ExtractOne_GenericValueChoices_WithStringQuery_UsesExtractor() { @@ -286,7 +301,7 @@ private static void AssertSameResults(IEnumerable> expecte } } - private static void AssertResult(ExtractedResult result, string value, int score, int index) + private static void AssertResult(ExtractedResult result, string value, double score, int index) { Assert.Equal(value, result.Value); Assert.Equal(score, result.Score); @@ -305,7 +320,7 @@ public Choice(string name) public sealed class MapScorer : IRatioScorer { - private readonly Dictionary _scores; + private readonly Dictionary _scores; private int _scoreCalls; public MapScorer() @@ -313,20 +328,20 @@ public MapScorer() { } - public MapScorer(params (string Value, int Score)[] scores) + public MapScorer(params (string Value, double Score)[] scores) { _scores = scores.ToDictionary(score => score.Value, score => score.Score); } public int ScoreCalls => _scoreCalls; - public int Score(string input1, string input2) + public double Score(string input1, string input2) { Interlocked.Increment(ref _scoreCalls); return _scores.TryGetValue(input2, out var score) ? score : 0; } - public int Score(string input1, string input2, Func preprocessor) + public double Score(string input1, string input2, Func preprocessor) { return Score(preprocessor(input1), preprocessor(input2)); } @@ -334,14 +349,14 @@ public int Score(string input1, string input2, Func preprocessor private sealed class CachedMapScorer : ICachedRatioScorer { - private readonly Dictionary _scores; + private readonly Dictionary _scores; - public CachedMapScorer(params (string Value, int Score)[] scores) + public CachedMapScorer(params (string Value, double Score)[] scores) { _scores = scores.ToDictionary(score => score.Value, score => score.Score); } - public int Score(string input2) + public double Score(string input2) { return _scores.TryGetValue(input2, out var score) ? score : 0; } diff --git a/FuzzySharp.Test/FuzzyTests/ProcessTests.cs b/FuzzySharp.Test/FuzzyTests/ProcessTests.cs index 23ecb70..c949c23 100644 --- a/FuzzySharp.Test/FuzzyTests/ProcessTests.cs +++ b/FuzzySharp.Test/FuzzyTests/ProcessTests.cs @@ -8,50 +8,20 @@ namespace Raffinert.FuzzySharp.Test.FuzzyTests; public class ProcessTests { - private string _s1; - private string _s1A; - private string _s2; - private string _s3; - private string _s4; - private string _s5; - private string _s6; - private string[] _cirqueStrings; - private string[] _baseballStrings; - - public ProcessTests() - { - _s1 = "new york mets"; - _s1A = "new york mets"; - _s2 = "new YORK mets"; - _s3 = "the wonderful new york mets"; - _s4 = "new york mets vs atlanta braves"; - _s5 = "atlanta braves vs new york mets"; - _s6 = "new york mets - atlanta braves"; - _cirqueStrings = new[] - { - "cirque du soleil - zarkana - las vegas", - "cirque du soleil ", - "cirque du soleil las vegas", - "zarkana las vegas", - "las vegas cirque du soleil at the bellagio", - "zarakana - cirque du soleil - bellagio" - }; - - _baseballStrings = new[] - { - "new york mets vs chicago cubs", - "chicago cubs vs chicago white sox", - "philladelphia phillies vs atlanta braves", - "braves vs mets", - }; - } + private readonly string[] _baseballStrings = + [ + "new york mets vs chicago cubs", + "chicago cubs vs chicago white sox", + "philladelphia phillies vs atlanta braves", + "braves vs mets" + ]; [Fact] public void TestGetBestChoice1() { var query = "new york mets at atlanta braves"; var best = Process.ExtractOne(query, _baseballStrings); - Assert.Equal(best.Value, "braves vs mets"); + Assert.Equal("braves vs mets", best.Value); } @@ -199,69 +169,4 @@ public void TestEmptyStrings() var best = Process.ExtractOne(query, choices); Assert.Equal(best.Value, choices[1]); } - -//[Fact] -//public void generate_choices() { -// choices = ['a', 'Bb', 'CcC'] -// for choice in choices { -// yield choice -// search = 'aaa' -// result = [(value, confidence) for value, confidence in -// Process.Extract(search, generate_choices())] -// .assertTrue(len(result) > 0) - -// } - -//[Fact] -//public void test_dict_like_Extract() { -// """We should be able to use a dict-like object for choices, not only a -// dict, and still get dict-like output. -// """ -// try { -// from UserDict import UserDict -// except ImportError { -// from collections import UserDict -// choices = UserDict({ 'aa' { 'bb', 'a1' { None}) -// search = 'aaa' -// result = Process.Extract(search, choices) -// .assertTrue(len(result) > 0) -// for value, confidence, key in result { -// .assertTrue(value in choices.values()) - -// } - -//[Fact] -//public void test_dedupe() { -// """We should be able to use a list-like object for contains_dupes -// """ -// // Test 1 -// contains_dupes = ['Frodo Baggins', 'Tom Sawyer', 'Bilbo Baggin', 'Samuel L. Jackson', 'F. Baggins', 'Frody Baggins', 'Bilbo Baggins'] - -// result = Process.dedupe(contains_dupes) -// .assertTrue(len(result) < len(contains_dupes)) - -// // Test 2 -// contains_dupes = ['Tom', 'Dick', 'Harry'] - -//// we should end up with the same list since no duplicates are contained in the list (e.g. original list is returned) -// deduped_list = ['Tom', 'Dick', 'Harry'] - -// result = Process.dedupe(contains_dupes) -// Assert.Equal(result, deduped_list) - -// } - -//[Fact] -//public void test_simplematch() { -// basic_string = 'a, b' -// match_strings = ['a, b'] - -// result = Process.ExtractOne(basic_string, match_strings, scorer=fuzz.ratio) -// part_result = Process.ExtractOne(basic_string, match_strings, scorer=fuzz.partial_ratio) - -// Assert.Equal(result, ('a, b', 100)) -// Assert.Equal(part_result, ('a, b', 100)) - -// } } - diff --git a/FuzzySharp.Test/FuzzyTests/RatioIssuesTests.cs b/FuzzySharp.Test/FuzzyTests/RatioIssuesTests.cs index c5c4614..09c881e 100644 --- a/FuzzySharp.Test/FuzzyTests/RatioIssuesTests.cs +++ b/FuzzySharp.Test/FuzzyTests/RatioIssuesTests.cs @@ -10,14 +10,14 @@ public class RatioIssuesTests [Fact] public void Issue76() { - Assert.Equal(82, Fuzz.PartialRatio("physics 2 vid", "study physics physics 2")); + Assert.Equal(81.81818181818181, Fuzz.PartialRatio("physics 2 vid", "study physics physics 2"), 10); Assert.Equal(100, Fuzz.PartialRatio("physics 2 vid", "study physics physics 2 video")); } [Fact] public void Issue90() { - Assert.Equal(86, Fuzz.PartialRatio("ax b", "a b a c b")); + Assert.Equal(85.71428571428571, Fuzz.PartialRatio("ax b", "a b a c b"), 10); } [Fact] @@ -25,7 +25,7 @@ public void Issue138() { var str1 = new string('a', 65); var str2 = "a" + (char)256 + new string('a', 63); - Assert.Equal(99, Fuzz.PartialRatio(str1, str2)); + Assert.Equal(99.22480620155039, Fuzz.PartialRatio(str1, str2), 10); } [Fact] @@ -51,13 +51,12 @@ public void PartialRatioAlignment() Assert.Equal(0, PartialRatioStrategy.PartialRatioAlignment(null, "test".AsSpan()).Score); Assert.Equal(0, PartialRatioStrategy.PartialRatioAlignment("test".AsSpan(), null).Score); - Assert.Equal(0, PartialRatioStrategy.PartialRatioAlignment("test".AsSpan(), "tesx".AsSpan(), scoreCutoff: 90).Score); } [Fact] public void Issue196() { - Assert.Equal(82, Fuzz.WeightedRatio("South Korea", "North Korea")); + Assert.Equal(81.81818181818181, Fuzz.WeightedRatio("South Korea", "North Korea"), 10); } [Fact] @@ -68,7 +67,6 @@ public void Issue231() var alignment = PartialRatioStrategy.PartialRatioAlignment(str1.AsSpan(), str2.AsSpan()); - Assert.NotNull(alignment); Assert.Equal(0, alignment.SrcStart); Assert.Equal(103, alignment.SrcEnd); Assert.Equal(0, alignment.DestStart); diff --git a/FuzzySharp.Test/FuzzyTests/RatioTests.cs b/FuzzySharp.Test/FuzzyTests/RatioTests.cs index ee02f3b..1d947a1 100644 --- a/FuzzySharp.Test/FuzzyTests/RatioTests.cs +++ b/FuzzySharp.Test/FuzzyTests/RatioTests.cs @@ -5,131 +5,109 @@ namespace Raffinert.FuzzySharp.Test.FuzzyTests; public class RatioTests { - #region Private Fields - private string _s1, - _s1A, - _s2, - _s3, - _s4, - _s5, - _s6, - _s7, - _s8, - _s8A, - _s9, - _s9A, - _s10, - _s10A; - - private string[] _cirqueStrings, _baseballStrings; - #endregion - - public RatioTests() - { - _s1 = "new york mets"; - _s1A = "new york mets"; - _s2 = "new YORK mets"; - _s3 = "the wonderful new york mets"; - _s4 = "new york mets vs atlanta braves"; - _s5 = "atlanta braves vs new york mets"; - _s6 = "new york mets - atlanta braves"; - _s7 = "new york city mets - atlanta braves"; - // Edge cases - _s8 = "{"; - _s8A = "{"; - _s9 = "{a"; - _s9A = "{a"; - _s10 = "a{"; - _s10A = "{b"; - } + private const string S1 = "new york mets", + S1A = "new york mets", + S2 = "new YORK mets", + S3 = "the wonderful new york mets", + S4 = "new york mets vs atlanta braves", + S5 = "atlanta braves vs new york mets", + S7 = "new york city mets - atlanta braves", + S8 = "{", + S8A = "{", + S9 = "{a", + S9A = "{a", + S10 = "a{", + S10A = "{b"; + + // Edge cases [Fact] public void Test_Equal() { - Assert.Equal(Fuzz.Ratio(_s1, _s1A), 100); - Assert.Equal(Fuzz.Ratio(_s8, _s8A), 100); - Assert.Equal(Fuzz.Ratio(_s9, _s9A), 100); + Assert.Equal(100, Fuzz.Ratio(S1, S1A)); + Assert.Equal(100, Fuzz.Ratio(S8, S8A)); + Assert.Equal(100, Fuzz.Ratio(S9, S9A)); } [Fact] public void Test_Case_Insensitive() { - Assert.NotEqual(Fuzz.Ratio(_s1, _s2), 100); - Assert.Equal(Fuzz.Ratio(_s1, _s2, StringPreprocessor.Full), 100); + Assert.NotEqual(100, Fuzz.Ratio(S1, S2)); + Assert.Equal(100, Fuzz.Ratio(S1, S2, StringPreprocessor.Full)); } [Fact] public void Test_Partial() { - Assert.Equal(Fuzz.PartialRatio(_s1, _s3), 100); + Assert.Equal(100, Fuzz.PartialRatio(S1, S3)); } [Fact] public void TestTokenSortRatio() { - Assert.Equal(Fuzz.TokenSortRatio(_s1, _s1A), 100); + Assert.Equal(100, Fuzz.TokenSortRatio(S1, S1A)); } [Fact] public void TestPartialTokenSortRatio() { - Assert.Equal(Fuzz.PartialTokenSortRatio(_s1, _s1A, StringPreprocessor.Full), 100); - Assert.Equal(Fuzz.PartialTokenSortRatio(_s4, _s5, StringPreprocessor.Full), 100); - Assert.Equal(Fuzz.PartialTokenSortRatio(_s8, _s8A), 100); - Assert.Equal(Fuzz.PartialTokenSortRatio(_s9, _s9A, StringPreprocessor.Full), 100); - Assert.Equal(Fuzz.PartialTokenSortRatio(_s9, _s9A), 100); + Assert.Equal(100, Fuzz.PartialTokenSortRatio(S1, S1A, StringPreprocessor.Full)); + Assert.Equal(100, Fuzz.PartialTokenSortRatio(S4, S5, StringPreprocessor.Full)); + Assert.Equal(100, Fuzz.PartialTokenSortRatio(S8, S8A)); + Assert.Equal(100, Fuzz.PartialTokenSortRatio(S9, S9A, StringPreprocessor.Full)); + Assert.Equal(100, Fuzz.PartialTokenSortRatio(S9, S9A)); //var al = Fuzz1.PartialRatioAlignment("a certain string".AsSpan(), "cetain".AsSpan()); - Assert.Equal(Fuzz.PartialTokenSortRatio(_s10, _s10A), 67); - Assert.Equal(Fuzz.PartialTokenSortRatio(_s10, _s10A, StringPreprocessor.Full), 0); + Assert.Equal(66.66666666666667, Fuzz.PartialTokenSortRatio(S10, S10A), 10); + Assert.Equal(0, Fuzz.PartialTokenSortRatio(S10, S10A, StringPreprocessor.Full)); } [Fact] public void TestTokenSetRatio() { - Assert.Equal(Fuzz.TokenSetRatio(_s4, _s5, StringPreprocessor.Full), 100); - Assert.Equal(Fuzz.TokenSetRatio(_s8, _s8A), 100); - Assert.Equal(Fuzz.TokenSetRatio(_s9, _s9A, StringPreprocessor.Full), 100); - Assert.Equal(Fuzz.TokenSetRatio(_s9, _s9A), 100); - Assert.Equal(Fuzz.TokenSetRatio(_s10, _s10A), 50); + Assert.Equal(100, Fuzz.TokenSetRatio(S4, S5, StringPreprocessor.Full)); + Assert.Equal(100, Fuzz.TokenSetRatio(S8, S8A)); + Assert.Equal(100, Fuzz.TokenSetRatio(S9, S9A, StringPreprocessor.Full)); + Assert.Equal(100, Fuzz.TokenSetRatio(S9, S9A)); + Assert.Equal(50, Fuzz.TokenSetRatio(S10, S10A)); } [Fact] public void TestTokenAbbreviationRatio() { - Assert.Equal(Fuzz.TokenAbbreviationRatio("bl 420", "Baseline section 420", StringPreprocessor.Full), 40); - Assert.Equal(Fuzz.PartialTokenAbbreviationRatio("bl 420", "Baseline section 420", StringPreprocessor.Full), 67); + Assert.Equal(40, Fuzz.TokenAbbreviationRatio("bl 420", "Baseline section 420", StringPreprocessor.Full)); + Assert.Equal(66.66666666666667, Fuzz.PartialTokenAbbreviationRatio("bl 420", "Baseline section 420", StringPreprocessor.Full), 10); } [Fact] public void TestPartialTokenSetRatio() { - Assert.Equal(Fuzz.PartialTokenSetRatio(_s4, _s7), 100); + Assert.Equal(100, Fuzz.PartialTokenSetRatio(S4, S7)); } [Fact] public void TestWeightedRatioEqual() { - Assert.Equal(Fuzz.WeightedRatio(_s1, _s1A), 100); + Assert.Equal(100, Fuzz.WeightedRatio(S1, S1A)); } [Fact] public void TestWeightedRatioCaseInsensitive() { - Assert.Equal(Fuzz.WeightedRatio(_s1, _s2, StringPreprocessor.Full), 100); + Assert.Equal(100, Fuzz.WeightedRatio(S1, S2, StringPreprocessor.Full)); } [Fact] public void TestWeightedRatioPartialMatch() { - Assert.Equal(Fuzz.WeightedRatio(_s1, _s3), 90); + Assert.Equal(90, Fuzz.WeightedRatio(S1, S3)); } [Fact] public void TestWeightedRatioMisorderedMatch() { - Assert.Equal(Fuzz.WeightedRatio(_s4, _s5), 95); + Assert.Equal(95, Fuzz.WeightedRatio(S4, S5)); } [Fact] @@ -144,14 +122,14 @@ public void TestEmptyStringsScore0() [Fact] public void TestIssueSeven() { - _s1 = "HSINCHUANG"; - _s2 = "SINJHUAN"; - _s3 = "LSINJHUANG DISTRIC"; - _s4 = "SINJHUANG DISTRICT"; + const string s1 = "HSINCHUANG"; + const string s2 = "SINJHUAN"; + const string s3 = "LSINJHUANG DISTRIC"; + const string s4 = "SINJHUANG DISTRICT"; - Assert.True(Fuzz.PartialRatio(_s1, _s2) > 75); - Assert.True(Fuzz.PartialRatio(_s1, _s3) > 75); - Assert.True(Fuzz.PartialRatio(_s1, _s4) > 75); + Assert.True(Fuzz.PartialRatio(s1, s2) > 75); + Assert.True(Fuzz.PartialRatio(s1, s3) > 75); + Assert.True(Fuzz.PartialRatio(s1, s4) > 75); } [Fact] @@ -176,53 +154,57 @@ public void TestIssueEight() public void MorePartialRatio() { Assert.Equal(100, Fuzz.PartialRatio("geeks for geeks", "geeks for geeks!")); - Assert.Equal(71, Fuzz.PartialRatio("geeks for geeks", "geeks geeks")); + Assert.Equal(70.58823529411765, Fuzz.PartialRatio("geeks for geeks", "geeks geeks"), 10); Assert.Equal(100, Fuzz.TokenSortRatio("geeks for geeks", "for geeks geeks")); } [Fact] public void TestPartialRatioUnicodeString() { - _s1 = "\u00C1"; - _s2 = "ABCD"; - var score = Fuzz.PartialRatio(_s1, _s2); + const string s1 = "\u00C1"; + const string s2 = "ABCD"; + var score = Fuzz.PartialRatio(s1, s2); Assert.Equal(0, score); } [Fact] public void TestZeroRatio() { - var ratio = Fuzz.PartialTokenSortRatio("abc", "def"); + const string s1 = "abc"; + const string s2 = "def"; + var ratio = Fuzz.PartialTokenSortRatio(s1, s2); - Assert.True(ratio == 0); + Assert.Equal(0, ratio); } [Fact] public void Test03() { - var ratio = Fuzz.PartialTokenSortRatio("new york mets", "atlanta braves vs new york mets"); + const string s1 = "new york mets"; + const string s2 = "atlanta braves vs new york mets"; + var ratio = Fuzz.PartialTokenSortRatio(s1, s2); - Assert.True(ratio == 77); + Assert.Equal(76.92307692307692, ratio, 10); } [Fact] public void TestRatioUnicodeString() { - _s1 = "\u00C1"; - _s2 = "ABCD"; - var score = Fuzz.WeightedRatio(_s1, _s2); + const string s1 = "\u00C1"; + const string s2 = "ABCD"; + var score = Fuzz.WeightedRatio(s1, s2); Assert.Equal(0, score); // Cyrillic. - _s1 = "\u043f\u0441\u0438\u0445\u043e\u043b\u043e\u0433"; - _s2 = "\u043f\u0441\u0438\u0445\u043e\u0442\u0435\u0440\u0430\u043f\u0435\u0432\u0442"; - score = Fuzz.WeightedRatio(_s1, _s2); + const string s3 = "\u043f\u0441\u0438\u0445\u043e\u043b\u043e\u0433"; + const string s4 = "\u043f\u0441\u0438\u0445\u043e\u0442\u0435\u0440\u0430\u043f\u0435\u0432\u0442"; + score = Fuzz.WeightedRatio(s3, s4); Assert.NotEqual(0, score); // Chinese. - _s1 = "\u6211\u4e86\u89e3\u6570\u5b66"; - _s2 = "\u6211\u5b66\u6570\u5b66"; - score = Fuzz.WeightedRatio(_s1, _s2); + const string s5 = "\u6211\u4e86\u89e3\u6570\u5b66"; + const string s6 = "\u6211\u5b66\u6570\u5b66"; + score = Fuzz.WeightedRatio(s5, s6); Assert.NotEqual(0, score); } } diff --git a/FuzzySharp.Test/FuzzyTests/RegressionTests.cs b/FuzzySharp.Test/FuzzyTests/RegressionTests.cs index 4988468..79e8019 100644 --- a/FuzzySharp.Test/FuzzyTests/RegressionTests.cs +++ b/FuzzySharp.Test/FuzzyTests/RegressionTests.cs @@ -28,13 +28,12 @@ public void TestScoringEmptyString() return []; }).ToList(); var scorerTypes = types.Where(t => scorerType.IsAssignableFrom(t) && !t.IsAbstract && t.IsClass).ToList(); - - string nullString = null; //Null doesn't seem to be handled by any scorer + string emptyString = ""; string whitespaceString = " "; string[] nullOrWhitespaceStrings = [emptyString, whitespaceString]; - MethodInfo getScorerCacheMethodInfo = typeof(ScorerCache).GetMethod("Get"); + MethodInfo getScorerCacheMethodInfo = typeof(ScorerCache).GetMethod("Get")!; foreach (var t in scorerTypes) { @@ -42,21 +41,21 @@ public void TestScoringEmptyString() MethodInfo m = getScorerCacheMethodInfo.MakeGenericMethod(t); IRatioScorer scorer = m.Invoke(this, []) as IRatioScorer; - foreach(string s in nullOrWhitespaceStrings) + foreach(var s in nullOrWhitespaceStrings) { System.Diagnostics.Debug.WriteLine($"Testing string '{s}'"); try { scorer.Score(s, "TEST"); } - catch (InvalidOperationException e) + catch (InvalidOperationException) { Assert.Fail($"{t.Name}.score failed with empty string as first parameter"); } try { scorer.Score("TEST", s); - } catch (InvalidOperationException e) + } catch (InvalidOperationException) { Assert.Fail($"{t.Name}.score failed with empty string as second parameter"); } @@ -64,15 +63,11 @@ public void TestScoringEmptyString() { scorer.Score(s, s); } - catch (InvalidOperationException e) + catch (InvalidOperationException) { Assert.Fail($"{t.Name}.score failed with empty string as both parameters"); } - } - } - } - } diff --git a/FuzzySharp.Test/LevenshteinTests.cs b/FuzzySharp.Test/LevenshteinTests.cs index 0f4e39d..bd43d61 100644 --- a/FuzzySharp.Test/LevenshteinTests.cs +++ b/FuzzySharp.Test/LevenshteinTests.cs @@ -69,20 +69,6 @@ public void TestLevenshteinDistance_Weighted(string s1, string s2, int insertCos Assert.Equal(expected, distance); } - [Fact] - public void TestLevenshteinDistance_WeightedScoreCutoff() - { - var distance = Levenshtein.Distance("abc", "", insertCost: 2, deleteCost: 1, replaceCost: 3, scoreCutoff: 2); - Assert.Equal(3, distance); - } - - [Fact] - public void TestLevenshteinSimilarity_UsesSimilarityCutoff() - { - var similarity = Levenshtein.Similarity("kitten", "sitting", scoreCutoff: 5); - Assert.Equal(0, similarity); - } - [Fact] public void TestLevenshteinMaximum_MatchesExpectedFormula() { @@ -90,21 +76,5 @@ public void TestLevenshteinMaximum_MatchesExpectedFormula() Assert.Equal(18, maximum); } - [Fact] - public void TestLevenshteinDistance_CutoffDoesNotExitEarly_ForRecoverablePrefix_SingleUlong() - { - var distance = Levenshtein.Distance("abc", "xabc", scoreCutoff: 1); - Assert.Equal(1, distance); - } - - [Fact] - public void TestLevenshteinDistance_CutoffDoesNotExitEarly_ForRecoverablePrefix_MultiUlong() - { - var source = new string('a', 70); - var target = "x" + source; - - var distance = Levenshtein.Distance(source, target, scoreCutoff: 1); - Assert.Equal(1, distance); - } } diff --git a/FuzzySharp/CachedScorerProcessExecutor.cs b/FuzzySharp/CachedScorerProcessExecutor.cs index 1baff06..0af7921 100644 --- a/FuzzySharp/CachedScorerProcessExecutor.cs +++ b/FuzzySharp/CachedScorerProcessExecutor.cs @@ -18,7 +18,7 @@ public static IEnumerable> ExtractAll( Func extractor, Func processor, ICachedRatioScorer scorer, - int cutoff, + double cutoff, bool useParallel, ParallelOptions parallelOptions) { @@ -35,7 +35,7 @@ public static IEnumerable> ExtractAll( IEnumerable choices, Func processor, ICachedRatioScorer scorer, - int cutoff, + double cutoff, bool useParallel, ParallelOptions parallelOptions) { @@ -53,7 +53,7 @@ public static IEnumerable> ExtractTop( Func processor, ICachedRatioScorer scorer, int limit, - int cutoff, + double cutoff, bool useParallel, ParallelOptions parallelOptions) { @@ -71,7 +71,7 @@ public static IEnumerable> ExtractTop( Func processor, ICachedRatioScorer scorer, int limit, - int cutoff, + double cutoff, bool useParallel, ParallelOptions parallelOptions) { @@ -89,7 +89,7 @@ public static IEnumerable> ExtractSorted( Func extractor, Func processor, ICachedRatioScorer scorer, - int cutoff, + double cutoff, bool useParallel, ParallelOptions parallelOptions) { @@ -106,7 +106,7 @@ public static IEnumerable> ExtractSorted( IEnumerable choices, Func processor, ICachedRatioScorer scorer, - int cutoff, + double cutoff, bool useParallel, ParallelOptions parallelOptions) { @@ -123,7 +123,7 @@ public static ExtractedResult ExtractOne( Func extractor, Func processor, ICachedRatioScorer scorer, - int cutoff, + double cutoff, bool useParallel, ParallelOptions parallelOptions) { @@ -140,7 +140,7 @@ public static ExtractedResult ExtractOne( IEnumerable choices, Func processor, ICachedRatioScorer scorer, - int cutoff, + double cutoff, bool useParallel, ParallelOptions parallelOptions) { diff --git a/FuzzySharp/Delegates.cs b/FuzzySharp/Delegates.cs index b6bf253..05dc53d 100644 --- a/FuzzySharp/Delegates.cs +++ b/FuzzySharp/Delegates.cs @@ -2,6 +2,6 @@ namespace Raffinert.FuzzySharp; -public delegate int Scorer(string input1, string input2); -public delegate int CachedScorer(string input2); -public delegate void Processor(ref ReadOnlySpan str) where T : IEquatable; \ No newline at end of file +public delegate double Scorer(string input1, string input2); +public delegate double CachedScorer(string input2); +public delegate void Processor(ref ReadOnlySpan str) where T : IEquatable; diff --git a/FuzzySharp/Extractor/ExtractedResult.cs b/FuzzySharp/Extractor/ExtractedResult.cs index 97b60e9..e212775 100644 --- a/FuzzySharp/Extractor/ExtractedResult.cs +++ b/FuzzySharp/Extractor/ExtractedResult.cs @@ -3,18 +3,18 @@ namespace Raffinert.FuzzySharp.Extractor; -public class ExtractedResult(T value, int score, int index) : IComparable> +public class ExtractedResult(T value, double score, int index) : IComparable> { public readonly T Value = value; - public readonly int Score = score; + public readonly double Score = score; public readonly int Index = index; - public ExtractedResult(T value, int score) : this(value, score, 0) + public ExtractedResult(T value, double score) : this(value, score, 0) { } public int CompareTo(ExtractedResult other) { - return Comparer.Default.Compare(this.Score, other.Score); + return Comparer.Default.Compare(this.Score, other.Score); } public override string ToString() @@ -25,4 +25,4 @@ public override string ToString() } return $"(value: {Value}, score: {Score}, index: {Index})"; } -} \ No newline at end of file +} diff --git a/FuzzySharp/Extractor/ResultExtractor.Cached.cs b/FuzzySharp/Extractor/ResultExtractor.Cached.cs index 027373f..62a6441 100644 --- a/FuzzySharp/Extractor/ResultExtractor.Cached.cs +++ b/FuzzySharp/Extractor/ResultExtractor.Cached.cs @@ -9,13 +9,13 @@ public static partial class ResultExtractor { public static class Cached { - public static IEnumerable> ExtractWithoutOrder(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer scorer, int cutoff = 0) + public static IEnumerable> ExtractWithoutOrder(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer scorer, double cutoff = 0) { int index = 0; foreach (var choice in choices) { - int score = scorer.Score(processor(extractor(choice))); + double score = scorer.Score(processor(extractor(choice))); if (score >= cutoff) { yield return new ExtractedResult(choice, score, index); @@ -24,13 +24,13 @@ public static IEnumerable> ExtractWithoutOrder(IEnumerable } } - public static IEnumerable> ExtractWithoutOrder(IEnumerable choices, Func processor, ICachedRatioScorer scorer, int cutoff = 0) + public static IEnumerable> ExtractWithoutOrder(IEnumerable choices, Func processor, ICachedRatioScorer scorer, double cutoff = 0) { int index = 0; foreach (var choice in choices) { - int score = scorer.Score(processor(choice)); + double score = scorer.Score(processor(choice)); if (score >= cutoff) { yield return new ExtractedResult(choice, score, index); @@ -39,32 +39,32 @@ public static IEnumerable> ExtractWithoutOrder(IEnumerab } } - public static ExtractedResult ExtractOne(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer scorer, int cutoff = 0) + public static ExtractedResult ExtractOne(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer scorer, double cutoff = 0) { return ExtractOneCore(choices, choice => scorer.Score(processor(extractor(choice))), cutoff); } - public static ExtractedResult ExtractOne(IEnumerable choices, Func processor, ICachedRatioScorer scorer, int cutoff = 0) + public static ExtractedResult ExtractOne(IEnumerable choices, Func processor, ICachedRatioScorer scorer, double cutoff = 0) { return ExtractOneCore(choices, choice => scorer.Score(processor(choice)), cutoff); } - public static IEnumerable> ExtractSorted(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer scorer, int cutoff = 0) + public static IEnumerable> ExtractSorted(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer scorer, double cutoff = 0) { return ExtractWithoutOrder(choices, extractor, processor, scorer, cutoff).OrderByDescending(r => r.Score); } - public static IEnumerable> ExtractSorted(IEnumerable choices, Func processor, ICachedRatioScorer scorer, int cutoff = 0) + public static IEnumerable> ExtractSorted(IEnumerable choices, Func processor, ICachedRatioScorer scorer, double cutoff = 0) { return ExtractWithoutOrder(choices, processor, scorer, cutoff).OrderByDescending(r => r.Score); } - public static IEnumerable> ExtractTop(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer scorer, int limit, int cutoff = 0) + public static IEnumerable> ExtractTop(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer scorer, int limit, double cutoff = 0) { return ExtractTopCore(choices, choice => scorer.Score(processor(extractor(choice))), limit, cutoff); } - public static IEnumerable> ExtractTop(IEnumerable choices, Func processor, ICachedRatioScorer scorer, int limit, int cutoff = 0) + public static IEnumerable> ExtractTop(IEnumerable choices, Func processor, ICachedRatioScorer scorer, int limit, double cutoff = 0) { return ExtractTopCore(choices, choice => scorer.Score(processor(choice)), limit, cutoff); } diff --git a/FuzzySharp/Extractor/ResultExtractor.Parallel.Cached.cs b/FuzzySharp/Extractor/ResultExtractor.Parallel.Cached.cs index 1b6c838..0b04455 100644 --- a/FuzzySharp/Extractor/ResultExtractor.Parallel.Cached.cs +++ b/FuzzySharp/Extractor/ResultExtractor.Parallel.Cached.cs @@ -12,14 +12,14 @@ public static partial class Parallel { public static class Cached { - public static IEnumerable> ExtractWithoutOrder(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer scorer, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractWithoutOrder(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer scorer, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var result = new ExtractedResult[materializedChoices.Count]; System.Threading.Tasks.Parallel.ForEach(materializedChoices, parallelOptions ?? DefaultParallelOptions, (choice, _, index) => { - int score = scorer.Score(processor(extractor(choice))); + double score = scorer.Score(processor(extractor(choice))); if (score >= cutoff) { result[index] = new ExtractedResult(choice, score, (int)index); @@ -28,14 +28,14 @@ public static IEnumerable> ExtractWithoutOrder(IEnumerable return result.Where(r => r != null); } - public static IEnumerable> ExtractWithoutOrder(IEnumerable choices, Func processor, ICachedRatioScorer scorer, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractWithoutOrder(IEnumerable choices, Func processor, ICachedRatioScorer scorer, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var result = new ExtractedResult[materializedChoices.Count]; System.Threading.Tasks.Parallel.ForEach(materializedChoices, parallelOptions ?? DefaultParallelOptions, (choice, _, index) => { - int score = scorer.Score(processor(choice)); + double score = scorer.Score(processor(choice)); if (score >= cutoff) { result[index] = new ExtractedResult(choice, score, (int)index); @@ -44,36 +44,36 @@ public static IEnumerable> ExtractWithoutOrder(IEnumerab return result.Where(r => r != null); } - public static ExtractedResult ExtractOne(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer calculator, int cutoff = 0, ParallelOptions parallelOptions = null) + public static ExtractedResult ExtractOne(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer calculator, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); return ExtractOneParallelCore(materializedChoices, choice => calculator.Score(processor(extractor(choice))), cutoff, parallelOptions); } - public static ExtractedResult ExtractOne(IEnumerable choices, Func processor, ICachedRatioScorer calculator, int cutoff = 0, ParallelOptions parallelOptions = null) + public static ExtractedResult ExtractOne(IEnumerable choices, Func processor, ICachedRatioScorer calculator, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); return ExtractOneParallelCore(materializedChoices, choice => calculator.Score(processor(choice)), cutoff, parallelOptions); } - public static IEnumerable> ExtractSorted(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer calculator, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractSorted(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer calculator, double cutoff = 0, ParallelOptions parallelOptions = null) { return ExtractWithoutOrder(choices, extractor, processor, calculator, cutoff, parallelOptions).OrderByDescending(r => r.Score); } - public static IEnumerable> ExtractSorted(IEnumerable choices, Func processor, ICachedRatioScorer calculator, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractSorted(IEnumerable choices, Func processor, ICachedRatioScorer calculator, double cutoff = 0, ParallelOptions parallelOptions = null) { return ExtractWithoutOrder(choices, processor, calculator, cutoff, parallelOptions).OrderByDescending(r => r.Score); } - public static IEnumerable> ExtractTop(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer calculator, int limit, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractTop(IEnumerable choices, Func extractor, Func processor, ICachedRatioScorer calculator, int limit, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var scores = ScoreParallel(materializedChoices, choice => calculator.Score(processor(extractor(choice))), parallelOptions); return ExtractTopParallelCore(materializedChoices, scores, limit, cutoff); } - public static IEnumerable> ExtractTop(IEnumerable choices, Func processor, ICachedRatioScorer calculator, int limit, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractTop(IEnumerable choices, Func processor, ICachedRatioScorer calculator, int limit, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var scores = ScoreParallel(materializedChoices, choice => calculator.Score(processor(choice)), parallelOptions); diff --git a/FuzzySharp/Extractor/ResultExtractor.Parallel.cs b/FuzzySharp/Extractor/ResultExtractor.Parallel.cs index df8a9ff..49893c1 100644 --- a/FuzzySharp/Extractor/ResultExtractor.Parallel.cs +++ b/FuzzySharp/Extractor/ResultExtractor.Parallel.cs @@ -15,8 +15,8 @@ public static partial class Parallel { private static ExtractedResult ExtractOneParallelCore( IList choices, - Func scoreSelector, - int cutoff, + Func scoreSelector, + double cutoff, ParallelOptions parallelOptions) { var sync = new object(); @@ -51,9 +51,9 @@ private static ExtractedResult ExtractOneParallelCore( private static IEnumerable> ExtractTopParallelCore( IList choices, - int[] scores, + double[] scores, int limit, - int cutoff) + double cutoff) { var heap = new MinHeap>(ScoredCandidateComparer.Instance); var comparer = ScoredCandidateComparer.Instance; @@ -73,12 +73,12 @@ private static IEnumerable> ExtractTopParallelCore( } } - private static int[] ScoreParallel( + private static double[] ScoreParallel( IList choices, - Func scoreSelector, + Func scoreSelector, ParallelOptions parallelOptions) { - var scores = new int[choices.Count]; + var scores = new double[choices.Count]; System.Threading.Tasks.Parallel.For(0, choices.Count, parallelOptions ?? DefaultParallelOptions, index => { @@ -88,7 +88,7 @@ private static int[] ScoreParallel( return scores; } - public static IEnumerable> ExtractWithoutOrder(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractWithoutOrder(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var result = new ExtractedResult[materializedChoices.Count]; @@ -96,7 +96,7 @@ public static IEnumerable> ExtractWithoutOrder(string quer System.Threading.Tasks.Parallel.ForEach(materializedChoices, parallelOptions ?? DefaultParallelOptions, (choice, _, index) => { - int score = scorer.Score(processedQuery, processor(extractor(choice))); + double score = scorer.Score(processedQuery, processor(extractor(choice))); if (score >= cutoff) { result[index] = new ExtractedResult(choice, score, (int)index); @@ -105,7 +105,7 @@ public static IEnumerable> ExtractWithoutOrder(string quer return result.Where(r => r != null); } - public static IEnumerable> ExtractWithoutOrder(string query, IEnumerable choices, Func processor, IRatioScorer scorer, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractWithoutOrder(string query, IEnumerable choices, Func processor, IRatioScorer scorer, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var result = new ExtractedResult[materializedChoices.Count]; @@ -113,7 +113,7 @@ public static IEnumerable> ExtractWithoutOrder(string qu System.Threading.Tasks.Parallel.ForEach(materializedChoices, parallelOptions ?? DefaultParallelOptions, (choice, _, index) => { - int score = scorer.Score(processedQuery, processor(choice)); + double score = scorer.Score(processedQuery, processor(choice)); if (score >= cutoff) { result[index] = new ExtractedResult(choice, score, (int)index); @@ -122,48 +122,48 @@ public static IEnumerable> ExtractWithoutOrder(string qu return result.Where(r => r != null); } - public static IEnumerable> ExtractWithoutOrder(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractWithoutOrder(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, double cutoff = 0, ParallelOptions parallelOptions = null) { return ExtractWithoutOrder(extractor(query), choices, extractor, processor, scorer, cutoff, parallelOptions); } - public static ExtractedResult ExtractOne(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, int cutoff = 0, ParallelOptions parallelOptions = null) + public static ExtractedResult ExtractOne(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var processedQuery = processor(extractor(query)); return ExtractOneParallelCore(materializedChoices, choice => calculator.Score(processedQuery, processor(extractor(choice))), cutoff, parallelOptions); } - public static ExtractedResult ExtractOne(string query, IEnumerable choices, Func processor, IRatioScorer calculator, int cutoff = 0, ParallelOptions parallelOptions = null) + public static ExtractedResult ExtractOne(string query, IEnumerable choices, Func processor, IRatioScorer calculator, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var processedQuery = processor(query); return ExtractOneParallelCore(materializedChoices, choice => calculator.Score(processedQuery, processor(choice)), cutoff, parallelOptions); } - public static ExtractedResult ExtractOne(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, int cutoff = 0, ParallelOptions parallelOptions = null) + public static ExtractedResult ExtractOne(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var processedQuery = processor(query); return ExtractOneParallelCore(materializedChoices, choice => calculator.Score(processedQuery, processor(extractor(choice))), cutoff, parallelOptions); } - public static IEnumerable> ExtractSorted(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractSorted(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, double cutoff = 0, ParallelOptions parallelOptions = null) { return ExtractWithoutOrder(query, choices, extractor, processor, calculator, cutoff, parallelOptions).OrderByDescending(r => r.Score); } - public static IEnumerable> ExtractSorted(string query, IEnumerable choices, Func processor, IRatioScorer calculator, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractSorted(string query, IEnumerable choices, Func processor, IRatioScorer calculator, double cutoff = 0, ParallelOptions parallelOptions = null) { return ExtractWithoutOrder(query, choices, processor, calculator, cutoff, parallelOptions).OrderByDescending(r => r.Score); } - public static IEnumerable> ExtractSorted(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractSorted(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, double cutoff = 0, ParallelOptions parallelOptions = null) { return ExtractWithoutOrder(query, choices, extractor, processor, calculator, cutoff, parallelOptions).OrderByDescending(r => r.Score); } - public static IEnumerable> ExtractTop(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, int limit, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractTop(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, int limit, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var processedQuery = processor(extractor(query)); @@ -171,7 +171,7 @@ public static IEnumerable> ExtractTop(T query, IEnumerable return ExtractTopParallelCore(materializedChoices, scores, limit, cutoff); } - public static IEnumerable> ExtractTop(string query, IEnumerable choices, Func processor, IRatioScorer calculator, int limit, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractTop(string query, IEnumerable choices, Func processor, IRatioScorer calculator, int limit, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var processedQuery = processor(query); @@ -179,7 +179,7 @@ public static IEnumerable> ExtractTop(string query, IEnu return ExtractTopParallelCore(materializedChoices, scores, limit, cutoff); } - public static IEnumerable> ExtractTop(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, int limit, int cutoff = 0, ParallelOptions parallelOptions = null) + public static IEnumerable> ExtractTop(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer calculator, int limit, double cutoff = 0, ParallelOptions parallelOptions = null) { var materializedChoices = choices.ToList(); var processedQuery = processor(query); diff --git a/FuzzySharp/Extractor/ResultExtractor.cs b/FuzzySharp/Extractor/ResultExtractor.cs index d4c8775..2e9ecbe 100644 --- a/FuzzySharp/Extractor/ResultExtractor.cs +++ b/FuzzySharp/Extractor/ResultExtractor.cs @@ -10,12 +10,12 @@ public static partial class ResultExtractor { private static ExtractedResult ExtractOneCore( IEnumerable choices, - Func scoreSelector, - int cutoff) + Func scoreSelector, + double cutoff) { var index = 0; var bestIndex = 0; - var bestScore = 0; + var bestScore = 0.0; T bestValue = default; var hasBest = false; @@ -43,9 +43,9 @@ private static ExtractedResult ExtractOneCore( private static IEnumerable> ExtractTopCore( IEnumerable choices, - Func scoreSelector, + Func scoreSelector, int limit, - int cutoff) + double cutoff) { var comparer = ScoredCandidateComparer.Instance; var heap = new MinHeap>(comparer); @@ -98,7 +98,7 @@ private static IEnumerable> CreateTopResults(MinHeap> ExtractWithoutOrder(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int cutoff = 0) + public static IEnumerable> ExtractWithoutOrder(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, double cutoff = 0) { int index = 0; processor ??= Process.DefaultStringProcessor; @@ -106,7 +106,7 @@ public static IEnumerable> ExtractWithoutOrder(string quer foreach (var choice in choices) { - int score = scorer.Score(processedQuery, processor(extractor(choice))); + double score = scorer.Score(processedQuery, processor(extractor(choice))); if (score >= cutoff) { yield return new ExtractedResult(choice, score, index); @@ -115,7 +115,7 @@ public static IEnumerable> ExtractWithoutOrder(string quer } } - public static IEnumerable> ExtractWithoutOrder(string query, IEnumerable choices, Func processor, IRatioScorer scorer, int cutoff = 0) + public static IEnumerable> ExtractWithoutOrder(string query, IEnumerable choices, Func processor, IRatioScorer scorer, double cutoff = 0) { int index = 0; processor ??= Process.DefaultStringProcessor; @@ -123,7 +123,7 @@ public static IEnumerable> ExtractWithoutOrder(string qu foreach (var choice in choices) { - int score = scorer.Score(processedQuery, processor(choice)); + double score = scorer.Score(processedQuery, processor(choice)); if (score >= cutoff) { yield return new ExtractedResult(choice, score, index); @@ -132,55 +132,55 @@ public static IEnumerable> ExtractWithoutOrder(string qu } } - public static IEnumerable> ExtractWithoutOrder(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int cutoff = 0) + public static IEnumerable> ExtractWithoutOrder(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, double cutoff = 0) { var extracted = extractor(query); return ExtractWithoutOrder(extracted, choices, extractor, processor, scorer, cutoff); } - public static ExtractedResult ExtractOne(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int cutoff = 0) + public static ExtractedResult ExtractOne(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, double cutoff = 0) { processor ??= Process.DefaultStringProcessor; var processedQuery = processor(extractor(query)); return ExtractOneCore(choices, choice => scorer.Score(processedQuery, processor(extractor(choice))), cutoff); } - public static ExtractedResult ExtractOne(string query, IEnumerable choices, Func processor, IRatioScorer scorer, int cutoff = 0) + public static ExtractedResult ExtractOne(string query, IEnumerable choices, Func processor, IRatioScorer scorer, double cutoff = 0) { processor ??= Process.DefaultStringProcessor; var processedQuery = processor(query); return ExtractOneCore(choices, choice => scorer.Score(processedQuery, processor(choice)), cutoff); } - public static ExtractedResult ExtractOne(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int cutoff = 0) + public static ExtractedResult ExtractOne(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, double cutoff = 0) { processor ??= Process.DefaultStringProcessor; var processedQuery = processor(query); return ExtractOneCore(choices, choice => scorer.Score(processedQuery, processor(extractor(choice))), cutoff); } - public static IEnumerable> ExtractSorted(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int cutoff = 0) + public static IEnumerable> ExtractSorted(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, double cutoff = 0) { return ExtractWithoutOrder(query, choices, extractor, processor, scorer, cutoff).OrderByDescending(r => r.Score); } - public static IEnumerable> ExtractSorted(string query, IEnumerable choices, Func processor, IRatioScorer scorer, int cutoff = 0) + public static IEnumerable> ExtractSorted(string query, IEnumerable choices, Func processor, IRatioScorer scorer, double cutoff = 0) { return ExtractWithoutOrder(query, choices, processor, scorer, cutoff).OrderByDescending(r => r.Score); } - public static IEnumerable> ExtractSorted(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int cutoff = 0) + public static IEnumerable> ExtractSorted(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, double cutoff = 0) { return ExtractWithoutOrder(query, choices, extractor, processor, scorer, cutoff).OrderByDescending(r => r.Score); } - public static IEnumerable> ExtractTop(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int limit, int cutoff = 0) + public static IEnumerable> ExtractTop(T query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int limit, double cutoff = 0) { var extracted = extractor(query); return ExtractTop(extracted, choices, extractor, processor, scorer, limit, cutoff); } - public static IEnumerable> ExtractTop(string query, IEnumerable choices, Func processor, IRatioScorer scorer, int limit, int cutoff = 0) + public static IEnumerable> ExtractTop(string query, IEnumerable choices, Func processor, IRatioScorer scorer, int limit, double cutoff = 0) { processor ??= Process.DefaultStringProcessor; return ExtractTopIterator(); @@ -195,7 +195,7 @@ IEnumerable> ExtractTopIterator() } } - public static IEnumerable> ExtractTop(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int limit, int cutoff = 0) + public static IEnumerable> ExtractTop(string query, IEnumerable choices, Func extractor, Func processor, IRatioScorer scorer, int limit, double cutoff = 0) { processor ??= Process.DefaultStringProcessor; return ExtractTopIterator(); diff --git a/FuzzySharp/Extractor/ScoredCandidate.cs b/FuzzySharp/Extractor/ScoredCandidate.cs index 2eec734..21e3015 100644 --- a/FuzzySharp/Extractor/ScoredCandidate.cs +++ b/FuzzySharp/Extractor/ScoredCandidate.cs @@ -2,11 +2,11 @@ namespace Raffinert.FuzzySharp.Extractor; -internal readonly struct ScoredCandidate(T value, int score, int index) +internal readonly struct ScoredCandidate(T value, double score, int index) { public T Value { get; } = value; - public int Score { get; } = score; + public double Score { get; } = score; public int Index { get; } = index; } @@ -27,7 +27,7 @@ internal struct BestCandidate public bool HasValue { get; private set; } - public void Consider(T value, int score, int index, int cutoff) + public void Consider(T value, double score, int index, double cutoff) { if (score < cutoff) { @@ -45,7 +45,7 @@ public void Consider(BestCandidate other) { if (other.HasValue) { - Consider(other.Candidate.Value, other.Candidate.Score, other.Candidate.Index, int.MinValue); + Consider(other.Candidate.Value, other.Candidate.Score, other.Candidate.Index, double.NegativeInfinity); } } } diff --git a/FuzzySharp/Fuzz.cs b/FuzzySharp/Fuzz.cs index 924cd49..77f9929 100644 --- a/FuzzySharp/Fuzz.cs +++ b/FuzzySharp/Fuzz.cs @@ -15,7 +15,7 @@ public static class Fuzz /// /// /// - public static int Ratio(string input1, string input2) + public static double Ratio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -28,7 +28,7 @@ public static int Ratio(string input1, string input2) /// /// /// - public static int Ratio(string input1, string input2, Func preprocessor) + public static double Ratio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -43,7 +43,7 @@ public static int Ratio(string input1, string input2, Func prepr /// /// /// - public static int PartialRatio(string input1, string input2) + public static double PartialRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -57,7 +57,7 @@ public static int PartialRatio(string input1, string input2) /// /// /// - public static int PartialRatio(string input1, string input2, Func preprocessor) + public static double PartialRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -72,7 +72,7 @@ public static int PartialRatio(string input1, string input2, Func /// /// - public static int TokenSortRatio(string input1, string input2) + public static double TokenSortRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -86,7 +86,7 @@ public static int TokenSortRatio(string input1, string input2) /// /// /// - public static int TokenSortRatio(string input1, string input2, Func preprocessor) + public static double TokenSortRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -99,7 +99,7 @@ public static int TokenSortRatio(string input1, string input2, Func /// /// - public static int PartialTokenSortRatio(string input1, string input2) + public static double PartialTokenSortRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -113,7 +113,7 @@ public static int PartialTokenSortRatio(string input1, string input2) /// /// /// - public static int PartialTokenSortRatio(string input1, string input2, Func preprocessor) + public static double PartialTokenSortRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -129,7 +129,7 @@ public static int PartialTokenSortRatio(string input1, string input2, Func /// /// - public static int TokenSetRatio(string input1, string input2) + public static double TokenSetRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -144,7 +144,7 @@ public static int TokenSetRatio(string input1, string input2) /// /// /// - public static int TokenSetRatio(string input1, string input2, Func preprocessor) + public static double TokenSetRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -158,7 +158,7 @@ public static int TokenSetRatio(string input1, string input2, Func /// /// - public static int PartialTokenSetRatio(string input1, string input2) + public static double PartialTokenSetRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -173,7 +173,7 @@ public static int PartialTokenSetRatio(string input1, string input2) /// /// /// - public static int PartialTokenSetRatio(string input1, string input2, Func preprocessor) + public static double PartialTokenSetRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -187,7 +187,7 @@ public static int PartialTokenSetRatio(string input1, string input2, Func /// /// - public static int TokenDifferenceRatio(string input1, string input2) + public static double TokenDifferenceRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -200,7 +200,7 @@ public static int TokenDifferenceRatio(string input1, string input2) /// /// /// - public static int TokenDifferenceRatio(string input1, string input2, Func preprocessor) + public static double TokenDifferenceRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -212,7 +212,7 @@ public static int TokenDifferenceRatio(string input1, string input2, Func /// /// - public static int PartialTokenDifferenceRatio(string input1, string input2) + public static double PartialTokenDifferenceRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -225,7 +225,7 @@ public static int PartialTokenDifferenceRatio(string input1, string input2) /// /// /// - public static int PartialTokenDifferenceRatio(string input1, string input2, Func preprocessor) + public static double PartialTokenDifferenceRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -238,7 +238,7 @@ public static int PartialTokenDifferenceRatio(string input1, string input2, Func /// /// /// - public static int TokenInitialismRatio(string input1, string input2) + public static double TokenInitialismRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -250,7 +250,7 @@ public static int TokenInitialismRatio(string input1, string input2) /// /// /// - public static int TokenInitialismRatio(string input1, string input2, Func preprocessor) + public static double TokenInitialismRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -261,7 +261,7 @@ public static int TokenInitialismRatio(string input1, string input2, Func /// /// - public static int PartialTokenInitialismRatio(string input1, string input2) + public static double PartialTokenInitialismRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -273,7 +273,7 @@ public static int PartialTokenInitialismRatio(string input1, string input2) /// /// /// - public static int PartialTokenInitialismRatio(string input1, string input2, Func preprocessor) + public static double PartialTokenInitialismRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -288,7 +288,7 @@ public static int PartialTokenInitialismRatio(string input1, string input2, Func /// /// /// - public static int TokenAbbreviationRatio(string input1, string input2) + public static double TokenAbbreviationRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -302,7 +302,7 @@ public static int TokenAbbreviationRatio(string input1, string input2) /// /// /// - public static int TokenAbbreviationRatio(string input1, string input2, Func preprocessor) + public static double TokenAbbreviationRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -315,7 +315,7 @@ public static int TokenAbbreviationRatio(string input1, string input2, Func /// /// - public static int PartialTokenAbbreviationRatio(string input1, string input2) + public static double PartialTokenAbbreviationRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -329,7 +329,7 @@ public static int PartialTokenAbbreviationRatio(string input1, string input2) /// /// /// - public static int PartialTokenAbbreviationRatio(string input1, string input2, Func preprocessor) + public static double PartialTokenAbbreviationRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } @@ -342,7 +342,7 @@ public static int PartialTokenAbbreviationRatio(string input1, string input2, Fu /// /// /// - public static int WeightedRatio(string input1, string input2) + public static double WeightedRatio(string input1, string input2) { return ScorerCache.Get().Score(input1, input2); } @@ -354,9 +354,9 @@ public static int WeightedRatio(string input1, string input2) /// /// /// - public static int WeightedRatio(string input1, string input2, Func preprocessor) + public static double WeightedRatio(string input1, string input2, Func preprocessor) { return ScorerCache.Get().Score(input1, input2, preprocessor); } #endregion -} \ No newline at end of file +} diff --git a/FuzzySharp/FuzzySharp.csproj b/FuzzySharp/FuzzySharp.csproj index 792c1be..caaa773 100644 --- a/FuzzySharp/FuzzySharp.csproj +++ b/FuzzySharp/FuzzySharp.csproj @@ -1,13 +1,13 @@  - 5.0.3.0 - 5.0.3 - 5.0.3 - 5.0.3.0 - 5.0.3.0 - 5.0.3 - 5.0.3 + 6.0.0.0 + 6.0.0 + 6.0.0 + 6.0.0.0 + 6.0.0.0 + 6.0.0 + 6.0.0 Yevhen Cherkes;Jacob Bayer diff --git a/FuzzySharp/Indel.Static.cs b/FuzzySharp/Indel.Static.cs index 155fcbb..aed6d28 100644 --- a/FuzzySharp/Indel.Static.cs +++ b/FuzzySharp/Indel.Static.cs @@ -10,25 +10,39 @@ namespace Raffinert.FuzzySharp; /// public sealed partial class Indel { + /// + /// Computes normalized Indel similarity for character spans using the + /// character-specialized common-affix trimming path. + /// + internal static double NormalizedSimilarityChar( + ReadOnlySpan s1, + ReadOnlySpan s2) + { + var maximum = s1.Length + s2.Length; + SequenceUtils.TrimCommonAffix(ref s1, ref s2); + SequenceUtils.SwapIfSourceIsLonger(ref s1, ref s2); + + var distance = DistanceImpl(s1, s2); + var normalizedDistance = maximum == 0 ? 0 : distance / (double)maximum; + var normalizedSimilarity = 1 - normalizedDistance; + + return normalizedSimilarity; + } + /// /// Computes the Indel distance using precomputed block data for the first sequence. /// /// Element type, must implement IEquatable<T>. /// Precomputed per-symbol bitmasks for s1. /// Second sequence. - /// Optional maximum distance threshold. If the distance exceeds this value, returns scoreCutoff + 1. /// The Indel distance between the two sequences. public static int BlockDistance(IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { var maximum = s1Vector.Length + s2.Length; var lcsSim = LongestCommonSubsequence.BlockSimilarity(s1Vector, s2); var dist = maximum - 2 * lcsSim; - var result = scoreCutoff == null || dist <= scoreCutoff.Value - ? dist - : scoreCutoff.Value + 1; - return result; + return dist; } /// @@ -37,20 +51,15 @@ public static int BlockDistance(IPatternMatchVector s1Vector, /// Element type, must implement IEquatable<T>. /// /// Second sequence. - /// Optional maximum normalized distance threshold. If the distance exceeds this value, returns 1. /// The normalized Indel distance between the two sequences. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double BlockNormalizedDistance(IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { var maximum = s1Vector.Length + s2.Length; var dist = BlockDistance(s1Vector, s2); var normDist = maximum == 0 ? 0 : dist / (double)maximum; - var result = scoreCutoff == null || normDist <= scoreCutoff.Value - ? normDist - : 1; - return result; + return normDist; } /// @@ -60,19 +69,14 @@ public static double BlockNormalizedDistance(IPatternMatchVector s1Vector, /// Element type, must implement IEquatable<T>. /// /// Second sequence. - /// Optional minimum similarity threshold. If the similarity is below this value, returns 0. /// The normalized Indel similarity between the two sequences. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double BlockNormalizedSimilarity(IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { var normDist = BlockNormalizedDistance(s1Vector, s2); var normSim = 1.0 - normDist; - var result = scoreCutoff == null || normSim >= scoreCutoff.Value - ? normSim - : 0; - return result; + return normSim; } /// @@ -83,13 +87,11 @@ public static double BlockNormalizedSimilarity(IPatternMatchVector s1Vecto /// First sequence. /// Second sequence. /// Optional preprocessor for normalization. - /// Optional maximum distance threshold. If the distance exceeds this value, returns scoreCutoff + 1. /// The Indel distance between the two sequences. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Distance(ReadOnlySpan s1, ReadOnlySpan s2, - Processor processor = null, - int? scoreCutoff = null) where T : IEquatable + Processor processor = null) where T : IEquatable { if (processor != null) { @@ -99,31 +101,25 @@ public static int Distance(ReadOnlySpan s1, SequenceUtils.TrimCommonAffixAndSwapIfNeeded(ref s1, ref s2); - return DistanceImpl(s1, s2, scoreCutoff); + return DistanceImpl(s1, s2); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int DistanceImpl(ReadOnlySpan s1, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { using var patternMatchVector = PatternMatchVector.Create(s1); - return DistanceImpl(patternMatchVector, s2, scoreCutoff); + return DistanceImpl(patternMatchVector, s2); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int DistanceImpl(IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { var maximum = s1Vector.Length + s2.Length; var lcsSim = LongestCommonSubsequence.SimilarityImpl(s1Vector, s2); var dist = maximum - 2 * lcsSim; - var result = scoreCutoff == null || dist <= scoreCutoff.Value - ? dist - : scoreCutoff.Value + 1; - - return result; + return dist; } /// @@ -133,13 +129,11 @@ internal static int DistanceImpl(IPatternMatchVector s1Vector, /// First sequence. /// Second sequence. /// Optional preprocessor for normalization. - /// Optional maximum normalized distance threshold. If the distance exceeds this value, returns scoreCutoff + 1. /// The normalized Indel distance between the two sequences. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double NormalizedDistance(ReadOnlySpan s1, ReadOnlySpan s2, - Processor processor = null, - int? scoreCutoff = null) where T : IEquatable + Processor processor = null) where T : IEquatable { if (processor != null) { @@ -149,37 +143,31 @@ public static double NormalizedDistance(ReadOnlySpan s1, SequenceUtils.TrimCommonAffixAndSwapIfNeeded(ref s1, ref s2); - return NormalizedDistanceImpl(s1, s2, scoreCutoff); + return NormalizedDistanceImpl(s1, s2); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double NormalizedDistanceImpl(IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { - var dist = DistanceImpl(s1Vector, s2, scoreCutoff); - return NormalizedDistanceImpl(s1Vector.Length, s2.Length, dist, scoreCutoff); + var dist = DistanceImpl(s1Vector, s2); + return NormalizedDistanceImpl(s1Vector.Length, s2.Length, dist); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double NormalizedDistanceImpl(ReadOnlySpan s1, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { var dist = Distance(s1, s2); - return NormalizedDistanceImpl(s1.Length, s2.Length, dist, scoreCutoff); + return NormalizedDistanceImpl(s1.Length, s2.Length, dist); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static double NormalizedDistanceImpl(int s1Length, int s2Length, int distance, - int? scoreCutoff = null) + internal static double NormalizedDistanceImpl(int s1Length, int s2Length, int distance) { var maximum = s1Length + s2Length; var normDist = maximum == 0 ? 0 : distance / (double)maximum; - var result = scoreCutoff == null || normDist <= scoreCutoff.Value - ? normDist - : scoreCutoff.Value + 1; - return result; + return normDist; } /// /// Computes the normalized Indel similarity between two sequences, in the range [0, 1]. @@ -189,13 +177,11 @@ internal static double NormalizedDistanceImpl(int s1Length, int s2Length, int di /// First sequence. /// Second sequence. /// Optional preprocessor for normalization. - /// Optional minimum similarity threshold. If the similarity is below this value, returns 0. /// The normalized Indel similarity between the two sequences. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static double NormalizedSimilarity(ReadOnlySpan s1, ReadOnlySpan s2, - Processor processor = null, - int? scoreCutoff = null) where T : IEquatable + Processor processor = null) where T : IEquatable { if (processor != null) { @@ -203,32 +189,24 @@ public static double NormalizedSimilarity(ReadOnlySpan s1, processor(ref s2); } - return NormalizedSimilarityImpl(s1, s2, scoreCutoff); + return NormalizedSimilarityImpl(s1, s2); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static double NormalizedSimilarityImpl(IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { - var normDist = NormalizedDistanceImpl(s1Vector, s2, scoreCutoff); + var normDist = NormalizedDistanceImpl(s1Vector, s2); var normSim = 1 - normDist; - var result = scoreCutoff == null || normSim >= scoreCutoff.Value - ? normSim - : 0; - return result; + return normSim; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static double NormalizedSimilarityImpl(ReadOnlySpan s1, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { var normDist = NormalizedDistanceImpl(s1, s2); var normSim = 1 - normDist; - var result = scoreCutoff == null || normSim >= scoreCutoff.Value - ? normSim - : 0; - return result; + return normSim; } } diff --git a/FuzzySharp/Levenshtein.Static.cs b/FuzzySharp/Levenshtein.Static.cs index 55574a6..0fbf7ca 100644 --- a/FuzzySharp/Levenshtein.Static.cs +++ b/FuzzySharp/Levenshtein.Static.cs @@ -15,23 +15,32 @@ namespace Raffinert.FuzzySharp; public sealed partial class Levenshtein { /// - /// Computes the Levenshtein distance between two strings with custom operation costs and optional cutoff. + /// Computes the Levenshtein distance between two strings with custom operation costs. /// /// Source string. /// Target string. /// Cost of an insertion. /// Cost of a deletion. /// Cost of a replacement. - /// Optional maximum distance threshold. /// The Levenshtein distance. public static int Distance( string source, string target, - int insertCost = 1, int deleteCost = 1, int replaceCost = 1, - int? scoreCutoff = null) - => Distance(source.AsSpan(), target.AsSpan(), insertCost, deleteCost, replaceCost, scoreCutoff); + int insertCost = 1, int deleteCost = 1, int replaceCost = 1) + { + var sourceSpan = source.AsSpan(); + var targetSpan = target.AsSpan(); + SequenceUtils.TrimCommonAffix(ref sourceSpan, ref targetSpan); + + if (insertCost == deleteCost) + { + SequenceUtils.SwapIfSourceIsLonger(ref sourceSpan, ref targetSpan); + } + + return DistanceTrimmed(sourceSpan, targetSpan, insertCost, deleteCost, replaceCost); + } /// - /// Computes the Levenshtein distance between two sequences with custom operation costs and optional cutoff. + /// Computes the Levenshtein distance between two sequences with custom operation costs. /// /// Element type, must implement IEquatable<T>. /// Source sequence. @@ -39,13 +48,11 @@ public static int Distance( /// Cost of an insertion. /// Cost of a deletion. /// Cost of a replacement. - /// Optional maximum distance threshold. /// The Levenshtein distance. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Distance( ReadOnlySpan source, ReadOnlySpan target, - int insertCost = 1, int deleteCost = 1, int replaceCost = 1, - int? scoreCutoff = null) where T : IEquatable + int insertCost = 1, int deleteCost = 1, int replaceCost = 1) where T : IEquatable { SequenceUtils.TrimCommonAffix(ref source, ref target); @@ -54,33 +61,27 @@ public static int Distance( SequenceUtils.SwapIfSourceIsLonger(ref source, ref target); } + return DistanceTrimmed(source, target, insertCost, deleteCost, replaceCost); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DistanceTrimmed( + ReadOnlySpan source, ReadOnlySpan target, + int insertCost, int deleteCost, int replaceCost) where T : IEquatable + { if (insertCost != 1 || deleteCost != 1 || (replaceCost != 1 && replaceCost != 2)) { - return GenericDistance(source, target, insertCost, deleteCost, replaceCost, scoreCutoff); + return GenericDistance(source, target, insertCost, deleteCost, replaceCost); } using var patternMatchVector = PatternMatchVector.Create(source); if (replaceCost == 1) { - return scoreCutoff.HasValue - ? Distance(patternMatchVector, target, scoreCutoff.Value) - : Distance(patternMatchVector, target); - } - - return Indel.DistanceImpl(patternMatchVector, target, scoreCutoff); - } - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int Distance(IPatternMatchVector sourceVector, ReadOnlySpan target, int scoreCutoff) where T : IEquatable - { - if (sourceVector.Length <= 64) - { - return DistanceSingleULong(sourceVector, target, scoreCutoff); + return Distance(patternMatchVector, target); } - return DistanceMultipleULongs(sourceVector, target, scoreCutoff); + return Indel.DistanceImpl(patternMatchVector, target); } @@ -92,7 +93,7 @@ private static int Distance(IPatternMatchVector sourceVector, ReadOnlySpan return DistanceSingleULong(sourceVector, target); } - return DistanceMultipleULongs(sourceVector, target, null); + return DistanceMultipleULongs(sourceVector, target); } /// @@ -457,21 +458,19 @@ public static (int Distance, List VP, List VN) MatrixSingleULo /// Cost of an insertion. /// Cost of a deletion. /// Cost of a replacement. - /// Optional maximum normalized distance threshold. /// Normalized distance (0 = identical, 1 = completely different). public static double NormalizedDistance( ReadOnlySpan source, ReadOnlySpan target, - int insertCost = 1, int deleteCost = 1, int replaceCost = 1, - double? scoreCutoff = null) + int insertCost = 1, int deleteCost = 1, int replaceCost = 1) { int len1 = source.Length, len2 = target.Length; if (len1 == 0 && len2 == 0) return 0.0; var maximum = LevenshteinMaximum(len1, len2, insertCost, deleteCost, replaceCost); if (maximum == 0) return 0.0; - var dist = Distance(source, target, insertCost, deleteCost, replaceCost, scoreCutoff.HasValue ? (int?)Math.Floor(scoreCutoff.Value * maximum) : null); + var dist = Distance(source, target, insertCost, deleteCost, replaceCost); var nd = dist / (double)maximum; - return nd > scoreCutoff ? 1.0 : nd; + return nd; } /// @@ -482,13 +481,11 @@ public static double NormalizedDistance( /// Cost of an insertion. /// Cost of a deletion. /// Cost of a replacement. - /// Optional maximum normalized distance threshold. /// Normalized distance (0 = identical, 1 = completely different). public static double NormalizedDistance( string source, string target, - int insertCost = 1, int deleteCost = 1, int replaceCost = 1, - double? scoreCutoff = null) - => NormalizedDistance(source.AsSpan(), target.AsSpan(), insertCost, deleteCost, replaceCost, scoreCutoff); + int insertCost = 1, int deleteCost = 1, int replaceCost = 1) + => NormalizedDistance(source.AsSpan(), target.AsSpan(), insertCost, deleteCost, replaceCost); /// /// Computes the normalized Levenshtein similarity in [0, 1] (1 - normalized distance). @@ -498,18 +495,13 @@ public static double NormalizedDistance( /// Cost of an insertion. /// Cost of a deletion. /// Cost of a replacement. - /// Optional minimum normalized similarity threshold. /// Normalized similarity (1 = identical, 0 = completely different). public static double NormalizedSimilarity( ReadOnlySpan source, ReadOnlySpan target, - int insertCost = 1, int deleteCost = 1, int replaceCost = 1, - double? scoreCutoff = null) + int insertCost = 1, int deleteCost = 1, int replaceCost = 1) { - double? distanceCutoff = 1.0 - scoreCutoff; - var nd = NormalizedDistance(source, target, insertCost, deleteCost, replaceCost, distanceCutoff); - var ns = 1.0 - nd; - - return ns < scoreCutoff ? 0.0 : ns; + var nd = NormalizedDistance(source, target, insertCost, deleteCost, replaceCost); + return 1.0 - nd; } /// @@ -520,13 +512,11 @@ public static double NormalizedSimilarity( /// Cost of an insertion. /// Cost of a deletion. /// Cost of a replacement. - /// Optional minimum normalized similarity threshold. /// Normalized similarity (1 = identical, 0 = completely different). public static double NormalizedSimilarity( string source, string target, - int insertCost = 1, int deleteCost = 1, int replaceCost = 1, - double? scoreCutoff = null) - => NormalizedSimilarity(source.AsSpan(), target.AsSpan(), insertCost, deleteCost, replaceCost, scoreCutoff); + int insertCost = 1, int deleteCost = 1, int replaceCost = 1) + => NormalizedSimilarity(source.AsSpan(), target.AsSpan(), insertCost, deleteCost, replaceCost); /// /// Computes the Levenshtein similarity (maximum possible distance minus actual distance). @@ -536,21 +526,15 @@ public static double NormalizedSimilarity( /// Cost of an insertion. /// Cost of a deletion. /// Cost of a replacement. - /// Optional minimum similarity threshold. /// The Levenshtein similarity score. public static int Similarity( ReadOnlySpan source, ReadOnlySpan target, - int insertCost = 1, int deleteCost = 1, int replaceCost = 1, - int? scoreCutoff = null) + int insertCost = 1, int deleteCost = 1, int replaceCost = 1) { int len1 = source.Length, len2 = target.Length; var maximum = LevenshteinMaximum(len1, len2, insertCost, deleteCost, replaceCost); - int? distanceCutoff = scoreCutoff.HasValue - ? Math.Max(0, maximum - scoreCutoff.Value) - : null; - var dist = Distance(source, target, insertCost, deleteCost, replaceCost, distanceCutoff); - var sim = maximum - dist; - return sim < scoreCutoff ? 0 : sim; + var dist = Distance(source, target, insertCost, deleteCost, replaceCost); + return maximum - dist; } /// @@ -561,13 +545,11 @@ public static int Similarity( /// Cost of an insertion. /// Cost of a deletion. /// Cost of a replacement. - /// Optional minimum similarity threshold. /// The Levenshtein similarity score. public static int Similarity( string source, string s2, - int insertCost = 1, int deleteCost = 1, int replaceCost = 1, - int? scoreCutoff = null) - => Similarity(source.AsSpan(), s2.AsSpan(), insertCost, deleteCost, replaceCost, scoreCutoff); + int insertCost = 1, int deleteCost = 1, int replaceCost = 1) + => Similarity(source.AsSpan(), s2.AsSpan(), insertCost, deleteCost, replaceCost); /// /// Computes the Levenshtein distance between two sequences with custom operation costs using a dynamic programming approach. @@ -578,12 +560,10 @@ public static int Similarity( /// Cost of an insertion. /// Cost of a deletion. /// Cost of a replacement. - /// Optional maximum distance threshold. /// The Levenshtein distance. private static int GenericDistance( ReadOnlySpan source, ReadOnlySpan target, - int insertCost, int deleteCost, int replaceCost, - int? scoreCutoff) where T : IEquatable + int insertCost, int deleteCost, int replaceCost) where T : IEquatable { var len1 = source.Length; // allocate a single row of len1+1 @@ -616,8 +596,6 @@ private static int GenericDistance( row[i + 1] = cost; } - if (scoreCutoff.HasValue && row[len1] > scoreCutoff.Value) - return scoreCutoff.Value + 1; } return row[len1]; @@ -638,21 +616,17 @@ private static (int Distance, List VP, List VN) Matrix(Read } /// - /// Computes the Levenshtein distance (Myers’s bit‐parallel over >64 bits), with an optional cutoff. + /// Computes the Levenshtein distance (Myers’s bit‐parallel over >64 bits). /// Uses a dictionary to store per‐character bitmasks rented from ArrayPool, and uses stackalloc if /// 6*blocks ≤ STACKALLOC_THRESHOLD_ULONGS; otherwise allocates a new ulong[] on the heap for the six lanes. /// private static int DistanceMultipleULongs(IPatternMatchVector sourceVector, - ReadOnlySpan target, - int? scoreCutoff) where T : IEquatable + ReadOnlySpan target) where T : IEquatable { var m = sourceVector.Length; if (m == 0) { - var d = target.Length; - return d > scoreCutoff - ? scoreCutoff.Value + 1 - : d; + return target.Length; } // Number of 64‐bit blocks needed to cover pattern length m @@ -661,7 +635,7 @@ private static int DistanceMultipleULongs(IPatternMatchVector sourceVector var scratchArray = ArrayPool.Shared.Rent(totalScratch); try { - var result = DistanceMultipleULongsImpl(sourceVector, target, scoreCutoff, m, blocks, scratchArray); + var result = DistanceMultipleULongsImpl(sourceVector, target, m, blocks, scratchArray); return result; } finally @@ -679,7 +653,6 @@ private static int DistanceMultipleULongs(IPatternMatchVector sourceVector // ───────────────────────────────────────────────────────────────────────────── private static int DistanceMultipleULongsImpl(IPatternMatchVector sourceVector, ReadOnlySpan target, - int? scoreCutoff, int m, int blocks, Span scratch) where T : IEquatable @@ -755,55 +728,6 @@ private static int DistanceMultipleULongsImpl(IPatternMatchVector sourceVe carryHN = hnHigh; } - if (scoreCutoff.HasValue) - { - var remaining = target.Length - (i + 1); - if (dist > scoreCutoff.Value + remaining) - { - return scoreCutoff.Value + 1; - } - } - } - - return dist; - } - - private static int DistanceSingleULong(IPatternMatchVector sourceVector, - ReadOnlySpan target, - int scoreCutoff) where T : IEquatable - { - var m = sourceVector.Length; - if (m == 0) return target.Length; - - // initial bitmask: lower m bits set - var VP = m < 64 ? (1UL << m) - 1 : ulong.MaxValue; - ulong VN = 0; - var highestBit = 1UL << (m - 1); - var dist = m; - - for (var i = 0; i < target.Length; i++) - { - var PM = sourceVector.GetOrZero(target[i])[0]; - - // Myers bit-parallel update - var X = PM | VN; - var D0 = (((X & VP) + VP) ^ VP) | X; - D0 |= VN; - var HP = VN | ~(D0 | VP); - var HN = D0 & VP; - - if ((HP & highestBit) != 0) dist++; - if ((HN & highestBit) != 0) dist--; - - var remaining = target.Length - (i + 1); - if (dist > scoreCutoff + remaining) - return scoreCutoff + 1; - - // shift in - HP = (HP << 1) | 1; - HN <<= 1; - VP = HN | ~(D0 | HP); - VN = HP & D0; } return dist; diff --git a/FuzzySharp/LongestCommonSubsequence.Static.cs b/FuzzySharp/LongestCommonSubsequence.Static.cs index 48ffced..989d22d 100644 --- a/FuzzySharp/LongestCommonSubsequence.Static.cs +++ b/FuzzySharp/LongestCommonSubsequence.Static.cs @@ -21,13 +21,11 @@ public sealed partial class LongestCommonSubsequence /// First sequence. /// Second sequence. /// Optional preprocessor for normalization. - /// Optional maximum distance threshold. /// The LCS distance (max(len1, len2) - LCS length). public static int Distance( ReadOnlySpan s1, ReadOnlySpan s2, - Processor processor = null, - int? scoreCutoff = null) where T : IEquatable + Processor processor = null) where T : IEquatable { if (processor != null) { @@ -37,22 +35,17 @@ public static int Distance( using var patternMatchVector = PatternMatchVector.Create(s1); - return DistanceImpl(patternMatchVector, s2, scoreCutoff); + return DistanceImpl(patternMatchVector, s2); } private static int DistanceImpl(IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { int maximum = Math.Max(s1Vector.Length, s2.Length); int sim = SimilarityImpl(s1Vector, s2); int dist = maximum - sim; - var result = scoreCutoff == null || dist <= scoreCutoff.Value - ? dist - : scoreCutoff.Value + 1; - - return result; + return dist; } /// @@ -209,13 +202,11 @@ public static (int Sim, List Matrix) Matrix( /// First sequence. /// Second sequence. /// Optional preprocessor for normalization. - /// Optional maximum normalized distance threshold. /// Normalized distance (0 = identical, 1 = completely different). public static double NormalizedDistance( ReadOnlySpan s1, ReadOnlySpan s2, - Processor processor = null, - int? scoreCutoff = null) where T : IEquatable + Processor processor = null) where T : IEquatable { if ((s1.IsEmpty && !s2.IsEmpty) || (!s1.IsEmpty && s2.IsEmpty)) { @@ -234,11 +225,7 @@ public static double NormalizedDistance( int maximum = Math.Max(s1.Length, s2.Length); double normDist = Distance(s1, s2) / (double)maximum; - var result = !scoreCutoff.HasValue || normDist <= scoreCutoff.Value - ? normDist - : 1.0; - - return result; + return normDist; } /// @@ -248,13 +235,11 @@ public static double NormalizedDistance( /// First sequence. /// Second sequence. /// Optional preprocessor for normalization. - /// Optional minimum normalized similarity threshold. /// Normalized similarity (1 = identical, 0 = completely different). public static double NormalizedSimilarity( ReadOnlySpan s1, ReadOnlySpan s2, - Processor processor = null, - int? scoreCutoff = null) where T : IEquatable + Processor processor = null) where T : IEquatable { if (s1.IsEmpty || s2.IsEmpty) @@ -270,11 +255,7 @@ public static double NormalizedSimilarity( double normSim = 1.0 - NormalizedDistance(s1, s2); - var result = !scoreCutoff.HasValue || normSim >= scoreCutoff.Value - ? normSim - : 0.0; - - return result; + return normSim; } /// @@ -302,13 +283,11 @@ public static List Opcodes( /// First sequence. /// Second sequence. /// Optional preprocessor for normalization. - /// Optional minimum similarity threshold. - /// The length of the LCS, or 0 if below cutoff. + /// The length of the LCS. public static int Similarity( ReadOnlySpan s1, ReadOnlySpan s2, - Processor processor = null, - int? scoreCutoff = null) where T : IEquatable + Processor processor = null) where T : IEquatable { if (processor != null) { @@ -318,22 +297,17 @@ public static int Similarity( using var patternMatchVector = PatternMatchVector.Create(s1); - return SimilarityImpl(patternMatchVector, s2, scoreCutoff); + return SimilarityImpl(patternMatchVector, s2); } internal static int SimilarityImpl(IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { var sim = s1Vector.Length > 64 ? BlockSimilarityMultipleULongs(s1Vector, s2) : BlockSimilaritySingleULong(s1Vector, s2); - var result = scoreCutoff == null || sim >= scoreCutoff.Value - ? sim - : 0; - - return result; + return sim; } /// @@ -342,23 +316,20 @@ internal static int SimilarityImpl(IPatternMatchVector s1Vector, /// Element type, must implement IEquatable<T>. /// Precomputed per-symbol bitmasks for s1. /// Second sequence (text). - /// Optional minimum similarity threshold. - /// The length of the longest common subsequence, or 0 if below cutoff. + /// The length of the longest common subsequence. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int BlockSimilarity(IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { return s1Vector.Length <= 64 - ? BlockSimilaritySingleULong(s1Vector, s2, scoreCutoff) - : BlockSimilarityMultipleULongs(s1Vector, s2, scoreCutoff); + ? BlockSimilaritySingleULong(s1Vector, s2) + : BlockSimilarityMultipleULongs(s1Vector, s2); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int BlockSimilaritySingleULong( IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null + ReadOnlySpan s2 ) where T : IEquatable { if (s1Vector.Length == 0) @@ -379,16 +350,12 @@ private static int BlockSimilaritySingleULong( } } - int lcs = CountZeroBits(S, len1); - return scoreCutoff == null || lcs >= scoreCutoff.Value - ? lcs - : 0; + return CountZeroBits(S, len1); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int BlockSimilarityMultipleULongs(IPatternMatchVector s1Vector, - ReadOnlySpan s2, - int? scoreCutoff = null) where T : IEquatable + ReadOnlySpan s2) where T : IEquatable { if (s1Vector.Length == 0) return 0; @@ -439,10 +406,7 @@ internal static int BlockSimilarityMultipleULongs(IPatternMatchVector s1Ve } // --- 4) count zero bits in the lower len1 positions of S --- - int lcs = CountZeroBits(S, len1); - return scoreCutoff == null || lcs >= scoreCutoff.Value - ? lcs - : 0; + return CountZeroBits(S, len1); } finally { diff --git a/FuzzySharp/Process.cs b/FuzzySharp/Process.cs index 4dfa66f..9a21234 100644 --- a/FuzzySharp/Process.cs +++ b/FuzzySharp/Process.cs @@ -36,7 +36,7 @@ public static IEnumerable> ExtractAll( IEnumerable choices, Func processor = null, IRatioScorer scorer = null, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -61,7 +61,7 @@ public static IEnumerable> ExtractAllBy( Func extractor, Func processor = null, IRatioScorer scorer = null, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -85,7 +85,7 @@ public static IEnumerable> ExtractAllBy( Func extractor, Func processor = null, IRatioScorer scorer = null, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -112,7 +112,7 @@ public static IEnumerable> ExtractTop( Func processor = null, IRatioScorer scorer = null, int limit = 5, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -138,7 +138,7 @@ public static IEnumerable> ExtractTopBy( Func processor = null, IRatioScorer scorer = null, int limit = 5, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -164,7 +164,7 @@ public static IEnumerable> ExtractTopBy( Func processor = null, IRatioScorer scorer = null, int limit = 5, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -189,7 +189,7 @@ public static IEnumerable> ExtractSorted( IEnumerable choices, Func processor = null, IRatioScorer scorer = null, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -212,7 +212,7 @@ public static IEnumerable> ExtractSortedBy( Func extractor, Func processor = null, IRatioScorer scorer = null, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -235,7 +235,7 @@ public static IEnumerable> ExtractSortedBy( Func extractor, Func processor = null, IRatioScorer scorer = null, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -259,7 +259,7 @@ public static ExtractedResult ExtractOne( IEnumerable choices, Func processor = null, IRatioScorer scorer = null, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -282,7 +282,7 @@ public static ExtractedResult ExtractOneBy( Func extractor, Func processor = null, IRatioScorer scorer = null, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -305,7 +305,7 @@ public static ExtractedResult ExtractOneBy( Func extractor, Func processor = null, IRatioScorer scorer = null, - int cutoff = 0) + double cutoff = 0) { processor ??= DefaultStringProcessor; scorer ??= DefaultScorer; @@ -324,4 +324,4 @@ public static ExtractedResult ExtractOne(string query, params string[] c } #endregion -} \ No newline at end of file +} diff --git a/FuzzySharp/ProcessExecutor.cs b/FuzzySharp/ProcessExecutor.cs index eef3200..f5c321a 100644 --- a/FuzzySharp/ProcessExecutor.cs +++ b/FuzzySharp/ProcessExecutor.cs @@ -20,7 +20,7 @@ public static IEnumerable> ExtractAll( string query, IEnumerable choices, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { if (processor == null) throw new ArgumentNullException(nameof(processor)); @@ -46,7 +46,7 @@ public static IEnumerable> ExtractAll( IEnumerable choices, Func extractor, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); @@ -73,7 +73,7 @@ public static IEnumerable> ExtractAll( IEnumerable choices, Func extractor, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); @@ -100,7 +100,7 @@ private static IEnumerable> ExtractAllCached( IEnumerable choices, Func extractor, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { using var scorer = new CachedWeightedRatioScorer(processor(query)); @@ -118,7 +118,7 @@ private static IEnumerable> ExtractAllCached( string query, IEnumerable choices, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { using var scorer = new CachedWeightedRatioScorer(processor(query)); @@ -141,7 +141,7 @@ public static IEnumerable> ExtractTop( IEnumerable choices, Func processor, int limit, - int cutoff, + double cutoff, ProcessOptions options) { if (processor == null) throw new ArgumentNullException(nameof(processor)); @@ -168,7 +168,7 @@ public static IEnumerable> ExtractTop( Func extractor, Func processor, int limit, - int cutoff, + double cutoff, ProcessOptions options) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); @@ -198,7 +198,7 @@ public static IEnumerable> ExtractTop( Func extractor, Func processor, int limit, - int cutoff, + double cutoff, ProcessOptions options) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); @@ -226,7 +226,7 @@ private static IEnumerable> ExtractTopCached( Func extractor, Func processor, int limit, - int cutoff, + double cutoff, ProcessOptions options) { using var scorer = new CachedWeightedRatioScorer(processor(query)); @@ -245,7 +245,7 @@ private static IEnumerable> ExtractTopCached( IEnumerable choices, Func processor, int limit, - int cutoff, + double cutoff, ProcessOptions options) { using var scorer = new CachedWeightedRatioScorer(processor(query)); @@ -267,7 +267,7 @@ public static IEnumerable> ExtractSorted( string query, IEnumerable choices, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { if (processor == null) throw new ArgumentNullException(nameof(processor)); @@ -293,7 +293,7 @@ public static IEnumerable> ExtractSorted( IEnumerable choices, Func extractor, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); @@ -320,7 +320,7 @@ public static IEnumerable> ExtractSorted( IEnumerable choices, Func extractor, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); @@ -347,7 +347,7 @@ private static IEnumerable> ExtractSortedCached( IEnumerable choices, Func extractor, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { using var scorer = new CachedWeightedRatioScorer(processor(query)); @@ -365,7 +365,7 @@ private static IEnumerable> ExtractSortedCached( string query, IEnumerable choices, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { using var scorer = new CachedWeightedRatioScorer(processor(query)); @@ -387,7 +387,7 @@ public static ExtractedResult ExtractOne( string query, IEnumerable choices, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { if (processor == null) throw new ArgumentNullException(nameof(processor)); @@ -416,7 +416,7 @@ public static ExtractedResult ExtractOne( IEnumerable choices, Func extractor, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); @@ -447,7 +447,7 @@ public static ExtractedResult ExtractOne( IEnumerable choices, Func extractor, Func processor, - int cutoff, + double cutoff, ProcessOptions options) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); diff --git a/FuzzySharp/ProcessPipeline.cs b/FuzzySharp/ProcessPipeline.cs index 83645bc..720971f 100644 --- a/FuzzySharp/ProcessPipeline.cs +++ b/FuzzySharp/ProcessPipeline.cs @@ -30,7 +30,7 @@ public IEnumerable> ExtractAll( string query, IEnumerable choices, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractAll(query, choices, processor ?? Process.DefaultStringProcessor, cutoff, _options); } @@ -44,7 +44,7 @@ public IEnumerable> ExtractAllBy( IEnumerable choices, Func extractor, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractAll(query, choices, extractor, processor ?? Process.DefaultStringProcessor, cutoff, _options); } @@ -58,7 +58,7 @@ public IEnumerable> ExtractAllBy( IEnumerable choices, Func extractor, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractAll(query, choices, extractor, processor ?? Process.DefaultStringProcessor, cutoff, _options); } @@ -76,7 +76,7 @@ public IEnumerable> ExtractTop( IEnumerable choices, Func processor = null, int limit = 5, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractTop(query, choices, processor ?? Process.DefaultStringProcessor, limit, cutoff, _options); } @@ -91,7 +91,7 @@ public IEnumerable> ExtractTopBy( Func extractor, Func processor = null, int limit = 5, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractTop(query, choices, extractor, processor ?? Process.DefaultStringProcessor, limit, cutoff, _options); } @@ -106,7 +106,7 @@ public IEnumerable> ExtractTopBy( Func extractor, Func processor = null, int limit = 5, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractTop(query, choices, extractor, processor ?? Process.DefaultStringProcessor, limit, cutoff, _options); } @@ -122,7 +122,7 @@ public IEnumerable> ExtractSorted( string query, IEnumerable choices, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractSorted(query, choices, processor ?? Process.DefaultStringProcessor, cutoff, _options); } @@ -135,7 +135,7 @@ public IEnumerable> ExtractSortedBy( IEnumerable choices, Func extractor, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractSorted(query, choices, extractor, processor ?? Process.DefaultStringProcessor, cutoff, _options); } @@ -148,7 +148,7 @@ public IEnumerable> ExtractSortedBy( IEnumerable choices, Func extractor, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractSorted(query, choices, extractor, processor ?? Process.DefaultStringProcessor, cutoff, _options); } @@ -164,7 +164,7 @@ public ExtractedResult ExtractOne( string query, IEnumerable choices, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractOne(query, choices, processor ?? Process.DefaultStringProcessor, cutoff, _options); } @@ -177,7 +177,7 @@ public ExtractedResult ExtractOneBy( IEnumerable choices, Func extractor, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractOne(query, choices, extractor, processor ?? Process.DefaultStringProcessor, cutoff, _options); } @@ -190,7 +190,7 @@ public ExtractedResult ExtractOneBy( IEnumerable choices, Func extractor, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return ProcessExecutor.ExtractOne(query, choices, extractor, processor ?? Process.DefaultStringProcessor, cutoff, _options); } @@ -229,7 +229,7 @@ internal CachedScorerProcessPipeline(CachedScorerProcessOptions options) public IEnumerable> ExtractAll( IEnumerable choices, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return CachedScorerProcessExecutor.ExtractAll( choices, processor ?? Process.DefaultStringProcessor, _options.CachedScorer, cutoff, @@ -244,7 +244,7 @@ public IEnumerable> ExtractAllBy( IEnumerable choices, Func extractor, Func processor = null, - int cutoff = 0) + double cutoff = 0) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); @@ -265,7 +265,7 @@ public IEnumerable> ExtractTop( IEnumerable choices, Func processor = null, int limit = 5, - int cutoff = 0) + double cutoff = 0) { return CachedScorerProcessExecutor.ExtractTop( choices, processor ?? Process.DefaultStringProcessor, _options.CachedScorer, limit, cutoff, @@ -281,7 +281,7 @@ public IEnumerable> ExtractTopBy( Func extractor, Func processor = null, int limit = 5, - int cutoff = 0) + double cutoff = 0) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); processor ??= Process.DefaultStringProcessor; @@ -300,7 +300,7 @@ public IEnumerable> ExtractTopBy( public IEnumerable> ExtractSorted( IEnumerable choices, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return CachedScorerProcessExecutor.ExtractSorted( choices, processor ?? Process.DefaultStringProcessor, _options.CachedScorer, cutoff, @@ -314,7 +314,7 @@ public IEnumerable> ExtractSortedBy( IEnumerable choices, Func extractor, Func processor = null, - int cutoff = 0) + double cutoff = 0) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); processor ??= Process.DefaultStringProcessor; @@ -333,7 +333,7 @@ public IEnumerable> ExtractSortedBy( public ExtractedResult ExtractOne( IEnumerable choices, Func processor = null, - int cutoff = 0) + double cutoff = 0) { return CachedScorerProcessExecutor.ExtractOne( choices, processor ?? Process.DefaultStringProcessor, _options.CachedScorer, cutoff, @@ -347,7 +347,7 @@ public ExtractedResult ExtractOneBy( IEnumerable choices, Func extractor, Func processor = null, - int cutoff = 0) + double cutoff = 0) { if (extractor == null) throw new ArgumentNullException(nameof(extractor)); processor ??= Process.DefaultStringProcessor; diff --git a/FuzzySharp/SimilarityRatio/Scorer/Composite/CachedWeightedRatioScorer.cs b/FuzzySharp/SimilarityRatio/Scorer/Composite/CachedWeightedRatioScorer.cs index 88f7f33..e72e496 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/Composite/CachedWeightedRatioScorer.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/Composite/CachedWeightedRatioScorer.cs @@ -25,7 +25,7 @@ public CachedWeightedRatioScorer(string input1) _tokenSetScorer = new CachedTokenSetScorer(_input1); } - public int Score(string input2) + public double Score(string input2) { int len1 = _input1.Length; int len2 = input2.Length; @@ -39,7 +39,7 @@ public int Score(string input2) double unbaseScale = UNBASE_SCALE; double partialScale = PARTIAL_SCALE; - int baseRatio = _baseRatioScorer.Score(input2); + double baseRatio = _baseRatioScorer.Score(input2); double lenRatio = (double)Math.Max(len1, len2) / Math.Min(len1, len2); // if strings are similar length don't use partials @@ -54,16 +54,16 @@ public int Score(string input2) double partialSor = _tokenSortScorer.Score(input2) * unbaseScale * partialScale; double partialSet = _tokenSetScorer.Score(input2) * unbaseScale * partialScale; - return (int)Math.Round(Math.Max(baseRatio, Math.Max(partial, Math.Max(partialSor, partialSet)))); + return Math.Max(baseRatio, Math.Max(partial, Math.Max(partialSor, partialSet))); } double tokenSort = _tokenSortScorer.Score(input2) * unbaseScale; double tokenSet = _tokenSetScorer.Score(input2) * unbaseScale; - return (int)Math.Round(Math.Max(baseRatio, Math.Max(tokenSort, tokenSet))); + return Math.Max(baseRatio, Math.Max(tokenSort, tokenSet)); } public void Dispose() { _strategy.Dispose(); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/Composite/WeightedRatioScorer.cs b/FuzzySharp/SimilarityRatio/Scorer/Composite/WeightedRatioScorer.cs index 6ead8f3..434f1b5 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/Composite/WeightedRatioScorer.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/Composite/WeightedRatioScorer.cs @@ -8,7 +8,7 @@ public class WeightedRatioScorer : ScorerBase private static readonly double PARTIAL_SCALE = .90; private static readonly bool TRY_PARTIALS = true; - public override int Score(string input1, string input2) + public override double Score(string input1, string input2) { int len1 = input1.Length; int len2 = input2.Length; @@ -22,7 +22,7 @@ public override int Score(string input1, string input2) double unbaseScale = UNBASE_SCALE; double partialScale = PARTIAL_SCALE; - int baseRatio = Fuzz.Ratio(input1, input2); + double baseRatio = Fuzz.Ratio(input1, input2); double lenRatio = (double)Math.Max(len1, len2) / Math.Min(len1, len2); // if strings are similar length don't use partials @@ -37,11 +37,11 @@ public override int Score(string input1, string input2) double partialSor = Fuzz.TokenSortRatio(input1, input2) * unbaseScale * partialScale; double partialSet = Fuzz.TokenSetRatio(input1, input2) * unbaseScale * partialScale; - return (int)Math.Round(Math.Max(baseRatio, Math.Max(partial, Math.Max(partialSor, partialSet)))); + return Math.Max(baseRatio, Math.Max(partial, Math.Max(partialSor, partialSet))); } double tokenSort = Fuzz.TokenSortRatio(input1, input2) * unbaseScale; double tokenSet = Fuzz.TokenSetRatio(input1, input2) * unbaseScale; - return (int)Math.Round(Math.Max(baseRatio, Math.Max(tokenSort, tokenSet))); + return Math.Max(baseRatio, Math.Max(tokenSort, tokenSet)); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/Generic/CachedScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/Generic/CachedScorerBase.cs index 6c3bc30..62647f6 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/Generic/CachedScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/Generic/CachedScorerBase.cs @@ -4,5 +4,5 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.Generic; public abstract class CachedScorerBase : ICachedRatioScorer where T : IEquatable { - public abstract int Score(T[] input2); -} \ No newline at end of file + public abstract double Score(T[] input2); +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/Generic/IRatioScorer.cs b/FuzzySharp/SimilarityRatio/Scorer/Generic/IRatioScorer.cs index 591f65c..459ac9c 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/Generic/IRatioScorer.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/Generic/IRatioScorer.cs @@ -4,10 +4,10 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.Generic; public interface IRatioScorer where T : IEquatable { - int Score(T[] input1, T[] input2); + double Score(T[] input1, T[] input2); } public interface ICachedRatioScorer where T : IEquatable { - int Score(T[] input2); -} \ No newline at end of file + double Score(T[] input2); +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/Generic/ScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/Generic/ScorerBase.cs index c7f5e81..1406c26 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/Generic/ScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/Generic/ScorerBase.cs @@ -4,5 +4,5 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.Generic; public abstract class ScorerBase : IRatioScorer where T : IEquatable { - public abstract int Score(T[] input1, T[] input2); -} \ No newline at end of file + public abstract double Score(T[] input1, T[] input2); +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/ICachedRatioScorer.cs b/FuzzySharp/SimilarityRatio/Scorer/ICachedRatioScorer.cs index 7d59a27..b71a0d6 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/ICachedRatioScorer.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/ICachedRatioScorer.cs @@ -4,5 +4,5 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer; public interface ICachedRatioScorer: IDisposable { - int Score(string input2); -} \ No newline at end of file + double Score(string input2); +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/IRatioScorer.cs b/FuzzySharp/SimilarityRatio/Scorer/IRatioScorer.cs index 22e904f..632b95e 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/IRatioScorer.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/IRatioScorer.cs @@ -4,6 +4,6 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer; public interface IRatioScorer { - int Score(string input1, string input2); - int Score(string input1, string input2, Func preprocessor); -} \ No newline at end of file + double Score(string input1, string input2); + double Score(string input1, string input2, Func preprocessor); +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/ScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/ScorerBase.cs index 281c679..c1e18f3 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/ScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/ScorerBase.cs @@ -5,13 +5,13 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer; public abstract class ScorerBase : IRatioScorer { - public abstract int Score(string input1, string input2); + public abstract double Score(string input1, string input2); - public int Score(string input1, string input2, Func preprocessor) + public double Score(string input1, string input2, Func preprocessor) { preprocessor ??= StringPreprocessor.Full; input1 = preprocessor(input1); input2 = preprocessor(input2); return Score(input1, input2); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Generic/StrategySensitiveScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Generic/StrategySensitiveScorerBase.cs index e16276e..601eb9a 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Generic/StrategySensitiveScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Generic/StrategySensitiveScorerBase.cs @@ -5,5 +5,5 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.StrategySensitive.Generic; public abstract class StrategySensitiveScorerBase : ScorerBase where T : IEquatable { - protected abstract Func Scorer { get; } -} \ No newline at end of file + protected abstract Func Scorer { get; } +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Simple/CachedSimpleRatioScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Simple/CachedSimpleRatioScorerBase.cs index 7fdd20c..de7fc81 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Simple/CachedSimpleRatioScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Simple/CachedSimpleRatioScorerBase.cs @@ -5,8 +5,8 @@ public abstract class CachedSimpleRatioScorerBase : ICachedRatioScorer protected abstract CachedScorer Scorer { get; } public abstract void Dispose(); - public int Score(string input2) + public double Score(string input2) { return Scorer(input2); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Simple/SimpleRatioScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Simple/SimpleRatioScorerBase.cs index 3a063c3..c8f4376 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Simple/SimpleRatioScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/Simple/SimpleRatioScorerBase.cs @@ -2,8 +2,8 @@ public abstract class SimpleRatioScorerBase : StrategySensitiveScorerBase { - public override int Score(string input1, string input2) + public override double Score(string input1, string input2) { return Scorer(input1, input2); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenAbbreviation/TokenAbbreviationScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenAbbreviation/TokenAbbreviationScorerBase.cs index 852763e..37c62c4 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenAbbreviation/TokenAbbreviationScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenAbbreviation/TokenAbbreviationScorerBase.cs @@ -6,7 +6,7 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.StrategySensitive; public abstract class TokenAbbreviationScorerBase : StrategySensitiveScorerBase { - public override int Score(string shorter, string longer) + public override double Score(string shorter, string longer) { SequenceUtils.SwapIfSourceIsLonger(ref shorter, ref longer); @@ -29,7 +29,7 @@ public override int Score(string shorter, string longer) var allPermutations = tokensLonger.PermutationsOfSize(tokensShorter.Count); - int maxScore = 0; + double maxScore = 0; foreach (var permutation in allPermutations) { @@ -44,7 +44,7 @@ public override int Score(string shorter, string longer) sum += score; } } - var avgScore = (int)(sum / tokensShorter.Count); + var avgScore = sum / tokensShorter.Count; if (avgScore > maxScore) { maxScore = avgScore; @@ -75,4 +75,4 @@ private static bool StringContainsInOrder(ReadOnlySpan s1, ReadOnlySpan preproces _scorer = new CachedDefaultRatioStrategy(tokens1); } - public int Score(string input2) + public double Score(string input2) { input2 = _preprocessor(input2); var tokens2 = input2.GetSortedWords(); @@ -29,4 +29,4 @@ public void Dispose() { _scorer.Dispose(); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/PartialTokenDifferenceScorer.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/PartialTokenDifferenceScorer.cs index 45ba63d..09a7b1d 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/PartialTokenDifferenceScorer.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/PartialTokenDifferenceScorer.cs @@ -5,5 +5,5 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.StrategySensitive; public sealed class PartialTokenDifferenceScorer : TokenDifferenceScorerBase { - protected override Func Scorer => static (strings1, strings2) => PartialRatioStrategy.Calculate(strings1.AsSpan(), strings2.AsSpan()); -} \ No newline at end of file + protected override Func Scorer => static (strings1, strings2) => PartialRatioStrategy.Calculate(strings1.AsSpan(), strings2.AsSpan()); +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/TokenDifferenceScorer.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/TokenDifferenceScorer.cs index ea51b78..f1b5c31 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/TokenDifferenceScorer.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/TokenDifferenceScorer.cs @@ -5,5 +5,5 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.StrategySensitive; public sealed class TokenDifferenceScorer : TokenDifferenceScorerBase { - protected override Func Scorer => DefaultRatioStrategy.Calculate; -} \ No newline at end of file + protected override Func Scorer => DefaultRatioStrategy.Calculate; +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/TokenDifferenceScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/TokenDifferenceScorerBase.cs index 0ebaaa3..f042aa8 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/TokenDifferenceScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenDifference/TokenDifferenceScorerBase.cs @@ -7,12 +7,12 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.StrategySensitive; public abstract class TokenDifferenceScorerBase : StrategySensitiveScorerBase, IRatioScorer { - public override int Score(string[] input1, string[] input2) + public override double Score(string[] input1, string[] input2) { return Scorer(input1, input2); } - public int Score(string input1, string input2) + public double Score(string input1, string input2) { var tokens1 = input1.GetSortedWords(); var tokens2 = input2.GetSortedWords(); @@ -21,7 +21,7 @@ public int Score(string input1, string input2) } - public int Score(string input1, string input2, Func preprocessor) + public double Score(string input1, string input2, Func preprocessor) { preprocessor ??= StringPreprocessor.Full; input1 = preprocessor(input1); @@ -29,4 +29,4 @@ public int Score(string input1, string input2, Func preprocessor return Score(input1, input2); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenInitialism/TokenInitialismScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenInitialism/TokenInitialismScorerBase.cs index 57413c3..9a619a1 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenInitialism/TokenInitialismScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenInitialism/TokenInitialismScorerBase.cs @@ -4,7 +4,7 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.StrategySensitive; public abstract class TokenInitialismScorerBase : StrategySensitiveScorerBase { - public override int Score(string input1, string input2) + public override double Score(string input1, string input2) { string shorter; string longer; @@ -29,4 +29,4 @@ public override int Score(string input1, string input2) return Scorer(initials, shorter); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSet/CachedTokenSetScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSet/CachedTokenSetScorerBase.cs index cf2ce99..ed3fc2b 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSet/CachedTokenSetScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSet/CachedTokenSetScorerBase.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Raffinert.FuzzySharp.Extensions; namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.StrategySensitive; @@ -10,18 +9,18 @@ public abstract class CachedTokenSetScorerBase(string input1) : ICachedRatioScor private HashSet Tokens1 { get; set; } = new HashSet(input1.SplitByAnySpace()); protected abstract FuzzySharp.Scorer Scorer { get; } - public int Score(string input2) + public double Score(string input2) { var tokens2 = new HashSet(input2.SplitByAnySpace()); var tokens1 = new HashSet(Tokens1); - var intersection = GetIntersectionAndExcept(tokens1, tokens2); + var intersection = TokenSetScorerHelpers.GetIntersectionAndExcept(tokens1, tokens2); intersection.Sort(); var sortedIntersection = string.Join(" ", intersection); - var sortedDiff1To2 = (sortedIntersection + " " + string.Join(" ", tokens1.OrderBy(s => s))).Trim(); - var sortedDiff2To1 = (sortedIntersection + " " + string.Join(" ", tokens2.OrderBy(s => s))).Trim(); + var sortedDiff1To2 = TokenSetScorerHelpers.JoinIntersectionAndDifference(sortedIntersection, tokens1); + var sortedDiff2To1 = TokenSetScorerHelpers.JoinIntersectionAndDifference(sortedIntersection, tokens2); var score1 = Scorer(sortedIntersection, sortedDiff1To2); var score2 = Scorer(sortedIntersection, sortedDiff2To1); @@ -30,23 +29,7 @@ public int Score(string input2) return Math.Max(score1, Math.Max(score2, score3)); } - private static List GetIntersectionAndExcept(HashSet first, HashSet second) - { - List intersection = []; - - foreach (var item in first.ToArray()) - { - if (second.Remove(item)) - { - first.Remove(item); - intersection.Add(item); - } - } - - return intersection; - } - public void Dispose() { } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSet/TokenSetScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSet/TokenSetScorerBase.cs index 818b10b..39fb203 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSet/TokenSetScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSet/TokenSetScorerBase.cs @@ -1,13 +1,12 @@ using Raffinert.FuzzySharp.Extensions; using System; using System.Collections.Generic; -using System.Linq; namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.StrategySensitive; public abstract class TokenSetScorerBase : StrategySensitiveScorerBase { - public override int Score(string input1, string input2) + public override double Score(string input1, string input2) { var tokens1 = new HashSet(input1.SplitByAnySpace()); var tokens2 = new HashSet(input2.SplitByAnySpace()); @@ -15,10 +14,9 @@ public override int Score(string input1, string input2) var intersection = GetIntersectionAndExcept(tokens1, tokens2); intersection.Sort(); - var sortedIntersection = string.Join(" ", intersection); - var sortedDiff1To2 = (sortedIntersection + " " + string.Join(" ", tokens1.OrderBy(s => s))).Trim(); - var sortedDiff2To1 = (sortedIntersection + " " + string.Join(" ", tokens2.OrderBy(s => s))).Trim(); + var sortedDiff1To2 = TokenSetScorerHelpers.JoinIntersectionAndDifference(sortedIntersection, tokens1); + var sortedDiff2To1 = TokenSetScorerHelpers.JoinIntersectionAndDifference(sortedIntersection, tokens2); var score1 = Scorer(sortedIntersection, sortedDiff1To2); var score2 = Scorer(sortedIntersection, sortedDiff2To1); @@ -27,19 +25,46 @@ public override int Score(string input1, string input2) return Math.Max(score1, Math.Max(score2, score3)); } - private static List GetIntersectionAndExcept(HashSet first, HashSet second) + private static List GetIntersectionAndExcept(HashSet first, HashSet second) => + TokenSetScorerHelpers.GetIntersectionAndExcept(first, second); +} + +internal static class TokenSetScorerHelpers +{ + internal static List GetIntersectionAndExcept(HashSet first, HashSet second) { - List intersection = []; + var intersection = new List(Math.Min(first.Count, second.Count)); - foreach (var item in first.ToArray()) + // It is safe to mutate the second set while enumerating the first. + // Remove the matched items from the first set after enumeration, which + // avoids allocating first.ToArray() on every score. + foreach (var item in first) { if (second.Remove(item)) { - first.Remove(item); intersection.Add(item); } } + first.ExceptWith(intersection); return intersection; } -} \ No newline at end of file + + internal static string JoinIntersectionAndDifference(string sortedIntersection, HashSet difference) + { + if (difference.Count == 0) + return sortedIntersection; + + var sortedDifference = JoinSorted(difference); + return sortedIntersection.Length == 0 + ? sortedDifference + : string.Concat(sortedIntersection, " ", sortedDifference); + } + + private static string JoinSorted(IEnumerable tokens) + { + var sortedTokens = new List(tokens); + sortedTokens.Sort(); + return string.Join(" ", sortedTokens); + } +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSort/CachedTokenSortScorer.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSort/CachedTokenSortScorer.cs index 9c07359..66913b0 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSort/CachedTokenSortScorer.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSort/CachedTokenSortScorer.cs @@ -21,7 +21,7 @@ public CachedTokenSortScorer(ICachedStrategy strategy, bool isStrategyOwner = fa _isStrategyOwner = isStrategyOwner; } - public int Score(string input2) + public double Score(string input2) { var sorted2 = input2.NormalizeSpacesAndSort(); return _strategy.Calculate(sorted2); @@ -34,4 +34,4 @@ public void Dispose() _strategy.Dispose(); } } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSort/TokenSortScorerBase.cs b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSort/TokenSortScorerBase.cs index 004dd19..cccb27a 100644 --- a/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSort/TokenSortScorerBase.cs +++ b/FuzzySharp/SimilarityRatio/Scorer/StrategySensitive/TokenSort/TokenSortScorerBase.cs @@ -4,11 +4,11 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Scorer.StrategySensitive; public abstract class TokenSortScorerBase : StrategySensitiveScorerBase { - public override int Score(string input1, string input2) + public override double Score(string input1, string input2) { var sorted1 = input1.NormalizeSpacesAndSort(); var sorted2 = input2.NormalizeSpacesAndSort(); return Scorer(sorted1, sorted2); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Strategy/CachedDefaultRatioStrategy.cs b/FuzzySharp/SimilarityRatio/Strategy/CachedDefaultRatioStrategy.cs index 590d41a..2245a3d 100644 --- a/FuzzySharp/SimilarityRatio/Strategy/CachedDefaultRatioStrategy.cs +++ b/FuzzySharp/SimilarityRatio/Strategy/CachedDefaultRatioStrategy.cs @@ -15,7 +15,7 @@ public CachedDefaultRatioStrategy(string input1, Func preprocess _indel = new Indel(_preprocessor(input1)); } - public int Calculate(string input2) + public double Calculate(string input2) { var processedInput2 = _preprocessor(input2); if (processedInput2.Length == 0) @@ -23,11 +23,11 @@ public int Calculate(string input2) return 0; } - return (int)Math.Round(100 * _indel.NormalizedSimilarityWith(processedInput2)); + return 100 * _indel.NormalizedSimilarityWith(processedInput2); } public void Dispose() { _indel.Dispose(); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Strategy/DefaultRatioStrategy.cs b/FuzzySharp/SimilarityRatio/Strategy/DefaultRatioStrategy.cs index 03e5228..a6041c6 100644 --- a/FuzzySharp/SimilarityRatio/Strategy/DefaultRatioStrategy.cs +++ b/FuzzySharp/SimilarityRatio/Strategy/DefaultRatioStrategy.cs @@ -4,7 +4,7 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Strategy; internal static class DefaultRatioStrategy { - public static int Calculate(string input1, string input2) + public static double Calculate(string input1, string input2) { if (input1.Length == 0 || input2.Length == 0) { @@ -14,6 +14,6 @@ public static int Calculate(string input1, string input2) var input1Span = input1.AsSpan(); var input2Span = input2.AsSpan(); - return (int)Math.Round(100 * Indel.NormalizedSimilarity(input1Span, input2Span)); + return 100 * Indel.NormalizedSimilarityChar(input1Span, input2Span); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Strategy/Generic/CachedDefaultRatioStrategyT.cs b/FuzzySharp/SimilarityRatio/Strategy/Generic/CachedDefaultRatioStrategyT.cs index 6f7c8ab..03fdd36 100644 --- a/FuzzySharp/SimilarityRatio/Strategy/Generic/CachedDefaultRatioStrategyT.cs +++ b/FuzzySharp/SimilarityRatio/Strategy/Generic/CachedDefaultRatioStrategyT.cs @@ -7,14 +7,14 @@ internal class CachedDefaultRatioStrategy(T[] input1) : IDisposable { private readonly IndelT _indel = new(input1); - public int Calculate(T[] input2) + public double Calculate(T[] input2) { if (input1.Length == 0 || input2.Length == 0) { return 0; } - var result = (int)Math.Round(100 * _indel.NormalizedSimilarityWith(input2)); + var result = 100 * _indel.NormalizedSimilarityWith(input2); return result; } @@ -23,4 +23,4 @@ public void Dispose() { _indel.Dispose(); } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Strategy/Generic/DefaultRatioStrategyT.cs b/FuzzySharp/SimilarityRatio/Strategy/Generic/DefaultRatioStrategyT.cs index 327f887..7926a2b 100644 --- a/FuzzySharp/SimilarityRatio/Strategy/Generic/DefaultRatioStrategyT.cs +++ b/FuzzySharp/SimilarityRatio/Strategy/Generic/DefaultRatioStrategyT.cs @@ -4,15 +4,15 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Strategy.Generic; internal static class DefaultRatioStrategy where T : IEquatable { - public static int Calculate(T[] input1, T[] input2) + public static double Calculate(T[] input1, T[] input2) { if (input1.Length == 0 || input2.Length == 0) { return 0; } - var result = (int)Math.Round(100 * Indel.NormalizedSimilarity((ReadOnlySpan)input1, (ReadOnlySpan)input2)); + var result = 100 * Indel.NormalizedSimilarity((ReadOnlySpan)input1, (ReadOnlySpan)input2); return result; } -} \ No newline at end of file +} diff --git a/FuzzySharp/SimilarityRatio/Strategy/Generic/PartialRatioStrategyT.cs b/FuzzySharp/SimilarityRatio/Strategy/Generic/PartialRatioStrategyT.cs index 3a3cd85..226325f 100644 --- a/FuzzySharp/SimilarityRatio/Strategy/Generic/PartialRatioStrategyT.cs +++ b/FuzzySharp/SimilarityRatio/Strategy/Generic/PartialRatioStrategyT.cs @@ -12,7 +12,7 @@ internal static class PartialRatioStrategy where T : IEquatable /// Searches for the optimal alignment of the shorter span in the longer span /// and returns the partial fuzz.ratio for that alignment, as a value in [0…100]. /// - public static int Calculate(ReadOnlySpan input1, ReadOnlySpan input2) + public static double Calculate(ReadOnlySpan input1, ReadOnlySpan input2) { if (input1.Length == 0 || input2.Length == 0) { @@ -21,18 +21,17 @@ public static int Calculate(ReadOnlySpan input1, ReadOnlySpan input2) var alignment = PartialRatioAlignment(input1, input2); - return (int)Math.Round(alignment.Score); + return alignment.Score; } /// /// Searches for the optimal alignment of the shorter span in the longer span - /// and returns a ScoreAlignment (with a score in [0…100]) or null if below cutoff. + /// and returns a ScoreAlignment with a score in [0…100]. /// internal static ScoreAlignment PartialRatioAlignment( ReadOnlySpan shorter, ReadOnlySpan longer, - Processor processor = null, - double? scoreCutoff = null + Processor processor = null ) { // 1) Optional preprocessing @@ -42,30 +41,22 @@ internal static ScoreAlignment PartialRatioAlignment( processor(ref longer); } - // 2) Normalize cutoff to 0…100 - double cutoff100 = scoreCutoff.GetValueOrDefault(); - - // 3) Handle both empty → perfect match + // 2) Handle both empty → perfect match if (shorter.IsEmpty && longer.IsEmpty) { return new ScoreAlignment(100.0, 0, 0, 0, 0); } - // 4) Determine shorter/longer + // 3) Determine shorter/longer var swapped = SequenceUtils.SwapIfSourceIsLonger(ref shorter, ref longer); - // 5) Call the core PartialRatioImpl with cutoff in [0..1] - double fracCutoff = cutoff100 / 100.0; - var res = PartialRatioImpl(shorter, longer, fracCutoff); + // 4) Call the core PartialRatioImpl + var res = PartialRatioImpl(shorter, longer); - // 6) If same-length inputs and not perfect, try the other direction + // 5) If same-length inputs and not perfect, try the other direction if (res.Score < 100.0 && shorter.Length == longer.Length) { - // bump cutoff to whatever we got - double newCutoff100 = Math.Max(cutoff100, res.Score); - double newFracCutoff = newCutoff100 / 100.0; - - var res2 = PartialRatioImpl(longer, shorter, newFracCutoff); + var res2 = PartialRatioImpl(longer, shorter, res.Score / 100.0); if (res2.Score > res.Score) { // swap src/dest @@ -79,11 +70,7 @@ internal static ScoreAlignment PartialRatioAlignment( } } - // 7) If below cutoff, return null - if (res.Score < cutoff100) - return res with { Score = 0 }; - - // 8) If we swapped at step 4, swap back the src/dest in the result + // 6) If we swapped at step 3, swap back the src/dest in the result if (swapped) { res = new ScoreAlignment( @@ -106,7 +93,7 @@ internal static ScoreAlignment PartialRatioAlignment( private static ScoreAlignment PartialRatioImpl( ReadOnlySpan s1, ReadOnlySpan s2, - double? scoreCutoff = null + double cutoff = 0.0 ) { int len1 = s1.Length, len2 = s2.Length; @@ -114,7 +101,7 @@ private static ScoreAlignment PartialRatioImpl( throw new ArgumentException("Requires s1.Length <= s2.Length"); using var patternMatchVector = PatternMatchVector.Create(s1); - return PartialRatioImpl(patternMatchVector, s2, scoreCutoff); + return PartialRatioImpl(patternMatchVector, s2, cutoff); } /// @@ -124,7 +111,7 @@ private static ScoreAlignment PartialRatioImpl( [MethodImpl(MethodImplOptions.AggressiveInlining)] private static ScoreAlignment PartialRatioImpl(IPatternMatchVector s1Vector, ReadOnlySpan s2, - double? scoreCutoff = null) + double cutoff = 0.0) { int len1 = s1Vector.Length, len2 = s2.Length; if (len1 > len2) @@ -136,8 +123,6 @@ private static ScoreAlignment PartialRatioImpl(IPatternMatchVector s1Vector, if (len1 == 0 || len2 == 0) return res; - double cutoff = scoreCutoff ?? 0.0; - if (len2 > len1) { int maximum = len1 + len1; diff --git a/FuzzySharp/SimilarityRatio/Strategy/ICachedStrategy.cs b/FuzzySharp/SimilarityRatio/Strategy/ICachedStrategy.cs index e7fcc30..28f41c7 100644 --- a/FuzzySharp/SimilarityRatio/Strategy/ICachedStrategy.cs +++ b/FuzzySharp/SimilarityRatio/Strategy/ICachedStrategy.cs @@ -4,5 +4,5 @@ namespace Raffinert.FuzzySharp.SimilarityRatio.Strategy; public interface ICachedStrategy : IDisposable { - int Calculate(string input2); -} \ No newline at end of file + double Calculate(string input2); +} diff --git a/FuzzySharp/SimilarityRatio/Strategy/PartialRatioStrategy.cs b/FuzzySharp/SimilarityRatio/Strategy/PartialRatioStrategy.cs index 9a4eb74..a1ba3f9 100644 --- a/FuzzySharp/SimilarityRatio/Strategy/PartialRatioStrategy.cs +++ b/FuzzySharp/SimilarityRatio/Strategy/PartialRatioStrategy.cs @@ -9,7 +9,7 @@ internal static class PartialRatioStrategy /// Searches for the optimal alignment of the shorter span in the longer span /// and returns the partial fuzz.ratio for that alignment, as a value in [0…100]. /// - public static int Calculate(string input1, string input2) + public static double Calculate(string input1, string input2) { if (input1.Length == 0 || input2.Length == 0) { @@ -20,4 +20,4 @@ public static int Calculate(string input1, string input2) return score; } -} \ No newline at end of file +} diff --git a/FuzzySharp/Utils/PatternMatchVector.cs b/FuzzySharp/Utils/PatternMatchVector.cs index e4b76f3..92cf700 100644 --- a/FuzzySharp/Utils/PatternMatchVector.cs +++ b/FuzzySharp/Utils/PatternMatchVector.cs @@ -13,9 +13,9 @@ public interface IPatternMatchVector : IDisposable where TKey : notnull bool ContainsKey(TKey key); } -internal interface IPatternMatchVectorImpl : IPatternMatchVector where TKey : IEquatable +internal interface IPatternMatchVectorImpl : IPatternMatchVector where TKey : IEquatable { - void AddBit(TKey key, int position); + void Populate(ReadOnlySpan source); } public sealed class PatternMatchVector @@ -28,12 +28,7 @@ public static IPatternMatchVector Create(ReadOnlySpan source) where T : ? (IPatternMatchVectorImpl)(object)new PatternMatchVectorChar(source.Length, estimatedNonAsciiCharCount: 8, blocks: blocks) : new PatternMatchVector(source.Length, 64, blocks); - var i = 0; - - foreach (var item in source) - { - pmv.AddBit(item, i++); - } + pmv.Populate(source); return pmv; } @@ -65,11 +60,19 @@ public PatternMatchVector(int length, int estimatedCharCount, int blocks, ArrayP public int Length { get; } public int Blocks { get; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddBit(T key, int position) + public void Populate(ReadOnlySpan source) { if (_disposed) throw new ObjectDisposedException(nameof(PatternMatchVector)); + for (var position = 0; position < source.Length; position++) + { + AddBit(source[position], position); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddBit(T key, int position) + { ref var index = ref _indexMap.GetOrAddValueRef(key); if (index == 0) @@ -148,4 +151,4 @@ public void Dispose() _disposed = true; } -} \ No newline at end of file +} diff --git a/FuzzySharp/Utils/PatternMatchVectorChar.cs b/FuzzySharp/Utils/PatternMatchVectorChar.cs index f1a9295..26b1d84 100644 --- a/FuzzySharp/Utils/PatternMatchVectorChar.cs +++ b/FuzzySharp/Utils/PatternMatchVectorChar.cs @@ -15,14 +15,14 @@ namespace Raffinert.FuzzySharp.Utils; internal sealed class PatternMatchVectorChar : IPatternMatchVectorImpl { private readonly ArrayPool _pool; - private readonly DictionarySlimPooled _indexMap; // non-ASCII only (1-based) + private DictionarySlimPooled _indexMap; // non-ASCII only (1-based), allocated lazily private readonly ulong[] _fixedData; // Single rental: [asciiMasks (256*blocks) | asciiPresence (4) | zeroMask (blocks)] private readonly int _asciiMasksOffset; private readonly int _asciiPresenceOffset; private readonly int _zeroMaskOffset; - private ulong[] _buffer; // capacity * blocks for non-ASCII (separate rental, can grow) + private ulong[] _buffer; // capacity * blocks for non-ASCII (rented lazily, can grow) private readonly int _blocks; private int _capacity; @@ -55,39 +55,74 @@ public PatternMatchVectorChar(int length, int estimatedNonAsciiCharCount, int bl // Clear all fixed data Array.Clear(_fixedData, 0, totalFixedSize); - // Non-ASCII buffer (separate rental, can grow) + // Non-ASCII state is allocated on first use so ASCII-only patterns avoid + // an extra object allocation and three pool operations. _capacity = Math.Max(2, estimatedNonAsciiCharCount); - _buffer = _pool.Rent(_capacity * _blocks); - // Intentionally not clearing entire _buffer. Each new key slice is cleared once. - - _indexMap = new DictionarySlimPooled(estimatedNonAsciiCharCount); _next = 0; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void AddBit(char key, int position) + public void Populate(ReadOnlySpan source) { if (_disposed) throw new ObjectDisposedException(nameof(PatternMatchVectorChar)); - int block = position >> 6; - int offset = position & 63; - - // Fast path: ASCII / extended ASCII - if ((uint)key <= 255u) + if (_blocks == 1) { - _fixedData[_asciiMasksOffset + (key * _blocks) + block] |= 1UL << offset; - - // Update presence bitmap - int presenceIndex = key >> 6; - int presenceOffset = key & 63; - _fixedData[_asciiPresenceOffset + presenceIndex] |= 1UL << presenceOffset; - + PopulateSingleBlock(source); return; } - // Non-ASCII: dictionary -> index -> buffer slice - ref int index = ref _indexMap.GetOrAddValueRef(key); + for (var block = 0; block < _blocks; block++) + { + var blockStart = block << 6; + var blockLength = Math.Min(64, source.Length - blockStart); + + for (var offset = 0; offset < blockLength; offset++) + { + var key = source[blockStart + offset]; + var bit = 1UL << offset; + + if (key <= 255u) + { + _fixedData[_asciiMasksOffset + (key * _blocks) + block] |= bit; + _fixedData[_asciiPresenceOffset + (key >> 6)] |= 1UL << (key & 63); + } + else + { + AddNonAsciiBit(key, block, bit); + } + } + } + } + + private void PopulateSingleBlock(ReadOnlySpan source) + { + for (var position = 0; position < source.Length; position++) + { + var key = source[position]; + var bit = 1UL << position; + + if (key <= 255u) + { + _fixedData[_asciiMasksOffset + key] |= bit; + _fixedData[_asciiPresenceOffset + (key >> 6)] |= 1UL << (key & 63); + } + else + { + AddNonAsciiBit(key, 0, bit); + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void AddNonAsciiBit(char key, int block, ulong bit) + { + if (_indexMap == null) + { + InitializeNonAsciiStorage(); + } + + ref int index = ref _indexMap!.GetOrAddValueRef(key); if (index == 0) { @@ -100,7 +135,14 @@ public void AddBit(char key, int position) Array.Clear(_buffer, (index - 1) * _blocks, _blocks); } - _buffer[(index - 1) * _blocks + block] |= 1UL << offset; + _buffer[(index - 1) * _blocks + block] |= bit; + } + + private void InitializeNonAsciiStorage() + { + _buffer = _pool.Rent(_capacity * _blocks); + // Intentionally do not clear the entire buffer. Each new key slice is cleared once. + _indexMap = new DictionarySlimPooled(_capacity); } @@ -109,7 +151,7 @@ public bool TryGetMask(char key, out ReadOnlySpan mask) { if (_disposed) throw new ObjectDisposedException(nameof(PatternMatchVectorChar)); - if ((uint)key <= 255u) + if (key <= 255u) { // Check presence bitmap instead of scanning the mask int presenceIndex = key >> 6; @@ -125,7 +167,7 @@ public bool TryGetMask(char key, out ReadOnlySpan mask) return false; } - if (_indexMap.TryGetValue(key, out int index)) + if (_indexMap != null && _indexMap.TryGetValue(key, out int index)) { mask = new ReadOnlySpan(_buffer, (index - 1) * _blocks, _blocks); return true; @@ -138,7 +180,21 @@ public bool TryGetMask(char key, out ReadOnlySpan mask) [MethodImpl(MethodImplOptions.AggressiveInlining)] public ReadOnlySpan GetOrZero(char key) { - return TryGetMask(key, out var mask) ? mask : new ReadOnlySpan(_fixedData, _zeroMaskOffset, _blocks); + if (_disposed) throw new ObjectDisposedException(nameof(PatternMatchVectorChar)); + + // Dense ASCII masks are already zero for absent characters, so the + // presence-bitmap lookup performed by TryGetMask is unnecessary here. + if (key <= 255u) + { + return new ReadOnlySpan(_fixedData, _asciiMasksOffset + (key * _blocks), _blocks); + } + + if (_indexMap != null && _indexMap.TryGetValue(key, out int index)) + { + return new ReadOnlySpan(_buffer, (index - 1) * _blocks, _blocks); + } + + return new ReadOnlySpan(_fixedData, _zeroMaskOffset, _blocks); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -156,14 +212,14 @@ public bool ContainsKey(char key) { if (_disposed) throw new ObjectDisposedException(nameof(PatternMatchVectorChar)); - if ((uint)key <= 255u) + if (key <= 255u) { int presenceIndex = key >> 6; int presenceOffset = key & 63; return (_fixedData[_asciiPresenceOffset + presenceIndex] & (1UL << presenceOffset)) != 0; } - return _indexMap.ContainsKey(key); + return _indexMap != null && _indexMap.ContainsKey(key); } [MethodImpl(MethodImplOptions.NoInlining)] @@ -185,11 +241,14 @@ public void Dispose() { if (_disposed) return; - _indexMap.Dispose(); - _pool.Return(_fixedData); - _pool.Return(_buffer); + + if (_indexMap != null) + { + _indexMap.Dispose(); + _pool.Return(_buffer); + } _disposed = true; } -} \ No newline at end of file +} diff --git a/FuzzySharp/Utils/Polyfill.cs b/FuzzySharp/Utils/Polyfill.cs index 9cde1ae..e3fd0f9 100644 --- a/FuzzySharp/Utils/Polyfill.cs +++ b/FuzzySharp/Utils/Polyfill.cs @@ -24,9 +24,7 @@ public static void ArrayFill(T[] array, T value, int startIndex, int count) { #if NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER Array.Fill(array, value, startIndex, count); - return; -#endif - +#else if (array == null) throw new ArgumentNullException(nameof(array)); @@ -40,5 +38,6 @@ public static void ArrayFill(T[] array, T value, int startIndex, int count) { array[i] = value; } +#endif } } \ No newline at end of file diff --git a/FuzzySharp/Utils/SequenceUtils.cs b/FuzzySharp/Utils/SequenceUtils.cs index 479dc1d..7153f67 100644 --- a/FuzzySharp/Utils/SequenceUtils.cs +++ b/FuzzySharp/Utils/SequenceUtils.cs @@ -6,6 +6,39 @@ namespace Raffinert.FuzzySharp.Utils; internal static class SequenceUtils { + /// + /// Removes the shared prefix and suffix from two character spans. + /// This is deliberately non-generic: it is on the string scoring hot path, + /// and direct character comparisons avoid the generic equality comparer. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (int PrefixLength, int SuffixLength) TrimCommonAffix( + ref ReadOnlySpan source, + ref ReadOnlySpan target) + { + var startIndex = 0; + var sourceEnd = source.Length; + var targetEnd = target.Length; + + while (startIndex < sourceEnd && startIndex < targetEnd && source[startIndex] == target[startIndex]) + { + startIndex++; + } + + while (startIndex < sourceEnd && startIndex < targetEnd && + source[sourceEnd - 1] == target[targetEnd - 1]) + { + sourceEnd--; + targetEnd--; + } + + var suffixLength = source.Length - sourceEnd; + source = source[startIndex..sourceEnd]; + target = target[startIndex..targetEnd]; + + return (startIndex, suffixLength); + } + public static int CommonPrefix(ReadOnlySpan s1, ReadOnlySpan s2) where T : IEquatable { int prefixLength = 0; @@ -71,10 +104,11 @@ public static (int PrefixLength, int SuffixLength) TrimCommonAffix(ref ReadOn var sourceLength = sourceEnd - startIndex; var targetLength = targetEnd - startIndex; + var suffixLength = source.Length - sourceEnd; source = source.Slice(startIndex, sourceLength); target = target.Slice(startIndex, targetLength); - return (startIndex, source.Length - sourceEnd); + return (startIndex, suffixLength); } [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -117,4 +151,4 @@ public static bool SwapIfSourceIsLonger(ref List collection1, ref List return true; } -} \ No newline at end of file +} diff --git a/README.md b/README.md index e75e39e..afcffad 100644 --- a/README.md +++ b/README.md @@ -84,14 +84,16 @@ Compare two strings directly: ```csharp Fuzz.Ratio("mysmilarstring", "mysimilarstring"); -// 97 +// 96.551724137931 Fuzz.WeightedRatio( "The quick brown fox jimps ofver the small lazy dog", "the quick brown fox jumps over the small lazy dog"); -// 95 +// 94.9494949494949 ``` +Similarity scores are `double` values in the range 0–100. Some extraction output examples below are rounded for readability. + By default, `Fuzz` methods compare strings as-is. `Process` extraction methods use `StringPreprocessor.Full` by default, which normalizes whitespace, lowercases, and strips non-alphanumeric characters. ## Choosing a Scorer @@ -389,7 +391,7 @@ var weighted = ScorerCache.Get(); Pre-initialize with a query string for repeated comparisons. These implement `IDisposable`: ```csharp using var scorer = new CachedWeightedRatioScorer("search query"); -int score = scorer.Score("candidate string"); +double score = scorer.Score("candidate string"); ``` Available cached scorers: @@ -523,6 +525,66 @@ The package name is `Raffinert.FuzzySharp`, and the default namespace is `Raffin - Use `StringPreprocessor.Full`, `StringPreprocessor.None`, or a custom delegate instead of `PreprocessMode.Full` or `PreprocessMode.None`. - Generic extraction methods that use extractor delegates are now named `Extract*By`, such as `ExtractOneBy` and `ExtractTopBy`. +### From Raffinert.FuzzySharp v5 to v6 + +Similarity scoring now uses `double` throughout the library so fractional scores are preserved instead of being rounded to integers: + +```csharp +double score = Fuzz.Ratio("new york mets", "new york mets!"); + +ExtractedResult match = Process.ExtractOne( + "goolge", + new[] { "google", "bing", "facebook" }); + +double matchScore = match.Score; +``` + +The following APIs changed from `int` to `double`: + +- `Fuzz` similarity methods +- `Scorer` and `CachedScorer` delegates +- `IRatioScorer`, `ICachedRatioScorer`, and their generic variants +- scoring strategies, including `ICachedStrategy` +- `ExtractedResult.Score` +- extraction `cutoff` parameters across standard, cached, parallel, and pipeline APIs + +Extraction cutoffs can therefore retain fractional thresholds: + +```csharp +var matches = Process.ExtractAll( + "goolge", + new[] { "google", "bing", "facebook" }, + cutoff: 87.5); +``` + +Custom scorers and method-group targets must update their return types accordingly: + +```csharp +public sealed class CustomScorer : IRatioScorer +{ + public double Score(string input1, string input2) => /* calculate score */ 0.0; + + public double Score( + string input1, + string input2, + Func preprocessor) => + Score(preprocessor(input1), preprocessor(input2)); +} +``` + +The optional `scoreCutoff` argument was removed from Indel, Levenshtein, and longest-common-subsequence distance and similarity APIs. Compute the result first and apply any threshold explicitly: + +```csharp +// v5 +int limitedDistance = Levenshtein.Distance("kitten", "sitting", scoreCutoff: 2); + +// v6 +int distance = Levenshtein.Distance("kitten", "sitting"); +bool isWithinCutoff = distance <= 2; +``` + +This does not remove extraction filtering: `Process` and `ResultExtractor` still accept `cutoff`, now as a `double`. Raw edit distances and unnormalized similarity counts remain `int`; normalized distance and similarity values remain `double` in the range 0–1. + ## Credits - [Adam Cohen (seatgeek/fuzzywuzzy)](https://chairnerd.seatgeek.com/fuzzywuzzy-fuzzy-string-matching-in-python/)