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,176 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// N-gram set similarity — Phase D.1.
|
|
4
|
+
//
|
|
5
|
+
// Four metrics over the multiset of character n-grams of each input:
|
|
6
|
+
//
|
|
7
|
+
// * **Jaccard.** J(A, B) = |A ∩ B| / |A ∪ B|
|
|
8
|
+
// * **Sørensen-Dice.** D(A, B) = 2·|A ∩ B| / (|A| + |B|)
|
|
9
|
+
// * **Cosine.** cos(A, B) = ⟨A, B⟩ / (‖A‖·‖B‖) over the
|
|
10
|
+
// multiset frequency vectors.
|
|
11
|
+
// * **Overlap.** O(A, B) = |A ∩ B| / min(|A|, |B|)
|
|
12
|
+
//
|
|
13
|
+
// All four are computed over MULTISETS (each n-gram counted with
|
|
14
|
+
// multiplicity). For ``a = "abab"`` and ``n = 2`` the multiset is
|
|
15
|
+
// ``{"ab": 2, "ba": 1}``; for ``a = "aaaa"`` and ``n = 2`` it is
|
|
16
|
+
// ``{"aa": 3}``. Set-form behaviour falls out for inputs whose n-grams
|
|
17
|
+
// are all distinct.
|
|
18
|
+
//
|
|
19
|
+
// Identity / edge cases:
|
|
20
|
+
//
|
|
21
|
+
// * Two empty inputs (or both inputs shorter than ``n``) give 1.0
|
|
22
|
+
// — vacuously identical, matches the rapidfuzz / scikit-learn
|
|
23
|
+
// convention.
|
|
24
|
+
// * One empty and one non-empty gives 0.0.
|
|
25
|
+
// * ``n == 0`` is rejected at the dispatch boundary.
|
|
26
|
+
//
|
|
27
|
+
// Engine works in codepoint space throughout. The dispatch wrapper
|
|
28
|
+
// widens Python ``str`` storage straight out of ``PyUnicode_DATA``
|
|
29
|
+
// into ``std::vector<Codepoint>`` — same convention as the other
|
|
30
|
+
// stride-align codepoint-engine entry points.
|
|
31
|
+
//
|
|
32
|
+
// Source: standard textbook formulas. The C++ here is original.
|
|
33
|
+
|
|
34
|
+
#include <algorithm>
|
|
35
|
+
#include <cmath>
|
|
36
|
+
#include <cstddef>
|
|
37
|
+
#include <cstdint>
|
|
38
|
+
#include <cstring>
|
|
39
|
+
#include <string>
|
|
40
|
+
#include <unordered_map>
|
|
41
|
+
#include <vector>
|
|
42
|
+
|
|
43
|
+
namespace stride_align::ngram {
|
|
44
|
+
|
|
45
|
+
using Codepoint = std::uint32_t;
|
|
46
|
+
|
|
47
|
+
// One key per n-gram: ``n`` consecutive codepoints packed as a binary
|
|
48
|
+
// ``std::string`` (``n * 4`` bytes). For default ``n = 2`` and typical
|
|
49
|
+
// ``n = 3 / 4`` the key fits in libstdc++'s small-string-optimisation
|
|
50
|
+
// buffer (15 bytes inline) and the multiset insert allocates only the
|
|
51
|
+
// hash-map node, not the key.
|
|
52
|
+
using NGramKey = std::string;
|
|
53
|
+
using NGramMultiset = std::unordered_map<NGramKey, std::uint32_t>;
|
|
54
|
+
|
|
55
|
+
// Construct the n-gram multiset of ``input``. When ``input.size() < n``
|
|
56
|
+
// the multiset is empty (the "shorter than n" edge case folds into the
|
|
57
|
+
// empty-input branch of every metric formula).
|
|
58
|
+
inline NGramMultiset build_multiset(const std::vector<Codepoint>& input,
|
|
59
|
+
std::size_t n) {
|
|
60
|
+
NGramMultiset m;
|
|
61
|
+
if (n == 0 || input.size() < n) return m;
|
|
62
|
+
const std::size_t count = input.size() - n + 1;
|
|
63
|
+
m.reserve(count);
|
|
64
|
+
NGramKey key(n * sizeof(Codepoint), '\0');
|
|
65
|
+
for (std::size_t i = 0; i < count; ++i) {
|
|
66
|
+
std::memcpy(key.data(), input.data() + i, n * sizeof(Codepoint));
|
|
67
|
+
++m[key];
|
|
68
|
+
}
|
|
69
|
+
return m;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Aggregated stats from a one-pass comparison of two multisets. Enough
|
|
73
|
+
// information here to compute all four metrics without re-walking the
|
|
74
|
+
// inputs.
|
|
75
|
+
struct ComparisonStats {
|
|
76
|
+
std::size_t size_a = 0; // sum of count_a (i.e. |A| with multiplicity)
|
|
77
|
+
std::size_t size_b = 0;
|
|
78
|
+
std::size_t intersection = 0; // sum over keys of ``min(count_a, count_b)``
|
|
79
|
+
std::uint64_t dot_product = 0;// sum over keys of ``count_a · count_b``
|
|
80
|
+
std::uint64_t norm_a_sq = 0; // sum over keys of ``count_a²``
|
|
81
|
+
std::uint64_t norm_b_sq = 0;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
inline ComparisonStats compare(const NGramMultiset& a, const NGramMultiset& b) {
|
|
85
|
+
ComparisonStats s;
|
|
86
|
+
// One pass over ``a`` covers size_a, norm_a_sq, intersection, dot.
|
|
87
|
+
// A separate pass over ``b`` picks up size_b and norm_b_sq; keys
|
|
88
|
+
// present in both sets are visited twice (once from each side) but
|
|
89
|
+
// contribute only to the corresponding side's totals.
|
|
90
|
+
for (const auto& [key, count_a] : a) {
|
|
91
|
+
s.size_a += count_a;
|
|
92
|
+
const auto ca = static_cast<std::uint64_t>(count_a);
|
|
93
|
+
s.norm_a_sq += ca * ca;
|
|
94
|
+
auto it = b.find(key);
|
|
95
|
+
if (it != b.end()) {
|
|
96
|
+
const auto count_b = it->second;
|
|
97
|
+
s.intersection += std::min(count_a, count_b);
|
|
98
|
+
s.dot_product += ca * static_cast<std::uint64_t>(count_b);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const auto& [_, count_b] : b) {
|
|
102
|
+
s.size_b += count_b;
|
|
103
|
+
const auto cb = static_cast<std::uint64_t>(count_b);
|
|
104
|
+
s.norm_b_sq += cb * cb;
|
|
105
|
+
}
|
|
106
|
+
return s;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Individual metric formulas. All four follow the same identity
|
|
110
|
+
// convention: two empty inputs → 1.0, one empty → 0.0.
|
|
111
|
+
|
|
112
|
+
inline double jaccard_from_stats(const ComparisonStats& s) {
|
|
113
|
+
if (s.size_a == 0 && s.size_b == 0) return 1.0;
|
|
114
|
+
const std::size_t union_size = s.size_a + s.size_b - s.intersection;
|
|
115
|
+
if (union_size == 0) return 0.0;
|
|
116
|
+
return static_cast<double>(s.intersection)
|
|
117
|
+
/ static_cast<double>(union_size);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
inline double dice_from_stats(const ComparisonStats& s) {
|
|
121
|
+
if (s.size_a == 0 && s.size_b == 0) return 1.0;
|
|
122
|
+
const std::size_t total = s.size_a + s.size_b;
|
|
123
|
+
if (total == 0) return 0.0;
|
|
124
|
+
return 2.0 * static_cast<double>(s.intersection)
|
|
125
|
+
/ static_cast<double>(total);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
inline double cosine_from_stats(const ComparisonStats& s) {
|
|
129
|
+
if (s.size_a == 0 && s.size_b == 0) return 1.0;
|
|
130
|
+
if (s.norm_a_sq == 0 || s.norm_b_sq == 0) return 0.0;
|
|
131
|
+
const double denom = std::sqrt(static_cast<double>(s.norm_a_sq)
|
|
132
|
+
* static_cast<double>(s.norm_b_sq));
|
|
133
|
+
return static_cast<double>(s.dot_product) / denom;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
inline double overlap_from_stats(const ComparisonStats& s) {
|
|
137
|
+
if (s.size_a == 0 && s.size_b == 0) return 1.0;
|
|
138
|
+
const std::size_t min_size = std::min(s.size_a, s.size_b);
|
|
139
|
+
if (min_size == 0) return 0.0;
|
|
140
|
+
return static_cast<double>(s.intersection)
|
|
141
|
+
/ static_cast<double>(min_size);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Convenience scalar entry points. ``n`` defaults to 2 (character
|
|
145
|
+
// bigrams), the rapidfuzz / scikit-learn default for character-n-gram
|
|
146
|
+
// similarities.
|
|
147
|
+
|
|
148
|
+
inline double jaccard(const std::vector<Codepoint>& a,
|
|
149
|
+
const std::vector<Codepoint>& b,
|
|
150
|
+
std::size_t n = 2) {
|
|
151
|
+
return jaccard_from_stats(compare(build_multiset(a, n),
|
|
152
|
+
build_multiset(b, n)));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
inline double dice(const std::vector<Codepoint>& a,
|
|
156
|
+
const std::vector<Codepoint>& b,
|
|
157
|
+
std::size_t n = 2) {
|
|
158
|
+
return dice_from_stats(compare(build_multiset(a, n),
|
|
159
|
+
build_multiset(b, n)));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
inline double cosine(const std::vector<Codepoint>& a,
|
|
163
|
+
const std::vector<Codepoint>& b,
|
|
164
|
+
std::size_t n = 2) {
|
|
165
|
+
return cosine_from_stats(compare(build_multiset(a, n),
|
|
166
|
+
build_multiset(b, n)));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
inline double overlap(const std::vector<Codepoint>& a,
|
|
170
|
+
const std::vector<Codepoint>& b,
|
|
171
|
+
std::size_t n = 2) {
|
|
172
|
+
return overlap_from_stats(compare(build_multiset(a, n),
|
|
173
|
+
build_multiset(b, n)));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
} // namespace stride_align::ngram
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// NYSIIS — New York State Identification and Intelligence System
|
|
4
|
+
// phonetic encoder (Taft, 1970).
|
|
5
|
+
//
|
|
6
|
+
// Developed for cross-referencing person records, NYSIIS tends to
|
|
7
|
+
// produce more discriminative codes than Soundex for English-
|
|
8
|
+
// language names. Returns a code up to 6 characters (the classic
|
|
9
|
+
// length); does not truncate when the input is shorter.
|
|
10
|
+
//
|
|
11
|
+
// Algorithm steps (canonical formulation):
|
|
12
|
+
//
|
|
13
|
+
// 1. Translate the first letters:
|
|
14
|
+
// MAC -> MCC, KN -> NN, K -> C, PH -> FF, PF -> FF,
|
|
15
|
+
// SCH -> SSS
|
|
16
|
+
// 2. Translate the last letters:
|
|
17
|
+
// EE -> Y, IE -> Y, DT/RT/RD/NT/ND -> D
|
|
18
|
+
// 3. First character of the key is the first character of the
|
|
19
|
+
// transformed name.
|
|
20
|
+
// 4. Walk the remaining characters; for each:
|
|
21
|
+
// EV -> AF
|
|
22
|
+
// any vowel (A/E/I/O/U) -> A
|
|
23
|
+
// Q -> G, Z -> S, M -> N
|
|
24
|
+
// KN -> N, K -> C
|
|
25
|
+
// SCH -> SSS, PH -> FF
|
|
26
|
+
// H -> if neighbour is non-vowel, replace with the previous
|
|
27
|
+
// key character (effectively "skip")
|
|
28
|
+
// W -> if previous is a vowel, replace with previous letter
|
|
29
|
+
// 5. Skip consecutive duplicate letters in the key.
|
|
30
|
+
// 6. If the key ends with S, drop it.
|
|
31
|
+
// 7. If the key ends with AY, replace with Y.
|
|
32
|
+
// 8. If the key ends with A, drop it.
|
|
33
|
+
// 9. Truncate to length 6.
|
|
34
|
+
//
|
|
35
|
+
// Sources:
|
|
36
|
+
// * Taft, R. L. "Name Search Techniques." New York State
|
|
37
|
+
// Identification and Intelligence System, 1970.
|
|
38
|
+
// * Apache Commons Codec Nysiis class.
|
|
39
|
+
// * https://en.wikipedia.org/wiki/New_York_State_Identification_and_Intelligence_System
|
|
40
|
+
|
|
41
|
+
#include <cstddef>
|
|
42
|
+
#include <string>
|
|
43
|
+
#include <string_view>
|
|
44
|
+
|
|
45
|
+
namespace stride_align::phonetic {
|
|
46
|
+
|
|
47
|
+
namespace nysiis_detail {
|
|
48
|
+
|
|
49
|
+
inline constexpr bool is_vowel(char c) noexcept {
|
|
50
|
+
return c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
inline constexpr char to_upper_ascii(char c) noexcept {
|
|
54
|
+
return (c >= 'a' && c <= 'z') ? static_cast<char>(c - ('a' - 'A')) : c;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
inline constexpr bool is_upper_alpha(char c) noexcept {
|
|
58
|
+
return c >= 'A' && c <= 'Z';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
inline bool starts_with(std::string_view s, std::string_view prefix) noexcept {
|
|
62
|
+
return s.size() >= prefix.size() &&
|
|
63
|
+
s.compare(0, prefix.size(), prefix) == 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
inline bool ends_with(std::string_view s, std::string_view suffix) noexcept {
|
|
67
|
+
return s.size() >= suffix.size() &&
|
|
68
|
+
s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
} // namespace nysiis_detail
|
|
72
|
+
|
|
73
|
+
inline std::string nysiis(std::string_view input) {
|
|
74
|
+
using namespace nysiis_detail;
|
|
75
|
+
|
|
76
|
+
// Pre-pass: keep ASCII letters only, upper-cased. No duplicate
|
|
77
|
+
// collapse here — NYSIIS handles that at the key-building stage.
|
|
78
|
+
std::string w;
|
|
79
|
+
w.reserve(input.size());
|
|
80
|
+
for (char c : input) {
|
|
81
|
+
const char uc = to_upper_ascii(c);
|
|
82
|
+
if (is_upper_alpha(uc)) w.push_back(uc);
|
|
83
|
+
}
|
|
84
|
+
if (w.empty()) return {};
|
|
85
|
+
|
|
86
|
+
// Step 1: first-letter translations.
|
|
87
|
+
if (starts_with(w, "MAC")) w.replace(0, 3, "MCC");
|
|
88
|
+
else if (starts_with(w, "KN")) w.replace(0, 2, "NN");
|
|
89
|
+
else if (w.front() == 'K') w.replace(0, 1, "C");
|
|
90
|
+
else if (starts_with(w, "PH")) w.replace(0, 2, "FF");
|
|
91
|
+
else if (starts_with(w, "PF")) w.replace(0, 2, "FF");
|
|
92
|
+
else if (starts_with(w, "SCH")) w.replace(0, 3, "SSS");
|
|
93
|
+
|
|
94
|
+
// Step 2: last-letter translations.
|
|
95
|
+
if (ends_with(w, "EE") || ends_with(w, "IE")) {
|
|
96
|
+
w.replace(w.size() - 2, 2, "Y");
|
|
97
|
+
} else if (ends_with(w, "DT") || ends_with(w, "RT") ||
|
|
98
|
+
ends_with(w, "RD") || ends_with(w, "NT") ||
|
|
99
|
+
ends_with(w, "ND")) {
|
|
100
|
+
w.replace(w.size() - 2, 2, "D");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Step 3: key starts with the first character.
|
|
104
|
+
std::string key;
|
|
105
|
+
key.reserve(w.size());
|
|
106
|
+
key.push_back(w.front());
|
|
107
|
+
|
|
108
|
+
// Steps 4-5: walk remaining characters, apply translations, skip
|
|
109
|
+
// consecutive duplicates in the key. Multi-letter transforms
|
|
110
|
+
// (EV→AF, KN→N, SCH→SSS, PH→FF) consume more than one input
|
|
111
|
+
// letter; we track that explicitly so the input cursor advances
|
|
112
|
+
// past every consumed letter rather than re-scanning them.
|
|
113
|
+
const std::size_t n = w.size();
|
|
114
|
+
for (std::size_t i = 1; i < n; ++i) {
|
|
115
|
+
const char c = w[i];
|
|
116
|
+
const char prev_in_word = w[i - 1];
|
|
117
|
+
const char nx = (i + 1 < n) ? w[i + 1] : '\0';
|
|
118
|
+
|
|
119
|
+
std::string translated; // 0-3 chars
|
|
120
|
+
std::size_t consumed = 1;
|
|
121
|
+
|
|
122
|
+
if (c == 'E' && nx == 'V') {
|
|
123
|
+
translated = "AF";
|
|
124
|
+
consumed = 2;
|
|
125
|
+
} else if (is_vowel(c)) {
|
|
126
|
+
translated = "A";
|
|
127
|
+
} else if (c == 'Q') {
|
|
128
|
+
translated = "G";
|
|
129
|
+
} else if (c == 'Z') {
|
|
130
|
+
translated = "S";
|
|
131
|
+
} else if (c == 'M') {
|
|
132
|
+
translated = "N";
|
|
133
|
+
} else if (c == 'K' && nx == 'N') {
|
|
134
|
+
translated = "N";
|
|
135
|
+
consumed = 2;
|
|
136
|
+
} else if (c == 'K') {
|
|
137
|
+
translated = "C";
|
|
138
|
+
} else if (c == 'S' && nx == 'C' && i + 2 < n && w[i + 2] == 'H') {
|
|
139
|
+
translated = "SSS";
|
|
140
|
+
consumed = 3;
|
|
141
|
+
} else if (c == 'P' && nx == 'H') {
|
|
142
|
+
translated = "FF";
|
|
143
|
+
consumed = 2;
|
|
144
|
+
} else if (c == 'H') {
|
|
145
|
+
// Modern NYSIIS interpretation (matches jellyfish): both
|
|
146
|
+
// neighbours vowel → keep H; both non-vowel (or end of word)
|
|
147
|
+
// → replace with previous letter (effectively silent via
|
|
148
|
+
// key-dedup); one of each → drop H entirely.
|
|
149
|
+
const bool prev_v = is_vowel(prev_in_word);
|
|
150
|
+
const bool next_v = (nx != '\0') && is_vowel(nx);
|
|
151
|
+
if (prev_v && next_v) {
|
|
152
|
+
translated.push_back('H');
|
|
153
|
+
} else if (!prev_v && !next_v) {
|
|
154
|
+
translated.push_back(prev_in_word);
|
|
155
|
+
}
|
|
156
|
+
// else: one-of-each, drop H (translated stays empty).
|
|
157
|
+
} else if (c == 'W') {
|
|
158
|
+
// If previous is a vowel, replace with the previous letter
|
|
159
|
+
// (which then dedupes against the previous key character).
|
|
160
|
+
if (is_vowel(prev_in_word)) {
|
|
161
|
+
translated.push_back(prev_in_word);
|
|
162
|
+
} else {
|
|
163
|
+
translated.push_back('W');
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
translated.push_back(c);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Step 5: append to key, skipping consecutive duplicates of the
|
|
170
|
+
// last key character. Internal duplicates inside ``translated``
|
|
171
|
+
// (e.g. the SSS in SCH→SSS) also collapse — both the leading
|
|
172
|
+
// SCH→SSS path and the mid-word case do the right thing.
|
|
173
|
+
for (char tc : translated) {
|
|
174
|
+
if (!key.empty() && key.back() == tc) continue;
|
|
175
|
+
key.push_back(tc);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Advance i by (consumed - 1) since the for-loop ++i covers
|
|
179
|
+
// the first letter on its own.
|
|
180
|
+
i += consumed - 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Steps 6-8: trailing-letter trim.
|
|
184
|
+
if (!key.empty() && key.back() == 'S') key.pop_back();
|
|
185
|
+
if (key.size() >= 2 && key[key.size() - 2] == 'A' && key.back() == 'Y') {
|
|
186
|
+
key.replace(key.size() - 2, 2, "Y");
|
|
187
|
+
}
|
|
188
|
+
if (!key.empty() && key.back() == 'A') key.pop_back();
|
|
189
|
+
|
|
190
|
+
// Truncation policy: the classical Taft 1970 paper truncates to
|
|
191
|
+
// six characters, but modern reference implementations (notably
|
|
192
|
+
// jellyfish, which is the de facto Python reference) don't. We
|
|
193
|
+
// follow the modern convention so cross-checks against jellyfish
|
|
194
|
+
// are bit-exact; callers wanting the classical six-character
|
|
195
|
+
// form can just ``[:6]`` the result themselves.
|
|
196
|
+
return key;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
} // namespace stride_align::phonetic
|