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,796 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
// Double Metaphone (Lawrence Philips, 2000).
|
|
4
|
+
//
|
|
5
|
+
// Produces two phonetic codes — a *primary* and an *alternate* —
|
|
6
|
+
// per input name. The alternate captures plausible non-English
|
|
7
|
+
// pronunciations (Italian, Spanish, German, Slavic, Greek
|
|
8
|
+
// patterns) and is empty (matches primary) for names that have
|
|
9
|
+
// only one reasonable encoding.
|
|
10
|
+
//
|
|
11
|
+
// Returns ``DoubleMetaphoneResult{primary, alternate}`` where
|
|
12
|
+
// ``alternate`` is an empty string when no second pronunciation
|
|
13
|
+
// applies. Maximum output length defaults to 4 (the original
|
|
14
|
+
// Philips spec), set via a kwarg on the public API.
|
|
15
|
+
//
|
|
16
|
+
// Reference: Lawrence Philips, "The Double Metaphone Search
|
|
17
|
+
// Algorithm", C/C++ Users Journal, June 2000. We mirror the
|
|
18
|
+
// algorithmic structure of Apache Commons Codec's DoubleMetaphone
|
|
19
|
+
// class (which is the de-facto open-source reference) — Slavo-
|
|
20
|
+
// Germanic and silent-start detection, the heavy letter handlers
|
|
21
|
+
// (C, G, S, T), and the per-character cursor-advance pattern.
|
|
22
|
+
//
|
|
23
|
+
// Cross-checked against the ``metaphone`` and ``doublemetaphone``
|
|
24
|
+
// Python packages. They agree on every name we tested except one
|
|
25
|
+
// pattern: GH preceded by a vowel at position 1 or 2 from the start
|
|
26
|
+
// (e.g. "Hugh", "High"). Apache Commons Codec (and the
|
|
27
|
+
// ``doublemetaphone`` PyPI port that mirrors it) say GH is silent
|
|
28
|
+
// here, giving "Hugh" -> "H". The ``metaphone`` PyPI package leaks
|
|
29
|
+
// the previous character's emission through a missing else clause,
|
|
30
|
+
// giving "Hugh" -> "HH". We treat that as a documented variation
|
|
31
|
+
// rather than picking a winner — ``DoubleMetaphoneVariant`` selects.
|
|
32
|
+
|
|
33
|
+
#include <cstddef>
|
|
34
|
+
#include <cstdint>
|
|
35
|
+
#include <initializer_list>
|
|
36
|
+
#include <string>
|
|
37
|
+
#include <string_view>
|
|
38
|
+
|
|
39
|
+
namespace stride_align::phonetic {
|
|
40
|
+
|
|
41
|
+
// Selects which published behaviour to mimic. The two values are
|
|
42
|
+
// NOT equal-weight algorithmic variations — ``kCommons`` is the
|
|
43
|
+
// faithful Lawrence Philips port (Apache Commons Codec, and the
|
|
44
|
+
// ``doublemetaphone`` PyPI package that mirrors it). ``kPython``
|
|
45
|
+
// reproduces a known bug in the ``metaphone`` PyPI package: a
|
|
46
|
+
// missing ``else`` in its ``process_g`` GH branch lets the previous
|
|
47
|
+
// character's ``self.next`` tuple leak into the next iteration, so
|
|
48
|
+
// names like "Hugh" and "High" come out "HH" instead of "H". We
|
|
49
|
+
// expose it as an opt-in for callers cross-checking against that
|
|
50
|
+
// specific library; if you don't have that constraint, stay on
|
|
51
|
+
// ``kCommons``.
|
|
52
|
+
enum class DoubleMetaphoneVariant : int {
|
|
53
|
+
// Apache Commons Codec semantics. "Hugh" -> ("H", "").
|
|
54
|
+
// Recommended default; correct per the published spec.
|
|
55
|
+
kCommons = 0,
|
|
56
|
+
// ``metaphone`` PyPI package bug-compat. "Hugh" -> ("HH", "").
|
|
57
|
+
// Opt in only when you need byte-for-byte agreement with that
|
|
58
|
+
// library; the divergent code paths are documented bugs, not
|
|
59
|
+
// legitimate algorithmic alternatives.
|
|
60
|
+
kPython = 1,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
struct DoubleMetaphoneResult {
|
|
64
|
+
std::string primary;
|
|
65
|
+
std::string alternate;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
namespace double_metaphone_detail {
|
|
69
|
+
|
|
70
|
+
inline constexpr char to_upper_ascii(char c) noexcept {
|
|
71
|
+
return (c >= 'a' && c <= 'z') ? static_cast<char>(c - ('a' - 'A')) : c;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
inline constexpr bool is_upper_alpha(char c) noexcept {
|
|
75
|
+
return c >= 'A' && c <= 'Z';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
inline bool is_vowel(const std::string& w, std::size_t pos) noexcept {
|
|
79
|
+
if (pos >= w.size()) return false;
|
|
80
|
+
const char c = w[pos];
|
|
81
|
+
return c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U' || c == 'Y';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Test whether ``w`` from position ``pos`` for ``len`` characters
|
|
85
|
+
// matches any of the strings in ``cands``. Returns false if the
|
|
86
|
+
// span runs off either end of ``w``. ``pos`` is signed so call
|
|
87
|
+
// sites passing ``pos - N`` (where ``pos`` is ``size_t``) don't
|
|
88
|
+
// trigger out-of-range exceptions on underflow — a wrapped-around
|
|
89
|
+
// size_t casts to a negative ptrdiff_t and gets rejected here.
|
|
90
|
+
inline bool contains_at(const std::string& w,
|
|
91
|
+
std::ptrdiff_t pos,
|
|
92
|
+
std::size_t len,
|
|
93
|
+
std::initializer_list<std::string_view> cands) noexcept {
|
|
94
|
+
if (pos < 0) return false;
|
|
95
|
+
const std::size_t upos = static_cast<std::size_t>(pos);
|
|
96
|
+
if (upos + len > w.size()) return false;
|
|
97
|
+
for (const auto& c : cands) {
|
|
98
|
+
if (c.size() != len) continue;
|
|
99
|
+
if (w.compare(upos, len, c.data(), c.size()) == 0) return true;
|
|
100
|
+
}
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Slavo-Germanic flag: presence of W, K, CZ, or WITZ anywhere.
|
|
105
|
+
inline bool is_slavo_germanic(const std::string& w) noexcept {
|
|
106
|
+
return w.find('W') != std::string::npos
|
|
107
|
+
|| w.find('K') != std::string::npos
|
|
108
|
+
|| w.find("CZ") != std::string::npos
|
|
109
|
+
|| w.find("WITZ") != std::string::npos;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
inline bool is_silent_start(const std::string& w) noexcept {
|
|
113
|
+
return contains_at(w, 0, 2, {"GN", "KN", "PN", "WR", "PS"});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Append to both primary and alternate (the common case).
|
|
117
|
+
inline void append(DoubleMetaphoneResult& r, char c) {
|
|
118
|
+
r.primary.push_back(c);
|
|
119
|
+
r.alternate.push_back(c);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
inline void append(DoubleMetaphoneResult& r, std::string_view s) {
|
|
123
|
+
r.primary.append(s);
|
|
124
|
+
r.alternate.append(s);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Append different characters to primary and alternate.
|
|
128
|
+
inline void append(DoubleMetaphoneResult& r, char p, char a) {
|
|
129
|
+
r.primary.push_back(p);
|
|
130
|
+
r.alternate.push_back(a);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
inline void append(DoubleMetaphoneResult& r,
|
|
134
|
+
std::string_view p, std::string_view a) {
|
|
135
|
+
r.primary.append(p);
|
|
136
|
+
r.alternate.append(a);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
inline bool full(const DoubleMetaphoneResult& r, std::size_t max_len) {
|
|
140
|
+
return r.primary.size() >= max_len && r.alternate.size() >= max_len;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---- per-letter handlers --------------------------------------------------
|
|
144
|
+
|
|
145
|
+
inline std::size_t handle_AEIOUY(
|
|
146
|
+
DoubleMetaphoneResult& r, std::size_t pos) {
|
|
147
|
+
if (pos == 0) append(r, 'A');
|
|
148
|
+
return pos + 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
inline std::size_t handle_B(
|
|
152
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
153
|
+
append(r, 'P');
|
|
154
|
+
return (pos + 1 < w.size() && w[pos + 1] == 'B') ? pos + 2 : pos + 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
inline std::size_t handle_C(
|
|
158
|
+
DoubleMetaphoneResult& r,
|
|
159
|
+
const std::string& w,
|
|
160
|
+
std::size_t pos,
|
|
161
|
+
bool slavo_germanic) {
|
|
162
|
+
const std::size_t n = w.size();
|
|
163
|
+
auto at = [&](std::size_t p) -> char {
|
|
164
|
+
return p < n ? w[p] : '\0';
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Various Germanic, Greek, otherwise contexts.
|
|
168
|
+
if (pos > 1
|
|
169
|
+
&& !is_vowel(w, pos - 2)
|
|
170
|
+
&& contains_at(w, pos - 1, 3, {"ACH"})
|
|
171
|
+
&& at(pos + 2) != 'I'
|
|
172
|
+
&& (at(pos + 2) != 'E'
|
|
173
|
+
|| contains_at(w, pos - 2, 6, {"BACHER", "MACHER"}))) {
|
|
174
|
+
append(r, 'K');
|
|
175
|
+
return pos + 2;
|
|
176
|
+
}
|
|
177
|
+
// Special case for Caesar.
|
|
178
|
+
if (pos == 0 && contains_at(w, 0, 6, {"CAESAR"})) {
|
|
179
|
+
append(r, 'S');
|
|
180
|
+
return pos + 2;
|
|
181
|
+
}
|
|
182
|
+
if (contains_at(w, pos, 4, {"CHIA"})) {
|
|
183
|
+
append(r, 'K');
|
|
184
|
+
return pos + 2;
|
|
185
|
+
}
|
|
186
|
+
if (contains_at(w, pos, 2, {"CH"})) {
|
|
187
|
+
if (pos > 0 && contains_at(w, pos, 4, {"CHAE"})) {
|
|
188
|
+
append(r, 'K', 'X');
|
|
189
|
+
return pos + 2;
|
|
190
|
+
}
|
|
191
|
+
// Greek roots, e.g. Chemistry, Chorus.
|
|
192
|
+
if (pos == 0
|
|
193
|
+
&& (contains_at(w, 1, 5, {"HARAC", "HARIS"})
|
|
194
|
+
|| contains_at(w, 1, 3, {"HOR", "HYM", "HIA", "HEM"}))
|
|
195
|
+
&& !contains_at(w, 0, 5, {"CHORE"})) {
|
|
196
|
+
append(r, 'K');
|
|
197
|
+
return pos + 2;
|
|
198
|
+
}
|
|
199
|
+
// Germanic, Greek, or otherwise.
|
|
200
|
+
if (contains_at(w, 0, 4, {"VAN ", "VON "})
|
|
201
|
+
|| contains_at(w, 0, 3, {"SCH"})
|
|
202
|
+
|| contains_at(w, pos - 2, 6, {"ORCHES", "ARCHIT", "ORCHID"})
|
|
203
|
+
|| contains_at(w, pos + 2, 1, {"T", "S"})
|
|
204
|
+
|| ((pos == 0 || contains_at(w, pos - 1, 1, {"A", "O", "U", "E"}))
|
|
205
|
+
&& contains_at(w, pos + 2, 1, {
|
|
206
|
+
"L", "R", "N", "M", "B", "H", "F", "V", "W", " "}))) {
|
|
207
|
+
append(r, 'K');
|
|
208
|
+
} else {
|
|
209
|
+
if (pos > 0) {
|
|
210
|
+
if (contains_at(w, 0, 2, {"MC"})) {
|
|
211
|
+
append(r, 'K');
|
|
212
|
+
} else {
|
|
213
|
+
append(r, 'X', 'K');
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
append(r, 'X');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return pos + 2;
|
|
220
|
+
}
|
|
221
|
+
// Italian CZ.
|
|
222
|
+
if (contains_at(w, pos, 2, {"CZ"})
|
|
223
|
+
&& !contains_at(w, pos - 2, 4, {"WICZ"})) {
|
|
224
|
+
append(r, 'S', 'X');
|
|
225
|
+
return pos + 2;
|
|
226
|
+
}
|
|
227
|
+
// Italian CIO/CIE/CIA.
|
|
228
|
+
if (contains_at(w, pos + 1, 3, {"CIA"})) {
|
|
229
|
+
append(r, 'X');
|
|
230
|
+
return pos + 3;
|
|
231
|
+
}
|
|
232
|
+
// Double CC.
|
|
233
|
+
if (contains_at(w, pos, 2, {"CC"})
|
|
234
|
+
&& !(pos == 1 && at(0) == 'M')) {
|
|
235
|
+
// ccI, ccE, ccH but not CCE/CCI in coup etc.
|
|
236
|
+
if (contains_at(w, pos + 2, 1, {"I", "E", "H"})
|
|
237
|
+
&& !contains_at(w, pos + 2, 2, {"HU"})) {
|
|
238
|
+
// Accident, Accede, Succeed
|
|
239
|
+
if ((pos == 1 && at(pos - 1) == 'A')
|
|
240
|
+
|| contains_at(w, pos - 1, 5, {"UCCEE", "UCCES"})) {
|
|
241
|
+
append(r, "KS");
|
|
242
|
+
} else {
|
|
243
|
+
append(r, 'X');
|
|
244
|
+
}
|
|
245
|
+
return pos + 3;
|
|
246
|
+
}
|
|
247
|
+
// Bacci, Bertucci, etc.
|
|
248
|
+
append(r, 'K');
|
|
249
|
+
return pos + 2;
|
|
250
|
+
}
|
|
251
|
+
if (contains_at(w, pos, 2, {"CK", "CG", "CQ"})) {
|
|
252
|
+
append(r, 'K');
|
|
253
|
+
return pos + 2;
|
|
254
|
+
}
|
|
255
|
+
if (contains_at(w, pos, 2, {"CI", "CE", "CY"})) {
|
|
256
|
+
// Italian vs. English.
|
|
257
|
+
if (contains_at(w, pos, 3, {"CIO", "CIE", "CIA"})) {
|
|
258
|
+
append(r, 'S', 'X');
|
|
259
|
+
} else {
|
|
260
|
+
append(r, 'S');
|
|
261
|
+
}
|
|
262
|
+
return pos + 2;
|
|
263
|
+
}
|
|
264
|
+
append(r, 'K');
|
|
265
|
+
// Mac+something - skip the second character.
|
|
266
|
+
if (contains_at(w, pos + 1, 2, {" C", " Q", " G"})) {
|
|
267
|
+
return pos + 3;
|
|
268
|
+
}
|
|
269
|
+
if (contains_at(w, pos + 1, 1, {"C", "K", "Q"})
|
|
270
|
+
&& !contains_at(w, pos + 1, 2, {"CE", "CI"})) {
|
|
271
|
+
return pos + 2;
|
|
272
|
+
}
|
|
273
|
+
return pos + 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
inline std::size_t handle_D(
|
|
277
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
278
|
+
if (contains_at(w, pos, 2, {"DG"})) {
|
|
279
|
+
if (contains_at(w, pos + 2, 1, {"I", "E", "Y"})) {
|
|
280
|
+
append(r, 'J');
|
|
281
|
+
return pos + 3;
|
|
282
|
+
}
|
|
283
|
+
append(r, "TK");
|
|
284
|
+
return pos + 2;
|
|
285
|
+
}
|
|
286
|
+
if (contains_at(w, pos, 2, {"DT", "DD"})) {
|
|
287
|
+
append(r, 'T');
|
|
288
|
+
return pos + 2;
|
|
289
|
+
}
|
|
290
|
+
append(r, 'T');
|
|
291
|
+
return pos + 1;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
inline std::size_t handle_G(
|
|
295
|
+
DoubleMetaphoneResult& r,
|
|
296
|
+
const std::string& w,
|
|
297
|
+
std::size_t pos,
|
|
298
|
+
bool slavo_germanic,
|
|
299
|
+
DoubleMetaphoneVariant variant) {
|
|
300
|
+
const std::size_t n = w.size();
|
|
301
|
+
auto at = [&](std::size_t p) -> char {
|
|
302
|
+
return p < n ? w[p] : '\0';
|
|
303
|
+
};
|
|
304
|
+
if (at(pos + 1) == 'H') {
|
|
305
|
+
if (pos > 0 && !is_vowel(w, pos - 1)) {
|
|
306
|
+
append(r, 'K');
|
|
307
|
+
return pos + 2;
|
|
308
|
+
}
|
|
309
|
+
if (pos == 0) {
|
|
310
|
+
if (at(pos + 2) == 'I') {
|
|
311
|
+
append(r, 'J');
|
|
312
|
+
} else {
|
|
313
|
+
append(r, 'K');
|
|
314
|
+
}
|
|
315
|
+
return pos + 2;
|
|
316
|
+
}
|
|
317
|
+
// metaphone-py compat: at this point ``pos > 0`` and the char
|
|
318
|
+
// before G is a vowel. The PyPI ``metaphone`` package has an
|
|
319
|
+
// elif chain whose ``position < start_index + 3`` clause
|
|
320
|
+
// shortcircuits Parker's rule and, when ``pos`` is 1 or 2,
|
|
321
|
+
// leaves ``self.next`` at the previous letter's tuple — so the
|
|
322
|
+
// previous emit fires again. We replicate the symptom directly
|
|
323
|
+
// here: re-append whatever was last emitted. Apache Commons
|
|
324
|
+
// falls through to Parker (the next block) instead.
|
|
325
|
+
if (variant == DoubleMetaphoneVariant::kPython && pos < 3) {
|
|
326
|
+
if (!r.primary.empty()) r.primary.push_back(r.primary.back());
|
|
327
|
+
if (!r.alternate.empty())r.alternate.push_back(r.alternate.back());
|
|
328
|
+
return pos + 2;
|
|
329
|
+
}
|
|
330
|
+
// Parker's rules — silent in many contexts.
|
|
331
|
+
if ((pos > 1 && contains_at(w, pos - 2, 1, {"B", "H", "D"}))
|
|
332
|
+
|| (pos > 2 && contains_at(w, pos - 3, 1, {"B", "H", "D"}))
|
|
333
|
+
|| (pos > 3 && contains_at(w, pos - 4, 1, {"B", "H"}))) {
|
|
334
|
+
return pos + 2;
|
|
335
|
+
}
|
|
336
|
+
// E.g. "laugh", "cough"
|
|
337
|
+
if (pos > 2 && at(pos - 1) == 'U'
|
|
338
|
+
&& contains_at(w, pos - 3, 1, {"C", "G", "L", "R", "T"})) {
|
|
339
|
+
append(r, 'F');
|
|
340
|
+
return pos + 2;
|
|
341
|
+
}
|
|
342
|
+
if (pos > 0 && at(pos - 1) != 'I') {
|
|
343
|
+
append(r, 'K');
|
|
344
|
+
}
|
|
345
|
+
return pos + 2;
|
|
346
|
+
}
|
|
347
|
+
if (at(pos + 1) == 'N') {
|
|
348
|
+
if (pos == 1 && is_vowel(w, 0) && !slavo_germanic) {
|
|
349
|
+
append(r, "KN", "N");
|
|
350
|
+
return pos + 2;
|
|
351
|
+
}
|
|
352
|
+
if (!contains_at(w, pos + 2, 2, {"EY"})
|
|
353
|
+
&& at(pos + 1) != 'Y' && !slavo_germanic) {
|
|
354
|
+
append(r, "N", "KN");
|
|
355
|
+
return pos + 2;
|
|
356
|
+
}
|
|
357
|
+
append(r, "KN");
|
|
358
|
+
return pos + 2;
|
|
359
|
+
}
|
|
360
|
+
if (contains_at(w, pos + 1, 2, {"LI"}) && !slavo_germanic) {
|
|
361
|
+
append(r, "KL", "L");
|
|
362
|
+
return pos + 2;
|
|
363
|
+
}
|
|
364
|
+
if (pos == 0
|
|
365
|
+
&& (at(pos + 1) == 'Y'
|
|
366
|
+
|| contains_at(w, pos + 1, 2, {
|
|
367
|
+
"ES", "EP", "EB", "EL", "EY", "IB", "IL", "IN", "IE", "EI", "ER"}))) {
|
|
368
|
+
append(r, 'K', 'J');
|
|
369
|
+
return pos + 2;
|
|
370
|
+
}
|
|
371
|
+
if ((contains_at(w, pos + 1, 2, {"ER"})
|
|
372
|
+
|| at(pos + 1) == 'Y')
|
|
373
|
+
&& !contains_at(w, 0, 6, {"DANGER", "RANGER", "MANGER"})
|
|
374
|
+
&& !contains_at(w, pos - 1, 1, {"E", "I"})
|
|
375
|
+
&& !contains_at(w, pos - 1, 3, {"RGY", "OGY"})) {
|
|
376
|
+
append(r, 'K', 'J');
|
|
377
|
+
return pos + 2;
|
|
378
|
+
}
|
|
379
|
+
if (contains_at(w, pos + 1, 1, {"E", "I", "Y"})
|
|
380
|
+
|| contains_at(w, pos - 1, 4, {"AGGI", "OGGI"})) {
|
|
381
|
+
if (contains_at(w, 0, 4, {"VAN ", "VON "})
|
|
382
|
+
|| contains_at(w, 0, 3, {"SCH"})
|
|
383
|
+
|| contains_at(w, pos + 1, 2, {"ET"})) {
|
|
384
|
+
append(r, 'K');
|
|
385
|
+
} else if (contains_at(w, pos + 1, 3, {"IER"})) {
|
|
386
|
+
append(r, 'J');
|
|
387
|
+
} else {
|
|
388
|
+
append(r, 'J', 'K');
|
|
389
|
+
}
|
|
390
|
+
return pos + 2;
|
|
391
|
+
}
|
|
392
|
+
if (at(pos + 1) == 'G') {
|
|
393
|
+
append(r, 'K');
|
|
394
|
+
return pos + 2;
|
|
395
|
+
}
|
|
396
|
+
append(r, 'K');
|
|
397
|
+
return pos + 1;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
inline std::size_t handle_H(
|
|
401
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
402
|
+
if ((pos == 0 || is_vowel(w, pos - 1)) && is_vowel(w, pos + 1)) {
|
|
403
|
+
append(r, 'H');
|
|
404
|
+
return pos + 2;
|
|
405
|
+
}
|
|
406
|
+
return pos + 1;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
inline std::size_t handle_J(
|
|
410
|
+
DoubleMetaphoneResult& r,
|
|
411
|
+
const std::string& w,
|
|
412
|
+
std::size_t pos,
|
|
413
|
+
bool slavo_germanic) {
|
|
414
|
+
const std::size_t n = w.size();
|
|
415
|
+
if (contains_at(w, pos, 4, {"JOSE"}) || contains_at(w, 0, 4, {"SAN "})) {
|
|
416
|
+
if ((pos == 0 && pos + 4 < n && w[pos + 4] == ' ')
|
|
417
|
+
|| contains_at(w, 0, 4, {"SAN "})) {
|
|
418
|
+
append(r, 'H');
|
|
419
|
+
} else {
|
|
420
|
+
append(r, 'J', 'H');
|
|
421
|
+
}
|
|
422
|
+
return pos + 1;
|
|
423
|
+
}
|
|
424
|
+
if (pos == 0 && !contains_at(w, pos, 4, {"JOSE"})) {
|
|
425
|
+
append(r, 'J', 'A');
|
|
426
|
+
} else if (is_vowel(w, pos - 1) && !slavo_germanic
|
|
427
|
+
&& (pos + 1 < n && (w[pos + 1] == 'A' || w[pos + 1] == 'O'))) {
|
|
428
|
+
append(r, 'J', 'H');
|
|
429
|
+
} else if (pos + 1 == n) {
|
|
430
|
+
append(r, 'J', ' ');
|
|
431
|
+
} else if (pos + 1 < n
|
|
432
|
+
&& !contains_at(w, pos + 1, 1, {"L", "T", "K", "S", "N", "M", "B", "Z"})
|
|
433
|
+
&& !(pos > 0
|
|
434
|
+
&& contains_at(w, pos - 1, 1, {"S", "K", "L"}))) {
|
|
435
|
+
append(r, 'J');
|
|
436
|
+
}
|
|
437
|
+
return (pos + 1 < n && w[pos + 1] == 'J') ? pos + 2 : pos + 1;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
inline std::size_t handle_L(
|
|
441
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
442
|
+
const std::size_t n = w.size();
|
|
443
|
+
if (pos + 1 < n && w[pos + 1] == 'L') {
|
|
444
|
+
// Spanish/Italian "ll": appended differently between primary
|
|
445
|
+
// and alternate when at certain word-final positions.
|
|
446
|
+
if ((pos == n - 3
|
|
447
|
+
&& contains_at(w, pos - 1, 4, {"ILLO", "ILLA", "ALLE"}))
|
|
448
|
+
|| ((contains_at(w, n - 2, 2, {"AS", "OS"}) || (n >= 1 && (w[n - 1] == 'A' || w[n - 1] == 'O')))
|
|
449
|
+
&& contains_at(w, pos - 1, 4, {"ALLE"}))) {
|
|
450
|
+
r.primary.push_back('L');
|
|
451
|
+
// alternate gets nothing (silent in the Spanish reading).
|
|
452
|
+
return pos + 2;
|
|
453
|
+
}
|
|
454
|
+
append(r, 'L');
|
|
455
|
+
return pos + 2;
|
|
456
|
+
}
|
|
457
|
+
append(r, 'L');
|
|
458
|
+
return pos + 1;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
inline std::size_t handle_M(
|
|
462
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
463
|
+
const std::size_t n = w.size();
|
|
464
|
+
append(r, 'M');
|
|
465
|
+
if ((pos + 1 < n && w[pos + 1] == 'M')
|
|
466
|
+
|| (pos > 0 && contains_at(w, pos - 1, 3, {"UMB"})
|
|
467
|
+
&& (pos + 1 == n
|
|
468
|
+
|| contains_at(w, pos + 2, 2, {"ER"})))) {
|
|
469
|
+
return pos + 2;
|
|
470
|
+
}
|
|
471
|
+
return pos + 1;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
inline std::size_t handle_N(
|
|
475
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
476
|
+
append(r, 'N');
|
|
477
|
+
return (pos + 1 < w.size() && w[pos + 1] == 'N') ? pos + 2 : pos + 1;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
inline std::size_t handle_P(
|
|
481
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
482
|
+
if (pos + 1 < w.size() && w[pos + 1] == 'H') {
|
|
483
|
+
append(r, 'F');
|
|
484
|
+
return pos + 2;
|
|
485
|
+
}
|
|
486
|
+
append(r, 'P');
|
|
487
|
+
return (pos + 1 < w.size() && (w[pos + 1] == 'P' || w[pos + 1] == 'B')) ? pos + 2 : pos + 1;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
inline std::size_t handle_Q(
|
|
491
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
492
|
+
append(r, 'K');
|
|
493
|
+
return (pos + 1 < w.size() && w[pos + 1] == 'Q') ? pos + 2 : pos + 1;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
inline std::size_t handle_R(
|
|
497
|
+
DoubleMetaphoneResult& r,
|
|
498
|
+
const std::string& w,
|
|
499
|
+
std::size_t pos,
|
|
500
|
+
bool slavo_germanic) {
|
|
501
|
+
const std::size_t n = w.size();
|
|
502
|
+
// French -IER: primary appends nothing, alternate appends R.
|
|
503
|
+
if (pos + 1 == n && !slavo_germanic
|
|
504
|
+
&& contains_at(w, pos - 2, 2, {"IE"})
|
|
505
|
+
&& !contains_at(w, pos - 4, 2, {"ME", "MA"})) {
|
|
506
|
+
r.alternate.push_back('R');
|
|
507
|
+
return pos + 1;
|
|
508
|
+
}
|
|
509
|
+
append(r, 'R');
|
|
510
|
+
return (pos + 1 < n && w[pos + 1] == 'R') ? pos + 2 : pos + 1;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
inline std::size_t handle_S(
|
|
514
|
+
DoubleMetaphoneResult& r,
|
|
515
|
+
const std::string& w,
|
|
516
|
+
std::size_t pos,
|
|
517
|
+
bool slavo_germanic) {
|
|
518
|
+
const std::size_t n = w.size();
|
|
519
|
+
if (pos > 0 && contains_at(w, pos - 1, 3, {"ISL", "YSL"})) {
|
|
520
|
+
return pos + 1;
|
|
521
|
+
}
|
|
522
|
+
if (pos == 0 && contains_at(w, 0, 5, {"SUGAR"})) {
|
|
523
|
+
append(r, 'X', 'S');
|
|
524
|
+
return pos + 1;
|
|
525
|
+
}
|
|
526
|
+
if (contains_at(w, pos, 2, {"SH"})) {
|
|
527
|
+
if (contains_at(w, pos + 1, 4, {"HEIM", "HOEK", "HOLM", "HOLZ"})) {
|
|
528
|
+
append(r, 'S');
|
|
529
|
+
} else {
|
|
530
|
+
append(r, 'X');
|
|
531
|
+
}
|
|
532
|
+
return pos + 2;
|
|
533
|
+
}
|
|
534
|
+
if (contains_at(w, pos, 3, {"SIO", "SIA"})
|
|
535
|
+
|| contains_at(w, pos, 4, {"SIAN"})) {
|
|
536
|
+
if (!slavo_germanic) append(r, 'S', 'X');
|
|
537
|
+
else append(r, 'S');
|
|
538
|
+
return pos + 3;
|
|
539
|
+
}
|
|
540
|
+
if ((pos == 0
|
|
541
|
+
&& contains_at(w, pos + 1, 1, {"M", "N", "L", "W"}))
|
|
542
|
+
|| contains_at(w, pos + 1, 1, {"Z"})) {
|
|
543
|
+
append(r, 'S', 'X');
|
|
544
|
+
if (pos + 1 < n && w[pos + 1] == 'Z') return pos + 2;
|
|
545
|
+
return pos + 1;
|
|
546
|
+
}
|
|
547
|
+
if (contains_at(w, pos, 2, {"SC"})) {
|
|
548
|
+
if (pos + 2 < n && w[pos + 2] == 'H') {
|
|
549
|
+
// Dutch origin: schermerhorn, schenker.
|
|
550
|
+
if (contains_at(w, pos + 3, 2, {
|
|
551
|
+
"OO", "ER", "EN", "UY", "ED", "EM"})) {
|
|
552
|
+
if (contains_at(w, pos + 3, 2, {"ER", "EN"})) {
|
|
553
|
+
append(r, "X", "SK");
|
|
554
|
+
} else {
|
|
555
|
+
append(r, "SK");
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
if (pos == 0 && !is_vowel(w, 3) && w.size() > 3 && w[3] != 'W') {
|
|
559
|
+
append(r, 'X', 'S');
|
|
560
|
+
} else {
|
|
561
|
+
append(r, 'X');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return pos + 3;
|
|
565
|
+
}
|
|
566
|
+
if (contains_at(w, pos + 2, 1, {"I", "E", "Y"})) {
|
|
567
|
+
append(r, 'S');
|
|
568
|
+
return pos + 3;
|
|
569
|
+
}
|
|
570
|
+
append(r, "SK");
|
|
571
|
+
return pos + 3;
|
|
572
|
+
}
|
|
573
|
+
// French: silent S at end of word like "Debois"/"des".
|
|
574
|
+
if (pos + 1 == n && contains_at(w, pos - 2, 2, {"AI", "OI"})) {
|
|
575
|
+
r.alternate.push_back('S');
|
|
576
|
+
return pos + 1;
|
|
577
|
+
}
|
|
578
|
+
append(r, 'S');
|
|
579
|
+
return (pos + 1 < n && (w[pos + 1] == 'S' || w[pos + 1] == 'Z')) ? pos + 2 : pos + 1;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
inline std::size_t handle_T(
|
|
583
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
584
|
+
const std::size_t n = w.size();
|
|
585
|
+
if (contains_at(w, pos, 4, {"TION"})
|
|
586
|
+
|| contains_at(w, pos, 3, {"TIA", "TCH"})) {
|
|
587
|
+
append(r, 'X');
|
|
588
|
+
return pos + 3;
|
|
589
|
+
}
|
|
590
|
+
if (contains_at(w, pos, 2, {"TH"})
|
|
591
|
+
|| contains_at(w, pos, 3, {"TTH"})) {
|
|
592
|
+
// Special case: Thomas, Thames.
|
|
593
|
+
if (contains_at(w, pos + 2, 2, {"OM", "AM"})
|
|
594
|
+
|| contains_at(w, 0, 4, {"VAN ", "VON "})
|
|
595
|
+
|| contains_at(w, 0, 3, {"SCH"})) {
|
|
596
|
+
append(r, 'T');
|
|
597
|
+
} else {
|
|
598
|
+
append(r, '0', 'T');
|
|
599
|
+
}
|
|
600
|
+
return pos + 2;
|
|
601
|
+
}
|
|
602
|
+
append(r, 'T');
|
|
603
|
+
return (pos + 1 < n && (w[pos + 1] == 'T' || w[pos + 1] == 'D')) ? pos + 2 : pos + 1;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
inline std::size_t handle_V(
|
|
607
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
608
|
+
append(r, 'F');
|
|
609
|
+
return (pos + 1 < w.size() && w[pos + 1] == 'V') ? pos + 2 : pos + 1;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
inline std::size_t handle_W(
|
|
613
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
614
|
+
const std::size_t n = w.size();
|
|
615
|
+
// WR — like R.
|
|
616
|
+
if (contains_at(w, pos, 2, {"WR"})) {
|
|
617
|
+
append(r, 'R');
|
|
618
|
+
return pos + 2;
|
|
619
|
+
}
|
|
620
|
+
if (pos == 0
|
|
621
|
+
&& (is_vowel(w, pos + 1) || contains_at(w, pos, 2, {"WH"}))) {
|
|
622
|
+
if (is_vowel(w, pos + 1)) {
|
|
623
|
+
append(r, 'A', 'F');
|
|
624
|
+
} else {
|
|
625
|
+
append(r, 'A');
|
|
626
|
+
}
|
|
627
|
+
return pos + 1;
|
|
628
|
+
}
|
|
629
|
+
if ((pos + 1 == n && is_vowel(w, pos - 1))
|
|
630
|
+
|| contains_at(w, pos - 1, 5, {"EWSKI", "EWSKY", "OWSKI", "OWSKY"})
|
|
631
|
+
|| contains_at(w, 0, 3, {"SCH"})) {
|
|
632
|
+
r.alternate.push_back('F');
|
|
633
|
+
return pos + 1;
|
|
634
|
+
}
|
|
635
|
+
if (contains_at(w, pos, 4, {"WICZ", "WITZ"})) {
|
|
636
|
+
append(r, "TS", "FX");
|
|
637
|
+
return pos + 4;
|
|
638
|
+
}
|
|
639
|
+
return pos + 1;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
inline std::size_t handle_X(
|
|
643
|
+
DoubleMetaphoneResult& r, const std::string& w, std::size_t pos) {
|
|
644
|
+
if (pos == 0) {
|
|
645
|
+
append(r, 'S');
|
|
646
|
+
return pos + 1;
|
|
647
|
+
}
|
|
648
|
+
const std::size_t n = w.size();
|
|
649
|
+
if (!(pos + 1 == n
|
|
650
|
+
&& (contains_at(w, pos - 3, 3, {"IAU", "EAU"})
|
|
651
|
+
|| contains_at(w, pos - 2, 2, {"AU", "OU"})))) {
|
|
652
|
+
append(r, "KS");
|
|
653
|
+
}
|
|
654
|
+
return (pos + 1 < n && (w[pos + 1] == 'C' || w[pos + 1] == 'X')) ? pos + 2 : pos + 1;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
inline std::size_t handle_Z(
|
|
658
|
+
DoubleMetaphoneResult& r,
|
|
659
|
+
const std::string& w,
|
|
660
|
+
std::size_t pos,
|
|
661
|
+
bool slavo_germanic) {
|
|
662
|
+
if (pos + 1 < w.size() && w[pos + 1] == 'H') {
|
|
663
|
+
append(r, 'J');
|
|
664
|
+
return pos + 2;
|
|
665
|
+
}
|
|
666
|
+
// Apache Commons Codec emits the split "S"/"TS" only when the
|
|
667
|
+
// preceding letter is NOT T — names like "Schwartz" (TZ ending) get
|
|
668
|
+
// a single S so the alternate isn't doubled. We had the predicate
|
|
669
|
+
// inverted in an earlier draft; see issue tested by Schwartz.
|
|
670
|
+
if (contains_at(w, pos + 1, 2, {"ZO", "ZI", "ZA"})
|
|
671
|
+
|| (slavo_germanic && pos > 0
|
|
672
|
+
&& !contains_at(w, pos - 1, 1, {"T"}))) {
|
|
673
|
+
append(r, std::string_view("S"), std::string_view("TS"));
|
|
674
|
+
} else {
|
|
675
|
+
append(r, 'S');
|
|
676
|
+
}
|
|
677
|
+
return (pos + 1 < w.size() && w[pos + 1] == 'Z') ? pos + 2 : pos + 1;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
} // namespace double_metaphone_detail
|
|
681
|
+
|
|
682
|
+
// max_length defaults to a generous bound rather than the
|
|
683
|
+
// historical 4 — modern reference implementations (the ``metaphone``
|
|
684
|
+
// Python package, Apache Commons Codec when configured) emit the
|
|
685
|
+
// full code and let the caller slice. Pass a smaller value if the
|
|
686
|
+
// classical 4-char Philips form is needed.
|
|
687
|
+
inline DoubleMetaphoneResult double_metaphone(
|
|
688
|
+
std::string_view input,
|
|
689
|
+
std::size_t max_length = 64,
|
|
690
|
+
DoubleMetaphoneVariant variant = DoubleMetaphoneVariant::kCommons) {
|
|
691
|
+
using namespace double_metaphone_detail;
|
|
692
|
+
|
|
693
|
+
DoubleMetaphoneResult r;
|
|
694
|
+
std::string w;
|
|
695
|
+
w.reserve(input.size());
|
|
696
|
+
for (char c : input) {
|
|
697
|
+
const char uc = to_upper_ascii(c);
|
|
698
|
+
if (is_upper_alpha(uc)) w.push_back(uc);
|
|
699
|
+
}
|
|
700
|
+
if (w.empty()) return r;
|
|
701
|
+
|
|
702
|
+
const bool slavo = is_slavo_germanic(w);
|
|
703
|
+
std::size_t pos = is_silent_start(w) ? 1 : 0;
|
|
704
|
+
|
|
705
|
+
if (w.front() == 'X') {
|
|
706
|
+
append(r, 'S');
|
|
707
|
+
pos = 1;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
while (pos < w.size() && !full(r, max_length)) {
|
|
711
|
+
const char c = w[pos];
|
|
712
|
+
switch (c) {
|
|
713
|
+
case 'A': case 'E': case 'I': case 'O': case 'U': case 'Y':
|
|
714
|
+
pos = handle_AEIOUY(r, pos);
|
|
715
|
+
break;
|
|
716
|
+
case 'B':
|
|
717
|
+
pos = handle_B(r, w, pos);
|
|
718
|
+
break;
|
|
719
|
+
case 'C':
|
|
720
|
+
pos = handle_C(r, w, pos, slavo);
|
|
721
|
+
break;
|
|
722
|
+
case 'D':
|
|
723
|
+
pos = handle_D(r, w, pos);
|
|
724
|
+
break;
|
|
725
|
+
case 'F':
|
|
726
|
+
append(r, 'F');
|
|
727
|
+
pos += (pos + 1 < w.size() && w[pos + 1] == 'F') ? 2 : 1;
|
|
728
|
+
break;
|
|
729
|
+
case 'G':
|
|
730
|
+
pos = handle_G(r, w, pos, slavo, variant);
|
|
731
|
+
break;
|
|
732
|
+
case 'H':
|
|
733
|
+
pos = handle_H(r, w, pos);
|
|
734
|
+
break;
|
|
735
|
+
case 'J':
|
|
736
|
+
pos = handle_J(r, w, pos, slavo);
|
|
737
|
+
break;
|
|
738
|
+
case 'K':
|
|
739
|
+
append(r, 'K');
|
|
740
|
+
pos += (pos + 1 < w.size() && w[pos + 1] == 'K') ? 2 : 1;
|
|
741
|
+
break;
|
|
742
|
+
case 'L':
|
|
743
|
+
pos = handle_L(r, w, pos);
|
|
744
|
+
break;
|
|
745
|
+
case 'M':
|
|
746
|
+
pos = handle_M(r, w, pos);
|
|
747
|
+
break;
|
|
748
|
+
case 'N':
|
|
749
|
+
pos = handle_N(r, w, pos);
|
|
750
|
+
break;
|
|
751
|
+
case 'P':
|
|
752
|
+
pos = handle_P(r, w, pos);
|
|
753
|
+
break;
|
|
754
|
+
case 'Q':
|
|
755
|
+
pos = handle_Q(r, w, pos);
|
|
756
|
+
break;
|
|
757
|
+
case 'R':
|
|
758
|
+
pos = handle_R(r, w, pos, slavo);
|
|
759
|
+
break;
|
|
760
|
+
case 'S':
|
|
761
|
+
pos = handle_S(r, w, pos, slavo);
|
|
762
|
+
break;
|
|
763
|
+
case 'T':
|
|
764
|
+
pos = handle_T(r, w, pos);
|
|
765
|
+
break;
|
|
766
|
+
case 'V':
|
|
767
|
+
pos = handle_V(r, w, pos);
|
|
768
|
+
break;
|
|
769
|
+
case 'W':
|
|
770
|
+
pos = handle_W(r, w, pos);
|
|
771
|
+
break;
|
|
772
|
+
case 'X':
|
|
773
|
+
pos = handle_X(r, w, pos);
|
|
774
|
+
break;
|
|
775
|
+
case 'Z':
|
|
776
|
+
pos = handle_Z(r, w, pos, slavo);
|
|
777
|
+
break;
|
|
778
|
+
default:
|
|
779
|
+
++pos;
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (r.primary.size() > max_length) r.primary.resize(max_length);
|
|
785
|
+
if (r.alternate.size() > max_length) r.alternate.resize(max_length);
|
|
786
|
+
// The Apache Commons reference always emits both codes even when
|
|
787
|
+
// identical. The Python ``metaphone`` package — our cross-check
|
|
788
|
+
// oracle — emits an empty alternate when no two-arg ``append``
|
|
789
|
+
// ever diverged it from primary. Match that here so callers can
|
|
790
|
+
// cheaply test ``if alt:`` for "did the name have a second
|
|
791
|
+
// pronunciation".
|
|
792
|
+
if (r.primary == r.alternate) r.alternate.clear();
|
|
793
|
+
return r;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
} // namespace stride_align::phonetic
|