stride-align 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/NOTICE +255 -0
- data/README.md +107 -0
- data/data/bmpm_data/gen_approx_any.txt +131 -0
- data/data/bmpm_data/gen_approx_arabic.txt +26 -0
- data/data/bmpm_data/gen_approx_common.txt +233 -0
- data/data/bmpm_data/gen_approx_cyrillic.txt +18 -0
- data/data/bmpm_data/gen_approx_czech.txt +18 -0
- data/data/bmpm_data/gen_approx_dutch.txt +18 -0
- data/data/bmpm_data/gen_approx_english.txt +47 -0
- data/data/bmpm_data/gen_approx_french.txt +25 -0
- data/data/bmpm_data/gen_approx_german.txt +73 -0
- data/data/bmpm_data/gen_approx_greek.txt +18 -0
- data/data/bmpm_data/gen_approx_greeklatin.txt +20 -0
- data/data/bmpm_data/gen_approx_hebrew.txt +18 -0
- data/data/bmpm_data/gen_approx_hungarian.txt +18 -0
- data/data/bmpm_data/gen_approx_italian.txt +18 -0
- data/data/bmpm_data/gen_approx_polish.txt +84 -0
- data/data/bmpm_data/gen_approx_portuguese.txt +18 -0
- data/data/bmpm_data/gen_approx_romanian.txt +18 -0
- data/data/bmpm_data/gen_approx_russian.txt +48 -0
- data/data/bmpm_data/gen_approx_spanish.txt +21 -0
- data/data/bmpm_data/gen_approx_turkish.txt +18 -0
- data/data/bmpm_data/gen_exact_any.txt +40 -0
- data/data/bmpm_data/gen_exact_approx_common.txt +79 -0
- data/data/bmpm_data/gen_exact_arabic.txt +18 -0
- data/data/bmpm_data/gen_exact_common.txt +32 -0
- data/data/bmpm_data/gen_exact_cyrillic.txt +18 -0
- data/data/bmpm_data/gen_exact_czech.txt +18 -0
- data/data/bmpm_data/gen_exact_dutch.txt +18 -0
- data/data/bmpm_data/gen_exact_english.txt +18 -0
- data/data/bmpm_data/gen_exact_french.txt +18 -0
- data/data/bmpm_data/gen_exact_german.txt +18 -0
- data/data/bmpm_data/gen_exact_greek.txt +18 -0
- data/data/bmpm_data/gen_exact_greeklatin.txt +18 -0
- data/data/bmpm_data/gen_exact_hebrew.txt +18 -0
- data/data/bmpm_data/gen_exact_hungarian.txt +18 -0
- data/data/bmpm_data/gen_exact_italian.txt +18 -0
- data/data/bmpm_data/gen_exact_polish.txt +23 -0
- data/data/bmpm_data/gen_exact_portuguese.txt +18 -0
- data/data/bmpm_data/gen_exact_romanian.txt +18 -0
- data/data/bmpm_data/gen_exact_russian.txt +19 -0
- data/data/bmpm_data/gen_exact_spanish.txt +19 -0
- data/data/bmpm_data/gen_exact_turkish.txt +18 -0
- data/data/bmpm_data/gen_hebrew_common.txt +113 -0
- data/data/bmpm_data/gen_lang.txt +295 -0
- data/data/bmpm_data/gen_languages.txt +36 -0
- data/data/bmpm_data/gen_rules_any.txt +367 -0
- data/data/bmpm_data/gen_rules_arabic.txt +76 -0
- data/data/bmpm_data/gen_rules_cyrillic.txt +99 -0
- data/data/bmpm_data/gen_rules_czech.txt +67 -0
- data/data/bmpm_data/gen_rules_dutch.txt +78 -0
- data/data/bmpm_data/gen_rules_english.txt +113 -0
- data/data/bmpm_data/gen_rules_french.txt +114 -0
- data/data/bmpm_data/gen_rules_german.txt +129 -0
- data/data/bmpm_data/gen_rules_greek.txt +97 -0
- data/data/bmpm_data/gen_rules_greeklatin.txt +118 -0
- data/data/bmpm_data/gen_rules_hebrew.txt +62 -0
- data/data/bmpm_data/gen_rules_hungarian.txt +83 -0
- data/data/bmpm_data/gen_rules_italian.txt +77 -0
- data/data/bmpm_data/gen_rules_polish.txt +185 -0
- data/data/bmpm_data/gen_rules_portuguese.txt +105 -0
- data/data/bmpm_data/gen_rules_romanian.txt +64 -0
- data/data/bmpm_data/gen_rules_russian.txt +142 -0
- data/data/bmpm_data/gen_rules_spanish.txt +85 -0
- data/data/bmpm_data/gen_rules_turkish.txt +50 -0
- data/data/keyboard_data/qwerty.npy +0 -0
- data/data/matrix_data/BLOSUM100 +31 -0
- data/data/matrix_data/BLOSUM30 +31 -0
- data/data/matrix_data/BLOSUM35 +31 -0
- data/data/matrix_data/BLOSUM40 +31 -0
- data/data/matrix_data/BLOSUM45 +25 -0
- data/data/matrix_data/BLOSUM50 +25 -0
- data/data/matrix_data/BLOSUM55 +31 -0
- data/data/matrix_data/BLOSUM60 +31 -0
- data/data/matrix_data/BLOSUM62 +25 -0
- data/data/matrix_data/BLOSUM65 +31 -0
- data/data/matrix_data/BLOSUM70 +31 -0
- data/data/matrix_data/BLOSUM75 +31 -0
- data/data/matrix_data/BLOSUM80 +25 -0
- data/data/matrix_data/BLOSUM85 +31 -0
- data/data/matrix_data/BLOSUM90 +25 -0
- data/data/matrix_data/NUC.4.4 +25 -0
- data/data/matrix_data/PAM10 +34 -0
- data/data/matrix_data/PAM100 +34 -0
- data/data/matrix_data/PAM110 +34 -0
- data/data/matrix_data/PAM120 +34 -0
- data/data/matrix_data/PAM130 +34 -0
- data/data/matrix_data/PAM140 +34 -0
- data/data/matrix_data/PAM150 +34 -0
- data/data/matrix_data/PAM160 +34 -0
- data/data/matrix_data/PAM170 +34 -0
- data/data/matrix_data/PAM180 +34 -0
- data/data/matrix_data/PAM190 +34 -0
- data/data/matrix_data/PAM20 +34 -0
- data/data/matrix_data/PAM200 +34 -0
- data/data/matrix_data/PAM210 +34 -0
- data/data/matrix_data/PAM220 +34 -0
- data/data/matrix_data/PAM230 +34 -0
- data/data/matrix_data/PAM240 +34 -0
- data/data/matrix_data/PAM250 +25 -0
- data/data/matrix_data/PAM260 +34 -0
- data/data/matrix_data/PAM270 +34 -0
- data/data/matrix_data/PAM280 +34 -0
- data/data/matrix_data/PAM290 +34 -0
- data/data/matrix_data/PAM30 +25 -0
- data/data/matrix_data/PAM300 +34 -0
- data/data/matrix_data/PAM310 +34 -0
- data/data/matrix_data/PAM320 +34 -0
- data/data/matrix_data/PAM330 +34 -0
- data/data/matrix_data/PAM340 +34 -0
- data/data/matrix_data/PAM350 +34 -0
- data/data/matrix_data/PAM360 +34 -0
- data/data/matrix_data/PAM370 +34 -0
- data/data/matrix_data/PAM380 +34 -0
- data/data/matrix_data/PAM390 +34 -0
- data/data/matrix_data/PAM40 +34 -0
- data/data/matrix_data/PAM400 +34 -0
- data/data/matrix_data/PAM410 +34 -0
- data/data/matrix_data/PAM420 +34 -0
- data/data/matrix_data/PAM430 +34 -0
- data/data/matrix_data/PAM440 +34 -0
- data/data/matrix_data/PAM450 +34 -0
- data/data/matrix_data/PAM460 +34 -0
- data/data/matrix_data/PAM470 +34 -0
- data/data/matrix_data/PAM480 +34 -0
- data/data/matrix_data/PAM490 +34 -0
- data/data/matrix_data/PAM50 +34 -0
- data/data/matrix_data/PAM500 +34 -0
- data/data/matrix_data/PAM60 +34 -0
- data/data/matrix_data/PAM70 +25 -0
- data/data/matrix_data/PAM80 +34 -0
- data/data/matrix_data/PAM90 +34 -0
- data/ext/stride_align/backend_avx2.cpp +2 -0
- data/ext/stride_align/backend_avx512bwvl.cpp +2 -0
- data/ext/stride_align/backend_generic.cpp +3 -0
- data/ext/stride_align/backend_impl.hpp +983 -0
- data/ext/stride_align/backend_lasx.cpp +2 -0
- data/ext/stride_align/backend_lsx.cpp +2 -0
- data/ext/stride_align/backend_neon.cpp +2 -0
- data/ext/stride_align/backend_rvv.cpp +2 -0
- data/ext/stride_align/backend_sse41.cpp +2 -0
- data/ext/stride_align/backend_sve.cpp +2 -0
- data/ext/stride_align/backend_sve2.cpp +2 -0
- data/ext/stride_align/backend_vsx.cpp +2 -0
- data/ext/stride_align/beider_morse_impl.cpp +5 -0
- data/ext/stride_align/cpu_detect.cpp +172 -0
- data/ext/stride_align/cpu_detect.hpp +6 -0
- data/ext/stride_align/extconf.rb +114 -0
- data/ext/stride_align/target_profile.hpp +82 -0
- data/ext/stride_align/vendor/beider_morse_impl.cpp +1467 -0
- data/ext/stride_align/vendor/stride_align/alignment.hpp +199 -0
- data/ext/stride_align/vendor/stride_align/batch.hpp +812 -0
- data/ext/stride_align/vendor/stride_align/beider_morse.hpp +121 -0
- data/ext/stride_align/vendor/stride_align/caverphone.hpp +222 -0
- data/ext/stride_align/vendor/stride_align/cologne_phonetic.hpp +202 -0
- data/ext/stride_align/vendor/stride_align/core.hpp +731 -0
- data/ext/stride_align/vendor/stride_align/daitch_mokotoff.hpp +631 -0
- data/ext/stride_align/vendor/stride_align/double_metaphone.hpp +796 -0
- data/ext/stride_align/vendor/stride_align/dtw.hpp +300 -0
- data/ext/stride_align/vendor/stride_align/encoded.hpp +235 -0
- data/ext/stride_align/vendor/stride_align/hamming.hpp +55 -0
- data/ext/stride_align/vendor/stride_align/indel.hpp +1200 -0
- data/ext/stride_align/vendor/stride_align/jaro.hpp +517 -0
- data/ext/stride_align/vendor/stride_align/lcs.hpp +159 -0
- data/ext/stride_align/vendor/stride_align/levenshtein.hpp +1247 -0
- data/ext/stride_align/vendor/stride_align/levenshtein_prepared.hpp +193 -0
- data/ext/stride_align/vendor/stride_align/match_rating.hpp +168 -0
- data/ext/stride_align/vendor/stride_align/metaphone.hpp +291 -0
- data/ext/stride_align/vendor/stride_align/ngram.hpp +176 -0
- data/ext/stride_align/vendor/stride_align/nysiis.hpp +199 -0
- data/ext/stride_align/vendor/stride_align/pairwise_alignment.hpp +465 -0
- data/ext/stride_align/vendor/stride_align/partial_ratio.hpp +486 -0
- data/ext/stride_align/vendor/stride_align/ratcliff_obershelp.hpp +101 -0
- data/ext/stride_align/vendor/stride_align/soundex.hpp +108 -0
- data/ext/stride_align/vendor/stride_align/token_ratios.hpp +445 -0
- data/ext/stride_align/vendor/stride_align/types.hpp +16 -0
- data/ext/stride_align/vendor/stride_align/utf8.hpp +512 -0
- data/ext/stride_align/vendor/stride_align/wratio.hpp +363 -0
- data/lib/stride_align/algorithms.rb +296 -0
- data/lib/stride_align/alignment_path.rb +217 -0
- data/lib/stride_align/backend.rb +47 -0
- data/lib/stride_align/batch.rb +705 -0
- data/lib/stride_align/core.rb +180 -0
- data/lib/stride_align/keyboard.rb +200 -0
- data/lib/stride_align/matrices.rb +403 -0
- data/lib/stride_align/version.rb +5 -0
- data/lib/stride_align.rb +87 -0
- metadata +231 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Compatibility-style WRatio kernel.
|
|
4
|
+
//
|
|
5
|
+
// Reimplements rapidfuzz's documented WRatio recipe in C++ so the
|
|
6
|
+
// whole computation (ratio + len-ratio branch + token variants +
|
|
7
|
+
// partial variants) crosses the Python boundary exactly once instead
|
|
8
|
+
// of once per component. Closes the ~4× WRatio gap on x86/ARM that
|
|
9
|
+
// was driven entirely by Python wrapper overhead and intermediate
|
|
10
|
+
// ``float()`` / ``max()`` work in the previous Python recipe.
|
|
11
|
+
//
|
|
12
|
+
// Recipe (matches the compatibility reference bit-exactly):
|
|
13
|
+
//
|
|
14
|
+
// base = indel_normalized(a, b) # range [0, 1]
|
|
15
|
+
// len_ratio = max(|a|, |b|) / min(|a|, |b|)
|
|
16
|
+
// UNBASE_SCALE = 0.95
|
|
17
|
+
//
|
|
18
|
+
// if len_ratio < 1.5:
|
|
19
|
+
// # similar-length regime — token-based variants
|
|
20
|
+
// token_sort = token_sort_ratio(a, b) * UNBASE_SCALE
|
|
21
|
+
// token_set = token_set_ratio(a, b) * UNBASE_SCALE
|
|
22
|
+
// score = max(base, token_sort, token_set)
|
|
23
|
+
// else:
|
|
24
|
+
// # length-mismatched regime — partial variants dominate
|
|
25
|
+
// partial_scale = 0.6 if len_ratio > 8 else 0.9
|
|
26
|
+
// partial = partial_ratio(a, b) * partial_scale
|
|
27
|
+
// partial_token_sort = partial_token_sort_ratio(a, b) * UNBASE_SCALE * partial_scale
|
|
28
|
+
// partial_token_set = partial_token_set_ratio(a, b) * UNBASE_SCALE * partial_scale
|
|
29
|
+
// score = max(base, partial, partial_token_sort, partial_token_set)
|
|
30
|
+
//
|
|
31
|
+
// return score
|
|
32
|
+
//
|
|
33
|
+
// **Short-circuit logic.** Every non-base component is multiplied by
|
|
34
|
+
// a scale factor ``<= 0.95`` (in the similar-length branch) or
|
|
35
|
+
// ``<= 0.9 / 0.6`` (in the length-mismatched branch). The component
|
|
36
|
+
// value itself is in ``[0, 1]``, so the scaled value is bounded by
|
|
37
|
+
// the scale factor. Once ``base`` is already at least the maximum
|
|
38
|
+
// possible component score, no further computation can change the
|
|
39
|
+
// answer — return ``base`` and skip the (expensive) token / partial
|
|
40
|
+
// passes. This is the dominant fast path for high-quality matches.
|
|
41
|
+
//
|
|
42
|
+
// All component subroutines (token_sort, token_set, partial,
|
|
43
|
+
// partial_token_sort, partial_token_set) are inlined from
|
|
44
|
+
// ``token_ratios.hpp`` and ``partial_ratio.hpp`` so the whole
|
|
45
|
+
// recipe is a single function instantiation per Token type. The native
|
|
46
|
+
// stride-align contract is implemented separately at the end of this file.
|
|
47
|
+
|
|
48
|
+
#include <algorithm>
|
|
49
|
+
#include <cstddef>
|
|
50
|
+
#include <cstdint>
|
|
51
|
+
#include <span>
|
|
52
|
+
#include <type_traits>
|
|
53
|
+
#include <vector>
|
|
54
|
+
|
|
55
|
+
#include "stride_align/indel.hpp"
|
|
56
|
+
#include "stride_align/lcs.hpp"
|
|
57
|
+
#include "stride_align/partial_ratio.hpp"
|
|
58
|
+
#include "stride_align/token_ratios.hpp"
|
|
59
|
+
|
|
60
|
+
namespace stride_align::wratio {
|
|
61
|
+
|
|
62
|
+
using Codepoint = ::stride_align::lcs::Codepoint;
|
|
63
|
+
|
|
64
|
+
// Combined ``partial_token_sort_ratio`` on already-tokenised inputs.
|
|
65
|
+
// Sorts the tokens, joins, hands off to ``partial_ratio``.
|
|
66
|
+
template <typename Token>
|
|
67
|
+
inline double partial_token_sort_ratio_engine(
|
|
68
|
+
std::span<const Token> a,
|
|
69
|
+
std::span<const Token> b) {
|
|
70
|
+
auto ta = ::stride_align::token_ratios::tokens_of(a);
|
|
71
|
+
auto tb = ::stride_align::token_ratios::tokens_of(b);
|
|
72
|
+
std::sort(ta.begin(), ta.end(),
|
|
73
|
+
::stride_align::token_ratios::LexLess<Token>{});
|
|
74
|
+
std::sort(tb.begin(), tb.end(),
|
|
75
|
+
::stride_align::token_ratios::LexLess<Token>{});
|
|
76
|
+
std::vector<Token> ja, jb;
|
|
77
|
+
::stride_align::token_ratios::join_with_space(ta, ja);
|
|
78
|
+
::stride_align::token_ratios::join_with_space(tb, jb);
|
|
79
|
+
if (ja.empty() && jb.empty()) return 1.0;
|
|
80
|
+
if (ja.empty() || jb.empty()) return 0.0;
|
|
81
|
+
|
|
82
|
+
// Byte inputs go straight through partial_ratio_bytes (the fast path
|
|
83
|
+
// fuzz.partial_ratio already uses) — no uint8->codepoint widening, and
|
|
84
|
+
// the byte partial_ratio kernel rather than the codepoint one.
|
|
85
|
+
if constexpr (std::is_same_v<Token, std::uint8_t>) {
|
|
86
|
+
return ::stride_align::partial_ratio::partial_ratio_bytes(
|
|
87
|
+
std::span<const std::uint8_t>(ja), std::span<const std::uint8_t>(jb));
|
|
88
|
+
} else {
|
|
89
|
+
std::vector<Codepoint> ja_cp(ja.begin(), ja.end());
|
|
90
|
+
std::vector<Codepoint> jb_cp(jb.begin(), jb.end());
|
|
91
|
+
return ::stride_align::partial_ratio::partial_ratio(ja_cp, jb_cp);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Combined ``partial_token_set_ratio``. Builds the three set
|
|
96
|
+
// candidates exactly as ``token_set_ratio_engine`` does, then runs
|
|
97
|
+
// ``partial_ratio`` over each pair and returns the max.
|
|
98
|
+
template <typename Token>
|
|
99
|
+
inline double partial_token_set_ratio_engine(
|
|
100
|
+
std::span<const Token> a,
|
|
101
|
+
std::span<const Token> b) {
|
|
102
|
+
using namespace ::stride_align::token_ratios;
|
|
103
|
+
auto ta = tokens_of(a);
|
|
104
|
+
auto tb = tokens_of(b);
|
|
105
|
+
if (ta.empty() || tb.empty()) return 0.0;
|
|
106
|
+
|
|
107
|
+
const LexLess<Token> lt;
|
|
108
|
+
std::sort(ta.begin(), ta.end(), lt);
|
|
109
|
+
ta.erase(std::unique(ta.begin(), ta.end(), LexEqual<Token>{}), ta.end());
|
|
110
|
+
std::sort(tb.begin(), tb.end(), lt);
|
|
111
|
+
tb.erase(std::unique(tb.begin(), tb.end(), LexEqual<Token>{}), tb.end());
|
|
112
|
+
|
|
113
|
+
std::vector<std::span<const Token>> intersect, diff_a, diff_b;
|
|
114
|
+
std::size_t i = 0, j = 0;
|
|
115
|
+
while (i < ta.size() && j < tb.size()) {
|
|
116
|
+
if (lt(ta[i], tb[j])) { diff_a.push_back(ta[i]); ++i; }
|
|
117
|
+
else if (lt(tb[j], ta[i])) { diff_b.push_back(tb[j]); ++j; }
|
|
118
|
+
else { intersect.push_back(ta[i]); ++i; ++j; }
|
|
119
|
+
}
|
|
120
|
+
while (i < ta.size()) diff_a.push_back(ta[i++]);
|
|
121
|
+
while (j < tb.size()) diff_b.push_back(tb[j++]);
|
|
122
|
+
|
|
123
|
+
if (diff_a.empty() && diff_b.empty()) return 1.0;
|
|
124
|
+
|
|
125
|
+
std::vector<std::span<const Token>> t1_parts(intersect);
|
|
126
|
+
t1_parts.insert(t1_parts.end(), diff_a.begin(), diff_a.end());
|
|
127
|
+
std::vector<std::span<const Token>> t2_parts(intersect);
|
|
128
|
+
t2_parts.insert(t2_parts.end(), diff_b.begin(), diff_b.end());
|
|
129
|
+
|
|
130
|
+
std::vector<Token> t0, t1, t2;
|
|
131
|
+
join_with_space(intersect, t0);
|
|
132
|
+
join_with_space(t1_parts, t1);
|
|
133
|
+
join_with_space(t2_parts, t2);
|
|
134
|
+
|
|
135
|
+
// Run partial_ratio on each pair (codepoint widening per pair).
|
|
136
|
+
auto pr = [](const std::vector<Token>& x, const std::vector<Token>& y) -> double {
|
|
137
|
+
if (x.empty() && y.empty()) return 1.0;
|
|
138
|
+
if (x.empty() || y.empty()) return 0.0;
|
|
139
|
+
if constexpr (std::is_same_v<Token, std::uint8_t>) {
|
|
140
|
+
return ::stride_align::partial_ratio::partial_ratio_bytes(
|
|
141
|
+
std::span<const std::uint8_t>(x), std::span<const std::uint8_t>(y));
|
|
142
|
+
} else {
|
|
143
|
+
std::vector<Codepoint> xc(x.begin(), x.end());
|
|
144
|
+
std::vector<Codepoint> yc(y.begin(), y.end());
|
|
145
|
+
return ::stride_align::partial_ratio::partial_ratio(xc, yc);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const double r0 = pr(t0, t1);
|
|
149
|
+
if (r0 >= 1.0) return 1.0;
|
|
150
|
+
const double r1 = pr(t0, t2);
|
|
151
|
+
if (r1 >= 1.0) return 1.0;
|
|
152
|
+
const double r2 = pr(t1, t2);
|
|
153
|
+
return std::max({r0, r1, r2});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// WRatio engine. Returns the WRatio score in ``[0, 1]`` (caller
|
|
157
|
+
// multiplies by 100 for the rapidfuzz convention).
|
|
158
|
+
//
|
|
159
|
+
// ``score_cutoff`` is interpreted as a *normalised* cutoff in
|
|
160
|
+
// ``[0, 1]`` — values at or below this threshold are returned as 0.
|
|
161
|
+
// This mirrors rapidfuzz's documented behaviour on WRatio.
|
|
162
|
+
template <typename Token>
|
|
163
|
+
inline double wratio_engine(
|
|
164
|
+
std::span<const Token> a,
|
|
165
|
+
std::span<const Token> b,
|
|
166
|
+
double score_cutoff /* normalised, [0, 1]; 0 == no cutoff */) {
|
|
167
|
+
if (a.empty() || b.empty()) return 0.0;
|
|
168
|
+
|
|
169
|
+
constexpr double UNBASE_SCALE = 0.95;
|
|
170
|
+
|
|
171
|
+
const double base = ::stride_align::token_ratios::indel_normalized<Token>(a, b);
|
|
172
|
+
const double len_a = static_cast<double>(a.size());
|
|
173
|
+
const double len_b = static_cast<double>(b.size());
|
|
174
|
+
const double len_ratio = (len_a > len_b ? len_a / len_b : len_b / len_a);
|
|
175
|
+
|
|
176
|
+
double best = base;
|
|
177
|
+
|
|
178
|
+
if (len_ratio < 1.5) {
|
|
179
|
+
// Similar-length regime. Token variants scale by 0.95, so once
|
|
180
|
+
// ``base >= 0.95`` no scaled token component can exceed it.
|
|
181
|
+
if (best >= UNBASE_SCALE) {
|
|
182
|
+
return best > score_cutoff ? best : 0.0;
|
|
183
|
+
}
|
|
184
|
+
// token_sort and token_set are both needed here. Use the combined
|
|
185
|
+
// engine that tokenises a/b once, sorts once, and shares the
|
|
186
|
+
// thread-local scratch buffers across the 1 + 3 indel calls.
|
|
187
|
+
double ts = 0.0, tx = 0.0;
|
|
188
|
+
::stride_align::token_ratios::compute_token_sort_and_set_ratio<Token>(
|
|
189
|
+
a, b, ts, tx);
|
|
190
|
+
const double ts_scaled = ts * UNBASE_SCALE;
|
|
191
|
+
if (ts_scaled > best) best = ts_scaled;
|
|
192
|
+
const double tx_scaled = tx * UNBASE_SCALE;
|
|
193
|
+
if (tx_scaled > best) best = tx_scaled;
|
|
194
|
+
return best > score_cutoff ? best : 0.0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Length-mismatched regime. The dominant variant is ``partial`` (no
|
|
198
|
+
// unbase penalty), so its scaling factor caps the achievable score.
|
|
199
|
+
// rapidfuzz/fuzzywuzzy: ``partial_scale = 0.6 if len_ratio > 8 else 0.9`` —
|
|
200
|
+
// len_ratio == 8 stays in the 0.9 bucket. (Was ``< 8.0``, which put the
|
|
201
|
+
// exact-8 boundary in the 0.6 bucket and diverged from rapidfuzz, ~30 pts.)
|
|
202
|
+
const double partial_scale = (len_ratio > 8.0) ? 0.6 : 0.9;
|
|
203
|
+
// partial_token_* are scaled by UNBASE_SCALE * partial_scale, which
|
|
204
|
+
// is strictly less than ``partial_scale``. So ``partial_scale`` is
|
|
205
|
+
// the true ceiling for any non-base component here.
|
|
206
|
+
if (best >= partial_scale) {
|
|
207
|
+
return best > score_cutoff ? best : 0.0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Byte tokens go straight through the byte partial_ratio path
|
|
211
|
+
// (no uint8→codepoint widen / re-narrow). Wider tokens still
|
|
212
|
+
// widen once into codepoints for the public entry.
|
|
213
|
+
double pr;
|
|
214
|
+
if constexpr (std::is_same_v<Token, std::uint8_t>) {
|
|
215
|
+
pr = ::stride_align::partial_ratio::partial_ratio_bytes(a, b);
|
|
216
|
+
} else {
|
|
217
|
+
std::vector<Codepoint> a_cp(a.begin(), a.end());
|
|
218
|
+
std::vector<Codepoint> b_cp(b.begin(), b.end());
|
|
219
|
+
pr = ::stride_align::partial_ratio::partial_ratio(a_cp, b_cp);
|
|
220
|
+
}
|
|
221
|
+
const double pr_scaled = pr * partial_scale;
|
|
222
|
+
if (pr_scaled > best) best = pr_scaled;
|
|
223
|
+
// After ``partial`` the ceiling drops to ``UNBASE_SCALE *
|
|
224
|
+
// partial_scale`` because the remaining variants pay both penalties.
|
|
225
|
+
const double remaining_ceiling = UNBASE_SCALE * partial_scale;
|
|
226
|
+
if (best >= remaining_ceiling) {
|
|
227
|
+
return best > score_cutoff ? best : 0.0;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const double pts = partial_token_sort_ratio_engine<Token>(a, b);
|
|
231
|
+
const double pts_scaled = pts * remaining_ceiling;
|
|
232
|
+
if (pts_scaled > best) best = pts_scaled;
|
|
233
|
+
if (best >= remaining_ceiling) {
|
|
234
|
+
return best > score_cutoff ? best : 0.0;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const double pset = partial_token_set_ratio_engine<Token>(a, b);
|
|
238
|
+
const double pset_scaled = pset * remaining_ceiling;
|
|
239
|
+
if (pset_scaled > best) best = pset_scaled;
|
|
240
|
+
return best > score_cutoff ? best : 0.0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Byte fast path: caller has already established byte-compatible
|
|
244
|
+
// inputs.
|
|
245
|
+
inline double wratio_bytes(
|
|
246
|
+
std::span<const std::uint8_t> a,
|
|
247
|
+
std::span<const std::uint8_t> b,
|
|
248
|
+
double score_cutoff = 0.0) {
|
|
249
|
+
return wratio_engine<std::uint8_t>(a, b, score_cutoff);
|
|
250
|
+
}
|
|
251
|
+
inline double partial_token_sort_ratio_bytes(
|
|
252
|
+
std::span<const std::uint8_t> a,
|
|
253
|
+
std::span<const std::uint8_t> b) {
|
|
254
|
+
return partial_token_sort_ratio_engine<std::uint8_t>(a, b);
|
|
255
|
+
}
|
|
256
|
+
inline double partial_token_set_ratio_bytes(
|
|
257
|
+
std::span<const std::uint8_t> a,
|
|
258
|
+
std::span<const std::uint8_t> b) {
|
|
259
|
+
return partial_token_set_ratio_engine<std::uint8_t>(a, b);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Public byte-fast-path-or-codepoint dispatcher.
|
|
263
|
+
inline double wratio(
|
|
264
|
+
const std::vector<Codepoint>& a,
|
|
265
|
+
const std::vector<Codepoint>& b,
|
|
266
|
+
double score_cutoff = 0.0) {
|
|
267
|
+
if (a.empty() || b.empty()) return 0.0;
|
|
268
|
+
bool fits_in_byte = true;
|
|
269
|
+
for (const auto cp : a) { if (cp >= 256U) { fits_in_byte = false; break; } }
|
|
270
|
+
if (fits_in_byte) {
|
|
271
|
+
for (const auto cp : b) { if (cp >= 256U) { fits_in_byte = false; break; } }
|
|
272
|
+
}
|
|
273
|
+
if (fits_in_byte) {
|
|
274
|
+
std::vector<std::uint8_t> ab(a.begin(), a.end());
|
|
275
|
+
std::vector<std::uint8_t> bb(b.begin(), b.end());
|
|
276
|
+
return wratio_engine<std::uint8_t>(
|
|
277
|
+
std::span<const std::uint8_t>(ab),
|
|
278
|
+
std::span<const std::uint8_t>(bb),
|
|
279
|
+
score_cutoff);
|
|
280
|
+
}
|
|
281
|
+
return wratio_engine<Codepoint>(
|
|
282
|
+
std::span<const Codepoint>(a),
|
|
283
|
+
std::span<const Codepoint>(b),
|
|
284
|
+
score_cutoff);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// stride-align's top-level WRatio contract predates the compatibility shim:
|
|
288
|
+
// it evaluates partial and token candidates in both length regimes. Keep the
|
|
289
|
+
// optimized compatibility recipe above available to the shim, while native
|
|
290
|
+
// language adapters call this complete recipe.
|
|
291
|
+
template <typename Token>
|
|
292
|
+
inline double native_wratio_engine(
|
|
293
|
+
std::span<const Token> a,
|
|
294
|
+
std::span<const Token> b) {
|
|
295
|
+
if (a.empty() || b.empty()) return a.empty() && b.empty() ? 1.0 : 0.0;
|
|
296
|
+
|
|
297
|
+
constexpr double kUnbaseScale = 0.95;
|
|
298
|
+
double partial_scale = 0.9;
|
|
299
|
+
double unbase_scale = kUnbaseScale;
|
|
300
|
+
const double len_a = static_cast<double>(a.size());
|
|
301
|
+
const double len_b = static_cast<double>(b.size());
|
|
302
|
+
const double len_ratio = len_a > len_b ? len_a / len_b : len_b / len_a;
|
|
303
|
+
if (len_ratio < 1.5) {
|
|
304
|
+
partial_scale = kUnbaseScale;
|
|
305
|
+
} else if (len_ratio >= 8.0) {
|
|
306
|
+
unbase_scale = 0.6;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
double best = ::stride_align::token_ratios::indel_normalized<Token>(a, b);
|
|
310
|
+
if (best >= 1.0) return best;
|
|
311
|
+
|
|
312
|
+
double partial;
|
|
313
|
+
if constexpr (std::is_same_v<Token, std::uint8_t>) {
|
|
314
|
+
partial = ::stride_align::partial_ratio::partial_ratio_bytes(a, b);
|
|
315
|
+
} else {
|
|
316
|
+
std::vector<Codepoint> a_cp(a.begin(), a.end());
|
|
317
|
+
std::vector<Codepoint> b_cp(b.begin(), b.end());
|
|
318
|
+
partial = ::stride_align::partial_ratio::partial_ratio(a_cp, b_cp);
|
|
319
|
+
}
|
|
320
|
+
best = std::max(best, partial * partial_scale);
|
|
321
|
+
|
|
322
|
+
double token_sort = 0.0;
|
|
323
|
+
double token_set = 0.0;
|
|
324
|
+
::stride_align::token_ratios::compute_token_sort_and_set_ratio<Token>(
|
|
325
|
+
a, b, token_sort, token_set);
|
|
326
|
+
best = std::max(best, token_sort * unbase_scale);
|
|
327
|
+
best = std::max(best, token_set * unbase_scale);
|
|
328
|
+
|
|
329
|
+
const double combined_scale = partial_scale * unbase_scale;
|
|
330
|
+
best = std::max(
|
|
331
|
+
best, partial_token_sort_ratio_engine<Token>(a, b) * combined_scale);
|
|
332
|
+
best = std::max(
|
|
333
|
+
best, partial_token_set_ratio_engine<Token>(a, b) * combined_scale);
|
|
334
|
+
return best;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
inline double native_wratio(
|
|
338
|
+
const std::vector<Codepoint>& a,
|
|
339
|
+
const std::vector<Codepoint>& b) {
|
|
340
|
+
bool fits_in_byte = true;
|
|
341
|
+
for (const auto codepoint : a) {
|
|
342
|
+
if (codepoint >= 256U) {
|
|
343
|
+
fits_in_byte = false;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (fits_in_byte) {
|
|
348
|
+
for (const auto codepoint : b) {
|
|
349
|
+
if (codepoint >= 256U) {
|
|
350
|
+
fits_in_byte = false;
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (fits_in_byte) {
|
|
356
|
+
const std::vector<std::uint8_t> left(a.begin(), a.end());
|
|
357
|
+
const std::vector<std::uint8_t> right(b.begin(), b.end());
|
|
358
|
+
return native_wratio_engine<std::uint8_t>(left, right);
|
|
359
|
+
}
|
|
360
|
+
return native_wratio_engine<Codepoint>(a, b);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
} // namespace stride_align::wratio
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StrideAlign
|
|
4
|
+
module MetaphoneVariant
|
|
5
|
+
PHILIPS = 0
|
|
6
|
+
JELLYFISH = 1
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module DoubleMetaphoneVariant
|
|
10
|
+
COMMONS = 0
|
|
11
|
+
PYTHON = 1
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module BmpmRuleType
|
|
15
|
+
APPROX = 0
|
|
16
|
+
EXACT = 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def lcs_length(query, target)
|
|
21
|
+
pairwise(query, target) { |left, right| __primary_backend.lcs_length(left, right) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def lcs_substring_length(query, target)
|
|
25
|
+
pairwise(query, target) do |left, right|
|
|
26
|
+
__primary_backend.lcs_substring_length(left, right)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def lcs_substring(query, target)
|
|
31
|
+
pairwise(query, target) { |left, right| __primary_backend.lcs_substring(left, right) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def jaccard(query, target, n: 2)
|
|
35
|
+
pairwise(query, target) { |left, right| __primary_backend.jaccard(left, right, n) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def dice(query, target, n: 2)
|
|
39
|
+
pairwise(query, target) { |left, right| __primary_backend.dice(left, right, n) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def cosine(query, target, n: 2)
|
|
43
|
+
pairwise(query, target) { |left, right| __primary_backend.cosine(left, right, n) }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def overlap(query, target, n: 2)
|
|
47
|
+
pairwise(query, target) { |left, right| __primary_backend.overlap(left, right, n) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def jaccard_similarities(query, targets, n: 2)
|
|
51
|
+
one_to_many(query, targets) { |left, right| jaccard(left, right, n: n) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def dice_similarities(query, targets, n: 2)
|
|
55
|
+
one_to_many(query, targets) { |left, right| dice(left, right, n: n) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def cosine_similarities(query, targets, n: 2)
|
|
59
|
+
one_to_many(query, targets) { |left, right| cosine(left, right, n: n) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def overlap_similarities(query, targets, n: 2)
|
|
63
|
+
one_to_many(query, targets) { |left, right| overlap(left, right, n: n) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ratcliff_obershelp_similarity(query, target)
|
|
67
|
+
pairwise(query, target) do |left, right|
|
|
68
|
+
__primary_backend.ratcliff_obershelp(left, right)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ratcliff_obershelp_similarities(query, targets)
|
|
73
|
+
one_to_many(query, targets) do |left, right|
|
|
74
|
+
ratcliff_obershelp_similarity(left, right)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def partial_ratio(query, target, processor: nil)
|
|
79
|
+
processed_pairwise(query, target, processor) do |left, right|
|
|
80
|
+
__primary_backend.partial_ratio(left, right)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def token_sort_ratio(query, target, processor: nil)
|
|
85
|
+
processed_pairwise(query, target, processor) do |left, right|
|
|
86
|
+
__primary_backend.token_sort_ratio(left, right)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def token_set_ratio(query, target, processor: nil)
|
|
91
|
+
processed_pairwise(query, target, processor) do |left, right|
|
|
92
|
+
__primary_backend.token_set_ratio(left, right)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def partial_token_sort_ratio(query, target, processor: nil)
|
|
97
|
+
processed_pairwise(query, target, processor) do |left, right|
|
|
98
|
+
__primary_backend.partial_token_sort_ratio(left, right)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def partial_token_set_ratio(query, target, processor: nil)
|
|
103
|
+
processed_pairwise(query, target, processor) do |left, right|
|
|
104
|
+
__primary_backend.partial_token_set_ratio(left, right)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def WRatio(query, target, processor: nil)
|
|
109
|
+
processed_pairwise(query, target, processor) do |left, right|
|
|
110
|
+
if left.nil? || right.nil?
|
|
111
|
+
nil
|
|
112
|
+
elsif left.empty? && right.empty?
|
|
113
|
+
1.0
|
|
114
|
+
else
|
|
115
|
+
__primary_backend.wratio(left, right)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def monge_elkan(query, target, inner: :jaro, processor: nil,
|
|
121
|
+
symmetric: false)
|
|
122
|
+
query = scalar_query(query)
|
|
123
|
+
target = scalar_query(target)
|
|
124
|
+
raise ArgumentError, "query and target cannot be nil" if query.nil? || target.nil?
|
|
125
|
+
query = apply_processor(query, processor)
|
|
126
|
+
target = apply_processor(target, processor)
|
|
127
|
+
scorer = resolve_inner_scorer(inner)
|
|
128
|
+
left = query.split
|
|
129
|
+
right = target.split
|
|
130
|
+
return 1.0 if left.empty? && right.empty?
|
|
131
|
+
return 0.0 if left.empty? || right.empty?
|
|
132
|
+
|
|
133
|
+
forward = monge_direction(left, right, scorer)
|
|
134
|
+
return forward unless symmetric
|
|
135
|
+
(forward + monge_direction(right, left, scorer)) / 2.0
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def soundex(input)
|
|
139
|
+
unary(input) { |value| __primary_backend.soundex(value) }
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def soundex_equal(query, target)
|
|
143
|
+
phonetic_equal(query, target, :soundex, MetaphoneVariant::PHILIPS)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def metaphone(input, variant: MetaphoneVariant::PHILIPS)
|
|
147
|
+
unary(input) { |value| __primary_backend.metaphone(value, variant) }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def metaphone_equal(query, target, variant: MetaphoneVariant::PHILIPS)
|
|
151
|
+
phonetic_equal(query, target, :metaphone, variant)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def nysiis(input)
|
|
155
|
+
unary(input) { |value| __primary_backend.nysiis(value) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def nysiis_equal(query, target)
|
|
159
|
+
phonetic_equal(query, target, :nysiis, MetaphoneVariant::PHILIPS)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def match_rating_codex(input)
|
|
163
|
+
unary(input) { |value| __primary_backend.match_rating_codex(value) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def match_rating_compare(query, target)
|
|
167
|
+
pairwise(query, target) do |left, right|
|
|
168
|
+
__primary_backend.match_rating_compare(left, right)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def caverphone(input)
|
|
173
|
+
unary(input) { |value| __primary_backend.caverphone(value) }
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def cologne_phonetic(input)
|
|
177
|
+
unary(input) { |value| __primary_backend.cologne_phonetic(value) }
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def daitch_mokotoff(input, branching: true, folding: true)
|
|
181
|
+
unary(input) do |value|
|
|
182
|
+
__primary_backend.daitch_mokotoff(value, branching, folding)
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def double_metaphone(input, max_length: 64,
|
|
187
|
+
variant: DoubleMetaphoneVariant::COMMONS)
|
|
188
|
+
unary(input) do |value|
|
|
189
|
+
result = __primary_backend.double_metaphone(value, max_length, variant)
|
|
190
|
+
result.nil? ? nil : { primary: result[0], alternate: result[1] }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def beider_morse(input, rule_type: BmpmRuleType::APPROX,
|
|
195
|
+
concat: true, max_phonemes: 20)
|
|
196
|
+
register_bmpm_resources
|
|
197
|
+
unary(input) do |value|
|
|
198
|
+
__primary_backend.beider_morse(value, rule_type, concat, max_phonemes)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def dtw(query, target, window: nil, distance: nil, score_cutoff: nil)
|
|
203
|
+
unless query.is_a?(Array) && target.is_a?(Array)
|
|
204
|
+
raise ArgumentError, "query and target must be numeric Arrays"
|
|
205
|
+
end
|
|
206
|
+
distance ||= query.all? { |value| value.is_a?(Integer) } ? "l1" : "l2_squared"
|
|
207
|
+
__primary_backend.dtw(query, target, window, distance.to_s, score_cutoff)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def dtw_distances(query, targets, window: nil, distance: nil,
|
|
211
|
+
score_cutoff: nil)
|
|
212
|
+
raise ArgumentError, "targets must be an Array of numeric Arrays" unless targets.is_a?(Array)
|
|
213
|
+
targets.map do |target|
|
|
214
|
+
dtw(
|
|
215
|
+
query, target, window: window, distance: distance,
|
|
216
|
+
score_cutoff: score_cutoff
|
|
217
|
+
)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private
|
|
222
|
+
|
|
223
|
+
def apply_processor(value, processor)
|
|
224
|
+
return value if processor.nil? || value.nil?
|
|
225
|
+
raise ArgumentError, "processor must respond to call or be nil" unless processor.respond_to?(:call)
|
|
226
|
+
result = processor.call(value)
|
|
227
|
+
raise ArgumentError, "processor must return one non-nil String" unless result.is_a?(String)
|
|
228
|
+
result.encode(Encoding::UTF_8)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def processed_pairwise(query, target, processor)
|
|
232
|
+
pairwise(query, target) do |left, right|
|
|
233
|
+
yield(apply_processor(left, processor), apply_processor(right, processor))
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def resolve_inner_scorer(inner)
|
|
238
|
+
return inner if inner.respond_to?(:call)
|
|
239
|
+
case inner.to_s
|
|
240
|
+
when "jaro" then method(:jaro_similarity)
|
|
241
|
+
when "jaro_winkler" then method(:jaro_winkler_similarity)
|
|
242
|
+
when "levenshtein_ratio" then method(:levenshtein_normalized_score)
|
|
243
|
+
when "indel_ratio" then method(:indel_normalized_score)
|
|
244
|
+
else
|
|
245
|
+
raise ArgumentError, "unknown inner similarity: #{inner}"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def monge_direction(left, right, scorer)
|
|
250
|
+
left.sum do |token|
|
|
251
|
+
right.map do |candidate|
|
|
252
|
+
value = scorer.call(token, candidate)
|
|
253
|
+
unless value.is_a?(Numeric) && value.finite?
|
|
254
|
+
raise ArgumentError, "inner must return one finite numeric score"
|
|
255
|
+
end
|
|
256
|
+
value.to_f
|
|
257
|
+
end.max
|
|
258
|
+
end / left.length.to_f
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def phonetic_equal(query, target, algorithm, variant)
|
|
262
|
+
pairwise(query, target) do |left, right|
|
|
263
|
+
if left.nil? || right.nil?
|
|
264
|
+
nil
|
|
265
|
+
else
|
|
266
|
+
left_code, right_code, equal = case algorithm
|
|
267
|
+
when :soundex
|
|
268
|
+
[__primary_backend.soundex(left),
|
|
269
|
+
__primary_backend.soundex(right),
|
|
270
|
+
__primary_backend.soundex_equal(left, right)]
|
|
271
|
+
when :metaphone
|
|
272
|
+
[__primary_backend.metaphone(left, variant),
|
|
273
|
+
__primary_backend.metaphone(right, variant),
|
|
274
|
+
__primary_backend.metaphone_equal(left, right, variant)]
|
|
275
|
+
else
|
|
276
|
+
[__primary_backend.nysiis(left),
|
|
277
|
+
__primary_backend.nysiis(right),
|
|
278
|
+
__primary_backend.nysiis_equal(left, right)]
|
|
279
|
+
end
|
|
280
|
+
!left_code.empty? && !right_code.empty? && equal
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def register_bmpm_resources
|
|
286
|
+
return if @bmpm_registered
|
|
287
|
+
directory = __data_directory("bmpm_data")
|
|
288
|
+
resources = Dir[File.join(directory, "*.txt")].each_with_object({}) do |path, output|
|
|
289
|
+
output[File.basename(path, ".txt")] = File.binread(path)
|
|
290
|
+
end
|
|
291
|
+
raise LoadError, "stride-align BMPM rule resources are missing" if resources.empty?
|
|
292
|
+
__primary_backend.bmpm_register(resources)
|
|
293
|
+
@bmpm_registered = true
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|