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,108 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// American Soundex (Russell & Odell, 1918; codified by the US Census).
|
|
4
|
+
//
|
|
5
|
+
// Standard rules:
|
|
6
|
+
// 1. First letter is kept verbatim (upper-cased) as the leading
|
|
7
|
+
// character of the four-character output.
|
|
8
|
+
// 2. Remaining letters are mapped to digit codes:
|
|
9
|
+
// A E I O U Y -> 0 (vowels — break adjacency, don't emit)
|
|
10
|
+
// H W -> transparent (don't emit, don't break
|
|
11
|
+
// adjacency; H/W between two same-coded
|
|
12
|
+
// letters lets them still collapse)
|
|
13
|
+
// B F P V -> 1
|
|
14
|
+
// C G J K Q S X Z -> 2
|
|
15
|
+
// D T -> 3
|
|
16
|
+
// L -> 4
|
|
17
|
+
// M N -> 5
|
|
18
|
+
// R -> 6
|
|
19
|
+
// 3. Adjacent letters mapping to the same digit are collapsed.
|
|
20
|
+
// 4. Vowels (including Y) break adjacency. H and W do not.
|
|
21
|
+
// 5. Output is padded with '0' to length 4, or truncated to 4.
|
|
22
|
+
//
|
|
23
|
+
// Non-ASCII codepoints in str inputs are skipped — Soundex is an
|
|
24
|
+
// ASCII algorithm by construction. Callers wanting accent-folding
|
|
25
|
+
// pre-normalise with ``unicodedata.normalize('NFKD',
|
|
26
|
+
// s).encode('ascii', 'ignore').decode()`` (matches jellyfish's
|
|
27
|
+
// behaviour) before calling.
|
|
28
|
+
|
|
29
|
+
#include <cstddef>
|
|
30
|
+
#include <cstdint>
|
|
31
|
+
#include <string>
|
|
32
|
+
#include <string_view>
|
|
33
|
+
|
|
34
|
+
namespace stride_align::phonetic {
|
|
35
|
+
|
|
36
|
+
namespace soundex_detail {
|
|
37
|
+
|
|
38
|
+
// Codes for A..Z; '0' for vowels (AEIOUY) and H/W. The collapse
|
|
39
|
+
// rule treats H/W specially (transparent); this table just gives
|
|
40
|
+
// the digit value, the loop handles the transparency.
|
|
41
|
+
// A B C D E F
|
|
42
|
+
constexpr char kSoundexCode[26] = { '0', '1', '2', '3', '0', '1',
|
|
43
|
+
// G H I J K L
|
|
44
|
+
'2', '0', '0', '2', '2', '4',
|
|
45
|
+
// M N O P Q R
|
|
46
|
+
'5', '5', '0', '1', '2', '6',
|
|
47
|
+
// S T U V W X
|
|
48
|
+
'2', '3', '0', '1', '0', '2',
|
|
49
|
+
// Y Z
|
|
50
|
+
'0', '2'};
|
|
51
|
+
|
|
52
|
+
inline constexpr char to_upper_ascii(char c) noexcept {
|
|
53
|
+
return (c >= 'a' && c <= 'z') ? static_cast<char>(c - ('a' - 'A')) : c;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
inline constexpr bool is_upper_alpha(char c) noexcept {
|
|
57
|
+
return c >= 'A' && c <= 'Z';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
} // namespace soundex_detail
|
|
61
|
+
|
|
62
|
+
// Compute the Soundex encoding of ``input`` (interpreted as a
|
|
63
|
+
// sequence of ASCII bytes — non-letters and non-ASCII bytes are
|
|
64
|
+
// skipped). Returns a 4-character string when ``input`` contains
|
|
65
|
+
// at least one ASCII letter, else an empty string.
|
|
66
|
+
inline std::string soundex(std::string_view input) {
|
|
67
|
+
using namespace soundex_detail;
|
|
68
|
+
|
|
69
|
+
std::string out;
|
|
70
|
+
|
|
71
|
+
// Find first ASCII letter.
|
|
72
|
+
std::size_t i = 0;
|
|
73
|
+
for (; i < input.size(); ++i) {
|
|
74
|
+
if (is_upper_alpha(to_upper_ascii(input[i]))) break;
|
|
75
|
+
}
|
|
76
|
+
if (i >= input.size()) return out;
|
|
77
|
+
|
|
78
|
+
out.reserve(4);
|
|
79
|
+
const char first = to_upper_ascii(input[i]);
|
|
80
|
+
out.push_back(first);
|
|
81
|
+
char last_code = kSoundexCode[first - 'A'];
|
|
82
|
+
|
|
83
|
+
for (++i; i < input.size() && out.size() < 4; ++i) {
|
|
84
|
+
const char c = to_upper_ascii(input[i]);
|
|
85
|
+
if (!is_upper_alpha(c)) continue;
|
|
86
|
+
|
|
87
|
+
// H and W are transparent: don't emit, don't break adjacency.
|
|
88
|
+
if (c == 'H' || c == 'W') continue;
|
|
89
|
+
|
|
90
|
+
const char code = kSoundexCode[c - 'A'];
|
|
91
|
+
|
|
92
|
+
if (code == '0') {
|
|
93
|
+
// True vowel (AEIOUY): breaks adjacency for the collapse
|
|
94
|
+
// rule but emits nothing.
|
|
95
|
+
last_code = '0';
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (code != last_code) out.push_back(code);
|
|
100
|
+
last_code = code;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Pad to length 4.
|
|
104
|
+
while (out.size() < 4) out.push_back('0');
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
} // namespace stride_align::phonetic
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// ``sa.token_sort_ratio`` and ``sa.token_set_ratio`` kernels.
|
|
4
|
+
//
|
|
5
|
+
// Both functions used to live in ``src/stride_align/_fuzz.py`` as pure
|
|
6
|
+
// Python: split-on-whitespace, set algebra, sort, join, then call into
|
|
7
|
+
// the Indel kernel. Profiling on a representative ``m=54 n=66`` pair
|
|
8
|
+
// showed 71% of token_set_ratio's per-call time was Python overhead
|
|
9
|
+
// (tokenisation + set ops + sort + join), only 18% was the actual
|
|
10
|
+
// Indel kernel. Lifting the whole recipe into C++ closes that gap.
|
|
11
|
+
//
|
|
12
|
+
// Tokenisation is whitespace-only (``' '``, ``'\t'``, ``'\n'``,
|
|
13
|
+
// ``'\r'``) to match the Python ``str.split()`` convention. Tokens
|
|
14
|
+
// are returned as views over the input buffer — no copies — so the
|
|
15
|
+
// per-call allocation is one ``std::vector<std::span<...>>`` for the
|
|
16
|
+
// token list plus one output buffer for each candidate string built
|
|
17
|
+
// by the set-ratio path.
|
|
18
|
+
//
|
|
19
|
+
// Algorithm (matches the Python reference bit-exactly):
|
|
20
|
+
//
|
|
21
|
+
// token_sort_ratio(a, b):
|
|
22
|
+
// ja = sort_join(tokens(a))
|
|
23
|
+
// jb = sort_join(tokens(b))
|
|
24
|
+
// return indel_normalized(ja, jb)
|
|
25
|
+
//
|
|
26
|
+
// token_set_ratio(a, b):
|
|
27
|
+
// ta = dedupe_sorted(tokens(a))
|
|
28
|
+
// tb = dedupe_sorted(tokens(b))
|
|
29
|
+
// intersect, diff_a, diff_b = three-way merge over (ta, tb)
|
|
30
|
+
// t0 = join(intersect)
|
|
31
|
+
// t1 = join(intersect ++ diff_a) # concat, NOT re-merged
|
|
32
|
+
// t2 = join(intersect ++ diff_b) # "
|
|
33
|
+
// return max(indel_normalized(t0, t1),
|
|
34
|
+
// indel_normalized(t0, t2),
|
|
35
|
+
// indel_normalized(t1, t2))
|
|
36
|
+
//
|
|
37
|
+
// The "concat NOT re-merged" detail matters: rapidfuzz preserves
|
|
38
|
+
// ``t1 = intersect_sorted then diff_a_sorted``, which is NOT the
|
|
39
|
+
// same as ``sorted(intersect ∪ diff_a)`` whenever the two have
|
|
40
|
+
// interleaving lex order. Tests pin this against rapidfuzz.
|
|
41
|
+
|
|
42
|
+
#include <algorithm>
|
|
43
|
+
#include <cstddef>
|
|
44
|
+
#include <cstdint>
|
|
45
|
+
#include <span>
|
|
46
|
+
#include <type_traits>
|
|
47
|
+
#include <vector>
|
|
48
|
+
|
|
49
|
+
#include "stride_align/indel.hpp"
|
|
50
|
+
#include "stride_align/lcs.hpp"
|
|
51
|
+
|
|
52
|
+
namespace stride_align::token_ratios {
|
|
53
|
+
|
|
54
|
+
using Codepoint = ::stride_align::lcs::Codepoint;
|
|
55
|
+
|
|
56
|
+
// Whitespace classifier. Matches Python's ``str.split()`` default
|
|
57
|
+
// (split on any sequence of ASCII whitespace).
|
|
58
|
+
template <typename Token>
|
|
59
|
+
inline bool is_ws(Token c) noexcept {
|
|
60
|
+
return c == Token{' '} || c == Token{'\t'} ||
|
|
61
|
+
c == Token{'\n'} || c == Token{'\r'};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Split ``s`` on runs of whitespace, returning views over the input
|
|
65
|
+
// buffer (no copies). Empty input yields an empty vector. Leading and
|
|
66
|
+
// trailing whitespace is skipped (matches ``str.split()``).
|
|
67
|
+
template <typename Token>
|
|
68
|
+
inline std::vector<std::span<const Token>> tokens_of(
|
|
69
|
+
std::span<const Token> s) {
|
|
70
|
+
std::vector<std::span<const Token>> out;
|
|
71
|
+
std::size_t i = 0;
|
|
72
|
+
while (i < s.size()) {
|
|
73
|
+
while (i < s.size() && is_ws(s[i])) ++i;
|
|
74
|
+
if (i >= s.size()) break;
|
|
75
|
+
const std::size_t start = i;
|
|
76
|
+
while (i < s.size() && !is_ws(s[i])) ++i;
|
|
77
|
+
out.emplace_back(s.data() + start, i - start);
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Lexicographic comparison of two token spans. Used for sorting and
|
|
83
|
+
// the three-way set-merge.
|
|
84
|
+
template <typename Token>
|
|
85
|
+
struct LexLess {
|
|
86
|
+
bool operator()(std::span<const Token> a,
|
|
87
|
+
std::span<const Token> b) const noexcept {
|
|
88
|
+
const std::size_t n = std::min(a.size(), b.size());
|
|
89
|
+
for (std::size_t i = 0; i < n; ++i) {
|
|
90
|
+
if (a[i] != b[i]) return a[i] < b[i];
|
|
91
|
+
}
|
|
92
|
+
return a.size() < b.size();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
template <typename Token>
|
|
97
|
+
struct LexEqual {
|
|
98
|
+
bool operator()(std::span<const Token> a,
|
|
99
|
+
std::span<const Token> b) const noexcept {
|
|
100
|
+
return std::equal(a.begin(), a.end(), b.begin(), b.end());
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Join token spans with a single space separator, writing into
|
|
105
|
+
// ``out``. Caller may reuse ``out`` across calls — it's cleared before
|
|
106
|
+
// writing.
|
|
107
|
+
template <typename Token>
|
|
108
|
+
inline void join_with_space(
|
|
109
|
+
const std::vector<std::span<const Token>>& parts,
|
|
110
|
+
std::vector<Token>& out) {
|
|
111
|
+
std::size_t total = 0;
|
|
112
|
+
if (!parts.empty()) {
|
|
113
|
+
for (const auto& p : parts) total += p.size();
|
|
114
|
+
total += parts.size() - 1U; // separators
|
|
115
|
+
}
|
|
116
|
+
out.clear();
|
|
117
|
+
out.reserve(total);
|
|
118
|
+
for (std::size_t k = 0; k < parts.size(); ++k) {
|
|
119
|
+
if (k > 0) out.push_back(Token{' '});
|
|
120
|
+
out.insert(out.end(), parts[k].begin(), parts[k].end());
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Indel-normalised similarity for two prepared sequences:
|
|
125
|
+
//
|
|
126
|
+
// 1 - indel(a, b) / (|a| + |b|)
|
|
127
|
+
//
|
|
128
|
+
// Empty-both convention matches rapidfuzz: ``("", "") -> 1.0``.
|
|
129
|
+
template <typename Token>
|
|
130
|
+
inline double indel_normalized(
|
|
131
|
+
std::span<const Token> a,
|
|
132
|
+
std::span<const Token> b) {
|
|
133
|
+
if (a.empty() && b.empty()) return 1.0;
|
|
134
|
+
const std::size_t total = a.size() + b.size();
|
|
135
|
+
if (total == 0U) return 1.0;
|
|
136
|
+
std::size_t d;
|
|
137
|
+
if constexpr (std::is_same_v<Token, std::uint8_t>) {
|
|
138
|
+
// Flat 256-entry PEQ + direct indexing — the bit-parallel byte path
|
|
139
|
+
// fuzz.ratio already uses. The generic indel_distance<Token> builds an
|
|
140
|
+
// unordered_map PEQ and hash-looks-up per text char; that kernel (not
|
|
141
|
+
// the tokenise/sort/join) was the token-ratio gap vs rapidfuzz.
|
|
142
|
+
d = ::stride_align::indel::indel_distance_u8(a, b);
|
|
143
|
+
} else {
|
|
144
|
+
d = ::stride_align::indel::indel_distance<Token>(a, b);
|
|
145
|
+
}
|
|
146
|
+
return 1.0 - static_cast<double>(d) / static_cast<double>(total);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Forward declarations of the thread-local scratch type and accessor
|
|
150
|
+
// used by the engines below. The full definitions appear later in
|
|
151
|
+
// the file — they need the engines' helper types.
|
|
152
|
+
template <typename Token>
|
|
153
|
+
struct TokenWorkspace;
|
|
154
|
+
template <typename Token>
|
|
155
|
+
inline TokenWorkspace<Token>& token_workspace();
|
|
156
|
+
|
|
157
|
+
// token_sort_ratio engine. Uses thread-local scratch buffers for
|
|
158
|
+
// the token lists and the joined output, so consecutive calls reuse
|
|
159
|
+
// allocations.
|
|
160
|
+
template <typename Token>
|
|
161
|
+
inline double token_sort_ratio_engine(
|
|
162
|
+
std::span<const Token> a,
|
|
163
|
+
std::span<const Token> b);
|
|
164
|
+
|
|
165
|
+
// token_set_ratio engine. Same thread-local scratch as the sort
|
|
166
|
+
// engine — the two share buffers when called in sequence (e.g. by
|
|
167
|
+
// the WRatio path below).
|
|
168
|
+
template <typename Token>
|
|
169
|
+
inline double token_set_ratio_engine(
|
|
170
|
+
std::span<const Token> a,
|
|
171
|
+
std::span<const Token> b);
|
|
172
|
+
|
|
173
|
+
// Public byte-fast-path-or-codepoint dispatchers. The byte fast path
|
|
174
|
+
// drops to ``std::uint8_t`` token type when every codepoint in both
|
|
175
|
+
// inputs is < 256, which routes the per-window Indel call through the
|
|
176
|
+
// flat 256-row PEQ and the K-specialised multi-word kernel.
|
|
177
|
+
inline double token_sort_ratio(
|
|
178
|
+
const std::vector<Codepoint>& a,
|
|
179
|
+
const std::vector<Codepoint>& b) {
|
|
180
|
+
bool fits_in_byte = true;
|
|
181
|
+
for (const auto cp : a) { if (cp >= 256U) { fits_in_byte = false; break; } }
|
|
182
|
+
if (fits_in_byte) {
|
|
183
|
+
for (const auto cp : b) { if (cp >= 256U) { fits_in_byte = false; break; } }
|
|
184
|
+
}
|
|
185
|
+
if (fits_in_byte) {
|
|
186
|
+
std::vector<std::uint8_t> ab(a.begin(), a.end());
|
|
187
|
+
std::vector<std::uint8_t> bb(b.begin(), b.end());
|
|
188
|
+
return token_sort_ratio_engine<std::uint8_t>(
|
|
189
|
+
std::span<const std::uint8_t>(ab),
|
|
190
|
+
std::span<const std::uint8_t>(bb));
|
|
191
|
+
}
|
|
192
|
+
return token_sort_ratio_engine<Codepoint>(
|
|
193
|
+
std::span<const Codepoint>(a),
|
|
194
|
+
std::span<const Codepoint>(b));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// In-place tokenisation variant that writes into a caller-owned
|
|
198
|
+
// buffer. Used by the combined WRatio token path that shares a
|
|
199
|
+
// thread-local scratch across token_sort + token_set computations.
|
|
200
|
+
template <typename Token>
|
|
201
|
+
inline void tokens_of_inplace(
|
|
202
|
+
std::span<const Token> s,
|
|
203
|
+
std::vector<std::span<const Token>>& out) {
|
|
204
|
+
out.clear();
|
|
205
|
+
std::size_t i = 0;
|
|
206
|
+
while (i < s.size()) {
|
|
207
|
+
while (i < s.size() && is_ws(s[i])) ++i;
|
|
208
|
+
if (i >= s.size()) break;
|
|
209
|
+
const std::size_t start = i;
|
|
210
|
+
while (i < s.size() && !is_ws(s[i])) ++i;
|
|
211
|
+
out.emplace_back(s.data() + start, i - start);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Thread-local scratch for the combined token-sort + token-set path.
|
|
216
|
+
// Carries every per-call buffer (token lists, sorted-vs-unique
|
|
217
|
+
// views, candidate joins) so consecutive WRatio calls reuse the
|
|
218
|
+
// allocations.
|
|
219
|
+
template <typename Token>
|
|
220
|
+
struct TokenWorkspace {
|
|
221
|
+
std::vector<std::span<const Token>> tokens_a, tokens_b;
|
|
222
|
+
std::vector<std::span<const Token>> intersect, diff_a, diff_b;
|
|
223
|
+
std::vector<std::span<const Token>> t1_parts, t2_parts;
|
|
224
|
+
std::vector<Token> ja, jb; // sort_ratio joined strings
|
|
225
|
+
std::vector<Token> t0, t1, t2; // set_ratio candidate strings
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
template <typename Token>
|
|
229
|
+
inline TokenWorkspace<Token>& token_workspace() {
|
|
230
|
+
thread_local TokenWorkspace<Token> w;
|
|
231
|
+
return w;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Combined entry: compute both token_sort_ratio AND token_set_ratio
|
|
235
|
+
// in one pass over the inputs. Used by ``wratio_engine`` in the
|
|
236
|
+
// length-similar branch (``len_ratio < 1.5``), where both ratios are
|
|
237
|
+
// needed. Tokenises ``a`` and ``b`` ONCE, sorts ONCE (with
|
|
238
|
+
// duplicates), reuses the sorted views for the dedupe + set-merge,
|
|
239
|
+
// and shares the thread-local join buffers across the 1 + 3 indel
|
|
240
|
+
// calls. Writes the two ratios into the out-params.
|
|
241
|
+
template <typename Token>
|
|
242
|
+
inline void compute_token_sort_and_set_ratio(
|
|
243
|
+
std::span<const Token> a,
|
|
244
|
+
std::span<const Token> b,
|
|
245
|
+
double& sort_score,
|
|
246
|
+
double& set_score) {
|
|
247
|
+
auto& w = token_workspace<Token>();
|
|
248
|
+
tokens_of_inplace(a, w.tokens_a);
|
|
249
|
+
tokens_of_inplace(b, w.tokens_b);
|
|
250
|
+
|
|
251
|
+
// Edge cases mirror the standalone engines.
|
|
252
|
+
const bool a_empty = w.tokens_a.empty();
|
|
253
|
+
const bool b_empty = w.tokens_b.empty();
|
|
254
|
+
if (a_empty && b_empty) {
|
|
255
|
+
sort_score = 1.0;
|
|
256
|
+
set_score = 0.0; // rapidfuzz convention: empty/empty token_set is 0
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (a_empty || b_empty) {
|
|
260
|
+
sort_score = 0.0;
|
|
261
|
+
set_score = 0.0;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const LexLess<Token> lt;
|
|
266
|
+
std::sort(w.tokens_a.begin(), w.tokens_a.end(), lt);
|
|
267
|
+
std::sort(w.tokens_b.begin(), w.tokens_b.end(), lt);
|
|
268
|
+
|
|
269
|
+
// --- token_sort_ratio leg: join the (sorted, with duplicates) views.
|
|
270
|
+
join_with_space(w.tokens_a, w.ja);
|
|
271
|
+
join_with_space(w.tokens_b, w.jb);
|
|
272
|
+
sort_score = indel_normalized<Token>(
|
|
273
|
+
std::span<const Token>(w.ja.data(), w.ja.size()),
|
|
274
|
+
std::span<const Token>(w.jb.data(), w.jb.size()));
|
|
275
|
+
|
|
276
|
+
// --- token_set_ratio leg: dedupe the already-sorted token lists
|
|
277
|
+
// in place. The set algebra then runs on the unique views without
|
|
278
|
+
// a second sort pass.
|
|
279
|
+
w.tokens_a.erase(std::unique(w.tokens_a.begin(), w.tokens_a.end(),
|
|
280
|
+
LexEqual<Token>{}),
|
|
281
|
+
w.tokens_a.end());
|
|
282
|
+
w.tokens_b.erase(std::unique(w.tokens_b.begin(), w.tokens_b.end(),
|
|
283
|
+
LexEqual<Token>{}),
|
|
284
|
+
w.tokens_b.end());
|
|
285
|
+
|
|
286
|
+
w.intersect.clear();
|
|
287
|
+
w.diff_a.clear();
|
|
288
|
+
w.diff_b.clear();
|
|
289
|
+
std::size_t i = 0, j = 0;
|
|
290
|
+
while (i < w.tokens_a.size() && j < w.tokens_b.size()) {
|
|
291
|
+
if (lt(w.tokens_a[i], w.tokens_b[j])) {
|
|
292
|
+
w.diff_a.push_back(w.tokens_a[i]); ++i;
|
|
293
|
+
} else if (lt(w.tokens_b[j], w.tokens_a[i])) {
|
|
294
|
+
w.diff_b.push_back(w.tokens_b[j]); ++j;
|
|
295
|
+
} else {
|
|
296
|
+
w.intersect.push_back(w.tokens_a[i]); ++i; ++j;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
while (i < w.tokens_a.size()) w.diff_a.push_back(w.tokens_a[i++]);
|
|
300
|
+
while (j < w.tokens_b.size()) w.diff_b.push_back(w.tokens_b[j++]);
|
|
301
|
+
|
|
302
|
+
if (w.diff_a.empty() && w.diff_b.empty()) {
|
|
303
|
+
set_score = 1.0;
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
w.t1_parts.assign(w.intersect.begin(), w.intersect.end());
|
|
308
|
+
w.t1_parts.insert(w.t1_parts.end(), w.diff_a.begin(), w.diff_a.end());
|
|
309
|
+
w.t2_parts.assign(w.intersect.begin(), w.intersect.end());
|
|
310
|
+
w.t2_parts.insert(w.t2_parts.end(), w.diff_b.begin(), w.diff_b.end());
|
|
311
|
+
|
|
312
|
+
join_with_space(w.intersect, w.t0);
|
|
313
|
+
join_with_space(w.t1_parts, w.t1);
|
|
314
|
+
join_with_space(w.t2_parts, w.t2);
|
|
315
|
+
|
|
316
|
+
const double r0 = indel_normalized<Token>(
|
|
317
|
+
std::span<const Token>(w.t0.data(), w.t0.size()),
|
|
318
|
+
std::span<const Token>(w.t1.data(), w.t1.size()));
|
|
319
|
+
if (r0 >= 1.0) { set_score = 1.0; return; }
|
|
320
|
+
const double r1 = indel_normalized<Token>(
|
|
321
|
+
std::span<const Token>(w.t0.data(), w.t0.size()),
|
|
322
|
+
std::span<const Token>(w.t2.data(), w.t2.size()));
|
|
323
|
+
if (r1 >= 1.0) { set_score = 1.0; return; }
|
|
324
|
+
const double r2 = indel_normalized<Token>(
|
|
325
|
+
std::span<const Token>(w.t1.data(), w.t1.size()),
|
|
326
|
+
std::span<const Token>(w.t2.data(), w.t2.size()));
|
|
327
|
+
set_score = std::max({r0, r1, r2});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Standalone engine definitions (forward-declared earlier in the file
|
|
331
|
+
// because they need ``TokenWorkspace``).
|
|
332
|
+
|
|
333
|
+
template <typename Token>
|
|
334
|
+
inline double token_sort_ratio_engine(
|
|
335
|
+
std::span<const Token> a,
|
|
336
|
+
std::span<const Token> b) {
|
|
337
|
+
auto& w = token_workspace<Token>();
|
|
338
|
+
tokens_of_inplace(a, w.tokens_a);
|
|
339
|
+
tokens_of_inplace(b, w.tokens_b);
|
|
340
|
+
if (w.tokens_a.empty() && w.tokens_b.empty()) {
|
|
341
|
+
return 1.0;
|
|
342
|
+
}
|
|
343
|
+
std::sort(w.tokens_a.begin(), w.tokens_a.end(), LexLess<Token>{});
|
|
344
|
+
std::sort(w.tokens_b.begin(), w.tokens_b.end(), LexLess<Token>{});
|
|
345
|
+
join_with_space(w.tokens_a, w.ja);
|
|
346
|
+
join_with_space(w.tokens_b, w.jb);
|
|
347
|
+
return indel_normalized<Token>(
|
|
348
|
+
std::span<const Token>(w.ja.data(), w.ja.size()),
|
|
349
|
+
std::span<const Token>(w.jb.data(), w.jb.size()));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
template <typename Token>
|
|
353
|
+
inline double token_set_ratio_engine(
|
|
354
|
+
std::span<const Token> a,
|
|
355
|
+
std::span<const Token> b) {
|
|
356
|
+
auto& w = token_workspace<Token>();
|
|
357
|
+
tokens_of_inplace(a, w.tokens_a);
|
|
358
|
+
tokens_of_inplace(b, w.tokens_b);
|
|
359
|
+
if (w.tokens_a.empty() || w.tokens_b.empty()) return 0.0;
|
|
360
|
+
|
|
361
|
+
const LexLess<Token> lt;
|
|
362
|
+
std::sort(w.tokens_a.begin(), w.tokens_a.end(), lt);
|
|
363
|
+
w.tokens_a.erase(std::unique(w.tokens_a.begin(), w.tokens_a.end(),
|
|
364
|
+
LexEqual<Token>{}),
|
|
365
|
+
w.tokens_a.end());
|
|
366
|
+
std::sort(w.tokens_b.begin(), w.tokens_b.end(), lt);
|
|
367
|
+
w.tokens_b.erase(std::unique(w.tokens_b.begin(), w.tokens_b.end(),
|
|
368
|
+
LexEqual<Token>{}),
|
|
369
|
+
w.tokens_b.end());
|
|
370
|
+
|
|
371
|
+
w.intersect.clear();
|
|
372
|
+
w.diff_a.clear();
|
|
373
|
+
w.diff_b.clear();
|
|
374
|
+
std::size_t i = 0, j = 0;
|
|
375
|
+
while (i < w.tokens_a.size() && j < w.tokens_b.size()) {
|
|
376
|
+
if (lt(w.tokens_a[i], w.tokens_b[j])) {
|
|
377
|
+
w.diff_a.push_back(w.tokens_a[i]); ++i;
|
|
378
|
+
} else if (lt(w.tokens_b[j], w.tokens_a[i])) {
|
|
379
|
+
w.diff_b.push_back(w.tokens_b[j]); ++j;
|
|
380
|
+
} else {
|
|
381
|
+
w.intersect.push_back(w.tokens_a[i]); ++i; ++j;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
while (i < w.tokens_a.size()) w.diff_a.push_back(w.tokens_a[i++]);
|
|
385
|
+
while (j < w.tokens_b.size()) w.diff_b.push_back(w.tokens_b[j++]);
|
|
386
|
+
|
|
387
|
+
if (w.diff_a.empty() && w.diff_b.empty()) return 1.0;
|
|
388
|
+
|
|
389
|
+
w.t1_parts.assign(w.intersect.begin(), w.intersect.end());
|
|
390
|
+
w.t1_parts.insert(w.t1_parts.end(), w.diff_a.begin(), w.diff_a.end());
|
|
391
|
+
w.t2_parts.assign(w.intersect.begin(), w.intersect.end());
|
|
392
|
+
w.t2_parts.insert(w.t2_parts.end(), w.diff_b.begin(), w.diff_b.end());
|
|
393
|
+
|
|
394
|
+
join_with_space(w.intersect, w.t0);
|
|
395
|
+
join_with_space(w.t1_parts, w.t1);
|
|
396
|
+
join_with_space(w.t2_parts, w.t2);
|
|
397
|
+
|
|
398
|
+
const double r0 = indel_normalized<Token>(
|
|
399
|
+
std::span<const Token>(w.t0.data(), w.t0.size()),
|
|
400
|
+
std::span<const Token>(w.t1.data(), w.t1.size()));
|
|
401
|
+
if (r0 >= 1.0) return 1.0;
|
|
402
|
+
const double r1 = indel_normalized<Token>(
|
|
403
|
+
std::span<const Token>(w.t0.data(), w.t0.size()),
|
|
404
|
+
std::span<const Token>(w.t2.data(), w.t2.size()));
|
|
405
|
+
if (r1 >= 1.0) return 1.0;
|
|
406
|
+
const double r2 = indel_normalized<Token>(
|
|
407
|
+
std::span<const Token>(w.t1.data(), w.t1.size()),
|
|
408
|
+
std::span<const Token>(w.t2.data(), w.t2.size()));
|
|
409
|
+
return std::max({r0, r1, r2});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Byte fast path entries: caller has already established
|
|
413
|
+
// byte-compatible inputs (ASCII Python str or bytes-like).
|
|
414
|
+
inline double token_sort_ratio_bytes(
|
|
415
|
+
std::span<const std::uint8_t> a,
|
|
416
|
+
std::span<const std::uint8_t> b) {
|
|
417
|
+
return token_sort_ratio_engine<std::uint8_t>(a, b);
|
|
418
|
+
}
|
|
419
|
+
inline double token_set_ratio_bytes(
|
|
420
|
+
std::span<const std::uint8_t> a,
|
|
421
|
+
std::span<const std::uint8_t> b) {
|
|
422
|
+
return token_set_ratio_engine<std::uint8_t>(a, b);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
inline double token_set_ratio(
|
|
426
|
+
const std::vector<Codepoint>& a,
|
|
427
|
+
const std::vector<Codepoint>& b) {
|
|
428
|
+
bool fits_in_byte = true;
|
|
429
|
+
for (const auto cp : a) { if (cp >= 256U) { fits_in_byte = false; break; } }
|
|
430
|
+
if (fits_in_byte) {
|
|
431
|
+
for (const auto cp : b) { if (cp >= 256U) { fits_in_byte = false; break; } }
|
|
432
|
+
}
|
|
433
|
+
if (fits_in_byte) {
|
|
434
|
+
std::vector<std::uint8_t> ab(a.begin(), a.end());
|
|
435
|
+
std::vector<std::uint8_t> bb(b.begin(), b.end());
|
|
436
|
+
return token_set_ratio_engine<std::uint8_t>(
|
|
437
|
+
std::span<const std::uint8_t>(ab),
|
|
438
|
+
std::span<const std::uint8_t>(bb));
|
|
439
|
+
}
|
|
440
|
+
return token_set_ratio_engine<Codepoint>(
|
|
441
|
+
std::span<const Codepoint>(a),
|
|
442
|
+
std::span<const Codepoint>(b));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
} // namespace stride_align::token_ratios
|